package canon 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. The client populates this struct from a // request envelope before signing; the server populates it from a received // envelope before verification. 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. 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 }