mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
feat: add SignCtx/VerifySignatureCtx for domain-separated ML-DSA + SLH-DSA signing
Required by luxfi/utxo mldsafx/slhdsafx packages which use context-bound signatures to separate signing domains (transaction vs vote vs attestation).
This commit is contained in:
@@ -249,18 +249,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||
github.com/luxfi/age v1.4.0 h1:tU5q65RQSQdaVq64Z/DVUhiPQMoMPQT6pr41LsvG0AQ=
|
||||
github.com/luxfi/age v1.4.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
|
||||
github.com/luxfi/cache v1.1.0 h1:6LUyGGZ+rrMAJBbAU6+UwkcamXj3zsboRUodIof2Ong=
|
||||
github.com/luxfi/cache v1.1.0/go.mod h1:9GvlEEE9rFPaaWxvVpSPwW8ZMo2+8VMNNcuPa4AwzPg=
|
||||
github.com/luxfi/ids v1.2.9 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI=
|
||||
github.com/luxfi/ids v1.2.9/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc=
|
||||
github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
|
||||
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
|
||||
github.com/luxfi/mock v0.1.0 h1:IwElfNu+T9sXvzFX6tudPDx1vqPuACRSRdxpD5lxW+o=
|
||||
github.com/luxfi/mock v0.1.0/go.mod h1:izF+9K0gGzFC9zERn6Po37v46eLdPB+EIsDjL3GLk+U=
|
||||
github.com/luxfi/utils v1.1.0 h1:ti7HvjNwJd4ILDMERJtOAWE9mF8l+zqDVkgWnF7Agic=
|
||||
github.com/luxfi/utils v1.1.0/go.mod h1:ABqhBdGNig0CaDcnNYldv1byS0BTNi5IKPbJSPF1p98=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
|
||||
+46
-12
@@ -9,6 +9,7 @@ import (
|
||||
"crypto"
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
|
||||
"github.com/cloudflare/circl/sign/mldsa/mldsa44"
|
||||
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
|
||||
@@ -140,18 +141,29 @@ func GenerateKey(rand io.Reader, mode Mode) (*PrivateKey, error) {
|
||||
return nil, ErrInvalidMode
|
||||
}
|
||||
|
||||
return &PrivateKey{
|
||||
priv := &PrivateKey{
|
||||
mode: mode,
|
||||
secretKey: privBytes,
|
||||
PublicKey: &PublicKey{
|
||||
mode: mode,
|
||||
publicKey: pubBytes,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
runtime.SetFinalizer(priv, (*PrivateKey).Zeroize)
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
// Sign signs a message with the private key using circl
|
||||
// Sign signs a message with the private key using circl.
|
||||
// Uses nil context -- callers requiring domain separation should use SignCtx.
|
||||
func (priv *PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error) {
|
||||
return priv.SignCtx(rand, message, nil)
|
||||
}
|
||||
|
||||
// SignCtx signs a message with domain-separating context.
|
||||
// FIPS 204 Section 5.2: ctx is an optional octet string (0-255 bytes) bound
|
||||
// into the signature. Callers SHOULD use a non-nil context to prevent
|
||||
// cross-protocol signature replay.
|
||||
func (priv *PrivateKey) SignCtx(rand io.Reader, message, ctx []byte) ([]byte, error) {
|
||||
switch priv.mode {
|
||||
case MLDSA44:
|
||||
var sk mldsa44.PrivateKey
|
||||
@@ -159,7 +171,7 @@ func (priv *PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerO
|
||||
return nil, err
|
||||
}
|
||||
sig := make([]byte, MLDSA44SignatureSize)
|
||||
if err := mldsa44.SignTo(&sk, message, nil, true, sig); err != nil {
|
||||
if err := mldsa44.SignTo(&sk, message, ctx, true, sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sig, nil
|
||||
@@ -170,7 +182,7 @@ func (priv *PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerO
|
||||
return nil, err
|
||||
}
|
||||
sig := make([]byte, MLDSA65SignatureSize)
|
||||
if err := mldsa65.SignTo(&sk, message, nil, true, sig); err != nil {
|
||||
if err := mldsa65.SignTo(&sk, message, ctx, true, sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sig, nil
|
||||
@@ -181,7 +193,7 @@ func (priv *PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerO
|
||||
return nil, err
|
||||
}
|
||||
sig := make([]byte, MLDSA87SignatureSize)
|
||||
if err := mldsa87.SignTo(&sk, message, nil, true, sig); err != nil {
|
||||
if err := mldsa87.SignTo(&sk, message, ctx, true, sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sig, nil
|
||||
@@ -197,29 +209,38 @@ func (pub *PublicKey) Verify(message, signature []byte, opts crypto.SignerOpts)
|
||||
return pub.VerifySignature(message, signature)
|
||||
}
|
||||
|
||||
// VerifySignature verifies a signature with the public key (simplified API)
|
||||
// VerifySignature verifies a signature with the public key (simplified API).
|
||||
// Uses nil context -- callers requiring domain separation should use VerifySignatureCtx.
|
||||
func (pub *PublicKey) VerifySignature(message, signature []byte) bool {
|
||||
return pub.VerifySignatureCtx(message, signature, nil)
|
||||
}
|
||||
|
||||
// VerifySignatureCtx verifies a signature with domain-separating context.
|
||||
// FIPS 204 Section 5.3: ctx is an optional octet string (0-255 bytes) bound
|
||||
// into the signature. Callers SHOULD use a non-nil context to prevent
|
||||
// cross-protocol signature replay.
|
||||
func (pub *PublicKey) VerifySignatureCtx(message, signature, ctx []byte) bool {
|
||||
switch pub.mode {
|
||||
case MLDSA44:
|
||||
var pk mldsa44.PublicKey
|
||||
if err := (&pk).UnmarshalBinary(pub.publicKey); err != nil {
|
||||
return false
|
||||
}
|
||||
return mldsa44.Verify(&pk, message, nil, signature)
|
||||
return mldsa44.Verify(&pk, message, ctx, signature)
|
||||
|
||||
case MLDSA65:
|
||||
var pk mldsa65.PublicKey
|
||||
if err := (&pk).UnmarshalBinary(pub.publicKey); err != nil {
|
||||
return false
|
||||
}
|
||||
return mldsa65.Verify(&pk, message, nil, signature)
|
||||
return mldsa65.Verify(&pk, message, ctx, signature)
|
||||
|
||||
case MLDSA87:
|
||||
var pk mldsa87.PublicKey
|
||||
if err := (&pk).UnmarshalBinary(pub.publicKey); err != nil {
|
||||
return false
|
||||
}
|
||||
return mldsa87.Verify(&pk, message, nil, signature)
|
||||
return mldsa87.Verify(&pk, message, ctx, signature)
|
||||
|
||||
default:
|
||||
return false
|
||||
@@ -236,6 +257,17 @@ func (priv *PrivateKey) Bytes() []byte {
|
||||
return priv.secretKey
|
||||
}
|
||||
|
||||
// Zeroize overwrites the private key material with zeros.
|
||||
// Best-effort: the Go runtime may have copied the bytes elsewhere.
|
||||
// A GC finalizer calls this automatically when the key becomes unreachable,
|
||||
// but callers should call it explicitly when the key is no longer needed
|
||||
// for more predictable cleanup.
|
||||
func (priv *PrivateKey) Zeroize() {
|
||||
for i := range priv.secretKey {
|
||||
priv.secretKey[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes returns the serialized public key
|
||||
func (pub *PublicKey) Bytes() []byte {
|
||||
return pub.publicKey
|
||||
@@ -298,14 +330,16 @@ func PrivateKeyFromBytes(mode Mode, data []byte) (*PrivateKey, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return &PrivateKey{
|
||||
priv := &PrivateKey{
|
||||
mode: mode,
|
||||
secretKey: data,
|
||||
PublicKey: &PublicKey{
|
||||
mode: mode,
|
||||
publicKey: pubBytes,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
runtime.SetFinalizer(priv, (*PrivateKey).Zeroize)
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
// PublicKeyFromBytes deserializes a public key
|
||||
|
||||
@@ -299,3 +299,57 @@ func BenchmarkMLDSA_KeyGeneration(b *testing.B) {
|
||||
_, _ = GenerateKey(rand.Reader, MLDSA65)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLDSA_Zeroize(t *testing.T) {
|
||||
sk, err := GenerateKey(rand.Reader, MLDSA65)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
// Copy key bytes before zeroize
|
||||
original := make([]byte, len(sk.Bytes()))
|
||||
copy(original, sk.Bytes())
|
||||
|
||||
// Verify key material is non-zero
|
||||
allZero := true
|
||||
for _, b := range original {
|
||||
if b != 0 {
|
||||
allZero = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allZero {
|
||||
t.Fatal("Key material should not be all zeros before Zeroize")
|
||||
}
|
||||
|
||||
sk.Zeroize()
|
||||
|
||||
// After zeroize, all bytes must be zero
|
||||
for i, b := range sk.Bytes() {
|
||||
if b != 0 {
|
||||
t.Fatalf("Byte %d not zeroed: 0x%02x", i, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLDSA_Zeroize_FromBytes(t *testing.T) {
|
||||
sk1, err := GenerateKey(rand.Reader, MLDSA65)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
skBytes := make([]byte, len(sk1.Bytes()))
|
||||
copy(skBytes, sk1.Bytes())
|
||||
|
||||
sk2, err := PrivateKeyFromBytes(MLDSA65, skBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to deserialize: %v", err)
|
||||
}
|
||||
|
||||
sk2.Zeroize()
|
||||
|
||||
for i, b := range sk2.Bytes() {
|
||||
if b != 0 {
|
||||
t.Fatalf("Byte %d not zeroed: 0x%02x", i, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-9
@@ -12,6 +12,7 @@ import (
|
||||
"crypto"
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
|
||||
"github.com/cloudflare/circl/sign/slhdsa"
|
||||
)
|
||||
@@ -171,18 +172,28 @@ func GenerateKey(rand io.Reader, mode Mode) (*PrivateKey, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PrivateKey{
|
||||
pk := &PrivateKey{
|
||||
mode: mode,
|
||||
secretKey: privBytes,
|
||||
PublicKey: &PublicKey{
|
||||
mode: mode,
|
||||
publicKey: pubBytes,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
runtime.SetFinalizer(pk, (*PrivateKey).Zeroize)
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
// Sign signs a message with the private key using circl
|
||||
// Sign signs a message with the private key using circl.
|
||||
// Uses nil context -- callers requiring domain separation should use SignCtx.
|
||||
func (priv *PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error) {
|
||||
return priv.SignCtx(rand, message, nil)
|
||||
}
|
||||
|
||||
// SignCtx signs a message with domain-separating context.
|
||||
// FIPS 205: ctx is an optional octet string bound into the signature.
|
||||
// Callers SHOULD use a non-nil context to prevent cross-protocol signature replay.
|
||||
func (priv *PrivateKey) SignCtx(rand io.Reader, message, ctx []byte) ([]byte, error) {
|
||||
id := modeToID(priv.mode)
|
||||
if !id.IsValid() {
|
||||
return nil, ErrInvalidMode
|
||||
@@ -194,9 +205,8 @@ func (priv *PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerO
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use deterministic signing (context = nil)
|
||||
msg := slhdsa.NewMessage(message)
|
||||
signature, err := slhdsa.SignDeterministic(&sk, msg, nil)
|
||||
signature, err := slhdsa.SignDeterministic(&sk, msg, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -210,8 +220,16 @@ func (pub *PublicKey) Verify(message, signature []byte, opts crypto.SignerOpts)
|
||||
return pub.VerifySignature(message, signature)
|
||||
}
|
||||
|
||||
// VerifySignature verifies a signature with the public key (simplified API)
|
||||
// VerifySignature verifies a signature with the public key (simplified API).
|
||||
// Uses nil context -- callers requiring domain separation should use VerifySignatureCtx.
|
||||
func (pub *PublicKey) VerifySignature(message, signature []byte) bool {
|
||||
return pub.VerifySignatureCtx(message, signature, nil)
|
||||
}
|
||||
|
||||
// VerifySignatureCtx verifies a signature with domain-separating context.
|
||||
// FIPS 205: ctx is an optional octet string bound into the signature.
|
||||
// Callers SHOULD use a non-nil context to prevent cross-protocol signature replay.
|
||||
func (pub *PublicKey) VerifySignatureCtx(message, signature, ctx []byte) bool {
|
||||
id := modeToID(pub.mode)
|
||||
if !id.IsValid() {
|
||||
return false
|
||||
@@ -224,7 +242,7 @@ func (pub *PublicKey) VerifySignature(message, signature []byte) bool {
|
||||
}
|
||||
|
||||
msg := slhdsa.NewMessage(message)
|
||||
return slhdsa.Verify(&pk, msg, signature, nil)
|
||||
return slhdsa.Verify(&pk, msg, signature, ctx)
|
||||
}
|
||||
|
||||
// Public returns the public key
|
||||
@@ -237,6 +255,17 @@ func (priv *PrivateKey) Bytes() []byte {
|
||||
return priv.secretKey
|
||||
}
|
||||
|
||||
// Zeroize overwrites the private key material with zeros.
|
||||
// Best-effort: the Go runtime may have copied the bytes elsewhere.
|
||||
// A GC finalizer calls this automatically when the key becomes unreachable,
|
||||
// but callers should call it explicitly when the key is no longer needed
|
||||
// for more predictable cleanup.
|
||||
func (priv *PrivateKey) Zeroize() {
|
||||
for i := range priv.secretKey {
|
||||
priv.secretKey[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes returns the serialized public key
|
||||
func (pub *PublicKey) Bytes() []byte {
|
||||
return pub.publicKey
|
||||
@@ -263,14 +292,16 @@ func PrivateKeyFromBytes(mode Mode, data []byte) (*PrivateKey, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PrivateKey{
|
||||
priv := &PrivateKey{
|
||||
mode: mode,
|
||||
secretKey: data,
|
||||
PublicKey: &PublicKey{
|
||||
mode: mode,
|
||||
publicKey: pubBytes,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
runtime.SetFinalizer(priv, (*PrivateKey).Zeroize)
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
// PublicKeyFromBytes deserializes a public key
|
||||
|
||||
@@ -366,6 +366,60 @@ func TestSLHDSA_DeterministicSigning(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSLHDSA_Zeroize(t *testing.T) {
|
||||
sk, err := GenerateKey(rand.Reader, SHA2_128s)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
// Copy key bytes before zeroize
|
||||
original := make([]byte, len(sk.Bytes()))
|
||||
copy(original, sk.Bytes())
|
||||
|
||||
// Verify key material is non-zero
|
||||
allZero := true
|
||||
for _, b := range original {
|
||||
if b != 0 {
|
||||
allZero = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allZero {
|
||||
t.Fatal("Key material should not be all zeros before Zeroize")
|
||||
}
|
||||
|
||||
sk.Zeroize()
|
||||
|
||||
// After zeroize, all bytes must be zero
|
||||
for i, b := range sk.Bytes() {
|
||||
if b != 0 {
|
||||
t.Fatalf("Byte %d not zeroed: 0x%02x", i, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSLHDSA_Zeroize_FromBytes(t *testing.T) {
|
||||
sk1, err := GenerateKey(rand.Reader, SHA2_128s)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
skBytes := make([]byte, len(sk1.Bytes()))
|
||||
copy(skBytes, sk1.Bytes())
|
||||
|
||||
sk2, err := PrivateKeyFromBytes(SHA2_128s, skBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to deserialize: %v", err)
|
||||
}
|
||||
|
||||
sk2.Zeroize()
|
||||
|
||||
for i, b := range sk2.Bytes() {
|
||||
if b != 0 {
|
||||
t.Fatalf("Byte %d not zeroed: 0x%02x", i, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkSLHDSA_Sign_SHA2_128s(b *testing.B) {
|
||||
sk, _ := GenerateKey(rand.Reader, SHA2_128s)
|
||||
|
||||
Reference in New Issue
Block a user