Files
soft_quay/core/catalog/verifier.go
T

100 lines
2.8 KiB
Go

package catalog
import (
"crypto/ed25519"
"encoding/base64"
"errors"
"fmt"
)
var (
ErrInvalidDocument = errors.New("invalid catalog document")
ErrDuplicateField = errors.New("duplicate catalog field")
ErrUnsupportedNumber = errors.New("unsupported catalog number")
ErrSignatureMissing = errors.New("catalog signature missing")
ErrSignatureInvalid = errors.New("catalog signature invalid")
ErrPublicKeyInvalid = errors.New("catalog public key invalid")
)
// VerifiedDocument contains the original signed document and its signing bytes.
type VerifiedDocument struct {
Bytes []byte
SignedPayload []byte
}
// Verifier validates signed Catalog JSON documents with one Ed25519 public key.
type Verifier struct {
publicKey ed25519.PublicKey
}
// NewVerifier copies and validates the public key.
func NewVerifier(publicKey []byte) (Verifier, error) {
if len(publicKey) != ed25519.PublicKeySize {
return Verifier{}, fmt.Errorf(
"%w: got %d bytes, want %d",
ErrPublicKeyInvalid,
len(publicKey),
ed25519.PublicKeySize,
)
}
keyCopy := append(ed25519.PublicKey(nil), publicKey...)
return Verifier{publicKey: keyCopy}, nil
}
// Verify rejects ambiguous JSON and validates the top-level signature.
func (verifier Verifier) Verify(document []byte) (VerifiedDocument, error) {
rootValue, err := parseRestrictedJSON(document)
if err != nil {
return VerifiedDocument{}, err
}
root, ok := rootValue.(map[string]any)
if !ok {
return VerifiedDocument{}, fmt.Errorf("%w: root must be an object", ErrInvalidDocument)
}
signatureValue, exists := root["signature"]
if !exists {
return VerifiedDocument{}, ErrSignatureMissing
}
signatureText, ok := signatureValue.(string)
if !ok {
return VerifiedDocument{}, fmt.Errorf("%w: signature must be a string", ErrSignatureInvalid)
}
delete(root, "signature")
signedPayload, err := canonicalJSON(root)
if err != nil {
return VerifiedDocument{}, err
}
signature, err := decodeCanonicalSignature(signatureText)
if err != nil {
return VerifiedDocument{}, fmt.Errorf("%w: %v", ErrSignatureInvalid, err)
}
if !ed25519.Verify(verifier.publicKey, signedPayload, signature) {
return VerifiedDocument{}, ErrSignatureInvalid
}
return VerifiedDocument{
Bytes: append([]byte(nil), document...),
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
}