Question-shaped report, not a vulnerability claim. Reading this alongside NewKeyFromSeed (same file, :240), the most plausible explanation is an intentional micro-optimization — avoiding a pkEncode + SHAKE256(·, 64) on every Public() call. A one-line rationale in a comment would give future auditors something concrete to cite. If you'd prefer to address it, the fix is a one-line value copy (pk.tr = sk.tr) — happy to send a CL.
Observation
sign/mldsa/mldsa65/internal/dilithium.go:474-485:
func (sk *PrivateKey) Public() *PublicKey {
var t0 VecK
pk := &PublicKey{
rho: sk.rho,
A: &sk.A,
tr: &sk.tr, // pointer-shared with sk.tr
}
sk.computeT0andT1(&t0, &pk.t1)
pk.t1.PackT1(pk.t1p[:])
return pk
}
PublicKey.UnmarshalBinary at :115-128 recomputes tr = SHAKE256(pk_bytes, 64) honestly. So the same pk has two tr states: one from Public() (aliased to sk.tr), one from Pack → UnmarshalBinary (recomputed). Signatures produced under a mutated sk.tr verify against the former, fail against the latter.
Spec reading
FIPS 204 Alg. 6 step 9 defines tr ← H(pkEncode(ρ, t1), 64) — a computed value. The SK-stored copy is a cache. Aliasing lets any divergence in sk.tr (including #581's unvalidated parse path) silently shape the derived pk.
Reach
(*PrivateKey).Public() is crypto.Signer's public interface.
What would be useful
Any of:
- "Intentional — here's why" (most likely; close WAI, comment welcome).
- "Worth tightening — patch welcome" (one-line
pk.tr = sk.tr value copy).
- "Reproducer first" (reproducer available).
Observation
sign/mldsa/mldsa65/internal/dilithium.go:474-485:PublicKey.UnmarshalBinaryat:115-128recomputestr = SHAKE256(pk_bytes, 64)honestly. So the same pk has twotrstates: one fromPublic()(aliased tosk.tr), one fromPack → UnmarshalBinary(recomputed). Signatures produced under a mutatedsk.trverify against the former, fail against the latter.Spec reading
FIPS 204 Alg. 6 step 9 defines
tr ← H(pkEncode(ρ, t1), 64)— a computed value. The SK-stored copy is a cache. Aliasing lets any divergence insk.tr(including #581's unvalidated parse path) silently shape the derived pk.Reach
(*PrivateKey).Public()iscrypto.Signer's public interface.What would be useful
Any of:
pk.tr = sk.trvalue copy).