diff options
| author | Robert Marko | 2026-10-12 17:41:50 +0000 |
|---|---|---|
| committer | Robert Marko | 2026-10-14 07:39:33 +0000 |
| commit | ddd77cc5757b6121cac8a1ed369b35b0c32fe92a (patch) | |
| tree | 7b5a9a43db8a2c8df39885240dcf3c2b35edce28 | |
| parent | 9a54bf699efeb2ebf7be47065db7ebb03067c9fa (diff) | |
boot: arm-trusted-firmware-microchipsw: make ECDSA signatures reproducible
Use RFC 6979 nonces with the staged LibreSSL host tool when
SOURCE_DATE_EPOCH is set. Retry deterministically for full-width
LAN969x signature scalars while preserving random signing otherwise.
Two clean builds produced identical FIP, FWU FIP and FWU HTML outputs.
Signed-off-by: Robert Marko <robert.marko@sartura.hr>
3 files changed, 423 insertions, 3 deletions
diff --git a/package/boot/arm-trusted-firmware-microchipsw/Makefile b/package/boot/arm-trusted-firmware-microchipsw/Makefile index 9af16aba87..f6a97cbb73 100644 --- a/package/boot/arm-trusted-firmware-microchipsw/Makefile +++ b/package/boot/arm-trusted-firmware-microchipsw/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=arm-trusted-firmware-microchipsw -PKG_RELEASE:=4 +PKG_RELEASE:=5 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL=https://github.com/microchip-ung/arm-trusted-firmware.git diff --git a/package/boot/arm-trusted-firmware-microchipsw/patches/0007-cert_create-make-ECDSA-signatures-reproducible.patch b/package/boot/arm-trusted-firmware-microchipsw/patches/0007-cert_create-make-ECDSA-signatures-reproducible.patch new file mode 100644 index 0000000000..9b3964f9f6 --- /dev/null +++ b/package/boot/arm-trusted-firmware-microchipsw/patches/0007-cert_create-make-ECDSA-signatures-reproducible.patch @@ -0,0 +1,420 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Robert Marko <robert.marko@sartura.hr> +Date: Mon, 12 Oct 2026 19:41:50 +0200 +Subject: [PATCH] cert_create: make ECDSA signatures reproducible + +Generate ECDSA nonces according to RFC 6979 when SOURCE_DATE_EPOCH is +set, using LibreSSL's public cryptographic APIs. Retry deterministically +when either signature scalar is too short for the LAN969x boot ROM. + +Retain random signatures when SOURCE_DATE_EPOCH is unset and leave RSA +signing unchanged. This makes certificate bytes stable across builds +with fixed signing keys and payloads. + +Signed-off-by: Robert Marko <robert.marko@sartura.hr> +--- +--- a/tools/cert_create/Makefile ++++ b/tools/cert_create/Makefile +@@ -25,6 +25,7 @@ endif + # Common source files. + OBJECTS := src/cert.o \ + src/cmd_opt.o \ ++ src/deterministic_ecdsa.o \ + src/ext.o \ + src/key.o \ + src/main.o \ +--- a/tools/cert_create/src/cert.c ++++ b/tools/cert_create/src/cert.c +@@ -11,6 +11,7 @@ + #include <time.h> + + #include <openssl/conf.h> ++#include <openssl/crypto.h> + #include <openssl/err.h> + #include <openssl/opensslv.h> + #include <openssl/ec.h> +@@ -21,6 +22,7 @@ + #include "cert.h" + #include "cmd_opt.h" + #include "debug.h" ++#include "deterministic_ecdsa.h" + #include "key.h" + #include "sha.h" + +@@ -279,6 +281,51 @@ out: + return rc; + } + ++/* ++ * X509_sign_ctx sets both signature algorithm fields. Replace its random ++ * ECDSA signature with one over the same TBS bytes using RFC 6979. ++ */ ++static int cert_sign_deterministic(X509 *x, EVP_PKEY *ikey, int md_alg, int fb) ++{ ++ const ASN1_BIT_STRING *old_sig = NULL; ++ const X509_ALGOR *alg = NULL; ++ const EVP_MD *md = get_digest(md_alg); ++ unsigned char digest[EVP_MAX_MD_SIZE]; ++ unsigned char *tbs = NULL, *der_sig = NULL; ++ unsigned int digest_len = 0; ++ ECDSA_SIG *sig = NULL; ++ int tbs_len, sig_len, ok = 0; ++ ++ if (md == NULL || !cert_sign_once(x, ikey, md_alg)) { ++ goto out; ++ } ++ tbs_len = i2d_re_X509_tbs(x, &tbs); ++ if (tbs_len <= 0 || ++ !EVP_Digest(tbs, tbs_len, digest, &digest_len, md, NULL)) { ++ goto out; ++ } ++ sig = ecdsa_sign_rfc6979(digest, digest_len, md, ++ EVP_PKEY_get0_EC_KEY(ikey), fb, ++ ECDSA_SIG_MAX_RETRIES); ++ if (sig == NULL) { ++ goto out; ++ } ++ sig_len = i2d_ECDSA_SIG(sig, &der_sig); ++ X509_get0_signature(&old_sig, &alg, x); ++ if (sig_len <= 0 || old_sig == NULL || ++ !ASN1_BIT_STRING_set((ASN1_BIT_STRING *)old_sig, der_sig, sig_len) || ++ X509_verify(x, ikey) != 1) { ++ goto out; ++ } ++ ok = 1; ++out: ++ OPENSSL_cleanse(digest, sizeof(digest)); ++ OPENSSL_free(tbs); ++ OPENSSL_free(der_sig); ++ ECDSA_SIG_free(sig); ++ return ok; ++} ++ + int cert_new( + int md_alg, + cert_t *cert, +@@ -392,18 +439,25 @@ int cert_new( + * signature almost immediate. + */ + fb = ec_operand_bytes(ikey); +- for (attempt = 0; attempt < ECDSA_SIG_MAX_RETRIES; attempt++) { +- if (!cert_sign_once(x, ikey, md_alg)) { ++ if (epoch_set && fb > 0) { ++ if (!cert_sign_deterministic(x, ikey, md_alg, fb) || ++ !cert_sig_full_width(x, fb)) { + goto END; + } +- if (fb == 0 || cert_sig_full_width(x, fb)) { +- break; ++ } else { ++ for (attempt = 0; attempt < ECDSA_SIG_MAX_RETRIES; attempt++) { ++ if (!cert_sign_once(x, ikey, md_alg)) { ++ goto END; ++ } ++ if (fb == 0 || cert_sig_full_width(x, fb)) { ++ break; ++ } ++ } ++ if (attempt == ECDSA_SIG_MAX_RETRIES) { ++ ERROR("%s: no full-width ECDSA signature after %d attempts\n", ++ cert->cn, ECDSA_SIG_MAX_RETRIES); ++ goto END; + } +- } +- if (attempt == ECDSA_SIG_MAX_RETRIES) { +- ERROR("%s: no full-width ECDSA signature after %d attempts\n", +- cert->cn, ECDSA_SIG_MAX_RETRIES); +- goto END; + } + + /* X509 certificate signed successfully */ +--- /dev/null ++++ b/tools/cert_create/include/deterministic_ecdsa.h +@@ -0,0 +1,16 @@ ++/* ++ * SPDX-License-Identifier: BSD-3-Clause ++ */ ++ ++#ifndef DETERMINISTIC_ECDSA_H ++#define DETERMINISTIC_ECDSA_H ++ ++#include <openssl/ec.h> ++#include <openssl/evp.h> ++ ++ECDSA_SIG *ecdsa_sign_rfc6979(const unsigned char *digest, ++ unsigned int digest_len, const EVP_MD *md, ++ const EC_KEY *key, int full_width, ++ int max_attempts); ++ ++#endif /* DETERMINISTIC_ECDSA_H */ +--- /dev/null ++++ b/tools/cert_create/src/deterministic_ecdsa.c +@@ -0,0 +1,268 @@ ++/* ++ * Deterministic ECDSA nonce generation per RFC 6979, section 3.2. ++ * ++ * SPDX-License-Identifier: BSD-3-Clause ++ */ ++ ++#include <string.h> ++ ++#include <openssl/bn.h> ++#include <openssl/crypto.h> ++#include <openssl/hmac.h> ++ ++#include "deterministic_ecdsa.h" ++ ++/* Large enough for the P-521 and Brainpool P-512 curves supported by TF-A. */ ++#define MAX_ORDER_BYTES 72 ++ ++struct nonce_state { ++ const EVP_MD *md; ++ int hlen; ++ int qbits; ++ int rolen; ++ unsigned char k[EVP_MAX_MD_SIZE]; ++ unsigned char v[EVP_MAX_MD_SIZE]; ++}; ++ ++/* Compute through a temporary buffer, so key, input and output may alias. */ ++static int hmac_step(struct nonce_state *st, const unsigned char *data, ++ size_t data_len, unsigned char *out) ++{ ++ unsigned char result[EVP_MAX_MD_SIZE]; ++ unsigned int result_len = 0; ++ int ok; ++ ++ ok = HMAC(st->md, st->k, st->hlen, data, data_len, result, ++ &result_len) != NULL && result_len == (unsigned int)st->hlen; ++ if (ok) { ++ memcpy(out, result, st->hlen); ++ } ++ OPENSSL_cleanse(result, sizeof(result)); ++ return ok; ++} ++ ++static int nonce_init(struct nonce_state *st, const EVP_MD *md, ++ const BIGNUM *priv, const BIGNUM *hash, ++ int qbits, int rolen) ++{ ++ unsigned char seed[EVP_MAX_MD_SIZE + 1 + 2 * MAX_ORDER_BYTES]; ++ size_t seed_len; ++ int ok = 0; ++ ++ st->md = md; ++ st->hlen = EVP_MD_size(md); ++ st->qbits = qbits; ++ st->rolen = rolen; ++ if (st->hlen <= 0 || st->hlen > EVP_MAX_MD_SIZE || ++ rolen <= 0 || rolen > MAX_ORDER_BYTES) { ++ return 0; ++ } ++ ++ memset(st->k, 0, st->hlen); ++ memset(st->v, 1, st->hlen); ++ seed_len = st->hlen + 1 + 2 * rolen; ++ memcpy(seed, st->v, st->hlen); ++ if (BN_bn2binpad(priv, seed + st->hlen + 1, rolen) != rolen || ++ BN_bn2binpad(hash, seed + st->hlen + 1 + rolen, rolen) != rolen) { ++ goto out; ++ } ++ ++ seed[st->hlen] = 0; ++ if (!hmac_step(st, seed, seed_len, st->k) || ++ !hmac_step(st, st->v, st->hlen, st->v)) { ++ goto out; ++ } ++ memcpy(seed, st->v, st->hlen); ++ seed[st->hlen] = 1; ++ if (!hmac_step(st, seed, seed_len, st->k) || ++ !hmac_step(st, st->v, st->hlen, st->v)) { ++ goto out; ++ } ++ ok = 1; ++out: ++ OPENSSL_cleanse(seed, sizeof(seed)); ++ return ok; ++} ++ ++/* RFC 6979 step h.2: extend T with successive HMAC_DRBG output blocks. */ ++static int nonce_next(struct nonce_state *st, BIGNUM *nonce) ++{ ++ unsigned char t[MAX_ORDER_BYTES]; ++ int used = 0; ++ int take; ++ int ok = 0; ++ ++ while (used < st->rolen) { ++ if (!hmac_step(st, st->v, st->hlen, st->v)) { ++ goto out; ++ } ++ take = st->rolen - used; ++ if (take > st->hlen) { ++ take = st->hlen; ++ } ++ memcpy(t + used, st->v, take); ++ used += take; ++ } ++ if (BN_bin2bn(t, st->rolen, nonce) == NULL) { ++ goto out; ++ } ++ if (8 * st->rolen > st->qbits && ++ !BN_rshift(nonce, nonce, 8 * st->rolen - st->qbits)) { ++ goto out; ++ } ++ ok = 1; ++out: ++ OPENSSL_cleanse(t, sizeof(t)); ++ return ok; ++} ++ ++/* RFC 6979 step h.3: advance the state after rejecting a candidate. */ ++static int nonce_reject(struct nonce_state *st) ++{ ++ unsigned char input[EVP_MAX_MD_SIZE + 1]; ++ ++ memcpy(input, st->v, st->hlen); ++ input[st->hlen] = 0; ++ if (!hmac_step(st, input, st->hlen + 1, st->k) || ++ !hmac_step(st, st->v, st->hlen, st->v)) { ++ return 0; ++ } ++ return 1; ++} ++ ++ECDSA_SIG *ecdsa_sign_rfc6979(const unsigned char *digest, ++ unsigned int digest_len, const EVP_MD *md, ++ const EC_KEY *key, int full_width, ++ int max_attempts) ++{ ++ const EC_GROUP *group = EC_KEY_get0_group(key); ++ const BIGNUM *priv = EC_KEY_get0_private_key(key); ++ BIGNUM *order = NULL; ++ struct nonce_state st; ++ BN_CTX *ctx = NULL; ++ EC_POINT *point = NULL; ++ BIGNUM *hash = NULL, *nonce = NULL, *x = NULL, *r = NULL, *s = NULL; ++ BIGNUM *kinv = NULL, *blind = NULL, *blind_inv = NULL; ++ BIGNUM *a = NULL, *b = NULL, *r_copy = NULL, *s_copy = NULL; ++ ECDSA_SIG *sig = NULL; ++ int qbits, rolen, attempt; ++ ++ if (group == NULL || priv == NULL || md == NULL || digest == NULL) { ++ return NULL; ++ } ++ qbits = EC_GROUP_order_bits(group); ++ rolen = (qbits + 7) / 8; ++ if (qbits < 80 || rolen > MAX_ORDER_BYTES || ++ digest_len > EVP_MAX_MD_SIZE || max_attempts <= 0) { ++ return NULL; ++ } ++ ++ ctx = BN_CTX_new(); ++ order = BN_new(); ++ point = EC_POINT_new(group); ++ hash = BN_bin2bn(digest, digest_len, NULL); ++ nonce = BN_new(); ++ x = BN_new(); ++ r = BN_new(); ++ s = BN_new(); ++ kinv = BN_new(); ++ blind = BN_new(); ++ blind_inv = BN_new(); ++ a = BN_new(); ++ b = BN_new(); ++ if (ctx == NULL || order == NULL || point == NULL || hash == NULL || ++ nonce == NULL || ++ x == NULL || r == NULL || s == NULL || kinv == NULL || ++ blind == NULL || blind_inv == NULL || a == NULL || b == NULL) { ++ goto out; ++ } ++ if (!EC_GROUP_get_order(group, order, ctx)) { ++ goto out; ++ } ++ ++ /* bits2int followed by bits2octets for RFC 6979 initialization. */ ++ if (8 * digest_len > (unsigned int)qbits && ++ !BN_rshift(hash, hash, 8 * digest_len - qbits)) { ++ goto out; ++ } ++ if (!BN_nnmod(hash, hash, order, ctx) || ++ !nonce_init(&st, md, priv, hash, qbits, rolen)) { ++ goto out; ++ } ++ ++ for (attempt = 0; attempt < max_attempts; attempt++) { ++ if (!nonce_next(&st, nonce)) { ++ goto out; ++ } ++ if (BN_is_zero(nonce) || BN_cmp(nonce, order) >= 0) { ++ goto reject; ++ } ++ BN_set_flags(nonce, BN_FLG_CONSTTIME); ++ if (!EC_POINT_mul(group, point, nonce, NULL, NULL, ctx) || ++ !EC_POINT_get_affine_coordinates(group, point, x, NULL, ctx) || ++ !BN_nnmod(r, x, order, ctx)) { ++ goto out; ++ } ++ if (BN_is_zero(r) || (full_width && BN_num_bytes(r) != full_width)) { ++ goto reject; ++ } ++ if (BN_mod_inverse(kinv, nonce, order, ctx) == NULL) { ++ goto out; ++ } ++ ++ /* Random blinding protects private-key arithmetic; it cancels out. */ ++ do { ++ if (!BN_rand_range(blind, order)) { ++ goto out; ++ } ++ } while (BN_is_zero(blind)); ++ BN_set_flags(blind, BN_FLG_CONSTTIME); ++ if (BN_mod_inverse(blind_inv, blind, order, ctx) == NULL || ++ !BN_mod_mul(a, blind, hash, order, ctx) || ++ !BN_mod_mul(b, blind, priv, order, ctx) || ++ !BN_mod_mul(b, b, r, order, ctx) || ++ !BN_mod_add(s, a, b, order, ctx) || ++ !BN_mod_mul(s, s, kinv, order, ctx) || ++ !BN_mod_mul(s, s, blind_inv, order, ctx)) { ++ goto out; ++ } ++ if (BN_is_zero(s) || (full_width && BN_num_bytes(s) != full_width)) { ++ goto reject; ++ } ++ ++ sig = ECDSA_SIG_new(); ++ r_copy = BN_dup(r); ++ s_copy = BN_dup(s); ++ if (sig == NULL || r_copy == NULL || s_copy == NULL || ++ !ECDSA_SIG_set0(sig, r_copy, s_copy)) { ++ ECDSA_SIG_free(sig); ++ sig = NULL; ++ goto out; ++ } ++ r_copy = NULL; ++ s_copy = NULL; ++ break; ++reject: ++ if (!nonce_reject(&st)) { ++ goto out; ++ } ++ } ++out: ++ OPENSSL_cleanse(&st, sizeof(st)); ++ BN_free(r_copy); ++ BN_free(s_copy); ++ BN_clear_free(hash); ++ BN_clear_free(nonce); ++ BN_clear_free(x); ++ BN_clear_free(r); ++ BN_clear_free(s); ++ BN_clear_free(kinv); ++ BN_clear_free(blind); ++ BN_clear_free(blind_inv); ++ BN_clear_free(a); ++ BN_clear_free(b); ++ BN_free(order); ++ EC_POINT_free(point); ++ BN_CTX_free(ctx); ++ return sig; ++} diff --git a/package/boot/arm-trusted-firmware-microchipsw/src/keys/README b/package/boot/arm-trusted-firmware-microchipsw/src/keys/README index c5ff6b9438..97e9142870 100644 --- a/package/boot/arm-trusted-firmware-microchipsw/src/keys/README +++ b/package/boot/arm-trusted-firmware-microchipsw/src/keys/README @@ -11,5 +11,5 @@ containing the same filenames via TFA_KEY_DIR. The root key's public X and Y coordinates must have no leading zero byte because of ERR-LAN969X-001. The package passes SOURCE_DATE_EPOCH to cert_create for stable certificate -validity and serials. ECDSA signatures are still random, so the FIP is not -yet reproducible. +validity, serials, and deterministic ECDSA signatures. Other inputs to the +FIP must also remain stable for reproducible builds. |