// Package gatewayv1contract provides public-contract helpers for the gateway // v1 authenticated transport without importing service-internal packages. package gatewayv1contract import ( "bytes" "crypto/ed25519" "crypto/sha256" "encoding/binary" "errors" ) const ( // ProtocolVersionV1 is the supported public protocol version literal. ProtocolVersionV1 = "v1" // SubscribeMessageType is the authenticated message type used to open the // gateway push stream. SubscribeMessageType = "gateway.subscribe" // ServerTimeEventType is the bootstrap event type emitted by the gateway // immediately after a push stream is opened. ServerTimeEventType = "gateway.server_time" requestDomainMarkerV1 = "galaxy-request-v1" eventDomainMarkerV1 = "galaxy-event-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") // ErrInvalidEventSignature reports that one gateway event signature is not // a raw Ed25519 signature for the canonical event signing input. ErrInvalidEventSignature = errors.New("invalid event signature") ) // RequestSigningFields stores the canonical public request fields bound into // one client signature input. type RequestSigningFields struct { // ProtocolVersion identifies the gateway transport envelope version. ProtocolVersion string // DeviceSessionID identifies the authenticated device session bound to the // request. DeviceSessionID string // MessageType is the stable authenticated gateway message type. MessageType string // TimestampMS carries the client request timestamp in milliseconds. TimestampMS int64 // RequestID is the transport correlation and anti-replay identifier. RequestID string // PayloadHash stores the raw SHA-256 digest of PayloadBytes. PayloadHash []byte } // EventSigningFields stores the canonical public stream-event fields bound // into one gateway event signature input. type EventSigningFields struct { // EventType identifies the stable client-facing event category. EventType string // EventID is the stable event correlation identifier. EventID string // TimestampMS carries the gateway event timestamp in milliseconds. TimestampMS int64 // RequestID optionally correlates the event to the opening client request. RequestID string // TraceID optionally carries the client-supplied trace correlation value. TraceID string // PayloadHash stores the raw SHA-256 digest of PayloadBytes. PayloadHash []byte } // ComputePayloadHash returns the canonical raw SHA-256 digest for payloadBytes. func ComputePayloadHash(payloadBytes []byte) []byte { sum := sha256.Sum256(payloadBytes) return bytes.Clone(sum[:]) } // VerifyPayloadHash reports whether payloadHash matches payloadBytes under the // public gateway payload-hash contract. 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 } // BuildRequestSigningInput returns the canonical byte sequence the v1 client // request signature covers. 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 } // BuildEventSigningInput returns the canonical byte sequence the v1 gateway // event signature covers. func BuildEventSigningInput(fields EventSigningFields) []byte { size := len(eventDomainMarkerV1) + len(fields.EventType) + len(fields.EventID) + len(fields.RequestID) + len(fields.TraceID) + len(fields.PayloadHash) + (6 * binary.MaxVarintLen64) + 8 buf := make([]byte, 0, size) buf = appendLengthPrefixedString(buf, eventDomainMarkerV1) buf = appendLengthPrefixedString(buf, fields.EventType) buf = appendLengthPrefixedString(buf, fields.EventID) buf = binary.BigEndian.AppendUint64(buf, uint64(fields.TimestampMS)) buf = appendLengthPrefixedString(buf, fields.RequestID) buf = appendLengthPrefixedString(buf, fields.TraceID) buf = appendLengthPrefixedBytes(buf, fields.PayloadHash) return buf } // SignRequest returns one raw Ed25519 client signature for the canonical v1 // request signing input. func SignRequest(privateKey ed25519.PrivateKey, fields RequestSigningFields) []byte { return ed25519.Sign(privateKey, BuildRequestSigningInput(fields)) } // VerifyEventSignature reports whether signature authenticates fields under // publicKey using the canonical gateway event signing input. func VerifyEventSignature(publicKey ed25519.PublicKey, signature []byte, fields EventSigningFields) error { if len(publicKey) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize { return ErrInvalidEventSignature } if !ed25519.Verify(publicKey, BuildEventSigningInput(fields), signature) { return ErrInvalidEventSignature } 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 }