Prototype signed catalog fallback (T-101)
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user