107 lines
3.5 KiB
Go
107 lines
3.5 KiB
Go
// 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
|
|
// `../../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
|
|
}
|