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" 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"` } 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 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 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) }