package authority import ( "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "strings" ) const ( AuthoritySchemaVersion = "rap.cluster_authority.v1" SignatureSchemaVersion = "rap.cluster_authority.signature.v1" AlgorithmEd25519 = "ed25519" ) var ( ErrInvalidKey = errors.New("invalid cluster authority key") ErrInvalidSignature = errors.New("invalid cluster authority signature") ErrInvalidPayload = errors.New("invalid cluster authority payload") ) type Signature struct { SchemaVersion string `json:"schema_version"` Algorithm string `json:"algorithm"` KeyFingerprint string `json:"key_fingerprint"` Signature string `json:"signature"` } func VerifyRaw(publicKeyB64 string, payload json.RawMessage, signature Signature) error { if signature.SchemaVersion != SignatureSchemaVersion { return fmt.Errorf("%w: schema_version must be %s", ErrInvalidSignature, SignatureSchemaVersion) } if signature.Algorithm != AlgorithmEd25519 { return fmt.Errorf("%w: algorithm must be %s", ErrInvalidSignature, AlgorithmEd25519) } publicKey, err := decodePublicKey(publicKeyB64) if err != nil { return err } if signature.KeyFingerprint != Fingerprint(publicKey) { return fmt.Errorf("%w: key fingerprint mismatch", ErrInvalidSignature) } canonical, err := CanonicalJSON(payload) if err != nil { return err } decodedSignature, err := decodeBase64(strings.TrimSpace(signature.Signature)) if err != nil || len(decodedSignature) != ed25519.SignatureSize { return fmt.Errorf("%w: signature must be base64 ed25519 signature", ErrInvalidSignature) } if !ed25519.Verify(publicKey, canonical, decodedSignature) { return ErrInvalidSignature } return nil } func Fingerprint(publicKey ed25519.PublicKey) string { sum := sha256.Sum256(publicKey) return "rap-ca-ed25519-" + hex.EncodeToString(sum[:16]) } func HashRaw(raw json.RawMessage) (string, error) { canonical, err := CanonicalJSON(raw) if err != nil { return "", err } sum := sha256.Sum256(canonical) return hex.EncodeToString(sum[:]), nil } func CanonicalJSON(raw json.RawMessage) ([]byte, error) { if len(raw) == 0 { return nil, fmt.Errorf("%w: empty payload", ErrInvalidPayload) } var value any if err := json.Unmarshal(raw, &value); err != nil { return nil, fmt.Errorf("%w: invalid json: %v", ErrInvalidPayload, err) } canonical, err := json.Marshal(value) if err != nil { return nil, fmt.Errorf("%w: canonical json: %v", ErrInvalidPayload, err) } return canonical, nil } func decodePublicKey(value string) (ed25519.PublicKey, error) { decoded, err := decodeBase64(strings.TrimSpace(value)) if err != nil { return nil, fmt.Errorf("%w: public key must be base64 encoded", ErrInvalidKey) } if len(decoded) != ed25519.PublicKeySize { return nil, fmt.Errorf("%w: public key must decode to %d bytes", ErrInvalidKey, ed25519.PublicKeySize) } return ed25519.PublicKey(decoded), nil } func decodeBase64(value string) ([]byte, error) { if value == "" { return nil, errors.New("empty base64 value") } decoded, err := base64.StdEncoding.DecodeString(value) if err == nil { return decoded, nil } return base64.RawStdEncoding.DecodeString(value) }