Files
Ilia Denisov dc1c9b109c phase 3
2026-05-07 09:40:37 +02:00

89 lines
2.8 KiB
Go

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
}