package authn import ( "bytes" "crypto/ed25519" "crypto/x509" "encoding/binary" "encoding/pem" "errors" "fmt" "os" ) const ( // ResponseDomainMarkerV1 binds the v1 server response signature to the // Galaxy gateway transport contract. ResponseDomainMarkerV1 = "galaxy-response-v1" ) var ( // ErrInvalidResponsePrivateKeyPEM reports that the configured response // signer private key is not a strict PKCS#8 PEM-encoded private key. ErrInvalidResponsePrivateKeyPEM = errors.New("response signer private key is not a valid PKCS#8 PEM block") // ErrInvalidResponsePrivateKey reports that the configured response signer // private key is not an Ed25519 private key. ErrInvalidResponsePrivateKey = errors.New("response signer private key must be an Ed25519 PKCS#8 private key") // ErrInvalidResponseSignature reports that a server response signature is // not a raw Ed25519 signature for the canonical response signing input. ErrInvalidResponseSignature = errors.New("invalid response signature") ) // ResponseSigningFields contains the canonical v1 response fields that are // bound into the server signing input. type ResponseSigningFields struct { // ProtocolVersion identifies the transport envelope version. ProtocolVersion string // RequestID is the transport correlation identifier copied from the // authenticated request. RequestID string // TimestampMS carries the server response timestamp in milliseconds. TimestampMS int64 // ResultCode is the opaque downstream result code returned to the client. ResultCode string // PayloadHash is the raw SHA-256 digest of response payload bytes. PayloadHash []byte } // ResponseSigner signs authenticated unary responses and client-facing stream // events with one server-side key. type ResponseSigner interface { // SignResponse returns the raw Ed25519 signature for the canonical response // signing input built from fields. SignResponse(fields ResponseSigningFields) ([]byte, error) // SignEvent returns the raw Ed25519 signature for the canonical event // signing input built from fields. SignEvent(fields EventSigningFields) ([]byte, error) } // Ed25519ResponseSigner signs authenticated responses with one Ed25519 private // key loaded during process startup. type Ed25519ResponseSigner struct { privateKey ed25519.PrivateKey } // NewEd25519ResponseSigner validates privateKey and constructs a signer using // a defensive key copy. func NewEd25519ResponseSigner(privateKey ed25519.PrivateKey) (*Ed25519ResponseSigner, error) { if len(privateKey) != ed25519.PrivateKeySize { return nil, ErrInvalidResponsePrivateKey } return &Ed25519ResponseSigner{ privateKey: bytes.Clone(privateKey), }, nil } // LoadEd25519ResponseSignerFromPEMFile loads a strict PKCS#8 PEM-encoded // Ed25519 private key from path and constructs a signer. func LoadEd25519ResponseSignerFromPEMFile(path string) (*Ed25519ResponseSigner, error) { pemBytes, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read response signer private key PEM: %w", err) } signer, err := ParseEd25519ResponseSignerPEM(pemBytes) if err != nil { return nil, err } return signer, nil } // ParseEd25519ResponseSignerPEM parses one strict PKCS#8 PEM-encoded Ed25519 // private key and constructs a signer from it. func ParseEd25519ResponseSignerPEM(pemBytes []byte) (*Ed25519ResponseSigner, error) { block, rest := pem.Decode(pemBytes) if block == nil || block.Type != "PRIVATE KEY" || len(bytes.TrimSpace(rest)) > 0 { return nil, ErrInvalidResponsePrivateKeyPEM } parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, ErrInvalidResponsePrivateKeyPEM } privateKey, ok := parsedKey.(ed25519.PrivateKey) if !ok { return nil, ErrInvalidResponsePrivateKey } return NewEd25519ResponseSigner(privateKey) } // PublicKey returns the Ed25519 public key that corresponds to the configured // response signer private key. func (s *Ed25519ResponseSigner) PublicKey() ed25519.PublicKey { if s == nil { return nil } publicKey, _ := s.privateKey.Public().(ed25519.PublicKey) return bytes.Clone(publicKey) } // SignResponse signs the canonical v1 response signing input built from // fields. func (s *Ed25519ResponseSigner) SignResponse(fields ResponseSigningFields) ([]byte, error) { if s == nil || len(s.privateKey) != ed25519.PrivateKeySize { return nil, ErrInvalidResponsePrivateKey } signature := ed25519.Sign(s.privateKey, BuildResponseSigningInput(fields)) return bytes.Clone(signature), nil } // SignEvent signs the canonical v1 stream-event signing input built from // fields. func (s *Ed25519ResponseSigner) SignEvent(fields EventSigningFields) ([]byte, error) { if s == nil || len(s.privateKey) != ed25519.PrivateKeySize { return nil, ErrInvalidResponsePrivateKey } signature := ed25519.Sign(s.privateKey, BuildEventSigningInput(fields)) return bytes.Clone(signature), nil } // BuildResponseSigningInput returns the canonical byte sequence the v1 server // response signature covers. String and byte fields are length-prefixed with // uvarint(len(field)) followed by raw bytes, while TimestampMS is appended as // an 8-byte big-endian uint64. func BuildResponseSigningInput(fields ResponseSigningFields) []byte { size := len(ResponseDomainMarkerV1) + len(fields.ProtocolVersion) + len(fields.RequestID) + len(fields.ResultCode) + len(fields.PayloadHash) + (5 * binary.MaxVarintLen64) + 8 buf := make([]byte, 0, size) buf = appendLengthPrefixedString(buf, ResponseDomainMarkerV1) buf = appendLengthPrefixedString(buf, fields.ProtocolVersion) buf = appendLengthPrefixedString(buf, fields.RequestID) buf = binary.BigEndian.AppendUint64(buf, uint64(fields.TimestampMS)) buf = appendLengthPrefixedString(buf, fields.ResultCode) buf = appendLengthPrefixedBytes(buf, fields.PayloadHash) return buf } // VerifyResponseSignature verifies that signature authenticates fields under // publicKey using the canonical v1 response signing input. func VerifyResponseSignature(publicKey ed25519.PublicKey, signature []byte, fields ResponseSigningFields) error { if len(publicKey) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize { return ErrInvalidResponseSignature } if !ed25519.Verify(publicKey, BuildResponseSigningInput(fields), signature) { return ErrInvalidResponseSignature } return nil }