mirror of
https://github.com/soypat/lneto.git
synced 2026-08-09 09:23:40 +00:00
752 lines
20 KiB
Go
752 lines
20 KiB
Go
package nts
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/binary"
|
|
"math/big"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/soypat/lneto"
|
|
"github.com/soypat/lneto/ntp"
|
|
// NOTE: tests exercising AES-SIV-CMAC-256 are skipped pending
|
|
// due to missing stdlib support.
|
|
)
|
|
|
|
func TestKERecord_RoundTrip(t *testing.T) {
|
|
body := []byte("hello NTS-KE")
|
|
buf := AppendKERecord(nil, true, RecordNewCookie, body)
|
|
|
|
rec, err := NewKERecord(buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.RecordType() != RecordNewCookie {
|
|
t.Errorf("RecordType = %v; want %v", rec.RecordType(), RecordNewCookie)
|
|
}
|
|
if !rec.IsCritical() {
|
|
t.Error("IsCritical = false; want true")
|
|
}
|
|
if !bytes.Equal(rec.Body(), body) {
|
|
t.Errorf("Body mismatch: got %x want %x", rec.Body(), body)
|
|
}
|
|
}
|
|
|
|
func TestKERecord_NonCritical(t *testing.T) {
|
|
buf := AppendKERecord(nil, false, RecordWarning, []byte{0, 1})
|
|
rec, err := NewKERecord(buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.IsCritical() {
|
|
t.Error("IsCritical = true; want false")
|
|
}
|
|
if rec.RecordType() != RecordWarning {
|
|
t.Errorf("RecordType = %v; want %v", rec.RecordType(), RecordWarning)
|
|
}
|
|
}
|
|
|
|
func TestKERecord_TruncatedBuffer(t *testing.T) {
|
|
buf := AppendKERecord(nil, true, RecordEndOfMessage, nil)
|
|
for i := range buf {
|
|
if _, err := NewKERecord(buf[:i]); err == nil {
|
|
t.Errorf("NewKERecord(buf[:%d]): expected error", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKERecord_ValidateSize(t *testing.T) {
|
|
buf := AppendKERecord(nil, false, RecordAEADAlgNeg, []byte{0, 15})
|
|
rec, _ := NewKERecord(buf)
|
|
var v lneto.Validator
|
|
rec.ValidateSize(&v)
|
|
if v.HasError() {
|
|
t.Errorf("ValidateSize: unexpected error: %v", v.ErrPop())
|
|
}
|
|
}
|
|
|
|
// generateSelfSignedCert returns a TLS certificate for localhost, suitable
|
|
// for in-process testing.
|
|
func generateSelfSignedCert(t *testing.T) tls.Certificate {
|
|
t.Helper()
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tmpl := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{CommonName: "localhost"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
DNSNames: []string{"localhost"},
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
}
|
|
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cert, err := x509.ParseCertificate(certDER)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
pool.AddCert(cert)
|
|
return tls.Certificate{Certificate: [][]byte{certDER}, PrivateKey: key, Leaf: cert}
|
|
}
|
|
|
|
// runMockKEServer runs a minimal NTS-KE server over conn that responds with
|
|
// one cookie and the chosen algorithm.
|
|
func runMockKEServer(t *testing.T, conn net.Conn, tlsCfg *tls.Config, cookie []byte) {
|
|
t.Helper()
|
|
tc := tls.Server(conn, tlsCfg)
|
|
if err := tc.Handshake(); err != nil {
|
|
t.Errorf("server TLS handshake: %v", err)
|
|
return
|
|
}
|
|
defer tc.Close()
|
|
|
|
// Read client records until EndOfMessage.
|
|
hdr := make([]byte, 4)
|
|
for {
|
|
if _, err := tc.Read(hdr); err != nil {
|
|
return
|
|
}
|
|
bodyLen := int(binary.BigEndian.Uint16(hdr[2:4]))
|
|
recType := KERecordType(binary.BigEndian.Uint16(hdr[0:2]) & 0x7FFF)
|
|
if bodyLen > 0 {
|
|
body := make([]byte, bodyLen)
|
|
tc.Read(body)
|
|
}
|
|
if recType == RecordEndOfMessage {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Send response per RFC 8915 §4.1: NextProtoNeg + AEAD + Cookie + EndOfMessage.
|
|
var resp []byte
|
|
var protoBody [2]byte
|
|
binary.BigEndian.PutUint16(protoBody[:], ntpv4ProtocolID)
|
|
resp = AppendKERecord(resp, true, RecordNextProtoNeg, protoBody[:])
|
|
var algBody [2]byte
|
|
binary.BigEndian.PutUint16(algBody[:], uint16(AlgAESSIVCMAC256))
|
|
resp = AppendKERecord(resp, true, RecordAEADAlgNeg, algBody[:])
|
|
resp = AppendKERecord(resp, false, RecordNewCookie, cookie)
|
|
resp = AppendKERecord(resp, true, RecordEndOfMessage, nil)
|
|
tc.Write(resp)
|
|
}
|
|
|
|
func TestPerformKE_E2E(t *testing.T) {
|
|
cert := generateSelfSignedCert(t)
|
|
pool := x509.NewCertPool()
|
|
leaf, _ := x509.ParseCertificate(cert.Certificate[0])
|
|
pool.AddCert(leaf)
|
|
|
|
serverCfg := &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
MinVersion: tls.VersionTLS13,
|
|
NextProtos: []string{"ntske/1"},
|
|
}
|
|
clientCfg := &tls.Config{
|
|
RootCAs: pool,
|
|
ServerName: "localhost",
|
|
MinVersion: tls.VersionTLS13,
|
|
NextProtos: []string{"ntske/1"},
|
|
}
|
|
|
|
wantCookie := []byte("test-cookie-data-1234")
|
|
|
|
serverConn, clientConn := net.Pipe()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
runMockKEServer(t, serverConn, serverCfg, wantCookie)
|
|
}()
|
|
|
|
tc := tls.Client(clientConn, clientCfg)
|
|
if err := tc.Handshake(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secrets, err := PerformKE(tc, KEConfig{})
|
|
// Close the underlying pipe rather than tc.Close(): a TLS close_notify
|
|
// write over the synchronous net.Pipe would otherwise block on crypto/tls'
|
|
// 5s close deadline since the peer is no longer reading.
|
|
clientConn.Close()
|
|
<-done
|
|
|
|
if err != nil {
|
|
t.Fatalf("PerformKE: %v", err)
|
|
}
|
|
if secrets.NumCookies != 1 {
|
|
t.Errorf("NumCookies = %d; want 1", secrets.NumCookies)
|
|
}
|
|
if !bytes.Equal(secrets.Cookies[0][:secrets.CookieLens[0]], wantCookie) {
|
|
t.Errorf("cookie mismatch: got %q want %q",
|
|
secrets.Cookies[0][:secrets.CookieLens[0]], wantCookie)
|
|
}
|
|
if secrets.ChosenAlg != AlgAESSIVCMAC256 {
|
|
t.Errorf("ChosenAlg = %v; want %v", secrets.ChosenAlg, AlgAESSIVCMAC256)
|
|
}
|
|
// Keys must be non-zero.
|
|
var zeroKey [32]byte
|
|
if secrets.C2SKey == zeroKey || secrets.S2CKey == zeroKey {
|
|
t.Error("derived keys are all-zero")
|
|
}
|
|
}
|
|
|
|
/*
|
|
DISABLED: missing AES-SIV-CMAC-256 in stdlib
|
|
|
|
// TestClient_E2E runs a full NTS Encapsulate→Demux cycle using
|
|
// AES-SIV-CMAC-256 as the AEAD.
|
|
func TestClient_E2E(t *testing.T) {
|
|
c2sKey := make([]byte, 32)
|
|
s2cKey := make([]byte, 32)
|
|
rand.Read(c2sKey)
|
|
rand.Read(s2cKey)
|
|
c2s, _ := siv.NewAESSIVCMAC256(c2sKey)
|
|
s2c, _ := siv.NewAESSIVCMAC256(s2cKey)
|
|
|
|
cookie := []byte("nts-cookie-12345678901234")
|
|
var cfg ClientConfig
|
|
cfg.C2S = c2s
|
|
cfg.S2C = s2c
|
|
cfg.ChosenAlg = AlgAESSIVCMAC256
|
|
for i := range 2 {
|
|
copy(cfg.Cookies[i][:], cookie)
|
|
cfg.CookieLens[i] = len(cookie)
|
|
}
|
|
cfg.NumCookies = 2
|
|
|
|
baseTime := ntp.BaseTime()
|
|
clockTime := baseTime.Add(10 * time.Second)
|
|
serverOffset := 200 * time.Millisecond
|
|
cfg.Now = func() time.Time { return clockTime }
|
|
cfg.Sysprec = -20
|
|
|
|
var client Client
|
|
if err := client.Reset(cfg); err != nil {
|
|
t.Fatalf("Reset: %v", err)
|
|
}
|
|
if client.IsDone() {
|
|
t.Fatal("should not be done before exchange")
|
|
}
|
|
|
|
carrier := make([]byte, 1500)
|
|
|
|
// --- First exchange ---
|
|
n, err := client.Encapsulate(carrier, 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("Encapsulate 1: %v", err)
|
|
}
|
|
if n < ntp.SizeHeader {
|
|
t.Fatalf("Encapsulate 1: n=%d too small", n)
|
|
}
|
|
|
|
// Simulate server response: build an NTP response and re-encrypt.
|
|
resp1 := buildTestResponse(t, carrier[:n], clockTime.Add(serverOffset), clockTime.Add(serverOffset+5*time.Millisecond), s2cKey)
|
|
clockTime = baseTime.Add(10*time.Second + 110*time.Millisecond)
|
|
if err = client.Demux(resp1, 0); err != nil {
|
|
t.Fatalf("Demux 1: %v", err)
|
|
}
|
|
if client.IsDone() {
|
|
t.Fatal("should not be done after first exchange only")
|
|
}
|
|
|
|
// --- Second exchange ---
|
|
carrier2 := make([]byte, 1500)
|
|
clockTime = baseTime.Add(10*time.Second + 200*time.Millisecond)
|
|
n2, err := client.Encapsulate(carrier2, 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("Encapsulate 2: %v", err)
|
|
}
|
|
if n2 == 0 {
|
|
t.Fatal("Encapsulate 2 returned 0 bytes")
|
|
}
|
|
|
|
resp2 := buildTestResponse(t, carrier2[:n2], clockTime.Add(serverOffset), clockTime.Add(serverOffset+5*time.Millisecond), s2cKey)
|
|
clockTime = baseTime.Add(10*time.Second + 310*time.Millisecond)
|
|
if err = client.Demux(resp2, 0); err != nil {
|
|
t.Fatalf("Demux 2: %v", err)
|
|
}
|
|
if !client.IsDone() {
|
|
t.Fatal("should be done after second exchange")
|
|
}
|
|
if client.RoundTripDelay() < 0 {
|
|
t.Errorf("RoundTripDelay = %v; want >= 0", client.RoundTripDelay())
|
|
}
|
|
}
|
|
|
|
func TestClient_Reset_Validation(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
aead, _ := siv.NewAESSIVCMAC256(key)
|
|
cookie := [MaxCookieLen]byte{}
|
|
cfg := ClientConfig{
|
|
C2S: aead, S2C: aead, NumCookies: 1,
|
|
Cookies: [MaxCookies][MaxCookieLen]byte{cookie},
|
|
CookieLens: [MaxCookies]int{8},
|
|
}
|
|
var c Client
|
|
if err := c.Reset(cfg); err != nil {
|
|
t.Fatalf("valid Reset: %v", err)
|
|
}
|
|
prevID := *c.ConnectionID()
|
|
|
|
cfg2 := cfg
|
|
cfg2.C2S = nil
|
|
if err := c.Reset(cfg2); err == nil {
|
|
t.Error("nil C2S: expected error")
|
|
}
|
|
if *c.ConnectionID() != prevID {
|
|
t.Error("connID should not increment on failed Reset")
|
|
}
|
|
|
|
cfg3 := cfg
|
|
cfg3.NumCookies = 0
|
|
if err := c.Reset(cfg3); err == nil {
|
|
t.Error("zero cookies: expected error")
|
|
}
|
|
}
|
|
|
|
func TestClient_ExhaustedCookies(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
aead, _ := siv.NewAESSIVCMAC256(key)
|
|
cfg := ClientConfig{
|
|
C2S: aead, S2C: aead, NumCookies: 1,
|
|
Cookies: [MaxCookies][MaxCookieLen]byte{[MaxCookieLen]byte{}},
|
|
CookieLens: [MaxCookies]int{8},
|
|
}
|
|
var c Client
|
|
c.Reset(cfg)
|
|
|
|
carrier := make([]byte, 1500)
|
|
if _, err := c.Encapsulate(carrier, 0, 0); err != nil {
|
|
t.Fatalf("first Encapsulate: %v", err)
|
|
}
|
|
if c.numCookies != 0 {
|
|
t.Fatalf("numCookies = %d; want 0", c.numCookies)
|
|
}
|
|
if _, err := c.Encapsulate(carrier, 0, 0); err != lneto.ErrExhausted {
|
|
t.Errorf("second Encapsulate: got %v; want ErrExhausted", err)
|
|
}
|
|
}
|
|
|
|
func TestClient_DemuxBadTag(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
aead, _ := siv.NewAESSIVCMAC256(key)
|
|
cookie := make([]byte, 16)
|
|
var cookies [MaxCookies][MaxCookieLen]byte
|
|
copy(cookies[0][:], cookie)
|
|
cfg := ClientConfig{
|
|
C2S: aead, S2C: aead, NumCookies: 1,
|
|
Cookies: cookies,
|
|
CookieLens: [MaxCookies]int{16},
|
|
}
|
|
var c Client
|
|
c.Reset(cfg)
|
|
|
|
carrier := make([]byte, 1500)
|
|
n, _ := c.Encapsulate(carrier, 0, 0)
|
|
|
|
resp := buildTestResponse(t, carrier[:n],
|
|
time.Now(), time.Now().Add(time.Millisecond), key)
|
|
// Tamper a byte in the NTP header (part of the AAD); this must cause
|
|
// the authentication tag to be rejected.
|
|
resp[ntp.SizeHeader-1] ^= 0xff
|
|
if err := c.Demux(resp, 0); err != lneto.ErrBadCRC {
|
|
t.Errorf("tampered Demux: got %v; want ErrBadCRC", err)
|
|
}
|
|
}
|
|
|
|
// buildTestResponse constructs a minimal NTS-authenticated NTP server response
|
|
// by echoing the client's UniqueID and re-sealing with s2cKey.
|
|
// The returned slice contains exactly the response bytes (no trailing zeros).
|
|
func buildTestResponse(t *testing.T, request []byte, serverRecv, serverXmt time.Time, s2cKey []byte) []byte {
|
|
t.Helper()
|
|
reqFrm, err := ntp.NewFrame(request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Extract the Unique-ID from the request so the response can echo it.
|
|
var uniqueID []byte
|
|
extBuf := reqFrm.ExtensionFields()
|
|
for off := 0; off < len(extBuf); {
|
|
field, n, e := ntp.NextExtField(extBuf[off:])
|
|
if e != nil || len(field.RawData()) == 0 {
|
|
break
|
|
}
|
|
if field.Type() == ntp.ExtNTSUniqueID {
|
|
uniqueID = field.Value()
|
|
break
|
|
}
|
|
off += n
|
|
}
|
|
|
|
// Build response in a new buffer.
|
|
resp := make([]byte, 1500)
|
|
respFrm, _ := ntp.NewFrame(resp)
|
|
respFrm.SetFlags(ntp.ModeServer, ntp.Version4, ntp.LeapNoWarning)
|
|
respFrm.SetStratum(ntp.StratumPrimary)
|
|
respFrm.SetPrecision(-20)
|
|
respFrm.SetOriginTime(reqFrm.TransmitTime())
|
|
recvTS, _ := ntp.TimestampFromTime(serverRecv)
|
|
xmtTS, _ := ntp.TimestampFromTime(serverXmt)
|
|
respFrm.SetReceiveTime(recvTS)
|
|
respFrm.SetTransmitTime(xmtTS)
|
|
|
|
// Append UniqueID extension field (echo).
|
|
respBuf := resp[:ntp.SizeHeader]
|
|
if len(uniqueID) > 0 {
|
|
respBuf = ntp.AppendExtField(respBuf, ntp.ExtNTSUniqueID, uniqueID)
|
|
}
|
|
|
|
// Build NTS-Auth field sealed with S2C key.
|
|
s2c, err := siv.NewAESSIVCMAC256(s2cKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
nonceLen := s2c.NonceSize()
|
|
overhead := s2c.Overhead()
|
|
|
|
var nonce [maxNonceLen]byte
|
|
rand.Read(nonce[:nonceLen])
|
|
|
|
aad := respBuf
|
|
|
|
var authBody [maxAuthBody]byte
|
|
binary.BigEndian.PutUint16(authBody[0:2], uint16(nonceLen))
|
|
binary.BigEndian.PutUint16(authBody[2:4], uint16(overhead))
|
|
copy(authBody[4:4+nonceLen], nonce[:nonceLen])
|
|
s2c.Seal(authBody[4+nonceLen:4+nonceLen], nonce[:nonceLen], nil, aad)
|
|
|
|
respBuf = ntp.AppendExtField(respBuf, ntp.ExtNTSAuthAndEEF, authBody[:4+nonceLen+overhead])
|
|
result := make([]byte, len(respBuf))
|
|
copy(result, respBuf)
|
|
return result
|
|
}
|
|
*/
|
|
|
|
func FuzzKERecord(f *testing.F) {
|
|
// Seed with valid records.
|
|
f.Add(AppendKERecord(nil, true, RecordEndOfMessage, nil))
|
|
f.Add(AppendKERecord(nil, false, RecordNewCookie, []byte("cookie")))
|
|
f.Add(AppendKERecord(nil, true, RecordAEADAlgNeg, []byte{0, 15}))
|
|
// Seed with short inputs.
|
|
f.Add([]byte{})
|
|
f.Add([]byte{0x80, 0x01})
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
rec, err := NewKERecord(data)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var v lneto.Validator
|
|
rec.ValidateSize(&v)
|
|
_ = rec.RecordType()
|
|
_ = rec.IsCritical()
|
|
_ = rec.Body()
|
|
})
|
|
}
|
|
|
|
func FuzzNextExtField(f *testing.F) {
|
|
f.Add(ntp.AppendExtField(nil, ntp.ExtNTSUniqueID, make([]byte, 32)))
|
|
f.Add(ntp.AppendExtField(nil, ntp.ExtNTSCookie, make([]byte, 64)))
|
|
f.Add([]byte{})
|
|
f.Add([]byte{0, 1, 0, 0})
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
field, n, _ := ntp.NextExtField(data)
|
|
_ = field.RawData()
|
|
_ = n
|
|
})
|
|
}
|
|
|
|
/*
|
|
DISABLED: depends on github.com/soypat/lneto/x/siv (see note above).
|
|
|
|
func TestServer_Reset_Validation(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
aead, _ := siv.NewAESSIVCMAC256(key)
|
|
var s Server
|
|
if err := s.Reset(ServerConfig{C2S: aead, S2C: aead, Stratum: ntp.StratumPrimary}); err != nil {
|
|
t.Fatalf("valid Reset: %v", err)
|
|
}
|
|
prevID := *s.ConnectionID()
|
|
if err := s.Reset(ServerConfig{C2S: nil, S2C: aead}); err == nil {
|
|
t.Error("nil C2S: expected error")
|
|
}
|
|
if *s.ConnectionID() != prevID {
|
|
t.Error("connID should not increment on failed Reset")
|
|
}
|
|
}
|
|
|
|
func TestServer_NoPendingReturnsZero(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
aead, _ := siv.NewAESSIVCMAC256(key)
|
|
var s Server
|
|
s.Reset(ServerConfig{C2S: aead, S2C: aead, Stratum: ntp.StratumPrimary})
|
|
carrier := make([]byte, 1500)
|
|
n, err := s.Encapsulate(carrier, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 0 {
|
|
t.Fatalf("expected 0, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestClientServer_E2E(t *testing.T) {
|
|
c2sKey := make([]byte, 32)
|
|
s2cKey := make([]byte, 32)
|
|
rand.Read(c2sKey)
|
|
rand.Read(s2cKey)
|
|
c2s, _ := siv.NewAESSIVCMAC256(c2sKey)
|
|
s2c, _ := siv.NewAESSIVCMAC256(s2cKey)
|
|
|
|
cookie := []byte("nts-cookie-round-trip-test")
|
|
|
|
baseTime := ntp.BaseTime()
|
|
clientTime := baseTime.Add(10 * time.Second)
|
|
serverTime := clientTime.Add(200 * time.Millisecond)
|
|
|
|
var clientCfg ClientConfig
|
|
clientCfg.C2S = c2s
|
|
clientCfg.S2C = s2c
|
|
clientCfg.ChosenAlg = AlgAESSIVCMAC256
|
|
clientCfg.Now = func() time.Time { return clientTime }
|
|
clientCfg.Sysprec = -20
|
|
for i := range 2 {
|
|
copy(clientCfg.Cookies[i][:], cookie)
|
|
clientCfg.CookieLens[i] = len(cookie)
|
|
}
|
|
clientCfg.NumCookies = 2
|
|
|
|
var client Client
|
|
if err := client.Reset(clientCfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Server uses same keys (C2S to verify client, S2C to seal responses).
|
|
var server Server
|
|
if err := server.Reset(ServerConfig{
|
|
C2S: c2s,
|
|
S2C: s2c,
|
|
Now: func() time.Time { return serverTime },
|
|
Stratum: ntp.StratumPrimary,
|
|
Prec: -20,
|
|
RefID: [4]byte{'G', 'P', 'S', 0},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for exchange := range 2 {
|
|
carrier := make([]byte, 1500)
|
|
n, err := client.Encapsulate(carrier, 0, 0)
|
|
if err != nil || n == 0 {
|
|
t.Fatalf("exchange %d: client Encapsulate: n=%d err=%v", exchange, n, err)
|
|
}
|
|
|
|
if err = server.Demux(carrier[:n], 0); err != nil {
|
|
t.Fatalf("exchange %d: server Demux: %v", exchange, err)
|
|
}
|
|
|
|
resp := make([]byte, 1500)
|
|
rn, err := server.Encapsulate(resp, 0, 0)
|
|
if err != nil || rn == 0 {
|
|
t.Fatalf("exchange %d: server Encapsulate: rn=%d err=%v", exchange, rn, err)
|
|
}
|
|
|
|
clientTime = clientTime.Add(100 * time.Millisecond)
|
|
serverTime = serverTime.Add(100 * time.Millisecond)
|
|
|
|
if err = client.Demux(resp[:rn], 0); err != nil {
|
|
t.Fatalf("exchange %d: client Demux: %v", exchange, err)
|
|
}
|
|
}
|
|
|
|
if !client.IsDone() {
|
|
t.Fatal("client should be done after two exchanges")
|
|
}
|
|
if client.RoundTripDelay() < 0 {
|
|
t.Errorf("RTD = %v; want >= 0", client.RoundTripDelay())
|
|
}
|
|
}
|
|
|
|
func TestClientServer_TamperedAADRejected(t *testing.T) {
|
|
c2sKey := make([]byte, 32)
|
|
s2cKey := make([]byte, 32)
|
|
rand.Read(c2sKey)
|
|
rand.Read(s2cKey)
|
|
c2s, _ := siv.NewAESSIVCMAC256(c2sKey)
|
|
s2c, _ := siv.NewAESSIVCMAC256(s2cKey)
|
|
|
|
cookie := []byte("cookie-tamper-test")
|
|
var cfg ClientConfig
|
|
cfg.C2S = c2s
|
|
cfg.S2C = s2c
|
|
cfg.NumCookies = 1
|
|
copy(cfg.Cookies[0][:], cookie)
|
|
cfg.CookieLens[0] = len(cookie)
|
|
|
|
var client Client
|
|
client.Reset(cfg)
|
|
|
|
carrier := make([]byte, 1500)
|
|
n, _ := client.Encapsulate(carrier, 0, 0)
|
|
|
|
// Tamper with NTP header (part of AAD).
|
|
carrier[ntp.SizeHeader-1] ^= 0xff
|
|
|
|
var server Server
|
|
server.Reset(ServerConfig{C2S: c2s, S2C: s2c, Stratum: ntp.StratumPrimary})
|
|
|
|
if err := server.Demux(carrier[:n], 0); err != lneto.ErrBadCRC {
|
|
t.Errorf("tampered Demux: got %v; want ErrBadCRC", err)
|
|
}
|
|
}
|
|
*/
|
|
|
|
func TestHandleKE_E2E(t *testing.T) {
|
|
cert := generateSelfSignedCert(t)
|
|
pool := x509.NewCertPool()
|
|
leaf, _ := x509.ParseCertificate(cert.Certificate[0])
|
|
pool.AddCert(leaf)
|
|
|
|
serverTLSCfg := &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
MinVersion: tls.VersionTLS13,
|
|
NextProtos: []string{"ntske/1"},
|
|
}
|
|
clientTLSCfg := &tls.Config{
|
|
RootCAs: pool,
|
|
ServerName: "localhost",
|
|
MinVersion: tls.VersionTLS13,
|
|
NextProtos: []string{"ntske/1"},
|
|
}
|
|
|
|
wantCookie := []byte("ke-server-cookie-data")
|
|
|
|
serverConn, clientConn := net.Pipe()
|
|
done := make(chan KESecrets, 1)
|
|
errc := make(chan error, 1)
|
|
go func() {
|
|
tc := tls.Server(serverConn, serverTLSCfg)
|
|
if err := tc.Handshake(); err != nil {
|
|
errc <- err
|
|
return
|
|
}
|
|
defer tc.Close()
|
|
secrets, err := HandleKE(tc, KEServerConfig{
|
|
Cookies: [][]byte{wantCookie},
|
|
})
|
|
if err != nil {
|
|
errc <- err
|
|
return
|
|
}
|
|
done <- secrets
|
|
}()
|
|
|
|
tc := tls.Client(clientConn, clientTLSCfg)
|
|
if err := tc.Handshake(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
clientSecrets, err := PerformKE(tc, KEConfig{})
|
|
// Close the underlying pipe rather than tc.Close(): a TLS close_notify
|
|
// write over the synchronous net.Pipe would otherwise block on crypto/tls'
|
|
// 5s close deadline since the peer is no longer reading.
|
|
clientConn.Close()
|
|
|
|
select {
|
|
case err := <-errc:
|
|
t.Fatalf("server KE: %v", err)
|
|
case serverSecrets := <-done:
|
|
if err != nil {
|
|
t.Fatalf("client KE: %v", err)
|
|
}
|
|
if clientSecrets.ChosenAlg != serverSecrets.ChosenAlg {
|
|
t.Errorf("ChosenAlg: client=%v server=%v; want equal", clientSecrets.ChosenAlg, serverSecrets.ChosenAlg)
|
|
}
|
|
if clientSecrets.C2SKey != serverSecrets.C2SKey {
|
|
t.Errorf("C2SKey mismatch: client and server derived different keys")
|
|
}
|
|
if clientSecrets.S2CKey != serverSecrets.S2CKey {
|
|
t.Errorf("S2CKey mismatch: client and server derived different keys")
|
|
}
|
|
if clientSecrets.NumCookies != 1 {
|
|
t.Errorf("NumCookies = %d; want 1", clientSecrets.NumCookies)
|
|
}
|
|
gotCookie := clientSecrets.Cookies[0][:clientSecrets.CookieLens[0]]
|
|
if !bytes.Equal(gotCookie, wantCookie) {
|
|
t.Errorf("cookie = %q; want %q", gotCookie, wantCookie)
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("HandleKE server goroutine timed out")
|
|
}
|
|
}
|
|
|
|
/*
|
|
DISABLED: missing AES-SIV-CMAC-256 in stdlib
|
|
|
|
func FuzzServerDemux(f *testing.F) {
|
|
key := make([]byte, 32)
|
|
aead, _ := siv.NewAESSIVCMAC256(key)
|
|
|
|
// Seed with a valid NTS request built by the client.
|
|
var c Client
|
|
c.Reset(ClientConfig{
|
|
C2S: aead, S2C: aead, NumCookies: 1,
|
|
Cookies: [MaxCookies][MaxCookieLen]byte{},
|
|
CookieLens: [MaxCookies]int{16},
|
|
})
|
|
carrier := make([]byte, 1500)
|
|
n, _ := c.Encapsulate(carrier, 0, 0)
|
|
if n > 0 {
|
|
f.Add(carrier[:n])
|
|
}
|
|
f.Add(make([]byte, ntp.SizeHeader))
|
|
f.Add([]byte{})
|
|
f.Add(make([]byte, 10))
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
fuzzKey := make([]byte, 32)
|
|
fuzzAEAD, _ := siv.NewAESSIVCMAC256(fuzzKey)
|
|
var s Server
|
|
s.Reset(ServerConfig{C2S: fuzzAEAD, S2C: fuzzAEAD, Stratum: ntp.StratumPrimary})
|
|
_ = s.Demux(data, 0)
|
|
})
|
|
}
|
|
|
|
func BenchmarkClient_Encapsulate(b *testing.B) {
|
|
key := make([]byte, 32)
|
|
aead, _ := siv.NewAESSIVCMAC256(key)
|
|
carrier := make([]byte, 1500)
|
|
|
|
var c Client
|
|
newCfg := func() ClientConfig {
|
|
var cookies [MaxCookies][MaxCookieLen]byte
|
|
var lens [MaxCookies]int
|
|
for i := range cookies {
|
|
copy(cookies[i][:], make([]byte, 32))
|
|
lens[i] = 32
|
|
}
|
|
return ClientConfig{
|
|
C2S: aead, S2C: aead, ChosenAlg: AlgAESSIVCMAC256,
|
|
NumCookies: MaxCookies, Cookies: cookies, CookieLens: lens,
|
|
Now: time.Now,
|
|
}
|
|
}
|
|
c.Reset(newCfg())
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
if c.numCookies == 0 {
|
|
c.Reset(newCfg())
|
|
}
|
|
c.Encapsulate(carrier, 0, 0)
|
|
}
|
|
}
|
|
*/
|