From 1833eb38f0524c99bcff11907afb9e0b8bfd8b53 Mon Sep 17 00:00:00 2001 From: Martin Hutchinson Date: Tue, 3 Nov 2026 17:09:43 +0000 Subject: [PATCH] Enforce canonical Base64 decoding across log, note, and policy C2SP specs (tlog-checkpoint, signed-note, tlog-cosignature, and tlog-policy) require RFC 4648 Section 3.5 canonical Base64 encoding and mandate that decoders reject non-canonical encodings (non-zero padding bits). Previously, proof.TLogProof.Unmarshal used base64.StdEncoding.Strict(), while log.Checkpoint.Unmarshal, note key/signature/checkpoint decoders, and policy.vkeyPublicKey used base64.StdEncoding.DecodeString, which silently ignored non-zero padding bits in checkpoint root hashes, ML-DSA-44/ECDSA/RFC6962 vkeys, and cosignature timestamps. Switch all remaining production base64.StdEncoding.DecodeString calls to base64.StdEncoding.Strict().DecodeString and add tests across log, note, and policy. --- log/checkpoint.go | 2 +- log/checkpoint_test.go | 5 +++++ log/note_test.go | 8 +++++++ note/note.go | 4 ++-- note/note_cosigv1.go | 12 +++++------ note/note_cosigv1_test.go | 44 ++++++++++++++++++++++++++++++++++++++ note/note_rfc6962.go | 6 +++--- note/note_rfc6962_test.go | 24 +++++++++++++++++++++ note/note_verifier.go | 4 ++-- note/note_verifier_test.go | 11 ++++++++++ policy/tlog_policy.go | 2 +- policy/tlog_policy_test.go | 16 ++++++++++++++ 12 files changed, 123 insertions(+), 15 deletions(-) diff --git a/log/checkpoint.go b/log/checkpoint.go index a434a8a..97c4e65 100644 --- a/log/checkpoint.go +++ b/log/checkpoint.go @@ -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) } diff --git a/log/checkpoint_test.go b/log/checkpoint_test.go index 6d453cb..fda4d15 100644 --- a/log/checkpoint_test.go +++ b/log/checkpoint_test.go @@ -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", diff --git a/log/note_test.go b/log/note_test.go index fb8d4a6..f61f5b1 100644 --- a/log/note_test.go +++ b/log/note_test.go @@ -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) { diff --git a/note/note.go b/note/note.go index ab415fe..0098827 100644 --- a/note/note.go +++ b/note/note.go @@ -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 } @@ -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) } diff --git a/note/note_cosigv1.go b/note/note_cosigv1.go index db8fc59..8b91318 100644 --- a/note/note_cosigv1.go +++ b/note/note_cosigv1.go @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } diff --git a/note/note_cosigv1_test.go b/note/note_cosigv1_test.go index ab723b4..9276503 100644 --- a/note/note_cosigv1_test.go +++ b/note/note_cosigv1_test.go @@ -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") diff --git a/note/note_rfc6962.go b/note/note_rfc6962.go index ab504f1..bb48036 100644 --- a/note/note_rfc6962.go +++ b/note/note_rfc6962.go @@ -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 } @@ -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 } @@ -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 } diff --git a/note/note_rfc6962_test.go b/note/note_rfc6962_test.go index 5f9390d..bd63264 100644 --- a/note/note_rfc6962_test.go +++ b/note/note_rfc6962_test.go @@ -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=", @@ -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") + } +} diff --git a/note/note_verifier.go b/note/note_verifier.go index f71c30f..be1421a 100644 --- a/note/note_verifier.go +++ b/note/note_verifier.go @@ -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) } @@ -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) } diff --git a/note/note_verifier_test.go b/note/note_verifier_test.go index 54fcdf9..830b4de 100644 --- a/note/note_verifier_test.go +++ b/note/note_verifier_test.go @@ -15,6 +15,7 @@ package note import ( + "strings" "testing" "golang.org/x/mod/sumdb/note" @@ -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) { @@ -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", diff --git a/policy/tlog_policy.go b/policy/tlog_policy.go index eebf8ab..aa8409d 100644 --- a/policy/tlog_policy.go +++ b/policy/tlog_policy.go @@ -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) } diff --git a/policy/tlog_policy_test.go b/policy/tlog_policy_test.go index 3708e5e..a318cd9 100644 --- a/policy/tlog_policy_test.go +++ b/policy/tlog_policy_test.go @@ -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. @@ -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),