feat: backend service
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
// ResponseDomainMarkerV1 binds the v1 server response signature to the
|
||||
// Galaxy gateway transport contract.
|
||||
ResponseDomainMarkerV1 = "galaxy-response-v1"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidResponsePrivateKeyPEM reports that the configured response
|
||||
// signer private key is not a strict PKCS#8 PEM-encoded private key.
|
||||
ErrInvalidResponsePrivateKeyPEM = errors.New("response signer private key is not a valid PKCS#8 PEM block")
|
||||
|
||||
// ErrInvalidResponsePrivateKey reports that the configured response signer
|
||||
// private key is not an Ed25519 private key.
|
||||
ErrInvalidResponsePrivateKey = errors.New("response signer private key must be an Ed25519 PKCS#8 private key")
|
||||
|
||||
// ErrInvalidResponseSignature reports that a server response signature is
|
||||
// not a raw Ed25519 signature for the canonical response signing input.
|
||||
ErrInvalidResponseSignature = errors.New("invalid response signature")
|
||||
)
|
||||
|
||||
// ResponseSigningFields contains the canonical v1 response fields that are
|
||||
// bound into the server signing input.
|
||||
type ResponseSigningFields struct {
|
||||
// ProtocolVersion identifies the transport envelope version.
|
||||
ProtocolVersion string
|
||||
|
||||
// RequestID is the transport correlation identifier copied from the
|
||||
// authenticated request.
|
||||
RequestID string
|
||||
|
||||
// TimestampMS carries the server response timestamp in milliseconds.
|
||||
TimestampMS int64
|
||||
|
||||
// ResultCode is the opaque downstream result code returned to the client.
|
||||
ResultCode string
|
||||
|
||||
// PayloadHash is the raw SHA-256 digest of response payload bytes.
|
||||
PayloadHash []byte
|
||||
}
|
||||
|
||||
// ResponseSigner signs authenticated unary responses and client-facing stream
|
||||
// events with one server-side key.
|
||||
type ResponseSigner interface {
|
||||
// SignResponse returns the raw Ed25519 signature for the canonical response
|
||||
// signing input built from fields.
|
||||
SignResponse(fields ResponseSigningFields) ([]byte, error)
|
||||
|
||||
// SignEvent returns the raw Ed25519 signature for the canonical event
|
||||
// signing input built from fields.
|
||||
SignEvent(fields EventSigningFields) ([]byte, error)
|
||||
}
|
||||
|
||||
// Ed25519ResponseSigner signs authenticated responses with one Ed25519 private
|
||||
// key loaded during process startup.
|
||||
type Ed25519ResponseSigner struct {
|
||||
privateKey ed25519.PrivateKey
|
||||
}
|
||||
|
||||
// NewEd25519ResponseSigner validates privateKey and constructs a signer using
|
||||
// a defensive key copy.
|
||||
func NewEd25519ResponseSigner(privateKey ed25519.PrivateKey) (*Ed25519ResponseSigner, error) {
|
||||
if len(privateKey) != ed25519.PrivateKeySize {
|
||||
return nil, ErrInvalidResponsePrivateKey
|
||||
}
|
||||
|
||||
return &Ed25519ResponseSigner{
|
||||
privateKey: bytes.Clone(privateKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadEd25519ResponseSignerFromPEMFile loads a strict PKCS#8 PEM-encoded
|
||||
// Ed25519 private key from path and constructs a signer.
|
||||
func LoadEd25519ResponseSignerFromPEMFile(path string) (*Ed25519ResponseSigner, error) {
|
||||
pemBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response signer private key PEM: %w", err)
|
||||
}
|
||||
|
||||
signer, err := ParseEd25519ResponseSignerPEM(pemBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
// ParseEd25519ResponseSignerPEM parses one strict PKCS#8 PEM-encoded Ed25519
|
||||
// private key and constructs a signer from it.
|
||||
func ParseEd25519ResponseSignerPEM(pemBytes []byte) (*Ed25519ResponseSigner, error) {
|
||||
block, rest := pem.Decode(pemBytes)
|
||||
if block == nil || block.Type != "PRIVATE KEY" || len(bytes.TrimSpace(rest)) > 0 {
|
||||
return nil, ErrInvalidResponsePrivateKeyPEM
|
||||
}
|
||||
|
||||
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidResponsePrivateKeyPEM
|
||||
}
|
||||
|
||||
privateKey, ok := parsedKey.(ed25519.PrivateKey)
|
||||
if !ok {
|
||||
return nil, ErrInvalidResponsePrivateKey
|
||||
}
|
||||
|
||||
return NewEd25519ResponseSigner(privateKey)
|
||||
}
|
||||
|
||||
// PublicKey returns the Ed25519 public key that corresponds to the configured
|
||||
// response signer private key.
|
||||
func (s *Ed25519ResponseSigner) PublicKey() ed25519.PublicKey {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
publicKey, _ := s.privateKey.Public().(ed25519.PublicKey)
|
||||
return bytes.Clone(publicKey)
|
||||
}
|
||||
|
||||
// SignResponse signs the canonical v1 response signing input built from
|
||||
// fields.
|
||||
func (s *Ed25519ResponseSigner) SignResponse(fields ResponseSigningFields) ([]byte, error) {
|
||||
if s == nil || len(s.privateKey) != ed25519.PrivateKeySize {
|
||||
return nil, ErrInvalidResponsePrivateKey
|
||||
}
|
||||
|
||||
signature := ed25519.Sign(s.privateKey, BuildResponseSigningInput(fields))
|
||||
return bytes.Clone(signature), nil
|
||||
}
|
||||
|
||||
// SignEvent signs the canonical v1 stream-event signing input built from
|
||||
// fields.
|
||||
func (s *Ed25519ResponseSigner) SignEvent(fields EventSigningFields) ([]byte, error) {
|
||||
if s == nil || len(s.privateKey) != ed25519.PrivateKeySize {
|
||||
return nil, ErrInvalidResponsePrivateKey
|
||||
}
|
||||
|
||||
signature := ed25519.Sign(s.privateKey, BuildEventSigningInput(fields))
|
||||
return bytes.Clone(signature), nil
|
||||
}
|
||||
|
||||
// BuildResponseSigningInput returns the canonical byte sequence the v1 server
|
||||
// response 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 BuildResponseSigningInput(fields ResponseSigningFields) []byte {
|
||||
size := len(ResponseDomainMarkerV1) +
|
||||
len(fields.ProtocolVersion) +
|
||||
len(fields.RequestID) +
|
||||
len(fields.ResultCode) +
|
||||
len(fields.PayloadHash) +
|
||||
(5 * binary.MaxVarintLen64) +
|
||||
8
|
||||
|
||||
buf := make([]byte, 0, size)
|
||||
buf = appendLengthPrefixedString(buf, ResponseDomainMarkerV1)
|
||||
buf = appendLengthPrefixedString(buf, fields.ProtocolVersion)
|
||||
buf = appendLengthPrefixedString(buf, fields.RequestID)
|
||||
buf = binary.BigEndian.AppendUint64(buf, uint64(fields.TimestampMS))
|
||||
buf = appendLengthPrefixedString(buf, fields.ResultCode)
|
||||
buf = appendLengthPrefixedBytes(buf, fields.PayloadHash)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// VerifyResponseSignature verifies that signature authenticates fields under
|
||||
// publicKey using the canonical v1 response signing input.
|
||||
func VerifyResponseSignature(publicKey ed25519.PublicKey, signature []byte, fields ResponseSigningFields) error {
|
||||
if len(publicKey) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize {
|
||||
return ErrInvalidResponseSignature
|
||||
}
|
||||
if !ed25519.Verify(publicKey, BuildResponseSigningInput(fields), signature) {
|
||||
return ErrInvalidResponseSignature
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user