diff --git a/NEWS.md b/NEWS.md index 460a3ce..258c9d5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,6 +17,9 @@ - Added `HashReaderOverlay`, a `tlog.HashReader` that extends a tree with the stored hashes of appended records, held in memory. +- Added `NewLogVerifier` to parse a log vkey, accepting regular Ed25519 keys + and ML-DSA-44 cosignature keys. + ### mpt - New package with a Merkle Patricia Trie implementation. @@ -30,12 +33,16 @@ - The bastion certificate is now generated once at startup. +- Logs can now sign checkpoints with ML-DSA-44 keys. + ### witnessctl - Added `set-bastions` command, which adds the given bastion(s) to every log that has none configured (for example after `pull-logs`), or replaces the bastions of every log with `-all`. +- `add-key` and `pull-logs` now accept logs that use ML-DSA-44 keys. + ### litebastion - Comments and empty lines are now allowed in the backends file. diff --git a/cmd/litewitness/testdata/gentest/mldsa.go b/cmd/litewitness/testdata/gentest/mldsa.go new file mode 100644 index 0000000..1481cab --- /dev/null +++ b/cmd/litewitness/testdata/gentest/mldsa.go @@ -0,0 +1,59 @@ +//go:build mldsa + +// Run with "go run -tags mldsa -mod=mod ./cmd/litewitness/testdata/gentest" +// and re-run "go mod tidy" after use to clean up its dependencies. + +package main + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "flag" + "fmt" + "log" + + "filippo.io/mldsa" + "filippo.io/torchwood" + "golang.org/x/mod/sumdb/tlog" +) + +var seedFlag = flag.String("seed", "", "hex-encoded seed") + +func main() { + origin := "example.com/mldsa-log" + + flag.Parse() + var seed []byte + if *seedFlag == "" { + seed = make([]byte, 32) + if _, err := rand.Read(seed); err != nil { + log.Fatal(err) + } + } else { + seed = make([]byte, hex.DecodedLen(len(*seedFlag))) + if _, err := hex.Decode(seed, []byte(*seedFlag)); err != nil { + log.Fatal(err) + } + } + fmt.Printf("- seed: %x\n", seed) + + logKey, err := mldsa.NewPrivateKey(mldsa.MLDSA44(), seed) + if err != nil { + log.Fatal(err) + } + signer, err := torchwood.NewCosignatureSigner(origin, logKey) + if err != nil { + log.Fatal(err) + } + fmt.Printf("- log vkey: %s\n", signer.Verifier().String()) + + // SignSubtree is used to get a deterministic timestamp (zero). + rootHash := tlog.RecordHash([]byte("testonly")) + checkpoint := fmt.Sprintf("%s\n%d\n%s\n", origin, 1, base64.StdEncoding.EncodeToString(rootHash[:])) + sigLine, err := signer.SignSubtree(origin, 0, 1, rootHash) + if err != nil { + log.Fatal(err) + } + fmt.Printf("- checkpoint (size 1):\n%s\n%s", checkpoint, string(sigLine)) +} diff --git a/cmd/litewitness/testdata/gentest/sigsum.go b/cmd/litewitness/testdata/gentest/sigsum.go index a308896..d126ba0 100644 --- a/cmd/litewitness/testdata/gentest/sigsum.go +++ b/cmd/litewitness/testdata/gentest/sigsum.go @@ -1,4 +1,6 @@ -// Run with "go run -mod=mod ./cmd/litewitness/testdata/gentest" +//go:build sigsum + +// Run with "go run -tags sigsum -mod=mod ./cmd/litewitness/testdata/gentest" // and re-run "go mod tidy" after use to clean up its dependencies. package main diff --git a/cmd/litewitness/testdata/mldsa.txt b/cmd/litewitness/testdata/mldsa.txt new file mode 100644 index 0000000..1477d5d --- /dev/null +++ b/cmd/litewitness/testdata/mldsa.txt @@ -0,0 +1,49 @@ +# gentest seed: 2886adc2b0eabfce67c0472939127998bf886cf256bfee8ba76a253fce1146e0 + +# set up log with an ML-DSA-44 key +exec witnessctl add-log -origin example.com/mldsa-log +exec witnessctl add-key -origin example.com/mldsa-log -key example.com/mldsa-log+b5df45c5+BuhtcL97l3dcitjZ8Y9J54gmQ1TWn/BMF0yePKpTzhlFCkN6ouJDf+9kT9wve4/Y+JGXBUjlRvktAynZ6XOQSO+fOaxyPOQAwrtbZKYXnZWSQj9gHPz18q6brnckUv87Rsk/m5GIBvH8J3ymxZjUQS+DnAUIbctLsG2DD6I/ou0JtXpPLvfGhPDTkwc9QeFyBLBa02Pr9uuWCMohciLqRiFUo5+leUUDKba1g5TYjOjK/VKfntpdbEwoqhTJTG4armjUeXdrYDHeaaMaCAnW5F0lMWYsHjqzRpKjeTjgItseaWKSm+ga/mzTAgqsbo5Ob/AwaXf4Av7/mN+ZM6vHON/NE7Ip5vpXpCiNb9YbEQavn+fjNucFxffCacvzXghvaaQ+3C/5h5lpR8jeajBFsFIOvdkYKizt29lCjqZDkYQgVfB9nWeH6rbayMkBdDpmRH03cI+urD0Eb5zvRuGJn3PgfwdyQJ3WcDH5Gqn5dkKs5NIVMBC+FfKeD47ruAIruda4JiiYCAxZbWgwzTI80N5lZFHEndmxKWaw6z/QyphuYkNyvZVZ0R3kH3JN4jdy9KKpNtFoQqnoBTGeEE/fXEEs5ByPlKnrrefRgWTqxssE4nPzHHr5GGgv/NKNTXNKI4LvNPH5yO3Vv/gIiDZxPPc1Jen8uKkmc0s3kT7XtcuWTlb+zdQeSme9ODj0IoVu29Eav2OsGJpReX7vZx9i2fVfHEMHY5nFew8L5RxcWmZFFiYhMFZnLvk/SWmHcwr3DhzZchrODhabHQ+S6S49Ws+5aoBBPM3mOrSSKHgobdI7BJrdzdT07kjzyiyS9Gyv8iylUHHMuuh1O05RNRcX5SXmenwGDPcT9HuLS+hoesO0FQgEt+jAR7vVyUUU2C6g6FC6QNfHL/EUsulvYIAS5fOK7acOeVnoJlpI57Bq+uzLZm6g7IZD0xxQwZPYyT4ek99l9HxDSrVnU0rAr+Tq5vVh5YxvDkQVWroYIWek/AI5awjmhgpW4LPapBhLk2ipkF5CmU3T0cm3ET5f14gYQZfz23nCsmZ7qyqiQilgOxJzVYacVoGAStGzJNY1wEf26MADW5McyQzYI/3Fpaa4Ay6p2yFmcJMfpqZnu4flgWMqN1UpMbbJ6I7hOrCFWryExK/nvSbRrErHTkPz2wvVfI+enbFz86c0iNb7naQKv6LoIZYEHFOCEsWGL/sCChn6BR4nk1qAakyXEQwi96CkIa3AX562F8lhw6CU4x7T+NWqi2AWfEXImJenORhMP69hGIFsiLAVIjHtsWtJOi8cu+Brg1bFERkB4ryUTrLf/jFeb6KifZ+s8PZWclDaOLMINZ3E78a5u6AzgXpigJsAJ1A4BY89muedrW2O8kFyh3TTTKyY4kedCW6L+8bFHV2Gce25ZulNC1mZs41XAI9CteeYaWaiL7uGMXgyQ73wr4ftwAEMdsoQVQsQriutS1AhpBGvjzPCwD5LC5p1fa0WXBQ1puuaNqr3IJodmDLhEIAsZcv1gdXMg8EQg9QLqgLLrBijU3GxFUQllS8lpQ4TLwFYUJ41+qt28JEeky/VzSEeQd/qob8cbyNM44o7INB+lbhEXEGWvVO8Ak+uWMY+PRKeY9OQcE6E27Nf3eUoHQXkwmXkjGgj0NKeXBIJc1k/Ws6WDbsU4graVKk5FQjwULcRngOpyiJQ/n8sObkAPXr+1rImK3lQJnxif/B6p5/tsh4rrcUht4yVIvFAaqsgAAE= + +# start ssh-agent +env SSH_AUTH_SOCK=$WORK/s # barely below the max path length +! exec ssh-agent -a $SSH_AUTH_SOCK -D & # ssh-agent always exits 2 +waitfor $SSH_AUTH_SOCK +chmod 600 witness_key.pem +exec ssh-add witness_key.pem + +# start litewitness +exec litewitness -listen=localhost:7382 -ssh-agent=$SSH_AUTH_SOCK -name=example.com/witness -key=65cabc55d6547110f83cdfe96ff79a28e3185f8de8febac7c18fa1b7532d5e70 &litewitness& +waitfor localhost:7382 + +# add-checkpoint +exec hurl --test --error-format long add-checkpoint.hurl + +# check that litewitness shut down cleanly +killall +wait litewitness +stderr 'shutting down' + + +-- witness_key.pem -- +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz +c2gtZWQyNTUxOQAAACDHnZBBq5a4LUY4qcDYcNeJ1Y/N88jp8MSc0OvB1bdoNwAA +AIg1rkpuNa5KbgAAAAtzc2gtZWQyNTUxOQAAACDHnZBBq5a4LUY4qcDYcNeJ1Y/N +88jp8MSc0OvB1bdoNwAAAEBqlQdTPUkNspRSay6C9pt0CJBPfKZKFQmh70+lhEnq +O8edkEGrlrgtRjipwNhw14nVj83zyOnwxJzQ68HVt2g3AAAAAAECAwQF +-----END OPENSSH PRIVATE KEY----- + +-- add-checkpoint.hurl -- +POST http://localhost:7382/add-checkpoint +``` +old 0 + +example.com/mldsa-log +1 ++zoErtsZtGO6sfiSV/v4UJSIyowisE8hvjw0q5+bD38= + +— example.com/mldsa-log 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 +``` +HTTP 200 +[Asserts] +body contains "— example.com/witness" diff --git a/cmd/witnessctl/loglist.go b/cmd/witnessctl/loglist.go index de59578..b4cf9ac 100644 --- a/cmd/witnessctl/loglist.go +++ b/cmd/witnessctl/loglist.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "golang.org/x/mod/sumdb/note" + "filippo.io/torchwood" ) func parseLogList(logList []byte, verbose bool) (map[string]string, error) { @@ -21,7 +21,7 @@ func parseLogList(logList []byte, verbose bool) (map[string]string, error) { vkey = "" origin = "" }() - v, err := note.NewVerifier(vkey) + v, err := torchwood.NewLogVerifier(vkey) if err != nil { if verbose { log.Printf("Skipping invalid vkey %q: %v", vkey, err) diff --git a/cmd/witnessctl/loglist_test.go b/cmd/witnessctl/loglist_test.go new file mode 100644 index 0000000..a9e9fa5 --- /dev/null +++ b/cmd/witnessctl/loglist_test.go @@ -0,0 +1,88 @@ +package main + +import ( + "crypto/ed25519" + "crypto/rand" + "fmt" + "testing" + + "filippo.io/mldsa" + "filippo.io/torchwood" + "golang.org/x/mod/sumdb/note" +) + +func TestParseLogListAccepts(t *testing.T) { + mldsaOrigin := "example.com/mldsa-log" + mldsaKey, err := mldsa.GenerateKey(mldsa.MLDSA44()) + if err != nil { + t.Fatal(err) + } + mldsaSigner, err := torchwood.NewCosignatureSigner(mldsaOrigin, mldsaKey) + if err != nil { + t.Fatal(err) + } + mldsaVkey := mldsaSigner.Verifier().String() + + _, edVkey, err := note.GenerateKey(rand.Reader, "example.com/ed25519-log") + if err != nil { + t.Fatal(err) + } + + list := fmt.Sprintf(` +# Log list that should be accepted +logs/v0 + +# ML-DSA-44 log +vkey %s +qpd 24 + + # should be ignored +unknown-key some-value +contact test@mldsa-log + +# Ed25519 log +vkey %s +qpd 3600 +contact test@ed25519-log +`, mldsaVkey, edVkey) + logs, err := parseLogList([]byte(list), false) + if err != nil { + t.Fatal(err) + } + if len(logs) != 2 { + t.Errorf("expected two logs, got: %d", len(logs)) + } + if logs[mldsaOrigin] != mldsaVkey { + t.Errorf("ML-DSA log not parsed: %v", logs) + } + if logs["example.com/ed25519-log"] != edVkey { + t.Errorf("Ed25519 log not parsed: %v", logs) + } +} + +func TestParseLogListSkipsEd25519Cosignature(t *testing.T) { + origin := "example.com/ed-cosig-log" + _, k, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + signer, err := torchwood.NewCosignatureSigner(origin, k) + if err != nil { + t.Fatal(err) + } + vkey := signer.Verifier().String() + + list := fmt.Sprintf(` +logs/v0 +vkey %s +qpd 24 +contact test@ed25519-cosig +`, vkey) + logs, err := parseLogList([]byte(list), true) + if err != nil { + t.Fatal(err) + } + if len(logs) != 0 { + t.Errorf("expected Ed25519 cosignature log to be skipped, got: %v", logs) + } +} diff --git a/cmd/witnessctl/witnessctl.go b/cmd/witnessctl/witnessctl.go index 46634c5..cb51c9b 100644 --- a/cmd/witnessctl/witnessctl.go +++ b/cmd/witnessctl/witnessctl.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "filippo.io/torchwood" "filippo.io/torchwood/internal/witness" "golang.org/x/mod/sumdb/note" sigsum "sigsum.org/sigsum-go/pkg/crypto" @@ -135,7 +136,7 @@ func addLog(db *sqlite.Conn, origin string) { } func checkKeyMatches(origin string, vk string) { - v, err := note.NewVerifier(vk) + v, err := torchwood.NewLogVerifier(vk) if err != nil { log.Fatalf("Error parsing verifier key: %v", err) } diff --git a/internal/witness/witness.go b/internal/witness/witness.go index af942c5..ac2e2ae 100644 --- a/internal/witness/witness.go +++ b/internal/witness/witness.go @@ -385,7 +385,7 @@ func (w *Witness) getKeys(origin string) (note.Verifiers, error) { } var verifiers []note.Verifier for _, k := range keys { - v, err := note.NewVerifier(k) + v, err := torchwood.NewLogVerifier(k) if err != nil { w.log.Warn("invalid key in database", "key", k, "error", err) return nil, fmt.Errorf("invalid key %q: %v", k, err) diff --git a/policy.go b/policy.go index ecbd78b..c8ccb0a 100644 --- a/policy.go +++ b/policy.go @@ -153,7 +153,7 @@ func ParsePolicy(p []byte) (Policy, error) { if len(fields) < 2 { return nil, fmt.Errorf("line %d: invalid log definition: %q", i+1, line) } - v, err := newLogVerifier(fields[1]) + v, err := NewLogVerifier(fields[1]) if err != nil { return nil, fmt.Errorf("line %d: invalid log vkey %q: %w", i+1, fields[1], err) } @@ -228,9 +228,9 @@ func ParsePolicy(p []byte) (Policy, error) { } } -// newLogVerifier parses a log vkey, which may use the Ed25519 signature +// NewLogVerifier parses a log vkey, which may use the Ed25519 signature // algorithm or the ML-DSA-44 (sub)tree cosignature algorithm. -func newLogVerifier(vkey string) (note.Verifier, error) { +func NewLogVerifier(vkey string) (note.Verifier, error) { _, rest, _ := strings.Cut(vkey, "+") _, key64, _ := strings.Cut(rest, "+") key, err := base64.StdEncoding.DecodeString(key64)