Skip to content

Commit 3ce7472

Browse files
committed
torchwood: support ML-DSA-44 log vkeys in ParsePolicy
1 parent 630cdf2 commit 3ce7472

2 files changed

Lines changed: 178 additions & 1 deletion

File tree

policy.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package torchwood
22

33
import (
4+
"encoding/base64"
45
"errors"
56
"fmt"
67
"strconv"
@@ -152,7 +153,7 @@ func ParsePolicy(p []byte) (Policy, error) {
152153
if len(fields) < 2 {
153154
return nil, fmt.Errorf("line %d: invalid log definition: %q", i+1, line)
154155
}
155-
v, err := note.NewVerifier(fields[1])
156+
v, err := newLogVerifier(fields[1])
156157
if err != nil {
157158
return nil, fmt.Errorf("line %d: invalid log vkey %q: %w", i+1, fields[1], err)
158159
}
@@ -226,3 +227,22 @@ func ParsePolicy(p []byte) (Policy, error) {
226227
return ThresholdPolicy(2, q, logPolicy), nil
227228
}
228229
}
230+
231+
// newLogVerifier parses a log vkey, which may use the Ed25519 signature
232+
// algorithm or the ML-DSA-44 (sub)tree cosignature algorithm.
233+
func newLogVerifier(vkey string) (note.Verifier, error) {
234+
_, rest, _ := strings.Cut(vkey, "+")
235+
_, key64, _ := strings.Cut(rest, "+")
236+
key, err := base64.StdEncoding.DecodeString(key64)
237+
if err != nil || len(key) == 0 {
238+
return nil, errors.New("malformed verifier id")
239+
}
240+
switch key[0] {
241+
case algEd25519:
242+
return note.NewVerifier(vkey)
243+
case algCosignatureMLDSA:
244+
return NewCosignatureVerifier(vkey)
245+
default:
246+
return nil, errors.New("unknown verifier algorithm for log vkey")
247+
}
248+
}

policy_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package torchwood_test
2+
3+
import (
4+
"crypto/ed25519"
5+
"crypto/rand"
6+
"testing"
7+
8+
"filippo.io/mldsa"
9+
"filippo.io/torchwood"
10+
"golang.org/x/mod/sumdb/note"
11+
"golang.org/x/mod/sumdb/tlog"
12+
)
13+
14+
func TestParsePolicyLogKey(t *testing.T) {
15+
origin := "example.com/log"
16+
checkpoint := torchwood.Checkpoint{
17+
Origin: origin,
18+
Tree: tlog.Tree{N: 123, Hash: tlog.RecordHash([]byte("test data"))},
19+
}
20+
21+
t.Run("Ed25519", func(t *testing.T) {
22+
skey, vkey, err := note.GenerateKey(rand.Reader, origin)
23+
if err != nil {
24+
t.Fatal(err)
25+
}
26+
signer, err := note.NewSigner(skey)
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
signed, err := note.Sign(&note.Note{Text: checkpoint.String()}, signer)
31+
if err != nil {
32+
t.Fatal(err)
33+
}
34+
35+
policy, err := torchwood.ParsePolicy([]byte("log " + vkey + "\nquorum none\n"))
36+
if err != nil {
37+
t.Fatal(err)
38+
}
39+
c, _, err := torchwood.VerifyCheckpoint(signed, policy)
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
if c.Origin != origin {
44+
t.Errorf("origin = %q; want %q", c.Origin, origin)
45+
}
46+
})
47+
48+
t.Run("ML-DSA", func(t *testing.T) {
49+
k, err := mldsa.GenerateKey(mldsa.MLDSA44())
50+
if err != nil {
51+
t.Fatal(err)
52+
}
53+
signer, err := torchwood.NewCosignatureSigner(origin, k)
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
signed, err := note.Sign(&note.Note{Text: checkpoint.String()}, signer)
58+
if err != nil {
59+
t.Fatal(err)
60+
}
61+
62+
policy, err := torchwood.ParsePolicy([]byte("log " + signer.Verifier().String() + "\nquorum none\n"))
63+
if err != nil {
64+
t.Fatal(err)
65+
}
66+
c, _, err := torchwood.VerifyCheckpoint(signed, policy)
67+
if err != nil {
68+
t.Fatal(err)
69+
}
70+
if c.Origin != origin {
71+
t.Errorf("origin = %q; want %q", c.Origin, origin)
72+
}
73+
74+
// A checkpoint for a different origin doesn't satisfy the policy, even
75+
// if the signature verifies.
76+
other := torchwood.Checkpoint{
77+
Origin: "example.com/other",
78+
Tree: checkpoint.Tree,
79+
}
80+
signedOther, err := note.Sign(&note.Note{Text: other.String()}, signer)
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
if _, _, err := torchwood.VerifyCheckpoint(signedOther, policy); err == nil {
85+
t.Error("expected error verifying checkpoint with wrong origin")
86+
}
87+
})
88+
89+
t.Run("Ed25519 cosignature key rejected", func(t *testing.T) {
90+
_, k, err := ed25519.GenerateKey(rand.Reader)
91+
if err != nil {
92+
t.Fatal(err)
93+
}
94+
signer, err := torchwood.NewCosignatureSigner(origin, k)
95+
if err != nil {
96+
t.Fatal(err)
97+
}
98+
_, err = torchwood.ParsePolicy([]byte("log " + signer.Verifier().String() + "\nquorum none\n"))
99+
if err == nil {
100+
t.Error("expected error parsing log line with Ed25519 cosignature vkey")
101+
}
102+
})
103+
}
104+
105+
func TestParsePolicyMLDSAWithWitness(t *testing.T) {
106+
origin := "example.com/log"
107+
checkpoint := torchwood.Checkpoint{
108+
Origin: origin,
109+
Tree: tlog.Tree{N: 123, Hash: tlog.RecordHash([]byte("test data"))},
110+
}
111+
112+
logKey, err := mldsa.GenerateKey(mldsa.MLDSA44())
113+
if err != nil {
114+
t.Fatal(err)
115+
}
116+
logSigner, err := torchwood.NewCosignatureSigner(origin, logKey)
117+
if err != nil {
118+
t.Fatal(err)
119+
}
120+
witnessKey, err := mldsa.GenerateKey(mldsa.MLDSA44())
121+
if err != nil {
122+
t.Fatal(err)
123+
}
124+
witnessSigner, err := torchwood.NewCosignatureSigner("witness.example/w1", witnessKey)
125+
if err != nil {
126+
t.Fatal(err)
127+
}
128+
129+
policy, err := torchwood.ParsePolicy([]byte(
130+
"log " + logSigner.Verifier().String() + "\n" +
131+
"witness W1 " + witnessSigner.Verifier().String() + "\n" +
132+
"quorum W1\n"))
133+
if err != nil {
134+
t.Fatal(err)
135+
}
136+
137+
signed, err := note.Sign(&note.Note{Text: checkpoint.String()}, logSigner, witnessSigner)
138+
if err != nil {
139+
t.Fatal(err)
140+
}
141+
c, _, err := torchwood.VerifyCheckpoint(signed, policy)
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
if c.Origin != origin {
146+
t.Errorf("origin = %q; want %q", c.Origin, origin)
147+
}
148+
149+
// Without the witness cosignature, the quorum is not satisfied.
150+
signedLogOnly, err := note.Sign(&note.Note{Text: checkpoint.String()}, logSigner)
151+
if err != nil {
152+
t.Fatal(err)
153+
}
154+
if _, _, err := torchwood.VerifyCheckpoint(signedLogOnly, policy); err == nil {
155+
t.Error("expected error verifying checkpoint without witness cosignature")
156+
}
157+
}

0 commit comments

Comments
 (0)