-
Notifications
You must be signed in to change notification settings - Fork 10
Tidy up note package #271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tidy up note package #271
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| // Copyright 2023 The Go Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style | ||
| // license that can be found in the LICENSE file. | ||
|
|
||
| package note | ||
|
|
||
| import ( | ||
| "crypto/ed25519" | ||
| "errors" | ||
| "fmt" | ||
| "strings" | ||
| "strconv" | ||
| "encoding/base64" | ||
| "golang.org/x/mod/sumdb/note" | ||
| ) | ||
|
|
||
| // These algorithm identifiers correspond to those in https://c2sp.org/signed-note@v1.0.0 | ||
| const ( | ||
| algEd25519 = 1 | ||
| algECDSAWithSHA256 = 2 | ||
| algEd25519CosignatureV1 = 4 | ||
| algRFC6962STH = 5 | ||
| algMLDSA44 = 6 | ||
| ) | ||
|
|
||
| const ( | ||
| // keyHashSize is the size in bytes of the key hash. | ||
| keyHashSize = 4 | ||
| ) | ||
|
|
||
| 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") | ||
| errSignerHash = errors.New("invalid verifier hash") | ||
| ) | ||
|
|
||
| // NewSigner returns a new Signer for keys with the following algorithms: | ||
| // - 0x01 The original SumDB Note ed25519 algorithm. | ||
| // - 0x04 ed25519 Cosignature/V1 | ||
| // - 0x06 ML-DSA-44 Cosignature/V1 | ||
| func NewSigner(skey string) (Signer, error) { | ||
| priv1, k, _ := strings.Cut(skey, "+") | ||
| priv2, k, _ := strings.Cut(k, "+") | ||
| name, k, _ := strings.Cut(k, "+") | ||
| hash16, key64, _ := strings.Cut(k, "+") | ||
| 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:] | ||
| switch alg { | ||
| case algEd25519: | ||
| // We "re-implement" this here so as to be able to return a local Signer instance (as | ||
| // opposed to a note.Signer). | ||
| // This has the benefit that all signers from this package carry a Verifier() getter. | ||
|
|
||
| // Adapted from sumdb/note/note.go | ||
| hash, err := strconv.ParseUint(hash16, 16, 32) | ||
| if err != nil { | ||
| return nil, errSignerID | ||
| } | ||
|
|
||
| // Note: hash is the hash of the public key and we have the private key. | ||
| // Must verify hash after deriving public key. | ||
|
|
||
| s := &signer{ | ||
| name: name, | ||
| hash: uint32(hash), | ||
| } | ||
| if len(key) != 32 { | ||
| return nil, errSignerID | ||
| } | ||
| key = ed25519.NewKeyFromSeed(key) | ||
| pubkey := append([]byte{algEd25519}, key[32:]...) | ||
| if uint32(hash) != keyHashEd25519(name, pubkey) { | ||
| return nil, errSignerHash | ||
| } | ||
|
|
||
| s.sign = func(msg []byte) ([]byte, error) { | ||
| return ed25519.Sign(key, msg), nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is different than
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, this is in the |
||
| } | ||
| s.verify = func(msg, sig []byte) bool { | ||
| return ed25519.Verify(key, msg, sig) | ||
| } | ||
|
|
||
| return s, nil | ||
| case algEd25519CosignatureV1, algMLDSA44: | ||
| return NewSignerForCosignatureV1(skey) | ||
| default: | ||
| return nil, errSignerAlg | ||
| } | ||
| } | ||
|
|
||
| // The structs below are used by most/all of the signers and verifiers in this package. | ||
|
|
||
| // Signer is a note.Signer which also provides access to the corresponding Verifier. | ||
| type Signer interface { | ||
| note.Signer | ||
| Verifier() note.Verifier | ||
| } | ||
|
|
||
| // signer is a concrete implementation of the extended Signer interface above. | ||
| type signer struct { | ||
| name string | ||
| hash uint32 | ||
| sign func([]byte) ([]byte, error) | ||
| verify func(msg, sig []byte) bool | ||
| } | ||
|
|
||
| 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{ | ||
| name: s.name, | ||
| keyHash: s.hash, | ||
| v: s.verify, | ||
| } | ||
| } | ||
|
|
||
| // NewVerifier returns a verifier for the given key, if the key's algo is known. | ||
| func NewVerifier(key string) (note.Verifier, error) { | ||
| parts := strings.SplitN(key, "+", 3) | ||
| if got, want := len(parts), 3; got != want { | ||
| return nil, fmt.Errorf("key has %d parts, expected %d: %q", got, want, key) | ||
| } | ||
| keyBytes, err := base64.StdEncoding.DecodeString(parts[2]) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("key has invalid base64 %q: %v", parts[2], err) | ||
| } | ||
| if len(keyBytes) < 2 { | ||
| return nil, fmt.Errorf("invalid key, key bytes too short") | ||
| } | ||
|
|
||
| switch keyBytes[0] { | ||
| case algECDSAWithSHA256: | ||
| return NewECDSAVerifier(key) | ||
| case algEd25519CosignatureV1, algMLDSA44: | ||
| return NewVerifierForCosignatureV1(key) | ||
| case algRFC6962STH: | ||
| return NewRFC6962Verifier(key) | ||
| default: | ||
| return note.NewVerifier(key) | ||
| } | ||
| } | ||
|
|
||
| // verifier is a note-compatible verifier. | ||
| type verifier struct { | ||
| name string | ||
| keyHash uint32 | ||
| v func(msg, sig []byte) bool | ||
| } | ||
|
|
||
| // Name returns the name associated with the key this verifier is based on. | ||
| func (v *verifier) Name() string { | ||
| return v.name | ||
| } | ||
|
|
||
| // KeyHash returns a truncated hash of the key this verifier is based on. | ||
| func (v *verifier) KeyHash() uint32 { | ||
| return v.keyHash | ||
| } | ||
|
|
||
| // Verify checks that the provided sig is valid over msg for the key this verifier is based on. | ||
| func (v *verifier) Verify(msg, sig []byte) bool { | ||
| return v.v(msg, sig) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // Copyright 2026 The Go Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style | ||
| // license that can be found in the LICENSE file. | ||
|
|
||
| package note | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "testing" | ||
| "golang.org/x/mod/sumdb/note" | ||
| ) | ||
|
|
||
| func TestSignerVerifier(t *testing.T) { | ||
| ns, nv, err := note.GenerateKey(nil, "note") | ||
| if err != nil { | ||
| t.Fatalf("note.GenerateKey: %v", err) | ||
| } | ||
| ms, mv, err := GenerateMLDSAKey("mldsa") | ||
| if err != nil { | ||
| t.Fatalf("GenerateMLDSAKey: %v", err) | ||
| } | ||
|
|
||
| for _, test := range []struct { | ||
| name string | ||
| s string | ||
| v string | ||
| } { | ||
| {name: "note", s: ns, v: nv}, | ||
| {name: "mldsa", s: ms, v: mv}, | ||
| } { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| ns, err := NewSigner(test.s) | ||
| if err != nil { | ||
| t.Fatalf("NewSigner: %v", err) | ||
| } | ||
| nv, err := NewVerifier(test.v) | ||
| if err != nil { | ||
| t.Fatalf("NewVerifier: %v", err) | ||
| } | ||
| checkRoundTrip(t, fmt.Sprintf("%s\n0\nblah\n", test.name), ns, nv) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func checkRoundTrip(t *testing.T, msg string, s note.Signer, v note.Verifier) { | ||
| t.Helper() | ||
| signed, err := note.Sign(¬e.Note{Text: msg}, s) | ||
| if err != nil { | ||
| t.Errorf("%s failed to sign: %v", msg, err) | ||
| } | ||
| n, err := note.Open(signed, note.VerifierList(v)) | ||
| if err != nil { | ||
| t.Errorf("%s failed to open: %v", msg, err) | ||
| } | ||
| if n.Text != msg { | ||
| t.Errorf("got %q want %q", n.Text, msg) | ||
| } | ||
| } | ||
|
|
||
| func TestNoteInterop(t *testing.T) { | ||
| sk, vk, err := note.GenerateKey(nil, "example") | ||
| if err != nil { | ||
| t.Fatalf("GenerateKey: %v", err) | ||
| } | ||
|
|
||
| sf, err := NewSigner(sk) | ||
| if err != nil { | ||
| t.Fatalf("NewSigner(%q): %v", sk, err) | ||
| } | ||
| vf, err := NewVerifier(vk) | ||
| if err != nil { | ||
| t.Fatalf("NewVerifier: %v", err) | ||
| } | ||
|
|
||
| sn, err := note.NewSigner(sk) | ||
| if err != nil { | ||
| t.Fatalf("note.NewSigner: %v", err) | ||
| } | ||
| vn, err := note.NewVerifier(vk) | ||
| if err != nil { | ||
| t.Fatalf("note.NewVerifier: %v", err) | ||
| } | ||
|
|
||
| for i, s := range []note.Signer{sf, sn} { | ||
| for j, v := range []note.Verifier{vf, vn} { | ||
| msg := fmt.Sprintf("%d, %d\n", i, j) | ||
| checkRoundTrip(t, msg, s, v) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"invalid signer hash"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this is correct as is - the hash is over the verifier key, even when it's being checked as part of creating a new signer. (Also, this is how upstream names it: https://cs.opensource.google/go/x/mod/+/refs/tags/v0.38.0:sumdb/note/note.go;l=335)