Files
galaxy-game/integration/internal/contracts/gatewayv1/contract.go
T
2026-04-09 15:27:14 +02:00

185 lines
5.9 KiB
Go

// 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
}