Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 12 additions & 23 deletions trees/cosignature/cosignature.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ func keyIDFor(name string, publicKey *mldsa.PublicKey) uint32 {
return binary.BigEndian.Uint32(h.Sum(nil)[:keyIDSize])
}

// marshalCosignedMessage serializes the cosigned.Message for a checkpoint
// marshalCheckpointMessage serializes the cosigned.Message for a checkpoint
// cosignature, with start 0 and end the tree size as the MTC draft section
// 5.3.1 requires for checkpoints. It rejects a non-positive end.
func marshalCosignedMessage(name string, timestamp uint64, origin string, end int64, rootHash tlog.Hash) ([]byte, error) {
func marshalCheckpointMessage(name string, timestamp uint64, origin string, end int64, rootHash tlog.Hash) ([]byte, error) {
if end <= 0 {
return nil, fmt.Errorf("non-positive end %d", end)
}
Expand Down Expand Up @@ -85,7 +85,7 @@ func signatureLineFor(name string, keyID uint32, timestampedSignature []byte) st
// "32473.2", nil otherwise.
func checkRelativeOID(id string) error {
if id == "" {
return errors.New("empty")
return errors.New("empty relative OID")
}
for _, arc := range strings.Split(id, ".") {
if arc == "" {
Expand All @@ -103,12 +103,12 @@ func checkRelativeOID(id string) error {
return nil
}

// Origin returns the log origin derived from the log ID per mtc-tlog: log ID
// "32473.2.0.42" has origin "oid/1.3.6.1.4.1.32473.2.0.42". It errors if logID
// is not a dotted decimal OID.
// originFor returns the log origin derived from the log ID per mtc-tlog: log
// ID "32473.2.0.42" has origin "oid/1.3.6.1.4.1.32473.2.0.42". It errors if
// logID is not a dotted decimal OID.
//
// https://c2sp.org/mtc-tlog
func Origin(logID string) (string, error) {
func originFor(logID string) (string, error) {
err := checkRelativeOID(logID)
if err != nil {
return "", fmt.Errorf("invalid log ID %q: %w", logID, err)
Expand All @@ -119,7 +119,7 @@ func Origin(logID string) (string, error) {
// Cosigner produces cosignatures over checkpoints as an MTC cosigner for a
// single log: the timestamp is zero, as required for cosignatures used in
// certificates, and the key is a crypto.Signer so it may be stored in an HSM.
// Both the CA cosigner and a mirror cosigning the CA's log take this role.
// Both the CA cosigner and the mirror cosigner use this.
//
// - https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#section-5.3.1
// - https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#section-6.2
Expand All @@ -140,7 +140,7 @@ func NewCosigner(cosignerID, logID string, signer crypto.Signer) (*Cosigner, err
if err != nil {
return nil, fmt.Errorf("invalid cosigner ID %q: %w", cosignerID, err)
}
origin, err := Origin(logID)
origin, err := originFor(logID)
if err != nil {
return nil, err
}
Expand All @@ -167,7 +167,7 @@ func (c *Cosigner) Origin() string {
// CosignCheckpoint cosigns the checkpoint described by tree and returns the
// cosignature as a timestamped_signature.
func (c *Cosigner) CosignCheckpoint(tree tlog.Tree) ([]byte, error) {
message, err := marshalCosignedMessage(c.name, 0, c.origin, tree.N, tree.Hash)
message, err := marshalCheckpointMessage(c.name, 0, c.origin, tree.N, tree.Hash)
if err != nil {
return nil, err
}
Expand All @@ -183,17 +183,6 @@ func (c *Cosigner) CosignCheckpoint(tree tlog.Tree) ([]byte, error) {
return out, nil
}

// CosignatureLine cosigns the checkpoint described by tree and returns the
// cosignature as a signature line, trailing newline included. For a
// timestamped_signature, use CosignCheckpoint.
func (c *Cosigner) CosignatureLine(tree tlog.Tree) (string, error) {
signature, err := c.CosignCheckpoint(tree)
if err != nil {
return "", err
}
return signatureLineFor(c.name, c.keyID, signature), nil
}

// Verifier is a note.Verifier that verifies an MTC cosigner's ML-DSA-44
// cosignatures over checkpoints.
//
Expand Down Expand Up @@ -249,7 +238,7 @@ func (v *Verifier) VerifyCheckpoint(origin string, tree tlog.Tree, timestampedSi
if timestamp > math.MaxInt64 {
return fmt.Errorf("timestamp %d exceeds 2^63-1", timestamp)
}
cosignedMessage, err := marshalCosignedMessage(v.name, timestamp, origin, tree.N, tree.Hash)
cosignedMessage, err := marshalCheckpointMessage(v.name, timestamp, origin, tree.N, tree.Hash)
if err != nil {
return err
}
Expand Down Expand Up @@ -283,7 +272,7 @@ func TimestampedSignature(noteText, signatureLine string, verifier *Verifier) ([
return nil, fmt.Errorf("opening the cosigned note: %s", err)
}
// verifier is the only verifier in the list, so every signature in n.Sigs
// is the cosigner's, verified and length-checked.
// is by the verifier's cosigner, verified and length-checked.
idSignature, err := base64.StdEncoding.DecodeString(n.Sigs[0].Base64)
if err != nil {
return nil, fmt.Errorf("decoding the signature by %s: %s", verifier.name, err)
Expand Down
78 changes: 38 additions & 40 deletions trees/cosignature/cosignature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"strings"
"testing"

"github.com/letsencrypt/boulder/privatekey"
"github.com/letsencrypt/boulder/trees/checkpoint"
"golang.org/x/mod/sumdb/note"
"golang.org/x/mod/sumdb/tlog"
Expand All @@ -32,9 +33,9 @@ const exampleCheckpoint = "example.com/behind-the-sofa\n20852163\n" + exampleHas
const cosignerID = "32473.9"
const cosignerName = oidPrefix + cosignerID

// testKey returns a deterministic ML-DSA-44 key, so cosignature tests are
// reproducible.
func testKey(t *testing.T) *mldsa.PrivateKey {
// testSigner returns a deterministic signer over a fixed-seed ML-DSA-44 key,
// so cosignature tests are reproducible.
func testSigner(t *testing.T) crypto.Signer {
t.Helper()
seed := make([]byte, 32)
for i := range seed {
Expand All @@ -44,12 +45,16 @@ func testKey(t *testing.T) *mldsa.PrivateKey {
if err != nil {
t.Fatalf("NewPrivateKey: %s", err)
}
return key
return privatekey.NewDeterministicSigner(key)
}

func testPubKey(t *testing.T) *mldsa.PublicKey {
t.Helper()
return testKey(t).PublicKey()
publicKey, ok := testSigner(t).Public().(*mldsa.PublicKey)
if !ok {
t.Fatal("testSigner's public key is not ML-DSA")
}
return publicKey
}

func newVerifier(t *testing.T) *Verifier {
Expand Down Expand Up @@ -124,19 +129,19 @@ func TestNewVerifierRejects(t *testing.T) {
// timestamped_signature is rejected when its timestamp exceeds the spec's
// 2^63-1 bound.
func TestVerifyRejectsOversizeTimestamp(t *testing.T) {
key := testKey(t)
signer := testSigner(t)
v := newVerifier(t)
parsed, err := checkpoint.Unmarshal(exampleCheckpoint)
if err != nil {
t.Fatalf("Unmarshal: %s", err)
}

timestamped := func(ts uint64) []byte {
message, err := marshalCosignedMessage(cosignerName, ts, parsed.Origin, parsed.Tree.N, parsed.Tree.Hash)
message, err := marshalCheckpointMessage(cosignerName, ts, parsed.Origin, parsed.Tree.N, parsed.Tree.Hash)
if err != nil {
t.Fatalf("marshalCosignedMessage: %s", err)
t.Fatalf("marshalCheckpointMessage: %s", err)
}
signature, err := key.SignDeterministic(message, nil)
signature, err := signer.Sign(nil, message, nil)
if err != nil {
t.Fatalf("Sign: %s", err)
}
Expand Down Expand Up @@ -193,18 +198,18 @@ func TestVerifyCheckpointErrors(t *testing.T) {
}
}

func TestOrigin(t *testing.T) {
origin, err := Origin("32473.2.0.42")
func TestOriginFor(t *testing.T) {
origin, err := originFor("32473.2.0.42")
if err != nil {
t.Fatalf("Origin: %s", err)
t.Fatalf("originFor: %s", err)
}
if origin != "oid/1.3.6.1.4.1.32473.2.0.42" {
t.Errorf("Origin = %q, want %q", origin, "oid/1.3.6.1.4.1.32473.2.0.42")
t.Errorf("originFor = %q, want %q", origin, "oid/1.3.6.1.4.1.32473.2.0.42")
}

_, err = Origin("32473..2")
_, err = originFor("32473..2")
if err == nil {
t.Error("Origin with a malformed log ID = nil error, want error")
t.Error("originFor with a malformed log ID = nil error, want error")
}
}

Expand All @@ -214,7 +219,7 @@ func TestOrigin(t *testing.T) {
// verifier against the matching checkpoint text, and reassembles into a
// signature line that opens.
func TestCosignerRoundTrip(t *testing.T) {
ca, err := NewCosigner("32473.2", "32473.2.0.42", testKey(t))
ca, err := NewCosigner("32473.2", "32473.2.0.42", testSigner(t))
if err != nil {
t.Fatalf("NewCosigner: %s", err)
}
Expand Down Expand Up @@ -268,13 +273,7 @@ func TestCosignerRoundTrip(t *testing.T) {
t.Error("VerifyCheckpoint accepted a cosignature over a different origin")
}

// ML-DSA signing through a crypto.Signer may be hedged, so
// CosignatureLine's line carries a fresh signature. signatureLineFor must
// rebuild that line byte for byte from the signature extracted out of it.
line, err := ca.CosignatureLine(parsed.Tree)
if err != nil {
t.Fatalf("CosignatureLine: %s", err)
}
line := signatureLineFor(ca.name, ca.keyID, signature)
if !strings.HasPrefix(line, noteSignatureLinePrefix+ca.name+" ") {
t.Errorf("line %q has unexpected prefix", line)
}
Expand All @@ -293,14 +292,14 @@ func TestCosignerRoundTrip(t *testing.T) {
}

func TestCosignerRejects(t *testing.T) {
key := testKey(t)
signer := testSigner(t)

for _, id := range []string{"", "has space", "32473..2", "32473.x", ".32473", "32473.", "32473.02"} {
_, err := NewCosigner(id, "32473.2.0.42", key)
_, err := NewCosigner(id, "32473.2.0.42", signer)
if err == nil {
t.Errorf("NewCosigner with cosigner ID %q = nil error, want error", id)
}
_, err = NewCosigner("32473.2", id, key)
_, err = NewCosigner("32473.2", id, signer)
if err == nil {
t.Errorf("NewCosigner with log ID %q = nil error, want error", id)
}
Expand All @@ -326,7 +325,7 @@ func TestCosignerRejects(t *testing.T) {
t.Error("NewCosigner with an Ed25519 key = nil error, want error")
}

ca, err := NewCosigner("32473.2", "32473.2.0.42", key)
ca, err := NewCosigner("32473.2", "32473.2.0.42", signer)
if err != nil {
t.Fatalf("NewCosigner: %s", err)
}
Expand Down Expand Up @@ -366,10 +365,6 @@ func TestCosignerRejectsShortSignature(t *testing.T) {
if err == nil {
t.Error("CosignCheckpoint with a truncated signature = nil error, want error")
}
_, err = ca.CosignatureLine(tlog.Tree{N: 1})
if err == nil {
t.Error("CosignatureLine with a truncated signature = nil error, want error")
}
}

// errSigner is a crypto.Signer with a valid ML-DSA-44 public key whose Sign
Expand Down Expand Up @@ -429,7 +424,7 @@ func TestRawSignature(t *testing.T) {
// verifies on its own, and that extraction errors for a verifier that did not
// sign.
func TestTimestampedSignature(t *testing.T) {
ca, err := NewCosigner("32473.2", "32473.2.0.42", testKey(t))
ca, err := NewCosigner("32473.2", "32473.2.0.42", testSigner(t))
if err != nil {
t.Fatalf("NewCosigner: %s", err)
}
Expand All @@ -438,10 +433,11 @@ func TestTimestampedSignature(t *testing.T) {
if err != nil {
t.Fatalf("checkpoint.Unmarshal: %s", err)
}
line, err := ca.CosignatureLine(parsed.Tree)
cosigned, err := ca.CosignCheckpoint(parsed.Tree)
if err != nil {
t.Fatalf("CosignatureLine: %s", err)
t.Fatalf("CosignCheckpoint: %s", err)
}
line := signatureLineFor(ca.name, ca.keyID, cosigned)

v, err := NewVerifier("32473.2", testPubKey(t))
if err != nil {
Expand Down Expand Up @@ -484,7 +480,7 @@ func TestTimestampedSignatureRejectsForeignFormat(t *testing.T) {
// signatures from unknown keys" with a note cosigned for one log by two MTC
// cosigners and opened by one verifier, the shape of every real exchange.
func TestOpenIgnoresUnknownSignatures(t *testing.T) {
known, err := NewCosigner("32473.2", "32473.2.0.42", testKey(t))
known, err := NewCosigner("32473.2", "32473.2.0.42", testSigner(t))
if err != nil {
t.Fatalf("NewCosigner: %s", err)
}
Expand All @@ -497,7 +493,7 @@ func TestOpenIgnoresUnknownSignatures(t *testing.T) {
if err != nil {
t.Fatalf("NewPrivateKey: %s", err)
}
unknown, err := NewCosigner("32473.9", "32473.2.0.42", otherKey)
unknown, err := NewCosigner("32473.9", "32473.2.0.42", privatekey.NewDeterministicSigner(otherKey))
if err != nil {
t.Fatalf("NewCosigner: %s", err)
}
Expand All @@ -507,14 +503,16 @@ func TestOpenIgnoresUnknownSignatures(t *testing.T) {
if err != nil {
t.Fatalf("checkpoint.Unmarshal: %s", err)
}
knownLine, err := known.CosignatureLine(parsed.Tree)
knownSignature, err := known.CosignCheckpoint(parsed.Tree)
if err != nil {
t.Fatalf("CosignatureLine: %s", err)
t.Fatalf("CosignCheckpoint: %s", err)
}
unknownLine, err := unknown.CosignatureLine(parsed.Tree)
knownLine := signatureLineFor(known.name, known.keyID, knownSignature)
unknownSignature, err := unknown.CosignCheckpoint(parsed.Tree)
if err != nil {
t.Fatalf("CosignatureLine: %s", err)
t.Fatalf("CosignCheckpoint: %s", err)
}
unknownLine := signatureLineFor(unknown.name, unknown.keyID, unknownSignature)
signed := []byte(text + "\n" + knownLine + unknownLine)

v, err := NewVerifier("32473.2", testPubKey(t))
Expand Down