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
2 changes: 1 addition & 1 deletion log/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (c *Checkpoint) Unmarshal(data []byte) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("invalid checkpoint - size invalid: %w", err)
}
h, err := base64.StdEncoding.DecodeString(string(l[2]))
h, err := base64.StdEncoding.Strict().DecodeString(string(l[2]))
if err != nil {
return nil, fmt.Errorf("invalid checkpoint - invalid hash: %w", err)
}
Expand Down
5 changes: 5 additions & 0 deletions log/checkpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ func TestUnmarshalLogState(t *testing.T) {
desc: "invalid roothash - not base64",
m: "Log\n123\nThisIsn'tBase64\n",
wantErr: true,
}, {
// "YmFuYW5hcw==" ("bananas") with non-zero padding bits ('w' -> 'x').
desc: "invalid roothash - non-canonical base64",
m: "Log\n123\nYmFuYW5hcx==\n",
wantErr: true,
}, {
desc: "invalid - CRLF on origin",
m: "Log\r\n123\nYmFuYW5hcw==\n",
Expand Down
8 changes: 8 additions & 0 deletions log/note_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ func TestParseCheckpoint(t *testing.T) {
sigs: []note.Signer{lns, k1ns, k2ns, uns},
wantErr: true,
wantSigs: 3,
}, {
// "YmFuYW5hcw==" ("bananas") with non-zero padding bits ('w' -> 'x').
desc: "non-canonical base64 in roothash",
logID: "TestParseCheckpoint",
noteBody: "TestParseCheckpoint\n42\nYmFuYW5hcx==\n",
sigs: []note.Signer{lns},
wantErr: true,
wantSigs: 1,
},
} {
t.Run(test.desc, func(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions note/note.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func NewSigner(skey string) (Signer, error) {
priv2, k, _ := strings.Cut(k, "+")
name, k, _ := strings.Cut(k, "+")
hash16, key64, _ := strings.Cut(k, "+")
key, err := base64.StdEncoding.DecodeString(key64)
key, err := base64.StdEncoding.Strict().DecodeString(key64)
if priv1 != "PRIVATE" || priv2 != "KEY" || len(hash16) != 8 || err != nil || !isValidName(name) || len(key) == 0 {
return nil, errSignerID
}
Expand Down Expand Up @@ -137,7 +137,7 @@ func NewVerifier(key string) (note.Verifier, error) {
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])
keyBytes, err := base64.StdEncoding.Strict().DecodeString(parts[2])
if err != nil {
return nil, fmt.Errorf("key has invalid base64 %q: %v", parts[2], err)
}
Expand Down
12 changes: 6 additions & 6 deletions note/note_cosigv1.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func NewMLDSASigner(skey string) (SubtreeSigner, error) {
priv2, skey, _ := strings.Cut(skey, "+")
name, skey, _ := strings.Cut(skey, "+")
hash16, key64, _ := strings.Cut(skey, "+")
key, err := base64.StdEncoding.DecodeString(key64)
key, err := base64.StdEncoding.Strict().DecodeString(key64)
if priv1 != "PRIVATE" || priv2 != "KEY" || len(hash16) != 8 || err != nil || !isValidName(name) || len(key) == 0 {
return nil, errSignerID
}
Expand Down Expand Up @@ -150,7 +150,7 @@ func NewMLDSAVerifier(vkey string) (SubtreeVerifier, error) {
}
name, vkey, _ := strings.Cut(vkey, "+")
hash16, key64, _ := strings.Cut(vkey, "+")
keyBytes, err := base64.StdEncoding.DecodeString(key64)
keyBytes, err := base64.StdEncoding.Strict().DecodeString(key64)
if len(hash16) != 8 || err != nil || !isValidName(name) || len(keyBytes) != mldsa.MLDSA44PublicKeySize+1 {
return nil, errVerifierID
}
Expand Down Expand Up @@ -190,7 +190,7 @@ func NewSignerForCosignatureV1(skey string) (Signer, error) {
priv2, skey, _ := strings.Cut(skey, "+")
name, skey, _ := strings.Cut(skey, "+")
hash16, key64, _ := strings.Cut(skey, "+")
key, err := base64.StdEncoding.DecodeString(key64)
key, err := base64.StdEncoding.Strict().DecodeString(key64)
if priv1 != "PRIVATE" || priv2 != "KEY" || len(hash16) != 8 || err != nil || !isValidName(name) || len(key) == 0 {
return nil, errSignerID
}
Expand Down Expand Up @@ -262,7 +262,7 @@ func NewVerifierForCosignatureV1(vkey string) (note.Verifier, error) {
}
name, vkey, _ := strings.Cut(vkey, "+")
hash16, key64, _ := strings.Cut(vkey, "+")
key, err := base64.StdEncoding.DecodeString(key64)
key, err := base64.StdEncoding.Strict().DecodeString(key64)
if len(hash16) != 8 || err != nil || !isValidName(name) || len(key) == 0 {
return nil, errVerifierID
}
Expand Down Expand Up @@ -305,7 +305,7 @@ func VKeyToCosignatureV1(vkey string) (string, error) {
}
name, vkey, _ := strings.Cut(vkey, "+")
hash16, key64, _ := strings.Cut(vkey, "+")
algKey, err := base64.StdEncoding.DecodeString(key64)
algKey, err := base64.StdEncoding.Strict().DecodeString(key64)
if len(hash16) != 8 || err != nil || !isValidName(name) || len(algKey) == 0 {
return "", errVerifierID
}
Expand Down Expand Up @@ -344,7 +344,7 @@ func CosignatureTimestamp(s note.Signature) (uint64, error) {
if strings.ContainsAny(s.Base64, "\r\n") {
return 0, errMalformedSig
}
r, err := base64.StdEncoding.DecodeString(s.Base64)
r, err := base64.StdEncoding.Strict().DecodeString(s.Base64)
if err != nil {
return 0, errMalformedSig
}
Expand Down
44 changes: 44 additions & 0 deletions note/note_cosigv1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,50 @@ func mustECDSASigner(t *testing.T) crypto.Signer {
return ecdsaK
}

func TestCosigV1_RejectNonCanonicalBase64(t *testing.T) {
// ML-DSA-44 vkey from TestMLDSAVerifyTorchwood (1313 bytes -> ends in "4=", where '4' has 00 padding bits; '5' sets a padding bit).
canonicalMLVK := "witness.example/w1+a0d0cd91+Bhh6t5zW2IMElD4GJw65/1+GnMosGFhPFst8+uCD+C9/lSovRbjzbFPV7YP9rox2T9pHbsqNmvR3GTnwSFLUZIatFUlXBt0CGLU1J75PMs7G1KNj5yL4R8vvInpzaDvlBUoCLjzegUOh7KImnowtDnJRX38puLf1mStewbP61oBYZmIlWhXproauXnBl0VGbqbd8qeLQ96ENJBhFATSuxbQoJZ3dhGwkHnEQUlqHdEPgYXKwuMWfzKz4X+54eDV4c+IOFcU3/Lmis2aqAG0LoAM4X+eE569R0YVmjXdybbqrCx6dc8gwM9DZp7+gUBwWtI6ccaXJoN6kmdPBC6qUv/PHlCaFCNq170DqAbFJauGRUOBKjGyP9mSQaF6yoNrkdEOsSh039J1uaqDD7X+g5KxFWwM+R1X2HibnEW6okJaIM+MaPsAnZeQLPYbaup/2emXDsHj58+b/5S03DRszJJStFlNxHnMTe+aNyj7vbU8/GD5B3T4es6HsvNbF5+w0PRmZ/1Kkkad4+D6aFj7pMU8eVCpMZtdU8zlYcHOyNZssna8mTL11jfq9NpD5wSo+CYAiDZ/hrW9QjUYpqwzUVTr9gN14mBXfax1g9iTxRAId1ZaHBoYqyitNpE2/797iJVEshYXeh49wGPsPUp+GQYUvYVGr0CmtEYEBoXXDYechA+G1dSyKGgeU5R5ufOkLnumvHW060RKxst3eXKMi1uNyEojsTUhHclL44a6irz95MAWE4k7d9E3Tu8S6mct5WVN4TzyFKZ7PPB+mIMhsbg6n5lI62D3gMKGHTJGbGz17iNZwj0FVp5trL4OGiF6qeK2FLXesOlWYOfOFXvfuLZXvNwmry3aSi/ST+65qXHmAGLQ0ip06A8yEUt3zGZ2VI8whagfAXj2CxcFcPVgg9KI+0aJnbldm6KYcaLoI+fs1iyGQ+zgXrejm/hCyGik+JFVfZGlH+hirnNNglxRsJzIYsFT73ss3+29SnAduHZOYFqLs1aNsDytZgEhC37EL8vqIO7jY1MR8ihldBljWiVV+AQiWpyKvseP5SFP82pcfkGi6DbAAQmifLjGGcm9pCqDIUWrXIyiwt/WbtTgzEuQhdtuYEFeK59BSdpXW9UxUAYG8AjlYL+ylVUjbtz9CYGHtaCoi3NxkaRaUZj2Mtx2WQSyDI2uUTUwOpiqVwJEAE3lpOBqHV58hEosDfAxR1A0bw7px4LFqsfuFcapsP1015sb2IIQn8zx8lfIVtTon2jYMyANqACceBA43Vu9+o7ij+wwEVedRsO5d6/F6KZjL+weSiaIzGRQUazSvkMGZr+3rgaol3wgHkBY/AvJRPqxEZA14+e+HVl7rOTR96o20g72AoDnTj+V5LpggrDEd2eJeYR+1SjphxPKE+Y4U4NJ/Zv68Cji9lmIXAo9ohBGxcjkPdw2Ha2HTE0WT3HwROtbBeFfEvKaAkke9VS5fGmPqkLXzuBmZ4OsRM5zwhW0mcZUjWp4anpuSyngVddL3NyTUg2UBoU3sQVAKr/VtboY0Fn6LuSi+jS6IXUhNkTfHEkZXnWN1oYnQs46/iWbJk0NxjPaCIYceU7DZTLpCuPOVRgm31FcF0AZT/T40gbgzAu7isTXhlOtswI0dpT9k2FidQPsrZh9QZr1lZjcyzqt0M2PeYcctUYY/Y75T5ZNBxxmgQ/EDwu6Y1RUAk9xa7ZrIeJ2ZHXGgwIT7yDQoj8c0nlb/k1qqMbqQey/63s4="
nonCanonicalMLVK := strings.TrimSuffix(canonicalMLVK, "4=") + "5="

if _, err := NewMLDSAVerifier(nonCanonicalMLVK); err == nil {
t.Error("NewMLDSAVerifier succeeded on non-canonical base64 vkey, want error")
}
if _, err := NewVerifierForCosignatureV1(nonCanonicalMLVK); err == nil {
t.Error("NewVerifierForCosignatureV1 succeeded on non-canonical base64 vkey, want error")
}
if _, err := NewVerifier(nonCanonicalMLVK); err == nil {
t.Error("NewVerifier succeeded on non-canonical base64 vkey, want error")
}

// 76-byte Ed25519 cosigv1 signature ending in "BA==" ('A' -> 'B' sets a padding bit).
nonCanonicalSig := note.Signature{Base64: "ZGhGuQAAAABm/qTPeyKXD+R2rzyQsxPiP8mXum7qq/iF0u4vanlqJyocWODBt97w9uL+8qT7S5gxEHWWOworDcFiEBYJXORmnFBOBB=="}
if _, err := CosignatureTimestamp(nonCanonicalSig); err == nil {
t.Error("CosignatureTimestamp succeeded on non-canonical base64 signature, want error")
}
if _, err := CoSigV1Timestamp(nonCanonicalSig); err == nil {
t.Error("CoSigV1Timestamp succeeded on non-canonical base64 signature, want error")
}

// ML-DSA-44 checkpoint signing and verification with non-canonical base64 root hash ('0=' -> '1=').
mlSk, _ := mustGenerateMLDSAKey(t, "mldsa")
mlSigner, err := NewMLDSASigner(mlSk)
if err != nil {
t.Fatal(err)
}
validMsg := []byte("test\n123\nf+7CoKgXKE/tNys9TTXcr/ad6U/K3xvznmzew9y6SP0=\n")
nonCanonicalMsg := []byte("test\n123\nf+7CoKgXKE/tNys9TTXcr/ad6U/K3xvznmzew9y6SP1=\n")
if _, err := mlSigner.Sign(nonCanonicalMsg); err == nil {
t.Error("MLDSA Sign succeeded on checkpoint with non-canonical base64 root hash, want error")
}
sig, err := mlSigner.Sign(validMsg)
if err != nil {
t.Fatalf("Sign(validMsg): %v", err)
}
if mlSigner.Verifier().Verify(nonCanonicalMsg, sig) {
t.Error("MLDSA Verifier().Verify() = true on checkpoint with non-canonical base64 root hash, want false")
}
}

func TestCosigV1_RejectCRLF(t *testing.T) {
edSk, edVk := mustGenerateEd25519Key(t, "ed25519")
mlSk, mlVk := mustGenerateMLDSAKey(t, "mldsa")
Expand Down
6 changes: 3 additions & 3 deletions note/note_rfc6962.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewRFC6962Verifier(vkey string) (note.Verifier, error) {
}
name, vkey, _ := strings.Cut(vkey, "+")
hash16, key64, _ := strings.Cut(vkey, "+")
key, err := base64.StdEncoding.DecodeString(key64)
key, err := base64.StdEncoding.Strict().DecodeString(key64)
if len(hash16) != 8 || err != nil || !isValidName(name) || len(key) == 0 {
return nil, errVerifierID
}
Expand Down Expand Up @@ -120,7 +120,7 @@ func RFC6962STHTimestamp(s note.Signature) (time.Time, error) {
if strings.ContainsAny(s.Base64, "\r\n") {
return time.UnixMilli(0), errMalformedSig
}
r, err := base64.StdEncoding.DecodeString(s.Base64)
r, err := base64.StdEncoding.Strict().DecodeString(s.Base64)
if err != nil {
return time.UnixMilli(0), errMalformedSig
}
Expand Down Expand Up @@ -245,7 +245,7 @@ func formatRFC6962STH(t uint64, msg []byte) (string, []byte, error) {
if err != nil {
return "", nil, err
}
root, err := base64.StdEncoding.DecodeString(lines[2])
root, err := base64.StdEncoding.Strict().DecodeString(lines[2])
if err != nil {
return "", nil, err
}
Expand Down
24 changes: 24 additions & 0 deletions note/note_rfc6962_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ func TestVerify(t *testing.T) {
wantErr bool
}{
{
// "2q1K6aiIJR+F7TyhiWOghoWOjY0/3dVBLsBbAvB4xCw=" with non-zero padding bits ('w=' -> 'x=').
name: "non-canonical base64 root hash",
cp: []byte(strings.Replace(romeCP, "4xCw=", "4xCx=", 1)),
verifier: "rome.ct.filippo.io/2024h1+78f4abae+BTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAFzPC3fap+uINc1RQ4eRbYMUt84+bKkA8GDLN8KwVdzAgYhYSv4kS8XSheGLAHCWhIJTJbuC3sL88bNMTtrsBM=",
wantErr: true,
}, {
name: "works - rome",
cp: []byte(romeCP),
verifier: "rome.ct.filippo.io/2024h1+78f4abae+BTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAFzPC3fap+uINc1RQ4eRbYMUt84+bKkA8GDLN8KwVdzAgYhYSv4kS8XSheGLAHCWhIJTJbuC3sL88bNMTtrsBM=",
Expand Down Expand Up @@ -224,3 +230,21 @@ func TestRFC6962_RejectCRLF(t *testing.T) {
t.Error("RFC6962 Verifier.Verify() = true with CRLF on root hash line, want false")
}
}

func TestRFC6962_RejectNonCanonicalBase64(t *testing.T) {
// 'M=' (index 12 = 001100) -> 'N=' (index 13 = 001101, non-zero padding bit).
nonCanonicalVK := "rome.ct.filippo.io/2024h1+78f4abae+BTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAFzPC3fap+uINc1RQ4eRbYMUt84+bKkA8GDLN8KwVdzAgYhYSv4kS8XSheGLAHCWhIJTJbuC3sL88bNMTtrsBN="
if _, err := NewRFC6962Verifier(nonCanonicalVK); err == nil {
t.Error("NewRFC6962Verifier succeeded on non-canonical base64 vkey, want error")
}
if _, err := NewVerifier(nonCanonicalVK); err == nil {
t.Error("NewVerifier succeeded on non-canonical base64 RFC6962 vkey, want error")
}

// 16-byte payload encoded in base64 ends in "=="; mutating 'A==' to 'B==' sets a padding bit.
nonCanonicalSig := base64.StdEncoding.EncodeToString(make([]byte, 16))
nonCanonicalSig = strings.TrimSuffix(nonCanonicalSig, "A==") + "B=="
if _, err := RFC6962STHTimestamp(note.Signature{Base64: nonCanonicalSig}); err == nil {
t.Error("RFC6962STHTimestamp succeeded on non-canonical base64 signature, want error")
}
}
4 changes: 2 additions & 2 deletions note/note_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func NewEd25519SignerVerifier(skey string) (note.Signer, note.Verifier, error) {
if parts[0] != "PRIVATE" || parts[1] != "KEY" {
return nil, nil, fmt.Errorf("expected first tokens to be [PRIVATE, KEY]")
}
key, err := base64.StdEncoding.DecodeString(parts[4])
key, err := base64.StdEncoding.Strict().DecodeString(parts[4])
if err != nil {
return nil, nil, fmt.Errorf("failed to decode base64: %v", err)
}
Expand Down Expand Up @@ -99,7 +99,7 @@ func NewECDSAVerifier(key string) (note.Verifier, error) {
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])
keyBytes, err := base64.StdEncoding.Strict().DecodeString(parts[2])
if err != nil {
return nil, fmt.Errorf("key has invalid base64 %q: %v", parts[2], err)
}
Expand Down
11 changes: 11 additions & 0 deletions note/note_verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package note

import (
"strings"
"testing"

"golang.org/x/mod/sumdb/note"
Expand Down Expand Up @@ -84,6 +85,11 @@ func TestNewVerifier(t *testing.T) {
}, {
name: "Static CT works",
key: "ct.googleapis.com/logs/us1/argon2024+7deb49d0+BTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABB25bKnLaZTFXOa2pgO70rjcVEMXKJkMBgFQHZ1kwFlGK9zIAx0FtC2oCfeZQe0E++VXuiYE9hFSzhRlOy92K8A=",
}, {
// "WLc=" ('c' = 011100) -> "WLd=" ('d' = 011101, non-zero padding bit).
name: "ECDSA non-canonical base64",
key: strings.TrimSuffix(sigStoreKey, "c=") + "d=",
wantErr: true,
},
} {
t.Run(test.name, func(t *testing.T) {
Expand Down Expand Up @@ -115,6 +121,11 @@ func TestNewECDSAVerifier(t *testing.T) {
name: "invalid base64",
pubK: "rekor.sigstore.dev+12345678+THIS_IS_NOT_BASE64!",
wantErr: true,
}, {
// "WLc=" ('c' = 011100) -> "WLd=" ('d' = 011101, non-zero padding bit).
name: "non-canonical base64",
pubK: strings.TrimSuffix(sigStoreKey, "c=") + "d=",
wantErr: true,
}, {
name: "invalid algo",
pubK: "rekor.sigstore.dev+12345678+AwEB",
Expand Down
2 changes: 1 addition & 1 deletion policy/tlog_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ func vkeyPublicKey(vkey string) ([]byte, error) {
if got, want := len(parts), 3; got != want {
return nil, fmt.Errorf("vkey has %d parts, expected %d", got, want)
}
key, err := base64.StdEncoding.DecodeString(parts[2])
key, err := base64.StdEncoding.Strict().DecodeString(parts[2])
if err != nil {
return nil, fmt.Errorf("vkey has invalid base64: %v", err)
}
Expand Down
16 changes: 16 additions & 0 deletions policy/tlog_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ func TestUnmarshalLogVerifierName(t *testing.T) {
}

func TestUnmarshal_Errors(t *testing.T) {
_, mldsaVkey, err := f_note.GenerateMLDSAKey("mldsa.example.com")
if err != nil {
t.Fatalf("GenerateMLDSAKey() failed: %v", err)
}
// 1313-byte ML-DSA-44 vkey ends in '=' with 2 zero padding bits; incrementing the char before '=' sets the lowest padding bit.
nonCanonicalMLDSAVkey := mldsaVkey[:len(mldsaVkey)-2] + string(mldsaVkey[len(mldsaVkey)-2]+1) + "="
ecdsaVkey := fmt.Sprintf("ecdsa.example.com+00000000+%s", base64.StdEncoding.EncodeToString([]byte{0x02, 1, 2, 3}))
// A well-formed vkey with an unknown algorithm octet and a correctly
// computed key id, which golang.org/x/mod/sumdb/note validates.
Expand Down Expand Up @@ -265,6 +271,16 @@ func TestUnmarshal_Errors(t *testing.T) {
policy: "log garbage\nquorum none\n",
errStr: "invalid log vkey",
},
{
desc: "log with non-canonical base64 vkey",
policy: fmt.Sprintf("log %s\nquorum none\n", nonCanonicalMLDSAVkey),
errStr: "invalid log vkey",
},
{
desc: "witness with non-canonical base64 vkey",
policy: fmt.Sprintf("witness w1 %s\nquorum w1\n", nonCanonicalMLDSAVkey),
errStr: "invalid witness vkey",
},
{
desc: "log with unknown key type",
policy: fmt.Sprintf("log %s\nquorum none\n", unknownAlgVkey),
Expand Down
Loading