diff --git a/go.mod b/go.mod index 969b9c9..849cde8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/transparency-dev/formats go 1.25.0 require ( + filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e github.com/google/go-cmp v0.7.0 + golang.org/x/crypto v0.50.0 golang.org/x/mod v0.35.0 ) diff --git a/go.sum b/go.sum index 51cc1dd..e2e27d7 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,8 @@ +filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e h1:VsUbObBMxXlc23Eb9VeeJYE4jvTs87qa5RqSN2U5FJU= +filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e/go.mod h1:32qQ5yj3R24Eu03iWFWchdC3OB653wPvoepWejkefbY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= diff --git a/note/note_cosigv1.go b/note/note_cosigv1.go index a60b666..48a6309 100644 --- a/note/note_cosigv1.go +++ b/note/note_cosigv1.go @@ -12,12 +12,15 @@ import ( "encoding/binary" "errors" "fmt" - "strings" "strconv" + "strings" "time" "unicode" "unicode/utf8" + "filippo.io/mldsa" + "github.com/transparency-dev/formats/log" + "golang.org/x/crypto/cryptobyte" "golang.org/x/mod/sumdb/note" ) @@ -26,6 +29,7 @@ const ( algECDSAWithSHA256 = 2 algEd25519CosignatureV1 = 4 algRFC6962STH = 5 + algMLDSA44 = 6 ) const ( @@ -33,11 +37,110 @@ const ( timestampSize = 8 ) +// NewMLDSASigner returns a signer for MLDSA cosignature v1. +func NewMLDSASigner(skey string) (*SubtreeSigner, error) { + priv1, skey, _ := strings.Cut(skey, "+") + priv2, skey, _ := strings.Cut(skey, "+") + name, skey, _ := strings.Cut(skey, "+") + hash16, key64, _ := strings.Cut(skey, "+") + key, err := base64.StdEncoding.DecodeString(key64) + if priv1 != "PRIVATE" || priv2 != "KEY" || len(hash16) != 8 || err != nil || !isValidName(name) || len(key) == 0 { + return nil, errSignerID + } + alg, key := key[0], key[1:] + if alg != algMLDSA44 { + return nil, errSignerID + } + return newMLDSASigner(name, key) +} + +// newMLDSASigner returns a signer for MLDSA cosignature v1, with the provided +// name and key bytes in the format: algo || private key. +func newMLDSASigner(name string, keyBytes []byte) (*SubtreeSigner, error) { + s := &SubtreeSigner{name: name} + if len(keyBytes) != mldsa.PrivateKeySize { + return nil, errSignerID + } + key, err := mldsa.NewPrivateKey(mldsa.MLDSA44(), keyBytes) + if err != nil { + return nil, err + } + pubKey := key.PublicKey() + pubKeyBytes := append([]byte{algMLDSA44}, pubKey.Bytes()...) + s.hash = keyHashMLDSA(name, pubKeyBytes) + s.signNote = func(msg []byte) ([]byte, error) { + t := uint64(time.Now().Unix()) + c := &log.Checkpoint{} + if _, err := c.Unmarshal(msg); err != nil { + return nil, err + } + return s.signSubtree(t, c.Origin, 0, c.Size, c.Hash) + } + s.signSubtree = func(timestamp uint64, logOrigin string, start, end uint64, root []byte) ([]byte, error) { + m, err := formatMLDSACosignatureV1(name, timestamp, logOrigin, start, end, root) + if err != nil { + return nil, err + } + sB, err := key.Sign(nil, m, nil) + if err != nil { + return nil, err + } + // The signature itself is encoded as timestamp || signature. + sig := make([]byte, 0, timestampSize+mldsa.MLDSA44SignatureSize) + sig = binary.BigEndian.AppendUint64(sig, timestamp) + sig = append(sig, sB...) + return sig, nil + } + s.verifier = &SubtreeVerifier{ + name: name, + keyHash: s.hash, + verifyNote: func(msg, sig []byte) bool { return verifyMLDSACosigV1(pubKey, name)(msg, sig) }, + verifySubtree: func(timestamp uint64, logOrigin string, start, end uint64, hash []byte, sig []byte) bool { + return verifyMLDSACosigV1Subtree(pubKey, name)(logOrigin, start, end, hash, sig) + }, + } + + return s, nil +} + +// NewMLDSAVerifier constructs a verifier for MLDSA cosignature v1. +func NewMLDSAVerifier(vkey string) (*SubtreeVerifier, error) { + name, vkey, _ := strings.Cut(vkey, "+") + hash16, key64, _ := strings.Cut(vkey, "+") + keyBytes, err := base64.StdEncoding.DecodeString(key64) + if len(hash16) != 8 || err != nil || !isValidName(name) || len(keyBytes) != mldsa.MLDSA44PublicKeySize+1 { + return nil, errVerifierID + } + alg, pubKeyBytes := keyBytes[0], keyBytes[1:] + if alg != algMLDSA44 { + return nil, errVerifierID + } + + v := &SubtreeVerifier{ + name: name, + keyHash: keyHashMLDSA(name, keyBytes), + } + + pubKey, err := mldsa.NewPublicKey(mldsa.MLDSA44(), pubKeyBytes) + if err != nil { + return nil, err + } + v.verifyNote = func(msg, sig []byte) bool { return verifyMLDSACosigV1(pubKey, name)(msg, sig) } + v.verifySubtree = func(timestamp uint64, logOrigin string, start, end uint64, hash []byte, sig []byte) bool { + return verifyMLDSACosigV1Subtree(pubKey, name)(logOrigin, start, end, hash, sig) + } + return v, nil +} + // NewSignerForCosignatureV1 constructs a new Signer that produces timestamped -// cosignature/v1 signatures from a standard Ed25519 encoded signer key. +// cosignature/v1 signatures using the provided skey-formated key. +// +// Supported skey algorithms are: +// - a standard Ed25519 encoded signer key (algo ID 0x01) +// - an Ed25519 cosignature/v1 encoded signer key (algo ID 0x04) +// - an ML-DSA-44 cosignature/v1 encoded signer key (algo ID 0x06) // -// (The returned Signer has a different key hash from a non-timestamped one, -// meaning it will differ from the key hash in the input encoding.) +// See https://c2sp.org/tlog-cosignature for more details. func NewSignerForCosignatureV1(skey string) (*Signer, error) { priv1, skey, _ := strings.Cut(skey, "+") priv2, skey, _ := strings.Cut(skey, "+") @@ -55,7 +158,7 @@ func NewSignerForCosignatureV1(skey string) (*Signer, error) { default: return nil, errSignerAlg - case algEd25519: + case algEd25519, algEd25519CosignatureV1: if len(key) != ed25519.SeedSize { return nil, errSignerID } @@ -64,7 +167,7 @@ func NewSignerForCosignatureV1(skey string) (*Signer, error) { s.hash = keyHashEd25519(name, pubkey) s.sign = func(msg []byte) ([]byte, error) { t := uint64(time.Now().Unix()) - m, err := formatCosignatureV1(t, msg) + m, err := formatEd25519CosignatureV1(t, msg) if err != nil { return nil, err } @@ -75,17 +178,33 @@ func NewSignerForCosignatureV1(skey string) (*Signer, error) { sig = append(sig, ed25519.Sign(key, m)...) return sig, nil } - s.verify = verifyCosigV1(pubkey[1:]) + s.verify = verifyEd25519CosigV1(pubkey[1:]) + + case algMLDSA44: + stSigner, err := newMLDSASigner(name, key) + if err != nil { + return nil, err + } + s.sign = stSigner.Sign + s.verify = stSigner.verifier.verifyNote + s.hash = stSigner.hash } return s, nil } // NewVerifierForCosignatureV1 constructs a new Verifier for timestamped -// cosignature/v1 signatures from either a standard Ed25519 encoded verifier key, or an Ed25519 CosignatureV1 key. +// cosignature/v1 signatures from the provided vkey-formatted public key. // -// (In the case of passing a standard Ed25519 key, the returned Verifier has a different key hash from a non-timestamped one, -// meaning it will differ from the key hash in the input encoding.) +// Supported vkey types are: +// - a standard Ed25519 verifier key (type 0x01) +// - an Ed25519 CosignatureV1 key (type 0x04) +// - an ML-DSA-44 CosignatureV1 key (type 0x06) +// +// Note: If a standard Ed25519 verifier key (type 0x01) is provided, it will +// be internally treated as an Ed25519 CosignatureV1 key (type 0x04), meaning +// the returned Verifier has a different key hash from a non-timestamped Ed25519 +// verifier key. func NewVerifierForCosignatureV1(vkey string) (note.Verifier, error) { name, vkey, _ := strings.Cut(vkey, "+") hash16, key64, _ := strings.Cut(vkey, "+") @@ -108,7 +227,18 @@ func NewVerifierForCosignatureV1(vkey string) (note.Verifier, error) { return nil, errVerifierID } v.keyHash = keyHashEd25519(name, append([]byte{algEd25519CosignatureV1}, key...)) - v.v = verifyCosigV1(key) + v.v = verifyEd25519CosigV1(key) + + case algMLDSA44: + if len(key) != mldsa.MLDSA44PublicKeySize { + return nil, errVerifierID + } + v.keyHash = keyHashMLDSA(name, append([]byte{algMLDSA44}, key...)) + pubKey, err := mldsa.NewPublicKey(mldsa.MLDSA44(), key) + if err != nil { + return nil, err + } + v.v = verifyMLDSACosigV1(pubKey, name) } return v, nil @@ -150,7 +280,8 @@ func CoSigV1Timestamp(s note.Signature) (time.Time, error) { if err != nil { return time.UnixMilli(0), errMalformedSig } - if len(r) != keyHashSize+timestampSize+ed25519.SignatureSize { + const minSigSize = 64 // min(ed25519.SignatureSize, mldsa.MLDSA44SignatureSize) + if len(r) < keyHashSize+timestampSize+minSigSize { return time.UnixMilli(0), errVerifierAlg } r = r[keyHashSize:] // Skip the hash @@ -158,15 +289,15 @@ func CoSigV1Timestamp(s note.Signature) (time.Time, error) { return time.Unix(int64(binary.BigEndian.Uint64(r)), 0), nil } -// verifyCosigV1 returns a verify function based on key. -func verifyCosigV1(key []byte) func(msg, sig []byte) bool { +// verifyEd25519CosigV1 returns a verify function based on key. +func verifyEd25519CosigV1(key []byte) func(msg, sig []byte) bool { return func(msg, sig []byte) bool { if len(sig) != timestampSize+ed25519.SignatureSize { return false } t := binary.BigEndian.Uint64(sig) sig = sig[timestampSize:] - m, err := formatCosignatureV1(t, msg) + m, err := formatEd25519CosignatureV1(t, msg) if err != nil { return false } @@ -174,8 +305,42 @@ func verifyCosigV1(key []byte) func(msg, sig []byte) bool { } } -func formatCosignatureV1(t uint64, msg []byte) ([]byte, error) { - // The signed message is in the following format +// verifyMLDSACosigV1 returns a checkpoint-cosignature verifying function +// based on the provided key and cosigner name. +// +// Checkpoint signatures are simply subtree signatures over the range [0, size), +// so we parse the checkpoint and then use the subtree verifier to verify +// the signature as a subtree signature. +func verifyMLDSACosigV1(pubKey *mldsa.PublicKey, name string) func(msg, sig []byte) bool { + verifySubtree := verifyMLDSACosigV1Subtree(pubKey, name) + return func(msg, sig []byte) bool { + c := &log.Checkpoint{} + if _, err := c.Unmarshal(msg); err != nil { + return false + } + return verifySubtree(c.Origin, 0, c.Size, c.Hash, sig) + } +} + +// verifyMLDSACosigV1Subtree returns a subtree-cosignature verifying function +// based on the provided key and cosigner name. +func verifyMLDSACosigV1Subtree(pubKey *mldsa.PublicKey, name string) func(logOrigin string, start, end uint64, root []byte, sig []byte) bool { + return func(logOrigin string, start, end uint64, root []byte, sig []byte) bool { + if len(sig) != timestampSize+mldsa.MLDSA44SignatureSize { + return false + } + t := binary.BigEndian.Uint64(sig) + sig = sig[timestampSize:] + m, err := formatMLDSACosignatureV1(name, t, logOrigin, start, end, root) + if err != nil { + return false + } + return mldsa.Verify(pubKey, m, sig, nil) == nil + } +} + +func formatEd25519CosignatureV1(t uint64, msg []byte) ([]byte, error) { + // The signed message is in the following format: // // cosignature/v1 // time TTTTTTTTTT @@ -191,6 +356,8 @@ func formatCosignatureV1(t uint64, msg []byte) ([]byte, error) { // understand that the witness is asserting observation of correct // append-only operation of the log based on the first three lines; // no semantic statement is made about any extra "extension" lines. + // + // See https://c2sp.org/tlog-cosignature for more details. if lines := bytes.Split(msg, []byte("\n")); len(lines) < 3 { return nil, errors.New("cosigned note format invalid") @@ -198,15 +365,51 @@ func formatCosignatureV1(t uint64, msg []byte) ([]byte, error) { return []byte(fmt.Sprintf("cosignature/v1\ntime %d\n%s", t, msg)), nil } +func formatMLDSACosignatureV1(cosignerName string, timestamp uint64, logOrigin string, start, end uint64, hash []byte) ([]byte, error) { + // SPEC: If start is not zero, timestamp MUST be zero. + if start > 0 && timestamp > 0 { + return nil, errInvalidTimestamp + } + + // The signed message is a binary TLS presentation encoding of the + // following structure: + // struct { + // uint8 label[12] = "subtree/v1\n\0"; + // opaque cosigner_name<1..2^8-1>; + // uint64 timestamp; + // opaque log_origin<1..2^8-1>; + // uint64 start; + // uint64 end; + // uint8 hash[32]; + // } cosigned_message; + // + // See https://c2sp.org/tlog-cosignature for more details. + + const label = "subtree/v1\n\x00" + r := cryptobyte.NewFixedBuilder(make([]byte, 0, len(label)+(2+len(cosignerName))+8+(2+len(logOrigin))+8+8+32)) + r.AddBytes([]byte(label)) + r.AddUint8(uint8(len(cosignerName))) + r.AddBytes([]byte(cosignerName)) + r.AddUint64(timestamp) + r.AddUint8(uint8(len(logOrigin))) + r.AddBytes([]byte(logOrigin)) + r.AddUint64(start) + r.AddUint64(end) + r.AddBytes(hash) + return r.Bytes() +} + var ( - errSignerID = errors.New("malformed signer id") - errSignerAlg = errors.New("unknown signer algorithm") - errVerifierID = errors.New("malformed verifier id") - errVerifierAlg = errors.New("unknown verifier algorithm") - errInvalidHash = errors.New("invalid key hash") - errMalformedSig = errors.New("malformed signature") + errSignerID = errors.New("malformed signer id") + errSignerAlg = errors.New("unknown signer algorithm") + errVerifierID = errors.New("malformed verifier id") + errVerifierAlg = errors.New("unknown verifier algorithm") + errInvalidHash = errors.New("invalid key hash") + errMalformedSig = errors.New("malformed signature") + errInvalidTimestamp = errors.New("invalid timestamp") ) +// Signer is a note.Signer which also provides access to the corresponding Verifier. type Signer struct { name string hash uint32 @@ -218,14 +421,60 @@ func (s *Signer) Name() string { return s.name } func (s *Signer) KeyHash() uint32 { return s.hash } func (s *Signer) Sign(msg []byte) ([]byte, error) { return s.sign(msg) } -func (s *Signer) Verifier() note.Verifier { - return &verifier{ +func (s *Signer) Verifier() *Verifier { + return &Verifier{ name: s.name, keyHash: s.hash, v: s.verify, } } +// Verifier is a note.Verifier. +type Verifier struct { + name string + keyHash uint32 + v func([]byte, []byte) bool +} + +func (v *Verifier) Name() string { return v.name } +func (v *Verifier) KeyHash() uint32 { return v.keyHash } +func (v *Verifier) Verify(msg, sig []byte) bool { return v.v(msg, sig) } + +// SubtreeSigner is a signer that can produce both note and subtree signatures. +type SubtreeSigner struct { + name string + hash uint32 + signNote func([]byte) ([]byte, error) + signSubtree func(timestamp uint64, logOrigin string, start, end uint64, root []byte) ([]byte, error) + verifier *SubtreeVerifier +} + +func (s *SubtreeSigner) Name() string { return s.name } +func (s *SubtreeSigner) KeyHash() uint32 { return s.hash } +func (s *SubtreeSigner) Sign(msg []byte) ([]byte, error) { return s.signNote(msg) } +func (s *SubtreeSigner) SignSubtree(timestamp uint64, logOrigin string, start, end uint64, root []byte) ([]byte, error) { + return s.signSubtree(timestamp, logOrigin, start, end, root) +} + +// SubtreeVerifier is a verifier that supports the verification of subtree signatures. +// +// This struct implements the note.Verifier interface to facilitate cosigning operations +// against tree roots represented as checkpoints, but it can also be used to verify +// arbitrary subtree roots using the VerifySubtree method. +type SubtreeVerifier struct { + name string + keyHash uint32 + verifyNote func([]byte, []byte) bool + verifySubtree func(timestamp uint64, logOrigin string, start, end uint64, hash []byte, sig []byte) bool +} + +func (v *SubtreeVerifier) Name() string { return v.name } +func (v *SubtreeVerifier) KeyHash() uint32 { return v.keyHash } +func (v *SubtreeVerifier) Verify(msg, sig []byte) bool { return v.verifyNote(msg, sig) } +func (v *SubtreeVerifier) VerifySubtree(timestamp uint64, logOrigin string, start, end uint64, hash []byte, sig []byte) bool { + return v.verifySubtree(timestamp, logOrigin, start, end, hash, sig) +} + // isValidName reports whether name is valid. // It must be non-empty and not have any Unicode spaces or pluses. func isValidName(name string) bool { @@ -240,3 +489,12 @@ func keyHashEd25519(name string, key []byte) uint32 { sum := h.Sum(nil) return binary.BigEndian.Uint32(sum) } + +func keyHashMLDSA(name string, key []byte) uint32 { + h := sha256.New() + h.Write([]byte(name)) + h.Write([]byte("\n")) + h.Write(key) + sum := h.Sum(nil) + return binary.BigEndian.Uint32(sum) +} diff --git a/note/note_cosigv1_test.go b/note/note_cosigv1_test.go index 564c3ce..b8f91c5 100644 --- a/note/note_cosigv1_test.go +++ b/note/note_cosigv1_test.go @@ -6,31 +6,93 @@ package note import ( "crypto/rand" + "encoding/base64" + "fmt" "testing" "time" + "filippo.io/mldsa" "golang.org/x/mod/sumdb/note" ) func TestSignerRoundtrip(t *testing.T) { - skey, _, err := note.GenerateKey(rand.Reader, "test") - if err != nil { - t.Fatal(err) - } + edSk, _ := mustGenerateEd25519Key(t, "ed25519") + mlSk, _ := mustGenerateMLDSAKey(t, "mldsa") - s, err := NewSignerForCosignatureV1(skey) - if err != nil { - t.Fatal(err) - } + for _, test := range []struct { + name string + skey string + }{ + { + name: "ed25519", + skey: edSk, + }, + { + name: "mldsa", + skey: mlSk, + }, + } { + t.Run(test.name, func(t *testing.T) { + s, err := NewSignerForCosignatureV1(test.skey) + if err != nil { + t.Fatal(err) + } - msg := "test\n123\nf+7CoKgXKE/tNys9TTXcr/ad6U/K3xvznmzew9y6SP0=\n" - n, err := note.Sign(¬e.Note{Text: msg}, s) - if err != nil { - t.Fatal(err) + msg := "test\n123\nf+7CoKgXKE/tNys9TTXcr/ad6U/K3xvznmzew9y6SP0=\n" + n, err := note.Sign(¬e.Note{Text: msg}, s) + if err != nil { + t.Fatal(err) + } + + if _, err := note.Open(n, note.VerifierList(s.Verifier())); err != nil { + t.Fatal(err) + } + }) } +} - if _, err := note.Open(n, note.VerifierList(s.Verifier())); err != nil { - t.Fatal(err) +func TestMLDSASignerVerifierRoundtrip(t *testing.T) { + edSk, edPk := mustGenerateEd25519Key(t, "ed25519") + mlSk, mlPk := mustGenerateMLDSAKey(t, "mldsa") + for _, test := range []struct { + name string + skey string + vkey string + }{ + { + name: "ed25519", + skey: edSk, + vkey: edPk, + }, + { + name: "mldsa", + skey: mlSk, + vkey: mlPk, + }, + } { + t.Run(test.name, func(t *testing.T) { + s, err := NewSignerForCosignatureV1(test.skey) + if err != nil { + t.Fatal(err) + } + + v, err := NewVerifierForCosignatureV1(test.vkey) + if err != nil { + t.Fatal(err) + } + + msg := "test\n123\nf+7CoKgXKE/tNys9TTXcr/ad6U/K3xvznmzew9y6SP0=\n" + n, err := note.Sign(¬e.Note{Text: msg}, s) + if err != nil { + t.Fatal(err) + } + + t.Logf("s.KeyHash(): %08x, v.KeyHash(): %08x", s.KeyHash(), v.KeyHash()) + + if _, err := note.Open(n, note.VerifierList(v)); err != nil { + t.Fatal(err) + } + }) } } @@ -117,6 +179,9 @@ func TestCoSigV1NewVerifier(t *testing.T) { }, { name: "works: native algEd25519CosignatureV1 verifier", pubK: "remora.n621.de+da77ade7+BOvN63jn/bLvkieywe8R6UYAtVtNbZpXh34x7onlmtw2", + }, { + name: "works: native algMLDSA44 verifier", + pubK: "test+5893dc2c+BtMcFiao6ZOdU6LZ40tLKbsWpDOU8smRapXBIYI3lXyESm62to+/AeDuWOEtbwUNVzrC9FacZ1q+gXES2hAhp5/C1TPwTiO/G+T9x0iAb8gSGkGwsbDJXmQMhsFM+Ub3tB5Fdujz4o7DF3NqCCUMC1zsD7jMyy9BTCFi6Av1I/ZDRQxJJOKFt31l0cJY6OHFcUGMSGSHcGEo839UikbMBlArWRgYk/Ve4aqW0pRl7G46Qk39pu/yFYwhk3gMYMkush5NKQo7EbvhnHvUlQWK27t2VsIbH2p/l9i73UDtEmHeqIMcqtwhCnFoqT6S7cL9/p7NLwxD1gICM0gCZIi3KrbnMFok+5uovBbrF9vISSXX67R1nprdjiE0MGAwZ3Prtt0ah2xchT5I1WgmUGSA0B1cnEDXWneUaA0axw/TQ47x88+jfKIN0kn8rg5bncI5q71hV2mF1n2xuE4G+WOdBRjMVGLWlt1rZcCh8IredoZe3SxWKx7amrLo00lFN8QL8TAw1bvDiFYRqyAZE4Z5M77H8OmAR2QahuZA+d8Q1SXmTdDtOu1RRXtHq54Nm3d2SbQl48UE7BsWvu7YdqGEti5EpTX3oMVmnnjj0FRH2QjlnpaRn5bE8tiblhL31f6KRz37E9lqIUoLuE19OQ+yYOj2B6avwEY6xWq5SrOOhENQKAODXTjacDYVL4Z1hsJA9+7qbFH5S3JjMCs4VFtZHOa4tkxbpO94lfPNnhqDnuFb5xm7sT51/In+xn0vAyCoaaIpm/rwG0nRFZmR6bafSPBJXcnrocdPy86sQ/C3ma7ldByWwsHuEm8YTNABAqn6hNICNECFuoV8EBwnA0BVkhClyjTkPSnyB1CAUEkzQwd/SW7VgjDoI9r6k4ot1oaxD9ZudZor7309jhMIbQhE1ODPMDxFM2B5XvhlJ6nl6r0JUI/q2uLnZGyzKDMGnL+T+uGDN+AvBrg4kyHEM1X39Dkr1XpJIMRlROY+GayJliTQ8eEdZ1yug5C0JGHJ9bEQu/L2Zf084+/+4BAUrrJ4JtmShbYulaDmMVjm72Osu5a9ld7KeZ2hn5uK9yhiUJqeBDGGLmqqZbbUwIRa9OWZoQVU+dCt2ust5migCq69O3H+83U2YKBj9r3QLv6FG/crj3IUrKagKIqv5fT71eeCEmSmdAGqVP+5hxBQMBcISqp+9rqDsS1NLba2YuUWYgFDlQ7Xq915t+d3N5ouHNWJHs1/2V0ZNsSvID1p73sUWVJlh2M4/B8/QT1uvteCPc+yycsJM80Xaomwv9KTfUpyNn60R860zr+Va432W8v5urT1Teu6QCTWKdq3ivhyYREBLvka07jb7SlIees2PKDfvibKBBanF/EmorgkdPIagG/88kT9GtXtOIQp/HJLw3Ej5QPwEgrLYZ0YY6t53ZC6BmjDt0eSEShPmqte74KC7Qgo2hvA3grqp03vA7RG3cMCmtn/k0PH9ZY4ytTF7eozJCRim9AWa0HudpkGKbv3ijxrkBBhAGTCGQe96Tt5nBiKatheV3z4i2o4TtoRC6SEQZUEnWLszXOWtghhfe/V7QqB8jfMLxHW2qkueUkTz2IatYI+2hlkyTQRIKGPjKrnF8qAKz5/PZcoaL6pBaWcpsUEcpwQs6/aT0tmB2UGZNzrj02lREEgGajjuqHMH/rwmqIQTV38KohBniPDkDV9jdIyU1HVlm5dElah29WMC+RWC6bN2GbOvn1+s6iQwNNLUGU=", }, { name: "wrong number of parts", pubK: "bananas.sigstore.dev+12344556", @@ -192,7 +257,7 @@ func TestVKeyToCosignatureV1(t *testing.T) { if err != nil { t.Fatalf("Failed to create cosignerv1: %v", err) } - covkey, err := VKeyToCosignatureV1(vkey) + covkey, err := VKeyToCosignatureV1(vkey) if err != nil { t.Fatalf("Failed to convert vkey to cosigv1 verifier: %v", err) } @@ -221,6 +286,100 @@ func TestVKeyToCosignatureV1(t *testing.T) { } // Now check that the standard vkey cannot open a cosig signature. if _, err = note.Open(n, note.VerifierList(v)); err == nil { - t.Errorf("Expected error trying to open cosigned note with standard vkey, but got success") + t.Errorf("Expected error trying to open cosigned note with standard vkey, but got success") + } + + // Check that VKeyToCosignatureV1 fails for MLDSA keys. + _, mlVkey := mustGenerateMLDSAKey(t, "mldsa") + if _, err := VKeyToCosignatureV1(mlVkey); err == nil { + t.Errorf("Expected error for MLDSA key in VKeyToCosignatureV1, got success") + } +} + +func TestSubtreeRoundtrip(t *testing.T) { + skey, vkey := mustGenerateMLDSAKey(t, "mldsa") + + signer, err := NewMLDSASigner(skey) + if err != nil { + t.Fatal(err) + } + + verifier, err := NewMLDSAVerifier(vkey) + if err != nil { + t.Fatal(err) + } + + origin := "test-log" + var start uint64 = 0 + var end uint64 = 10 + root := make([]byte, 32) + if _, err := rand.Read(root); err != nil { + t.Fatal(err) + } + timestamp := uint64(time.Now().Unix()) + + sig, err := signer.SignSubtree(timestamp, origin, start, end, root) + if err != nil { + t.Fatal(err) + } + + if !verifier.VerifySubtree(timestamp, origin, start, end, root, sig) { + t.Error("Failed to verify valid subtree signature") } + + // Test failure cases + wrongRoot := make([]byte, 32) + wrongRoot[0] = 1 + if verifier.VerifySubtree(timestamp, origin, start, end, wrongRoot, sig) { + t.Error("VerifySubtree succeeded with wrong root") + } + + if verifier.VerifySubtree(timestamp, "wrong origin", start, end, root, sig) { + t.Error("VerifySubtree succeeded with wrong origin") + } +} + +func TestMLDSAInvalidTimestamp(t *testing.T) { + skey, _ := mustGenerateMLDSAKey(t, "mldsa") + signer, err := NewMLDSASigner(skey) + if err != nil { + t.Fatal(err) + } + + origin := "test-log" + var start uint64 = 10 // > 0 + var end uint64 = 20 + root := make([]byte, 32) + timestamp := uint64(time.Now().Unix()) // > 0 + + _, err = signer.SignSubtree(timestamp, origin, start, end, root) + if err == nil { + t.Error("Expected error for invalid timestamp (start > 0 && timestamp > 0), got nil") + } +} + +func mustGenerateEd25519Key(t *testing.T, name string) (string, string) { + t.Helper() + skey, vkey, err := note.GenerateKey(rand.Reader, name) + if err != nil { + t.Fatalf("Failed to generate Ed25519 key: %v", err) + } + return skey, vkey +} + +func mustGenerateMLDSAKey(t *testing.T, name string) (string, string) { + t.Helper() + key, err := mldsa.GenerateKey(mldsa.MLDSA44()) + if err != nil { + t.Fatalf("Failed to generate MLDSA key: %v", err) + } + privBytes := key.Bytes() + pubBytes := key.PublicKey().Bytes() + + pubKeyWithAlg := append([]byte{algMLDSA44}, pubBytes...) + hash := keyHashMLDSA(name, pubKeyWithAlg) + + skey := fmt.Sprintf("PRIVATE+KEY+%s+%08x+%s", name, hash, base64.StdEncoding.EncodeToString(append([]byte{algMLDSA44}, privBytes...))) + vkey := fmt.Sprintf("%s+%08x+%s", name, hash, base64.StdEncoding.EncodeToString(pubKeyWithAlg)) + return skey, vkey } diff --git a/note/note_verifier.go b/note/note_verifier.go index 260dead..4e852ab 100644 --- a/note/note_verifier.go +++ b/note/note_verifier.go @@ -88,7 +88,7 @@ func NewVerifier(key string) (note.Verifier, error) { switch keyBytes[0] { case algECDSAWithSHA256: return NewECDSAVerifier(key) - case algEd25519CosignatureV1: + case algEd25519CosignatureV1, algMLDSA44: return NewVerifierForCosignatureV1(key) case algRFC6962STH: return NewRFC6962Verifier(key)