Freeze catalog signature vectors (T-614)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user