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 := base64.StdEncoding.Strict().DecodeString(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, ) } if !ed25519.Verify(verifier.publicKey, signedPayload, signature) { return VerifiedDocument{}, ErrSignatureInvalid } return VerifiedDocument{ Bytes: append([]byte(nil), document...), SignedPayload: append([]byte(nil), signedPayload...), }, nil }