mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 20:26:52 +01:00
handshake: Add ix_hybrid_mlkem1024, DefinedNet/noise hybrid IX with ML-KEM-1024 over version 3 certificates
This commit is contained in:
1 parent
1a8fb8d67e
commit
ab89556c5c
8 files changed
+401
-31
No files matched your search
+30
-8
@@ -14,39 +14,61 @@ type Credential struct {
|
||||
Cert cert.Certificate // the certificate
|
||||
Bytes []byte // pre-marshaled certificate bytes
|
||||
privateKey []byte // static private key (public half lives in Cert)
|
||||
publicKey []byte // static public key, the classical half for a v3 Cert
|
||||
kemKey noise.KEMKey // static ML-KEM-1024 keypair, only for a v3 Cert
|
||||
cipherSuite noise.CipherSuite // pre-built cipher suite (DH + cipher + hash)
|
||||
}
|
||||
|
||||
// NewCredential creates a Credential with all material needed for handshake
|
||||
// participation. The cipherSuite should be pre-built by the caller with the
|
||||
// appropriate DH function, cipher, and hash.
|
||||
//
|
||||
// For a version 3 certificate privateKey is the composite private key,
|
||||
// classical || ML-KEM-1024 seed, matching the composite Cert.PublicKey().
|
||||
func NewCredential(
|
||||
c cert.Certificate,
|
||||
hsBytes []byte,
|
||||
privateKey []byte,
|
||||
cipherSuite noise.CipherSuite,
|
||||
) *Credential {
|
||||
return &Credential{
|
||||
hc := &Credential{
|
||||
Cert: c,
|
||||
Bytes: hsBytes,
|
||||
privateKey: privateKey,
|
||||
publicKey: c.PublicKey(),
|
||||
cipherSuite: cipherSuite,
|
||||
}
|
||||
if v2 := cert.V2Form(c); v2 != nil {
|
||||
classical := v2.PublicKey()
|
||||
hc.publicKey = classical
|
||||
hc.kemKey = noise.KEMKey{
|
||||
Private: privateKey[len(privateKey)-cert.MLKEM1024PrivateKeySize:],
|
||||
Public: c.PublicKey()[len(classical):],
|
||||
}
|
||||
hc.privateKey = privateKey[:len(privateKey)-cert.MLKEM1024PrivateKeySize]
|
||||
}
|
||||
return hc
|
||||
}
|
||||
|
||||
// buildHandshakeState creates a noise.HandshakeState from this credential.
|
||||
// pskPlacement selects the noise psk modifier; 0 means none. A psk for
|
||||
// placements 2 and up is supplied later with SetPresharedKey.
|
||||
func (hc *Credential) buildHandshakeState(initiator bool, pattern noise.HandshakePattern, pskPlacement int) (*noise.HandshakeState, error) {
|
||||
return noise.NewHandshakeState(noise.Config{
|
||||
// placements 2 and up is supplied later with SetPresharedKey. A hybrid
|
||||
// pattern also gets ML-KEM-1024 and the static ML-KEM key.
|
||||
func (hc *Credential) buildHandshakeState(initiator bool, info subtypeInfo) (*noise.HandshakeState, error) {
|
||||
cfg := noise.Config{
|
||||
CipherSuite: hc.cipherSuite,
|
||||
Random: rand.Reader,
|
||||
Pattern: pattern,
|
||||
Pattern: info.pattern,
|
||||
Initiator: initiator,
|
||||
StaticKeypair: noise.DHKey{Private: hc.privateKey, Public: hc.Cert.PublicKey()},
|
||||
StaticKeypair: noise.DHKey{Private: hc.privateKey, Public: hc.publicKey},
|
||||
PresharedKey: []byte{},
|
||||
PresharedKeyPlacement: pskPlacement,
|
||||
})
|
||||
PresharedKeyPlacement: info.pskPlacement,
|
||||
}
|
||||
if info.hybrid {
|
||||
cfg.KEMFunc = KEMMLKEM1024
|
||||
cfg.StaticKEMKeypair = hc.kemKey
|
||||
}
|
||||
return noise.NewHandshakeState(cfg)
|
||||
}
|
||||
|
||||
// GetCredentialFunc returns the handshake credential for the given version,
|
||||
|
||||
@@ -19,4 +19,5 @@ var (
|
||||
ErrAsymmetricCipherKeys = errors.New("noise produced only one cipher key")
|
||||
ErrMultiMessageUnsupported = errors.New("multi-message handshake patterns are not yet supported by the manager")
|
||||
ErrSubtypeMismatch = errors.New("packet subtype does not match handshake machine subtype")
|
||||
ErrCertVersionMismatch = errors.New("handshake subtype does not allow this certificate version")
|
||||
)
|
||||
@@ -0,0 +1,259 @@
|
||||
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)
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/DefinedNet/noise"
|
||||
)
|
||||
|
||||
// kemPSKSalt separates the psk derivation from any other use of the secret.
|
||||
@@ -66,3 +69,39 @@ func (m *Machine) kemDecapsulate(msg []byte) ([]byte, error) {
|
||||
}
|
||||
return msg[mlkem.CiphertextSize1024:], nil
|
||||
}
|
||||
|
||||
// KEMMLKEM1024 is ML-KEM-1024 for the noise library's hybrid patterns. Private
|
||||
// keys are the 64 byte seed, the same form v3 private keys store.
|
||||
var KEMMLKEM1024 noise.KEMFunc = kemMLKEM1024{}
|
||||
|
||||
type kemMLKEM1024 struct{}
|
||||
|
||||
// GenerateKeypair ignores random, crypto/mlkem always draws from crypto/rand.
|
||||
func (kemMLKEM1024) GenerateKeypair(_ io.Reader) (noise.KEMKey, error) {
|
||||
dk, err := mlkem.GenerateKey1024()
|
||||
if err != nil {
|
||||
return noise.KEMKey{}, fmt.Errorf("mlkem1024: GenerateKey: %w", err)
|
||||
}
|
||||
return noise.KEMKey{Private: dk.Bytes(), Public: dk.EncapsulationKey().Bytes()}, nil
|
||||
}
|
||||
|
||||
func (kemMLKEM1024) Encapsulate(pubkey []byte) ([]byte, []byte, error) {
|
||||
ek, err := mlkem.NewEncapsulationKey1024(pubkey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%w: %w", ErrInvalidKEMKey, err)
|
||||
}
|
||||
ss, ct := ek.Encapsulate()
|
||||
return ct, ss, nil
|
||||
}
|
||||
|
||||
func (kemMLKEM1024) Decapsulate(privkey, ciphertext []byte) ([]byte, error) {
|
||||
dk, err := mlkem.NewDecapsulationKey1024(privkey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mlkem1024: NewDecapsulationKey: %w", err)
|
||||
}
|
||||
return dk.Decapsulate(ciphertext)
|
||||
}
|
||||
|
||||
func (kemMLKEM1024) PublicKeyLen() int { return mlkem.EncapsulationKeySize1024 }
|
||||
func (kemMLKEM1024) CiphertextLen() int { return mlkem.CiphertextSize1024 }
|
||||
func (kemMLKEM1024) KEMName() string { return "MLKEM1024" }
|
||||
+32
-17
@@ -65,6 +65,7 @@ type Machine struct {
|
||||
msgs []msgFlags
|
||||
myVersion cert.Version
|
||||
subtype header.MessageSubType
|
||||
hybrid bool
|
||||
indexAllocated bool
|
||||
remoteCertSet bool
|
||||
payloadSet bool
|
||||
@@ -91,12 +92,18 @@ func NewMachine(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Version 3 certificates go with hybrid patterns and nothing else, the
|
||||
// classical and phase 1 subtypes use the v2 form instead.
|
||||
if info.hybrid != (version == cert.Version3) {
|
||||
return nil, fmt.Errorf("%w: %v", ErrCertVersionMismatch, version)
|
||||
}
|
||||
|
||||
cred := getCred(version)
|
||||
if cred == nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrNoCredential, version)
|
||||
}
|
||||
|
||||
hs, err := cred.buildHandshakeState(initiator, info.pattern, info.pskPlacement)
|
||||
hs, err := cred.buildHandshakeState(initiator, info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build noise state: %w", err)
|
||||
}
|
||||
@@ -105,6 +112,7 @@ func NewMachine(
|
||||
hs: hs,
|
||||
subtype: subtype,
|
||||
msgs: info.msgs,
|
||||
hybrid: info.hybrid,
|
||||
getCred: getCred,
|
||||
allocIndex: allocIndex,
|
||||
verifier: verifier,
|
||||
@@ -226,11 +234,6 @@ func (m *Machine) ProcessPacket(out, packet []byte) ([]byte, *Result, error) {
|
||||
return nil, nil, ErrInitiateNotCalled
|
||||
}
|
||||
|
||||
// The (eKey, dKey) ordering here is correct for IX, where the initiator
|
||||
// completes the handshake by reading the responder's stage-2 message.
|
||||
// noise returns (cs1, cs2) where cs1 is the initiator->responder cipher.
|
||||
// For 3-message patterns where a responder finishes by reading the final
|
||||
// message, this ordering would be wrong; revisit when XX/pqIX lands.
|
||||
noiseMsg := packet[header.Len:]
|
||||
if m.myMsgFlags().kemCiphertext {
|
||||
var err error
|
||||
@@ -242,7 +245,8 @@ func (m *Machine) ProcessPacket(out, packet []byte) ([]byte, *Result, error) {
|
||||
}
|
||||
}
|
||||
|
||||
msg, eKey, dKey, err := m.hs.ReadMessage(nil, noiseMsg)
|
||||
msg, cs1, cs2, err := m.hs.ReadMessage(nil, noiseMsg)
|
||||
eKey, dKey := m.cipherStates(cs1, cs2)
|
||||
if err != nil {
|
||||
// Noise ReadMessage failed. The noise library checkpoints and rolls back
|
||||
// on failure, so the Machine is still alive. The caller can retry with
|
||||
@@ -272,7 +276,7 @@ func (m *Machine) ProcessPacket(out, packet []byte) ([]byte, *Result, error) {
|
||||
}
|
||||
|
||||
// ReadMessage didn't complete, produce the next outgoing message
|
||||
out, dk, ek, err := m.buildResponse(out)
|
||||
out, ek, dk, err := m.buildResponse(out)
|
||||
if err != nil {
|
||||
m.failed = true
|
||||
return nil, nil, err
|
||||
@@ -292,6 +296,15 @@ func (m *Machine) ProcessPacket(out, packet []byte) ([]byte, *Result, error) {
|
||||
return out, nil, nil
|
||||
}
|
||||
|
||||
// cipherStates orders the cipher states noise returns, (cs1, cs2) with cs1 the
|
||||
// initiator to responder direction, into this side's (eKey, dKey).
|
||||
func (m *Machine) cipherStates(cs1, cs2 *noise.CipherState) (*noise.CipherState, *noise.CipherState) {
|
||||
if m.result.Initiator {
|
||||
return cs1, cs2
|
||||
}
|
||||
return cs2, cs1
|
||||
}
|
||||
|
||||
func (m *Machine) completed(eKey, dKey *noise.CipherState) *Result {
|
||||
m.kemKey = nil
|
||||
m.kemCiphertext = nil
|
||||
@@ -372,6 +385,14 @@ func (m *Machine) processPayload(msg []byte, flags msgFlags) error {
|
||||
}
|
||||
|
||||
func (m *Machine) validateCert(payload Payload) error {
|
||||
// A v3 certificate only ever comes with a hybrid pattern, and a hybrid
|
||||
// pattern only ever with one. The composite static key would not recombine
|
||||
// with anything else anyway, but say so plainly.
|
||||
if m.hybrid != (cert.Version(payload.CertVersion) == cert.Version3) {
|
||||
m.failed = true
|
||||
return fmt.Errorf("%w: %v", ErrCertVersionMismatch, payload.CertVersion)
|
||||
}
|
||||
|
||||
cred := m.getCred(m.myVersion)
|
||||
if cred == nil {
|
||||
m.failed = true
|
||||
@@ -487,17 +508,11 @@ func (m *Machine) buildResponse(out []byte) ([]byte, *noise.CipherState, *noise.
|
||||
|
||||
// noise.WriteMessage appends the encrypted handshake message to out,
|
||||
// reusing capacity when present.
|
||||
//
|
||||
// The (dKey, eKey) ordering here is correct for IX, where the responder
|
||||
// completes the handshake by writing the stage-2 message. noise returns
|
||||
// (cs1, cs2) where cs1 is the initiator->responder cipher (which is the
|
||||
// responder's decrypt key). For 3-message patterns where an initiator
|
||||
// finishes by writing the final message, this ordering would be wrong;
|
||||
// revisit when XX/pqIX lands.
|
||||
out, dKey, eKey, err := m.hs.WriteMessage(out, hsBytes)
|
||||
out, cs1, cs2, err := m.hs.WriteMessage(out, hsBytes)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("noise WriteMessage: %w", err)
|
||||
}
|
||||
|
||||
return out, dKey, eKey, nil
|
||||
eKey, dKey := m.cipherStates(cs1, cs2)
|
||||
return out, eKey, dKey, nil
|
||||
}
|
||||
@@ -21,6 +21,10 @@ type subtypeInfo struct {
|
||||
pattern noise.HandshakePattern
|
||||
pskPlacement int // noise psk modifier position, 0 means no psk token
|
||||
msgs []msgFlags
|
||||
|
||||
// hybrid patterns carry composite static keys and need version 3
|
||||
// certificates on both sides; every other pattern refuses them.
|
||||
hybrid bool
|
||||
}
|
||||
|
||||
// subtypeInfos defines the noise pattern and message content layout for each
|
||||
@@ -51,6 +55,29 @@ var subtypeInfos = map[header.MessageSubType]subtypeInfo{
|
||||
},
|
||||
},
|
||||
|
||||
// Hybrid IX (github.com/DefinedNet/noise, see its PQ_RESEARCH.md):
|
||||
// msg1 (I->R): e, s payload + cert
|
||||
// msg2 (R->I): ekem, skem, e, ee, se, s, es payload + cert
|
||||
// msg3 (I->R): skem
|
||||
// e and s are composite, classical || ML-KEM-1024. The static ML-KEM key is
|
||||
// the post-quantum half of the v3 certificate's public key, so it travels in
|
||||
// s and the certificate leaves it out. The responder authenticates the
|
||||
// initiator when msg3 decrypts, and the initiator the responder when the
|
||||
// first packet under the new keys does.
|
||||
//
|
||||
// TODO: the responder's e also carries an ephemeral ML-KEM key that no token
|
||||
// ever encapsulates to, 1568 bytes of msg2 and a key generation for nothing.
|
||||
// Dropping it needs a change to the noise library.
|
||||
header.HandshakeIXHybridMLKEM1024: {
|
||||
pattern: noise.HandshakeHybridIX,
|
||||
hybrid: true,
|
||||
msgs: []msgFlags{
|
||||
{expectsPayload: true, expectsCert: true},
|
||||
{expectsPayload: true, expectsCert: true},
|
||||
{},
|
||||
},
|
||||
},
|
||||
|
||||
// XX: 3 messages
|
||||
// msg1 (I->R): payload only
|
||||
// msg2 (R->I): payload + cert
|
||||
|
||||
Reference in new issue
Block a user