Skip to content

Commit 404c0d5

Browse files
authored
Add support for native CosignatureV1 verifier keys (#216)
Enables the creation and use of CosignatureV1 (algo 0x04) vkeys.
1 parent c085883 commit 404c0d5

2 files changed

Lines changed: 81 additions & 4 deletions

File tree

note/note_cosigv1.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"errors"
1414
"fmt"
1515
"strings"
16+
"strconv"
1617
"time"
1718
"unicode"
1819
"unicode/utf8"
@@ -81,9 +82,9 @@ func NewSignerForCosignatureV1(skey string) (*Signer, error) {
8182
}
8283

8384
// NewVerifierForCosignatureV1 constructs a new Verifier for timestamped
84-
// cosignature/v1 signatures from a standard Ed25519 encoded verifier key.
85+
// cosignature/v1 signatures from either a standard Ed25519 encoded verifier key, or an Ed25519 CosignatureV1 key.
8586
//
86-
// (The returned Verifier has a different key hash from a non-timestamped one,
87+
// (In the case of passing a standard Ed25519 key, the returned Verifier has a different key hash from a non-timestamped one,
8788
// meaning it will differ from the key hash in the input encoding.)
8889
func NewVerifierForCosignatureV1(vkey string) (note.Verifier, error) {
8990
name, vkey, _ := strings.Cut(vkey, "+")
@@ -102,7 +103,7 @@ func NewVerifierForCosignatureV1(vkey string) (note.Verifier, error) {
102103
default:
103104
return nil, errVerifierAlg
104105

105-
case algEd25519:
106+
case algEd25519, algEd25519CosignatureV1:
106107
if len(key) != 32 {
107108
return nil, errVerifierID
108109
}
@@ -113,6 +114,36 @@ func NewVerifierForCosignatureV1(vkey string) (note.Verifier, error) {
113114
return v, nil
114115
}
115116

117+
// VKeyToCosignatureV1 converts a standard Ed25519 vkey to an Ed25519CosignatureV1 vkey.
118+
func VKeyToCosignatureV1(vkey string) (string, error) {
119+
name, vkey, _ := strings.Cut(vkey, "+")
120+
hash16, key64, _ := strings.Cut(vkey, "+")
121+
algKey, err := base64.StdEncoding.DecodeString(key64)
122+
if len(hash16) != 8 || err != nil || !isValidName(name) || len(algKey) == 0 {
123+
return "", errVerifierID
124+
}
125+
126+
alg, key := algKey[0], algKey[1:]
127+
if alg != algEd25519 {
128+
return "", errVerifierAlg
129+
}
130+
hash, err := strconv.ParseUint(hash16, 16, 32)
131+
if err != nil {
132+
return "", errInvalidHash
133+
}
134+
135+
if uint32(hash) != keyHashEd25519(name, algKey) {
136+
return "", errInvalidHash
137+
}
138+
if len(key) != 32 {
139+
return "", errVerifierID
140+
}
141+
pubKey := append([]byte{algEd25519CosignatureV1}, key...)
142+
h := keyHashEd25519(name, pubKey)
143+
144+
return fmt.Sprintf("%s+%08x+%s", name, h, base64.StdEncoding.EncodeToString(pubKey)), nil
145+
}
146+
116147
// CoSigV1Timestamp extracts the embedded timestamp from a CoSigV1 signature.
117148
func CoSigV1Timestamp(s note.Signature) (time.Time, error) {
118149
r, err := base64.StdEncoding.DecodeString(s.Base64)
@@ -172,6 +203,7 @@ var (
172203
errSignerAlg = errors.New("unknown signer algorithm")
173204
errVerifierID = errors.New("malformed verifier id")
174205
errVerifierAlg = errors.New("unknown verifier algorithm")
206+
errInvalidHash = errors.New("invalid key hash")
175207
errMalformedSig = errors.New("malformed signature")
176208
)
177209

note/note_cosigv1_test.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,11 @@ func TestCoSigV1NewVerifier(t *testing.T) {
112112
wantErr bool
113113
}{
114114
{
115-
name: "works",
115+
name: "works: convert from algEd25519",
116116
pubK: "TEST+7997405c+AQcC+FTVKf0jlTdHDY3rbevmnKxxPjigCXlVtGe6RIr6",
117+
}, {
118+
name: "works: native algEd25519CosignatureV1 verifier",
119+
pubK: "remora.n621.de+da77ade7+BOvN63jn/bLvkieywe8R6UYAtVtNbZpXh34x7onlmtw2",
117120
}, {
118121
name: "wrong number of parts",
119122
pubK: "bananas.sigstore.dev+12344556",
@@ -179,3 +182,45 @@ func TestCoSigV1Timestamp(t *testing.T) {
179182
})
180183
}
181184
}
185+
186+
func TestVKeyToCosignatureV1(t *testing.T) {
187+
skey, vkey, err := note.GenerateKey(rand.Reader, "TestKey")
188+
if err != nil {
189+
t.Fatalf("Failed to generate keys: %v", err)
190+
}
191+
cosigner, err := NewSignerForCosignatureV1(skey)
192+
if err != nil {
193+
t.Fatalf("Failed to create cosignerv1: %v", err)
194+
}
195+
covkey, err := VKeyToCosignatureV1(vkey)
196+
if err != nil {
197+
t.Fatalf("Failed to convert vkey to cosigv1 verifier: %v", err)
198+
}
199+
workingVKeys := []string{
200+
vkey,
201+
covkey,
202+
}
203+
n, err := note.Sign(&note.Note{Text: "Note\n\n"}, cosigner)
204+
if err != nil {
205+
t.Fatalf("Failed to sign note: %v", err)
206+
}
207+
for _, k := range workingVKeys {
208+
coverifier, err := NewVerifierForCosignatureV1(k)
209+
if err != nil {
210+
t.Errorf("Failed to create verifier from %q: %v", k, err)
211+
continue
212+
}
213+
if _, err = note.Open(n, note.VerifierList(coverifier)); err != nil {
214+
t.Errorf("Failed to open note with verifier %q: %v", k, err)
215+
}
216+
}
217+
218+
v, err := note.NewVerifier(vkey)
219+
if err != nil {
220+
t.Fatalf("Failed to create standard verifier: %v", err)
221+
}
222+
// Now check that the standard vkey cannot open a cosig signature.
223+
if _, err = note.Open(n, note.VerifierList(v)); err == nil {
224+
t.Errorf("Expected error trying to open cosigned note with standard vkey, but got success")
225+
}
226+
}

0 commit comments

Comments
 (0)