mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 17:16:52 +01:00
260 lines
9.9 KiB
Go
260 lines
9.9 KiB
Go
package handshake
|
|
|
|
import (
|
|
"crypto/mlkem"
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DefinedNet/noise"
|
|
"github.com/slackhq/nebula/cert"
|
|
ct "github.com/slackhq/nebula/cert_test"
|
|
"github.com/slackhq/nebula/header"
|
|
"github.com/slackhq/nebula/noiseutil"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func hybridCipherSuite(curve cert.Curve) noise.CipherSuite {
|
|
if curve == cert.Curve_P256 {
|
|
return noise.NewCipherSuite(noiseutil.DHP256, noise.CipherAESGCM, noise.HashSHA256)
|
|
}
|
|
return noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashSHA256)
|
|
}
|
|
|
|
// newHybridCertState builds a peer holding a v3 certificate, with credentials for it and for its v2 form, the way
|
|
// the pki does.
|
|
func newHybridCertState(t *testing.T, curve cert.Curve, ca cert.Certificate, caKey []byte, name string, network string) (*testCertState, []byte) {
|
|
t.Helper()
|
|
c, _, rawPrivKey, _ := ct.NewTestCert(
|
|
cert.Version3, curve, ca, caKey,
|
|
name, ca.NotBefore(), ca.NotAfter(), []netip.Prefix{netip.MustParsePrefix(network)}, nil, nil,
|
|
)
|
|
priv, _, _, err := cert.UnmarshalPrivateKeyFromPEM(rawPrivKey)
|
|
require.NoError(t, err)
|
|
return hybridCertStateFor(t, curve, c, priv), priv
|
|
}
|
|
|
|
func hybridCertStateFor(t *testing.T, curve cert.Curve, c cert.Certificate, priv []byte) *testCertState {
|
|
t.Helper()
|
|
hs3, err := c.MarshalForHandshakes()
|
|
require.NoError(t, err)
|
|
v2 := cert.V2Form(c)
|
|
hs2, err := v2.MarshalForHandshakes()
|
|
require.NoError(t, err)
|
|
|
|
ncs := hybridCipherSuite(curve)
|
|
return &testCertState{
|
|
version: cert.Version3,
|
|
creds: map[cert.Version]*Credential{
|
|
cert.Version3: NewCredential(c, hs3, priv, ncs),
|
|
cert.Version2: NewCredential(v2, hs2, priv[:len(priv)-cert.MLKEM1024PrivateKeySize], ncs),
|
|
},
|
|
}
|
|
}
|
|
|
|
func newHybridMachine(t *testing.T, cs *testCertState, verifier CertVerifier, initiator bool, localIndex uint32) *Machine {
|
|
t.Helper()
|
|
m, err := NewMachine(
|
|
cert.Version3, cs.getCredential,
|
|
verifier, func() (uint32, error) { return localIndex, nil },
|
|
initiator, header.HandshakeIXHybridMLKEM1024,
|
|
)
|
|
require.NoError(t, err)
|
|
return m
|
|
}
|
|
|
|
type hybridPeers struct {
|
|
pool *cert.CAPool
|
|
ca cert.Certificate
|
|
caKey []byte
|
|
initCS, respCS *testCertState
|
|
initPriv, respPriv []byte
|
|
}
|
|
|
|
func newHybridPeers(t *testing.T, curve cert.Curve) hybridPeers {
|
|
t.Helper()
|
|
ca, _, caKey, _ := ct.NewTestCaCert(cert.Version3, curve, time.Now().Add(-time.Hour), time.Now().Add(time.Hour), nil, nil, nil)
|
|
pool := cert.NewCAPool()
|
|
require.NoError(t, pool.AddCA(ca))
|
|
initCS, initPriv := newHybridCertState(t, curve, ca, caKey, "initiator", "10.0.0.1/24")
|
|
respCS, respPriv := newHybridCertState(t, curve, ca, caKey, "responder", "10.0.0.2/24")
|
|
return hybridPeers{pool: pool, ca: ca, caKey: caKey, initCS: initCS, respCS: respCS, initPriv: initPriv, respPriv: respPriv}
|
|
}
|
|
|
|
func TestHybridIX(t *testing.T) {
|
|
for _, curve := range []cert.Curve{cert.Curve_CURVE25519, cert.Curve_P256} {
|
|
t.Run(curve.String(), func(t *testing.T) {
|
|
p := newHybridPeers(t, curve)
|
|
v := testVerifier(p.pool)
|
|
initM := newHybridMachine(t, p.initCS, v, true, 1000)
|
|
respM := newHybridMachine(t, p.respCS, v, false, 2000)
|
|
|
|
msg1, err := initM.Initiate(nil)
|
|
require.NoError(t, err)
|
|
|
|
// The responder answers msg1 but can not complete until msg3.
|
|
msg2, respResult, err := respM.ProcessPacket(nil, msg1)
|
|
require.NoError(t, err)
|
|
require.Nil(t, respResult)
|
|
require.NotEmpty(t, msg2)
|
|
|
|
// The initiator completes by writing msg3.
|
|
msg3, initResult, err := initM.ProcessPacket(nil, msg2)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, initResult)
|
|
require.NotEmpty(t, msg3)
|
|
|
|
out, respResult, err := respM.ProcessPacket(nil, msg3)
|
|
require.NoError(t, err)
|
|
require.Nil(t, out)
|
|
require.NotNil(t, respResult)
|
|
|
|
t.Logf("msg1 %d B, msg2 %d B, msg3 %d B", len(msg1), len(msg2), len(msg3))
|
|
for i, msg := range [][]byte{msg1, msg2, msg3} {
|
|
h := &header.H{}
|
|
require.NoError(t, h.Parse(msg))
|
|
assert.Equal(t, header.HandshakeIXHybridMLKEM1024, h.Subtype)
|
|
assert.Equal(t, uint64(i+1), h.MessageCounter)
|
|
}
|
|
|
|
assert.Equal(t, uint32(2000), initResult.RemoteIndex)
|
|
assert.Equal(t, uint32(1000), respResult.RemoteIndex)
|
|
assert.Equal(t, cert.Version3, initResult.RemoteCert.Certificate.Version())
|
|
assert.Equal(t, cert.Version3, respResult.RemoteCert.Certificate.Version())
|
|
assert.Equal(t, "responder", initResult.RemoteCert.Certificate.Name())
|
|
assert.Equal(t, "initiator", respResult.RemoteCert.Certificate.Name())
|
|
assert.Equal(t, cert.Version3, initResult.MyCert.Version())
|
|
assert.Equal(t, uint64(3), initResult.MessageIndex)
|
|
assert.Equal(t, uint64(3), respResult.MessageIndex)
|
|
|
|
// The keys pair up in both directions.
|
|
for _, pair := range [][2]*Result{{initResult, respResult}, {respResult, initResult}} {
|
|
ct, err := pair[0].EKey.Encrypt(nil, nil, []byte("hello"))
|
|
require.NoError(t, err)
|
|
pt, err := pair[1].DKey.Decrypt(nil, nil, ct)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("hello"), pt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A hybrid handshake needs v3 certificates, and a v3 certificate needs a hybrid handshake.
|
|
func TestHybridIX_CertVersions(t *testing.T) {
|
|
p := newHybridPeers(t, cert.Curve_CURVE25519)
|
|
v := testVerifier(p.pool)
|
|
|
|
_, err := NewMachine(cert.Version2, p.initCS.getCredential, v, func() (uint32, error) { return 1, nil }, true, header.HandshakeIXHybridMLKEM1024)
|
|
require.ErrorIs(t, err, ErrCertVersionMismatch)
|
|
_, err = NewMachine(cert.Version3, p.initCS.getCredential, v, func() (uint32, error) { return 1, nil }, true, header.HandshakeIXPSK0)
|
|
require.ErrorIs(t, err, ErrCertVersionMismatch)
|
|
_, err = NewMachine(cert.Version3, p.initCS.getCredential, v, func() (uint32, error) { return 1, nil }, true, header.HandshakeIXPSK2MLKEM1024)
|
|
require.ErrorIs(t, err, ErrCertVersionMismatch)
|
|
|
|
// The v2 form still does a classical handshake, here with a peer that only has v2.
|
|
ca2 := cert.V2Form(p.ca)
|
|
pool2 := cert.NewCAPool()
|
|
require.NoError(t, pool2.AddCA(ca2))
|
|
classicalCAKey := p.caKey[:len(p.caKey)-cert.MLDSA87PrivateKeySize]
|
|
v2Peer := newTestCertState(t, ca2, classicalCAKey, "v2peer", []netip.Prefix{netip.MustParsePrefix("10.0.0.3/24")})
|
|
p.initCS.version = cert.Version2
|
|
initResult, respResult := doFullHandshake(t, p.initCS, v2Peer, pool2)
|
|
assert.Equal(t, cert.Version2, initResult.RemoteCert.Certificate.Version())
|
|
assert.Equal(t, cert.Version2, respResult.RemoteCert.Certificate.Version())
|
|
assert.Equal(t, cert.Version2, initResult.MyCert.Version())
|
|
}
|
|
|
|
// A responder that only trusts the v2 form of the CA can not verify a v3 certificate.
|
|
func TestHybridIX_V2TrustOnly(t *testing.T) {
|
|
p := newHybridPeers(t, cert.Curve_CURVE25519)
|
|
pool2 := cert.NewCAPool()
|
|
require.NoError(t, pool2.AddCA(cert.V2Form(p.ca)))
|
|
|
|
initM := newHybridMachine(t, p.initCS, testVerifier(p.pool), true, 1000)
|
|
respM := newHybridMachine(t, p.respCS, testVerifier(pool2), false, 2000)
|
|
msg1, err := initM.Initiate(nil)
|
|
require.NoError(t, err)
|
|
_, _, err = respM.ProcessPacket(nil, msg1)
|
|
require.ErrorIs(t, err, cert.ErrCaNotFound)
|
|
}
|
|
|
|
// A corrupted msg3 is dropped without killing the responder, and the real one still completes.
|
|
func TestHybridIX_CorruptMsg3(t *testing.T) {
|
|
p := newHybridPeers(t, cert.Curve_CURVE25519)
|
|
v := testVerifier(p.pool)
|
|
initM := newHybridMachine(t, p.initCS, v, true, 1000)
|
|
respM := newHybridMachine(t, p.respCS, v, false, 2000)
|
|
|
|
msg1, err := initM.Initiate(nil)
|
|
require.NoError(t, err)
|
|
msg2, _, err := respM.ProcessPacket(nil, msg1)
|
|
require.NoError(t, err)
|
|
msg3, _, err := initM.ProcessPacket(nil, msg2)
|
|
require.NoError(t, err)
|
|
|
|
bad := append([]byte(nil), msg3...)
|
|
bad[header.Len+10] ^= 1
|
|
_, res, err := respM.ProcessPacket(nil, bad)
|
|
require.Error(t, err)
|
|
require.Nil(t, res)
|
|
require.False(t, respM.Failed())
|
|
|
|
_, res, err = respM.ProcessPacket(nil, msg3)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
}
|
|
|
|
// withKEMKey returns a peer holding c and its classical private key, but some other ML-KEM key: someone who broke
|
|
// the classical key, say with a quantum computer, and wants to be that peer.
|
|
func withKEMKey(t *testing.T, curve cert.Curve, c cert.Certificate, priv []byte) *testCertState {
|
|
t.Helper()
|
|
dk, err := mlkem.GenerateKey1024()
|
|
require.NoError(t, err)
|
|
forged := append(append([]byte(nil), priv[:len(priv)-cert.MLKEM1024PrivateKeySize]...), dk.Bytes()...)
|
|
return hybridCertStateFor(t, curve, c, forged)
|
|
}
|
|
|
|
// The classical private key alone does not get anyone through as the initiator: msg2 is unreadable without the
|
|
// initiator's ML-KEM key.
|
|
func TestHybridIX_ImpersonateInitiator(t *testing.T) {
|
|
p := newHybridPeers(t, cert.Curve_CURVE25519)
|
|
v := testVerifier(p.pool)
|
|
victim := p.initCS.creds[cert.Version3].Cert
|
|
attacker := withKEMKey(t, cert.Curve_CURVE25519, victim, p.initPriv)
|
|
|
|
initM := newHybridMachine(t, attacker, v, true, 1000)
|
|
respM := newHybridMachine(t, p.respCS, v, false, 2000)
|
|
msg1, err := initM.Initiate(nil)
|
|
require.NoError(t, err)
|
|
msg2, _, err := respM.ProcessPacket(nil, msg1)
|
|
require.NoError(t, err)
|
|
|
|
_, res, err := initM.ProcessPacket(nil, msg2)
|
|
require.Error(t, err)
|
|
require.Nil(t, res)
|
|
}
|
|
|
|
// The classical private key alone does not get anyone through as the responder either: the initiator completes,
|
|
// but msg3 is unreadable without the responder's ML-KEM key, so no packet under the new keys can ever come back.
|
|
func TestHybridIX_ImpersonateResponder(t *testing.T) {
|
|
p := newHybridPeers(t, cert.Curve_CURVE25519)
|
|
v := testVerifier(p.pool)
|
|
victim := p.respCS.creds[cert.Version3].Cert
|
|
attacker := withKEMKey(t, cert.Curve_CURVE25519, victim, p.respPriv)
|
|
|
|
initM := newHybridMachine(t, p.initCS, v, true, 1000)
|
|
respM := newHybridMachine(t, attacker, v, false, 2000)
|
|
msg1, err := initM.Initiate(nil)
|
|
require.NoError(t, err)
|
|
msg2, _, err := respM.ProcessPacket(nil, msg1)
|
|
require.NoError(t, err)
|
|
msg3, initResult, err := initM.ProcessPacket(nil, msg2)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, initResult)
|
|
|
|
_, res, err := respM.ProcessPacket(nil, msg3)
|
|
require.Error(t, err)
|
|
require.Nil(t, res)
|
|
}
|