// Package authn defines the authenticated transport helpers used by // the gateway edge verification pipeline. The package is public so // that external clients (notably the integration test suite under // `galaxy/integration/testenv`) can reuse the canonical signing // input builders and the response/event verifiers without having to // duplicate the wire contract documented in // `../../docs/ARCHITECTURE.md` ยง15. package authn import ( "bytes" "crypto/sha256" "encoding/binary" "errors" ) const ( // RequestDomainMarkerV1 binds the v1 client request signature to the Galaxy // gateway transport contract. RequestDomainMarkerV1 = "galaxy-request-v1" ) var ( // ErrInvalidPayloadHash reports that payloadHash is not a raw SHA-256 digest. ErrInvalidPayloadHash = errors.New("payload_hash must be a 32-byte SHA-256 digest") // ErrPayloadHashMismatch reports that payloadHash does not match payloadBytes. ErrPayloadHashMismatch = errors.New("payload_hash does not match payload_bytes") ) // RequestSigningFields contains the canonical v1 request fields that are bound // into the client signing input after the gateway validates and normalizes the // request envelope. type RequestSigningFields struct { // ProtocolVersion identifies the transport envelope version. ProtocolVersion string // DeviceSessionID identifies the authenticated device session bound to the // request. DeviceSessionID string // MessageType is the stable downstream routing key. MessageType string // TimestampMS carries the client request timestamp in milliseconds. TimestampMS int64 // RequestID is the transport correlation and anti-replay identifier. RequestID string // PayloadHash is the raw SHA-256 digest of payload bytes. PayloadHash []byte } // BuildRequestSigningInput returns the canonical byte sequence the v1 client // request 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. The caller is expected to pass fields that have // already passed earlier envelope validation. func BuildRequestSigningInput(fields RequestSigningFields) []byte { size := len(RequestDomainMarkerV1) + len(fields.ProtocolVersion) + len(fields.DeviceSessionID) + len(fields.MessageType) + len(fields.RequestID) + len(fields.PayloadHash) + (6 * binary.MaxVarintLen64) + 8 buf := make([]byte, 0, size) buf = appendLengthPrefixedString(buf, RequestDomainMarkerV1) buf = appendLengthPrefixedString(buf, fields.ProtocolVersion) buf = appendLengthPrefixedString(buf, fields.DeviceSessionID) buf = appendLengthPrefixedString(buf, fields.MessageType) buf = binary.BigEndian.AppendUint64(buf, uint64(fields.TimestampMS)) buf = appendLengthPrefixedString(buf, fields.RequestID) buf = appendLengthPrefixedBytes(buf, fields.PayloadHash) return buf } // VerifyPayloadHash checks that payloadHash is the raw SHA-256 digest of // payloadBytes. Empty payloadBytes are valid and must use sha256.Sum256(nil). func VerifyPayloadHash(payloadBytes, payloadHash []byte) error { if len(payloadHash) != sha256.Size { return ErrInvalidPayloadHash } sum := sha256.Sum256(payloadBytes) if !bytes.Equal(sum[:], payloadHash) { return ErrPayloadHashMismatch } return nil } func appendLengthPrefixedString(dst []byte, value string) []byte { return appendLengthPrefixedBytes(dst, []byte(value)) } func appendLengthPrefixedBytes(dst []byte, value []byte) []byte { dst = binary.AppendUvarint(dst, uint64(len(value))) dst = append(dst, value...) return dst }