package relayproto
import (
"bytes"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"github.com/tmc/go-iroh/internal/postcard"
"github.com/tmc/go-iroh/key"
"lukechampine.com/blake3"
)
const domainSepChallenge = "iroh-relay handshake v1 challenge signature"
const ClientAuthHeader = "x-iroh-relay-client-auth-v1"
const domainSepTLSExportLabel = "iroh-relay handshake v1"
var (
ErrServerDeniedAuth = errors .New ("relayproto: the relay denied authentication" )
ErrSignatureInvalid = errors .New ("relayproto: client signature invalid" )
ErrHandshakeDeserial = errors .New ("relayproto: handshake frame deserialization failed" )
ErrUnexpectedFrameTag = errors .New ("relayproto: unexpected handshake frame type" )
ErrNoKeyingMaterial = errors .New ("relayproto: no TLS keying material" )
ErrKeyMaterialSuffix = errors .New ("relayproto: TLS keying material suffix mismatch" )
)
type ServerChallenge struct {
Challenge [16 ]byte
}
func (c ServerChallenge ) messageToSign () [32 ]byte {
var out [32 ]byte
blake3 .DeriveKey (out [:], domainSepChallenge , c .Challenge [:])
return out
}
func (c ServerChallenge ) AppendTo (dst []byte ) []byte {
dst = writeFrameType (dst , FrameServerChallenge )
return append (dst , c .Challenge [:]...)
}
type ClientAuth struct {
PublicKey key .PublicKey
Signature key .Signature
}
type KeyMaterialClientAuth struct {
PublicKey key .PublicKey
Signature key .Signature
KeyMaterialSuffix [16 ]byte
}
func NewClientAuth (secretKey key .SecretKey , challenge ServerChallenge ) ClientAuth {
msg := challenge .messageToSign ()
return ClientAuth {
PublicKey : secretKey .Public (),
Signature : secretKey .Sign (msg [:]),
}
}
func (a ClientAuth ) Verify (challenge ServerChallenge ) error {
msg := challenge .messageToSign ()
if err := a .PublicKey .Verify (msg [:], a .Signature ); err != nil {
return fmt .Errorf ("%w: %v" , ErrSignatureInvalid , err )
}
return nil
}
func NewKeyMaterialClientAuth (secretKey key .SecretKey , state *tls .ConnectionState ) (KeyMaterialClientAuth , error ) {
if state == nil {
return KeyMaterialClientAuth {}, ErrNoKeyingMaterial
}
publicKey := secretKey .Public ()
pk := publicKey .Bytes ()
keyMaterial , err := state .ExportKeyingMaterial (domainSepTLSExportLabel , pk [:], 32 )
if err != nil {
return KeyMaterialClientAuth {}, fmt .Errorf ("%w: %v" , ErrNoKeyingMaterial , err )
}
auth := KeyMaterialClientAuth {
PublicKey : publicKey ,
Signature : secretKey .Sign (keyMaterial [:16 ]),
}
copy (auth .KeyMaterialSuffix [:], keyMaterial [16 :])
return auth , nil
}
func KeyMaterialClientAuthFromHeader (value string ) (KeyMaterialClientAuth , error ) {
b , err := base64 .RawURLEncoding .DecodeString (value )
if err != nil {
return KeyMaterialClientAuth {}, fmt .Errorf ("%w: %v" , ErrHandshakeDeserial , err )
}
var auth KeyMaterialClientAuth
if err := postcard .Unmarshal (b , &auth ); err != nil {
return KeyMaterialClientAuth {}, fmt .Errorf ("%w: %v" , ErrHandshakeDeserial , err )
}
return auth , nil
}
func (a KeyMaterialClientAuth ) HeaderValue () (string , error ) {
b , err := postcard .Marshal (a )
if err != nil {
return "" , fmt .Errorf ("relayproto: encode key-material auth: %w" , err )
}
return base64 .RawURLEncoding .EncodeToString (b ), nil
}
func (a KeyMaterialClientAuth ) Verify (state *tls .ConnectionState ) error {
if state == nil {
return ErrNoKeyingMaterial
}
pk := a .PublicKey .Bytes ()
keyMaterial , err := state .ExportKeyingMaterial (domainSepTLSExportLabel , pk [:], 32 )
if err != nil {
return fmt .Errorf ("%w: %v" , ErrNoKeyingMaterial , err )
}
if !bytes .Equal (keyMaterial [16 :], a .KeyMaterialSuffix [:]) {
return ErrKeyMaterialSuffix
}
if err := a .PublicKey .Verify (keyMaterial [:16 ], a .Signature ); err != nil {
return fmt .Errorf ("%w: %v" , ErrSignatureInvalid , err )
}
return nil
}
func (a KeyMaterialClientAuth ) EncodePostcard (e *postcard .Encoder ) error {
pk := a .PublicKey .Bytes ()
e .RawBytes (pk [:])
sig := a .Signature .Bytes ()
e .BytesValue (sig [:])
e .RawBytes (a .KeyMaterialSuffix [:])
return nil
}
func (a *KeyMaterialClientAuth ) DecodePostcard (d *postcard .Decoder ) error {
pkBytes , err := d .RawBytes (key .PublicKeySize )
if err != nil {
return err
}
pk , err := key .PublicKeyFromSlice (pkBytes )
if err != nil {
return err
}
sigBytes , err := d .BytesValue ()
if err != nil {
return err
}
sig , err := key .SignatureFromSlice (sigBytes )
if err != nil {
return err
}
suffix , err := d .RawBytes (16 )
if err != nil {
return err
}
a .PublicKey = pk
a .Signature = sig
copy (a .KeyMaterialSuffix [:], suffix )
return nil
}
func (a ClientAuth ) AppendTo (dst []byte ) []byte {
dst = writeFrameType (dst , FrameClientAuth )
pk := a .PublicKey .Bytes ()
dst = append (dst , pk [:]...)
sig := a .Signature .Bytes ()
dst = appendPostcardVarint (dst , uint64 (len (sig )))
return append (dst , sig [:]...)
}
type ServerConfirmsAuth struct {}
func (ServerConfirmsAuth ) AppendTo (dst []byte ) []byte {
return writeFrameType (dst , FrameServerConfirmsAuth )
}
type ServerDeniesAuth struct {
Reason string
}
func (d ServerDeniesAuth ) AppendTo (dst []byte ) []byte {
dst = writeFrameType (dst , FrameServerDeniesAuth )
dst = appendPostcardVarint (dst , uint64 (len (d .Reason )))
return append (dst , d .Reason ...)
}
func ParseHandshakeFrame (content []byte ) (any , error ) {
ft , body , err := readFrameType (content )
if err != nil {
return nil , err
}
switch ft {
case FrameServerChallenge :
if len (body ) != 16 {
return nil , ErrHandshakeDeserial
}
var c ServerChallenge
copy (c .Challenge [:], body )
return &c , nil
case FrameClientAuth :
if len (body ) < key .PublicKeySize {
return nil , ErrHandshakeDeserial
}
pk , err := key .PublicKeyFromSlice (body [:key .PublicKeySize ])
if err != nil {
return nil , fmt .Errorf ("%w: %v" , ErrHandshakeDeserial , err )
}
body = body [key .PublicKeySize :]
n , rest , err := readPostcardVarint (body )
if err != nil || n != uint64 (key .SignatureSize ) || len (rest ) != key .SignatureSize {
return nil , ErrHandshakeDeserial
}
sig , err := key .SignatureFromSlice (rest )
if err != nil {
return nil , fmt .Errorf ("%w: %v" , ErrHandshakeDeserial , err )
}
return &ClientAuth {PublicKey : pk , Signature : sig }, nil
case FrameServerConfirmsAuth :
return &ServerConfirmsAuth {}, nil
case FrameServerDeniesAuth :
n , rest , err := readPostcardVarint (body )
if err != nil || n != uint64 (len (rest )) {
return nil , ErrHandshakeDeserial
}
return &ServerDeniesAuth {Reason : string (rest )}, nil
default :
return nil , fmt .Errorf ("%w: %s" , ErrUnexpectedFrameTag , ft )
}
}
The pages are generated with Golds v0.8.4 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .