the identity parses its key once, and says why in terms of the code

Parsing is the expensive half and the result is immutable, so a per-call parse
buys nothing; holding it also means the key is zeroed once, at Wipe. The
comments explaining that were a walkthrough of a library defect instead, which
is both more than a reader needs and — since crypto v1.20.5 makes the
constructor own its copy — no longer true.

crypto v1.20.5 comes with it, so this module gets the owning constructor too.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-15 01:13:57 -07:00
co-authored by Hanzo Dev
parent 250d1cdf60
commit 65d1fd2fcd
3 changed files with 12 additions and 20 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.26.4
require (
github.com/luxfi/address v1.1.1
github.com/luxfi/constants v1.6.2
github.com/luxfi/crypto v1.20.2
github.com/luxfi/crypto v1.20.5
github.com/luxfi/go-bip32 v1.1.0
github.com/luxfi/go-bip39 v1.2.0
github.com/luxfi/ids v1.3.2
+2
View File
@@ -77,6 +77,8 @@ github.com/luxfi/container v0.2.1 h1:MTnfKXzS5+oxV5jKZerdOxSA6iMPaQI9/FWGufizzaw
github.com/luxfi/container v0.2.1/go.mod h1:B+uM0wP0lGvt/SSK7QOEn/qBcsHzILVHlKikdCyzSgM=
github.com/luxfi/crypto v1.20.2 h1:L81WEsU/hs2A76F5PWBusG0yU74QqkDdUqqgexWUxh4=
github.com/luxfi/crypto v1.20.2/go.mod h1:qYHOM0lO4PRh7LEaObxFQUIMjmT1/paVm/WgZkobT1k=
github.com/luxfi/crypto v1.20.5 h1:kL69Ewyr90A/wQsTnIK8Zj6cHUPYvM1WHhgSErc2Yh0=
github.com/luxfi/crypto v1.20.5/go.mod h1:gic3n9puSJhZFonBfmIwc5Cck/SMqetxpcfzrk9gna8=
github.com/luxfi/geth v1.20.1 h1:QUGQr4AKvADjwMi7t8a0OfoyxShgEcI9pwie1jFYfm0=
github.com/luxfi/geth v1.20.1/go.mod h1:GV5bIMEgWviRN+jPXERyVpI16H3iHqPcdIokDoZdrvU=
github.com/luxfi/go-bip32 v1.1.0 h1:zjy19WKa1KJnGamRNyOZM3l/MPtV/sax7M4NMPwLHZQ=
+9 -19
View File
@@ -165,17 +165,12 @@ type ServiceIdentity struct {
// sentinel.
privateKey []byte
// signer is the parsed ML-DSA-65 private key, cached ONCE at
// construction. Sign() reuses it instead of re-parsing s.privateKey
// on every call. This is deliberate and load-bearing: mldsa.
// PrivateKeyFromBytes stores its input slice by reference
// (secretKey: data) and arms a GC finalizer that Zeroize()s it. A
// per-call transient parse therefore ALIASES a byte slice whose
// finalizer, once the transient goes unreachable, zeroes the shared
// key mid-run — corrupting the next Sign (bites the CUSTODY+Save path
// that signs two envelopes per boot). Holding the parsed key on the
// struct keeps it reachable for the identity's whole lifetime, so the
// finalizer only fires at Wipe/teardown. Never exposed via a getter.
// signer is the parsed ML-DSA-65 private key. The identity parses once,
// at construction, and every Sign uses it: parsing is the expensive part
// and the result is immutable, so doing it per call buys nothing. Holding
// it for the identity's lifetime also means the key is zeroed exactly
// once, at Wipe, rather than whenever a transient happens to be
// collected. Never exposed through a getter.
signer *mldsa.PrivateKey
}
@@ -250,9 +245,8 @@ func NewServiceIdentity(mnemonic, servicePath string) (*ServiceIdentity, error)
FullDigest: full,
PublicKey: pubBytes,
privateKey: privBytes,
// Cache the freshly-generated key as the signer. Its secretKey is
// independent of privBytes (privBytes is a copy), and holding it on
// the struct keeps its Zeroize finalizer dormant until Wipe.
// The generated key is the signer; privBytes is its serialized form,
// kept for Save and wiped alongside it.
signer: priv,
}, nil
}
@@ -276,11 +270,7 @@ func (s *ServiceIdentity) Sign(envelope []byte) ([]byte, error) {
return nil, errors.New("keys: service identity is empty (wiped?)")
}
digest := envelopeDigest(s.FullDigest, envelope)
// Reuse the cached signer — do NOT re-parse s.privateKey here. A
// per-call PrivateKeyFromBytes builds a transient that aliases
// s.privateKey and arms a Zeroize finalizer; once the transient goes
// unreachable the finalizer zeroes the shared key mid-run. See the
// signer field doc. circl reads its own randomness for the FIPS 204
// circl reads its own randomness for the FIPS 204
// §5.2 hedged sign; the EnvelopeDomain context binds the signature to
// this envelope shape so a cross-protocol replay of the same key
// against a different shape rejects.