package clusterauth import ( "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "strings" "time" ) const ( AuthoritySchemaVersion = "rap.cluster_authority.v1" SignatureSchemaVersion = "rap.cluster_authority.signature.v1" QuorumSchemaVersion = "rap.cluster_authority.quorum.v1" QuorumEnvelopeVersion = "rap.cluster_authority.quorum_envelope.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 KeyPair struct { PublicKeyB64 string PrivateKeyB64 string Fingerprint string } type Signature struct { SchemaVersion string `json:"schema_version"` Algorithm string `json:"algorithm"` KeyFingerprint string `json:"key_fingerprint"` Signature string `json:"signature"` SignedAt time.Time `json:"signed_at"` } type QuorumMember struct { NodeID string `json:"node_id,omitempty"` Role string `json:"role,omitempty"` PublicKey string `json:"public_key"` PublicKeyFingerprint string `json:"public_key_fingerprint"` Scopes []string `json:"scopes,omitempty"` } type QuorumDescriptor struct { SchemaVersion string `json:"schema_version"` ClusterID string `json:"cluster_id"` Epoch string `json:"epoch"` Threshold int `json:"threshold"` Members []QuorumMember `json:"members"` } type QuorumEnvelope struct { SchemaVersion string `json:"schema_version"` ClusterID string `json:"cluster_id"` Epoch string `json:"epoch"` Threshold int `json:"threshold"` PayloadSHA256 string `json:"payload_sha256"` QuorumSHA256 string `json:"quorum_sha256"` Signatures []Signature `json:"signatures"` AllowedScopes []string `json:"allowed_scopes,omitempty"` DecisionReason string `json:"decision_reason,omitempty"` } func GenerateKeyPair() (KeyPair, error) { publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return KeyPair{}, err } fingerprint := Fingerprint(publicKey) return KeyPair{ PublicKeyB64: base64.StdEncoding.EncodeToString(publicKey), PrivateKeyB64: base64.StdEncoding.EncodeToString(privateKey), Fingerprint: fingerprint, }, nil } func Fingerprint(publicKey ed25519.PublicKey) string { sum := sha256.Sum256(publicKey) return "rap-ca-ed25519-" + hex.EncodeToString(sum[:16]) } func FingerprintFromBase64(publicKeyB64 string) (string, error) { publicKey, err := DecodePublicKey(publicKeyB64) if err != nil { return "", err } return Fingerprint(publicKey), nil } func SignRaw(privateKeyB64 string, payload json.RawMessage, signedAt time.Time) (Signature, error) { privateKey, err := DecodePrivateKey(privateKeyB64) if err != nil { return Signature{}, err } canonical, err := CanonicalJSON(payload) if err != nil { return Signature{}, err } publicKey, ok := privateKey.Public().(ed25519.PublicKey) if !ok { return Signature{}, ErrInvalidKey } signature := ed25519.Sign(privateKey, canonical) return Signature{ SchemaVersion: SignatureSchemaVersion, Algorithm: AlgorithmEd25519, KeyFingerprint: Fingerprint(publicKey), Signature: base64.StdEncoding.EncodeToString(signature), SignedAt: signedAt.UTC(), }, nil } func SignPayload(privateKeyB64 string, payload any, signedAt time.Time) (json.RawMessage, Signature, error) { raw, err := json.Marshal(payload) if err != nil { return nil, Signature{}, fmt.Errorf("%w: marshal: %v", ErrInvalidPayload, err) } signature, err := SignRaw(privateKeyB64, raw, signedAt) if err != nil { return nil, Signature{}, err } return json.RawMessage(raw), signature, nil } 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 VerifyQuorumRaw(descriptor QuorumDescriptor, payload json.RawMessage, envelope QuorumEnvelope, requiredScope string) error { if descriptor.SchemaVersion != QuorumSchemaVersion { return fmt.Errorf("%w: quorum schema_version must be %s", ErrInvalidSignature, QuorumSchemaVersion) } if envelope.SchemaVersion != QuorumEnvelopeVersion { return fmt.Errorf("%w: quorum envelope schema_version must be %s", ErrInvalidSignature, QuorumEnvelopeVersion) } if strings.TrimSpace(descriptor.ClusterID) == "" || descriptor.ClusterID != envelope.ClusterID { return fmt.Errorf("%w: quorum cluster mismatch", ErrInvalidSignature) } if strings.TrimSpace(descriptor.Epoch) == "" || descriptor.Epoch != envelope.Epoch { return fmt.Errorf("%w: quorum epoch mismatch", ErrInvalidSignature) } threshold := descriptor.Threshold if envelope.Threshold > threshold { threshold = envelope.Threshold } if threshold <= 0 || threshold > len(descriptor.Members) { return fmt.Errorf("%w: invalid quorum threshold", ErrInvalidSignature) } payloadHash, err := HashRaw(payload) if err != nil { return err } if envelope.PayloadSHA256 != payloadHash { return fmt.Errorf("%w: quorum payload hash mismatch", ErrInvalidSignature) } descriptorHash, err := QuorumDescriptorHash(descriptor) if err != nil { return err } if envelope.QuorumSHA256 != descriptorHash { return fmt.Errorf("%w: quorum descriptor hash mismatch", ErrInvalidSignature) } members := map[string]QuorumMember{} for _, member := range descriptor.Members { fingerprint := strings.TrimSpace(member.PublicKeyFingerprint) if fingerprint == "" { publicKey, err := DecodePublicKey(member.PublicKey) if err != nil { return err } fingerprint = Fingerprint(publicKey) } if _, exists := members[fingerprint]; exists { return fmt.Errorf("%w: duplicate quorum member", ErrInvalidSignature) } member.PublicKeyFingerprint = fingerprint members[fingerprint] = member } seen := map[string]bool{} valid := 0 for _, signature := range envelope.Signatures { fingerprint := strings.TrimSpace(signature.KeyFingerprint) if seen[fingerprint] { continue } member, ok := members[fingerprint] if !ok { return fmt.Errorf("%w: quorum signer is not a member", ErrInvalidSignature) } if requiredScope != "" && !memberAllowsScope(member, requiredScope) { return fmt.Errorf("%w: quorum signer scope mismatch", ErrInvalidSignature) } if err := VerifyRaw(member.PublicKey, payload, signature); err != nil { return err } seen[fingerprint] = true valid++ } if valid < threshold { return fmt.Errorf("%w: quorum threshold not met", ErrInvalidSignature) } return 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 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 QuorumDescriptorHash(descriptor QuorumDescriptor) (string, error) { raw, err := json.Marshal(descriptor) if err != nil { return "", err } return HashRaw(raw) } func memberAllowsScope(member QuorumMember, requiredScope string) bool { requiredScope = strings.TrimSpace(requiredScope) if requiredScope == "" { return true } for _, scope := range member.Scopes { scope = strings.TrimSpace(scope) if scope == "*" || scope == requiredScope { return true } } return false } 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 DecodePrivateKey(value string) (ed25519.PrivateKey, error) { decoded, err := decodeBase64(strings.TrimSpace(value)) if err != nil { return nil, fmt.Errorf("%w: private key must be base64 encoded", ErrInvalidKey) } if len(decoded) != ed25519.PrivateKeySize { return nil, fmt.Errorf("%w: private key must decode to %d bytes", ErrInvalidKey, ed25519.PrivateKeySize) } return ed25519.PrivateKey(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) }