Freeze catalog signature vectors (T-614)

This commit is contained in:
ila
2026-07-18 16:46:17 +08:00
parent 9e5f3f4840
commit 0c1b7662c6
11 changed files with 370 additions and 32 deletions
+76 -1
View File
@@ -10,12 +10,15 @@ import (
"unicode/utf8"
)
var integerJSONNumber = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)
var integerJSONNumber = regexp.MustCompile(`^(0|[1-9][0-9]*|-[1-9][0-9]*)$`)
func parseRestrictedJSON(data []byte) (any, error) {
if !utf8.Valid(data) {
return nil, fmt.Errorf("%w: input is not valid UTF-8", ErrInvalidDocument)
}
if err := validateJSONStringSurrogates(data); err != nil {
return nil, err
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
@@ -33,6 +36,78 @@ func parseRestrictedJSON(data []byte) (any, error) {
return value, nil
}
func validateJSONStringSurrogates(data []byte) error {
for index := 0; index < len(data); index++ {
if data[index] != '"' {
continue
}
next, err := scanJSONStringSurrogates(data, index)
if err != nil {
return err
}
index = next - 1
}
return nil
}
func scanJSONStringSurrogates(data []byte, start int) (int, error) {
for index := start + 1; index < len(data); index++ {
switch data[index] {
case '"':
return index + 1, nil
case '\\':
if index+1 >= len(data) {
return 0, fmt.Errorf("%w: incomplete string escape", ErrInvalidDocument)
}
if data[index+1] != 'u' {
index++
continue
}
codeUnit, ok := decodeJSONHexCodeUnit(data, index+2)
if !ok {
return 0, fmt.Errorf("%w: invalid unicode escape", ErrInvalidDocument)
}
switch {
case codeUnit >= 0xd800 && codeUnit <= 0xdbff:
if index+7 >= len(data) || data[index+6] != '\\' || data[index+7] != 'u' {
return 0, fmt.Errorf("%w: high surrogate is not paired", ErrInvalidDocument)
}
lowSurrogate, ok := decodeJSONHexCodeUnit(data, index+8)
if !ok || lowSurrogate < 0xdc00 || lowSurrogate > 0xdfff {
return 0, fmt.Errorf("%w: high surrogate is not followed by a low surrogate", ErrInvalidDocument)
}
index += 11
case codeUnit >= 0xdc00 && codeUnit <= 0xdfff:
return 0, fmt.Errorf("%w: low surrogate has no high surrogate", ErrInvalidDocument)
default:
index += 5
}
}
}
return 0, fmt.Errorf("%w: unterminated string", ErrInvalidDocument)
}
func decodeJSONHexCodeUnit(data []byte, start int) (uint16, bool) {
if start+4 > len(data) {
return 0, false
}
var value uint16
for _, digit := range data[start : start+4] {
value <<= 4
switch {
case digit >= '0' && digit <= '9':
value |= uint16(digit - '0')
case digit >= 'a' && digit <= 'f':
value |= uint16(digit-'a') + 10
case digit >= 'A' && digit <= 'F':
value |= uint16(digit-'A') + 10
default:
return 0, false
}
}
return value, true
}
func decodeJSONValue(decoder *json.Decoder) (any, error) {
token, err := decoder.Token()
if err != nil {
+171
View File
@@ -0,0 +1,171 @@
package catalog
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"testing"
)
type canonicalVectorCorpus struct {
SchemaVersion int `json:"schema_version"`
PublicKeyBase64 string `json:"public_key_base64"`
Vectors []canonicalVector `json:"vectors"`
}
type canonicalVector struct {
Name string `json:"name"`
Document string `json:"document"`
SignedPayloadBase64 string `json:"signed_payload_base64"`
Signature string `json:"signature"`
WantError string `json:"want_error"`
}
func TestVerifierCanonicalVectors(t *testing.T) {
corpus := readCanonicalVectorCorpus(t)
publicKey, err := base64.StdEncoding.DecodeString(corpus.PublicKeyBase64)
if err != nil {
t.Fatalf("decode corpus public key: %v", err)
}
verifier, err := NewVerifier(publicKey)
if err != nil {
t.Fatalf("NewVerifier() error = %v", err)
}
for _, vector := range corpus.Vectors {
vector := vector
t.Run(vector.Name, func(t *testing.T) {
verified, err := verifier.Verify([]byte(vector.Document))
if vector.WantError != "" {
want := canonicalVectorError(t, vector.WantError)
if !errors.Is(err, want) {
t.Fatalf("Verify() error = %v, want %v", err, want)
}
return
}
if err != nil {
t.Fatalf("Verify() error = %v", err)
}
expectedPayload, err := base64.StdEncoding.DecodeString(vector.SignedPayloadBase64)
if err != nil {
t.Fatalf("decode static signed payload: %v", err)
}
if !bytes.Equal(verified.SignedPayload, expectedPayload) {
t.Fatalf(
"SignedPayload = %q, want static vector %q",
verified.SignedPayload,
expectedPayload,
)
}
signature, err := base64.StdEncoding.DecodeString(vector.Signature)
if err != nil {
t.Fatalf("decode static signature: %v", err)
}
if !ed25519.Verify(ed25519.PublicKey(publicKey), expectedPayload, signature) {
t.Fatal("static signature does not verify the static signed payload")
}
if got := vectorDocumentSignature(t, vector.Document); got != vector.Signature {
t.Fatalf("document signature = %q, want static vector %q", got, vector.Signature)
}
})
}
}
func TestParserRejectsNonCanonicalSignatureVectorText(t *testing.T) {
corpus := readCanonicalVectorCorpus(t)
for _, vector := range corpus.Vectors {
if vector.WantError != "signature_invalid" {
continue
}
vector := vector
t.Run(vector.Name, func(t *testing.T) {
if err := validateSignature(vectorDocumentSignature(t, vector.Document)); err == nil {
t.Fatal("validateSignature() accepted a non-canonical signature text")
}
})
}
}
func TestParserRejectsNonCanonicalPackageSignatureVectors(t *testing.T) {
corpus := readCanonicalVectorCorpus(t)
for _, vector := range corpus.Vectors {
if vector.WantError != "signature_invalid" {
continue
}
vector := vector
t.Run(vector.Name, func(t *testing.T) {
manifest := validManifestForTest()
publishedPackage := manifest.Apps[0].Packages[ArchitectureAMD64]
publishedPackage.Signature = vectorDocumentSignature(t, vector.Document)
manifest.Apps[0].Packages[ArchitectureAMD64] = publishedPackage
_, err := parseSignedManifestForTest(t, manifest, ChannelModern)
if !errors.Is(err, ErrInvalidManifest) {
t.Fatalf("Parse() error = %v, want %v", err, ErrInvalidManifest)
}
})
}
}
func readCanonicalVectorCorpus(t *testing.T) canonicalVectorCorpus {
t.Helper()
path := filepath.Join("..", "..", "testdata", "catalog", "canonical-vectors.json")
file, err := os.Open(path)
if err != nil {
t.Fatalf("open canonical vector corpus: %v", err)
}
defer file.Close()
decoder := json.NewDecoder(file)
decoder.DisallowUnknownFields()
var corpus canonicalVectorCorpus
if err := decoder.Decode(&corpus); err != nil {
t.Fatalf("decode canonical vector corpus: %v", err)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
t.Fatalf("canonical vector corpus has trailing data: %v", err)
}
if corpus.SchemaVersion != 1 {
t.Fatalf("corpus schema_version = %d, want 1", corpus.SchemaVersion)
}
if len(corpus.Vectors) == 0 {
t.Fatal("corpus has no vectors")
}
return corpus
}
func canonicalVectorError(t *testing.T, value string) error {
t.Helper()
switch value {
case "invalid_document":
return ErrInvalidDocument
case "unsupported_number":
return ErrUnsupportedNumber
case "signature_invalid":
return ErrSignatureInvalid
default:
t.Fatalf("unsupported corpus want_error %q", value)
return nil
}
}
func vectorDocumentSignature(t *testing.T, document string) string {
t.Helper()
var root struct {
Signature string `json:"signature"`
}
if err := json.Unmarshal([]byte(document), &root); err != nil {
t.Fatalf("decode vector document signature: %v", err)
}
if root.Signature == "" {
t.Fatal("vector document has no signature")
}
return root.Signature
}
+2 -6
View File
@@ -2,7 +2,6 @@ package catalog
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
@@ -231,12 +230,9 @@ func validatePackage(architecture Architecture, publishedPackage Package) error
}
func validateSignature(value string) error {
signature, err := base64.StdEncoding.Strict().DecodeString(value)
_, err := decodeCanonicalSignature(value)
if err != nil {
return fmt.Errorf("must be strict Base64: %v", err)
}
if len(signature) != 64 {
return fmt.Errorf("must decode to 64 bytes")
return fmt.Errorf("must be canonical padded Base64 for 64 bytes: %v", err)
}
return nil
}
+20 -10
View File
@@ -66,17 +66,9 @@ func (verifier Verifier) Verify(document []byte) (VerifiedDocument, error) {
if err != nil {
return VerifiedDocument{}, err
}
signature, err := base64.StdEncoding.Strict().DecodeString(signatureText)
signature, err := decodeCanonicalSignature(signatureText)
if err != nil {
return VerifiedDocument{}, fmt.Errorf("%w: base64: %v", ErrSignatureInvalid, err)
}
if len(signature) != ed25519.SignatureSize {
return VerifiedDocument{}, fmt.Errorf(
"%w: got %d signature bytes, want %d",
ErrSignatureInvalid,
len(signature),
ed25519.SignatureSize,
)
return VerifiedDocument{}, fmt.Errorf("%w: %v", ErrSignatureInvalid, err)
}
if !ed25519.Verify(verifier.publicKey, signedPayload, signature) {
return VerifiedDocument{}, ErrSignatureInvalid
@@ -87,3 +79,21 @@ func (verifier Verifier) Verify(document []byte) (VerifiedDocument, error) {
SignedPayload: append([]byte(nil), signedPayload...),
}, nil
}
func decodeCanonicalSignature(value string) ([]byte, error) {
signature, err := base64.StdEncoding.Strict().DecodeString(value)
if err != nil {
return nil, fmt.Errorf("invalid standard Base64: %w", err)
}
if base64.StdEncoding.EncodeToString(signature) != value {
return nil, errors.New("signature must use canonical padded Base64")
}
if len(signature) != ed25519.SignatureSize {
return nil, fmt.Errorf(
"got %d signature bytes, want %d",
len(signature),
ed25519.SignatureSize,
)
}
return signature, nil
}