Load v3 certificates and composite keys, speak v2 messages over v3 tunnels, and match ca_sha on either CA fingerprint

This commit is contained in:
JackDoan committed 2026-10-14 11:03:04 -05:00
1 parent ab89556c5c
commit 5c5254dc18
6 files changed
+156 -14

No files matched your search

+1 -1
View File
@@ -272,7 +272,7 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
InitiatorRelayIndex: index,
}
switch newhostinfo.GetCert().Certificate.Version() {
switch protocolVersion(newhostinfo.GetCert().Certificate.Version()) {
case cert.Version1:
if !relayFrom.Is4() {
cm.l.Error("can not migrate v1 relay with a v6 network because the relay is not running a current nebula version")
+8
View File
@@ -764,6 +764,14 @@ func (fc *FirewallCA) match(p firewall.Packet, c *cert.CachedCertificate, caPool
return false
}
// A version 3 certificate names its CA by the fingerprint of the CA's v2 form,
// which is matched above. A rule may also use the v3 CA's own fingerprint.
if c.Certificate.Version() == cert.Version3 {
if t, ok := fc.CAShas[s.Fingerprint]; ok && t.match(p, c) {
return true
}
}
return fc.CANames[s.Certificate.Name()].match(p, c)
}
+58 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/gaissmai/bart"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/firewall"
"github.com/slackhq/nebula/iputil"
@@ -1256,7 +1257,7 @@ func TestNewFirewallFromConfig(t *testing.T) {
l := test.NewLogger()
// Test a bad rule definition
c := &dummyCert{}
cs, err := newCertState(cert.Version2, nil, c, false, cert.Curve_CURVE25519, nil, "aes")
cs, err := newCertState(cert.Version2, nil, c, nil, false, cert.Curve_CURVE25519, nil, "aes")
require.NoError(t, err)
conf := config.NewC(test.NewLogger())
@@ -1745,3 +1746,59 @@ func resetConntrack(fw *Firewall) {
fw.Conntrack.Conns = map[firewall.Packet]*conn{}
fw.Conntrack.Unlock()
}
// A version 3 certificate's CA answers to both its fingerprints in ca_sha rules: the v2 form's, which is what the
// certificate names as issuer, and its own. A v2 certificate only to the v2 one.
func TestFirewall_CAShaV3(t *testing.T) {
l := test.NewLogger()
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version3, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
host, _, _, _ := cert_test.NewTestCert(cert.Version3, cert.Curve_CURVE25519, ca, caKey, "host", time.Time{}, time.Time{}, []netip.Prefix{netip.MustParsePrefix("1.2.3.4/24")}, nil, nil)
cp := cert.NewCAPool()
require.NoError(t, cp.AddCA(ca))
require.NoError(t, cp.AddCA(cert.V2Form(ca)))
v3Sha, err := ca.Fingerprint()
require.NoError(t, err)
v2Sha, err := cert.V2Form(ca).Fingerprint()
require.NoError(t, err)
myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
p := firewall.Packet{
LocalAddr: netip.MustParseAddr("1.2.3.4"),
RemoteAddr: netip.MustParseAddr("1.2.3.4"),
LocalPort: 10,
RemotePort: 90,
Protocol: iputil.IPProtocolUDP,
}
for _, tc := range []struct {
name string
c cert.Certificate
caSha string
match bool
}{
{"v3 by v2 sha", host, v2Sha, true},
{"v3 by v3 sha", host, v3Sha, true},
{"v2 form by v2 sha", cert.V2Form(host), v2Sha, true},
{"v2 form by v3 sha", cert.V2Form(host), v3Sha, false},
} {
t.Run(tc.name, func(t *testing.T) {
cc, err := cp.VerifyCertificate(time.Now(), tc.c)
require.NoError(t, err)
h := HostInfo{
ConnectionState: &ConnectionState{peerCert: cc},
vpnAddrs: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
}
h.buildNetworks(myVpnNetworksTable, tc.c)
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, tc.c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, nil, "", "", "", "", tc.caSha))
err = fw.Drop(p, true, &h, cp, nil)
if tc.match {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, ErrNoMatchingRule)
}
})
}
}
+2 -2
View File
@@ -839,7 +839,7 @@ func (lh *LightHouse) innerQueryServer(addr netip.Addr, nb, out []byte) {
for _, lhVpnAddr := range lighthouses {
hi := lh.ifce.GetHostInfo(lhVpnAddr)
if hi != nil {
v = hi.ConnectionState.myCert.Version()
v = protocolVersion(hi.ConnectionState.myCert.Version())
} else {
v = lh.ifce.GetCertState().initiatingVersion
}
@@ -1001,7 +1001,7 @@ func (lh *LightHouse) SendUpdate() {
var v cert.Version
hi := lh.ifce.GetHostInfo(lhVpnAddr)
if hi != nil {
v = hi.ConnectionState.myCert.Version()
v = protocolVersion(hi.ConnectionState.myCert.Version())
} else {
v = lh.ifce.GetCertState().initiatingVersion
}
+85 -8
View File
@@ -38,6 +38,11 @@ type CertState struct {
v2Cert cert.Certificate
v2Credential *handshake.Credential
// v3Cert, when present, is what v2Cert is the v2 form of. It is only used by
// hybrid handshakes, every other handshake presents v2Cert.
v3Cert cert.Certificate
v3Credential *handshake.Credential
initiatingVersion cert.Version
privateKey []byte
pkcs11Backed bool
@@ -223,6 +228,8 @@ func (cs *CertState) GetCredential(v cert.Version) *handshake.Credential {
return cs.v1Credential
case cert.Version2:
return cs.v2Credential
case cert.Version3:
return cs.v3Credential
}
return nil
}
@@ -233,11 +240,23 @@ func (cs *CertState) getCertificate(v cert.Version) cert.Certificate {
return cs.v1Cert
case cert.Version2:
return cs.v2Cert
case cert.Version3:
return cs.v3Cert
}
return nil
}
// protocolVersion is the version of the messages spoken over a tunnel built on a
// certificate of version v. Version 3 only changes the handshake, everything
// after it is version 2.
func protocolVersion(v cert.Version) cert.Version {
if v == cert.Version3 {
return cert.Version2
}
return v
}
// newCipherSuite builds the noise.CipherSuite for the given curve and cipher.
// When fips140Enforced is true (FIPS 140-only mode), non-approved algorithms
// (Curve25519 and ChaChaPoly) are rejected with an error. Callers pass
@@ -287,7 +306,14 @@ func (cs *CertState) MarshalJSON() ([]byte, error) {
msg = append(msg, b)
}
if cs.v2Cert != nil {
// A v3 certificate shows its v2 form's fingerprint itself.
if cs.v3Cert != nil {
b, err := cs.v3Cert.MarshalJSON()
if err != nil {
return nil, err
}
msg = append(msg, b)
} else if cs.v2Cert != nil {
b, err := cs.v2Cert.MarshalJSON()
if err != nil {
return nil, err
@@ -329,7 +355,7 @@ func newCertStateFromConfig(c *config.C, cipher string) (*CertState, error) {
}
}
var crt, v1, v2 cert.Certificate
var crt, v1, v2, v3 cert.Certificate
for {
// Load the certificate
crt, rawCert, err = loadCertificate(rawCert)
@@ -352,6 +378,11 @@ func newCertStateFromConfig(c *config.C, cipher string) (*CertState, error) {
return nil, fmt.Errorf("v2 certificate already found in pki.cert")
}
v2 = crt
case cert.Version3:
if v3 != nil {
return nil, fmt.Errorf("v3 certificate already found in pki.cert")
}
v3 = crt
default:
return nil, fmt.Errorf("unknown certificate version %v", crt.Version())
}
@@ -361,6 +392,25 @@ func newCertStateFromConfig(c *config.C, cipher string) (*CertState, error) {
}
}
if v3 != nil {
// A v3 certificate brings its own v2 form. Listing that too is fine, anything else is a mistake.
form := cert.V2Form(v3)
if v2 != nil {
want, err := form.Fingerprint()
if err != nil {
return nil, err
}
got, err := v2.Fingerprint()
if err != nil {
return nil, err
}
if want != got {
return nil, errors.New("the v2 certificate in pki.cert is not the v2 form of the v3 certificate")
}
}
v2 = form
}
if v1 == nil && v2 == nil {
return nil, errors.New("no certificates found in pki.cert")
}
@@ -386,10 +436,12 @@ func newCertStateFromConfig(c *config.C, cipher string) (*CertState, error) {
return nil, fmt.Errorf("unknown pki.initiating_version: %v", rawInitiatingVersion)
}
return newCertState(initiatingVersion, v1, v2, isPkcs11, curve, rawKey, cipher)
return newCertState(initiatingVersion, v1, v2, v3, isPkcs11, curve, rawKey, cipher)
}
func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, privateKeyCurve cert.Curve, privateKey []byte, cipher string) (*CertState, error) {
// newCertState builds the handshake credentials. v3, when present, must be what v2 is the v2 form of. privateKey is
// then the composite key, classical || ML-KEM-1024 seed, and v1 and v2 get its classical half.
func newCertState(dv cert.Version, v1, v2, v3 cert.Certificate, pkcs11backed bool, privateKeyCurve cert.Curve, privateKey []byte, cipher string) (*CertState, error) {
cs := CertState{
privateKey: privateKey,
pkcs11Backed: pkcs11backed,
@@ -415,11 +467,36 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
cs.initiatingVersion = dv
}
classicalKey := privateKey
if !pkcs11backed && len(privateKey) > cert.MLKEM1024PrivateKeySize {
classicalKey = privateKey[:len(privateKey)-cert.MLKEM1024PrivateKeySize]
}
if v3 != nil {
if pkcs11backed {
return nil, errors.New("v3 certificates need their private key in pki.key, not a PKCS#11 token")
}
if err := v3.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
return nil, fmt.Errorf("private key is not a pair with public key in v3 nebula cert: %w", err)
}
v3hs, err := v3.MarshalForHandshakes()
if err != nil {
return nil, fmt.Errorf("error marshalling v3 certificate for handshake: %w", err)
}
ncs, err := newCipherSuite(v3.Curve(), false, cipher, fips140.Enforced())
if err != nil {
return nil, err
}
cs.v3Cert = v3
cs.v3Credential = handshake.NewCredential(v3, v3hs, privateKey, ncs)
}
if v1 != nil {
if pkcs11backed {
//NOTE: We do not currently have a method to verify a public private key pair when the private key is in an hsm
} else {
if err := v1.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
if err := v1.VerifyPrivateKey(privateKeyCurve, classicalKey); err != nil {
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert: %w", err)
}
}
@@ -433,7 +510,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
return nil, err
}
cs.v1Cert = v1
cs.v1Credential = handshake.NewCredential(v1, v1hs, privateKey, ncs)
cs.v1Credential = handshake.NewCredential(v1, v1hs, classicalKey, ncs)
if cs.initiatingVersion == 0 {
cs.initiatingVersion = cert.Version1
@@ -444,7 +521,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
if pkcs11backed {
//NOTE: We do not currently have a method to verify a public private key pair when the private key is in an hsm
} else {
if err := v2.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
if err := v2.VerifyPrivateKey(privateKeyCurve, classicalKey); err != nil {
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert: %w", err)
}
}
@@ -458,7 +535,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
return nil, err
}
cs.v2Cert = v2
cs.v2Credential = handshake.NewCredential(v2, v2hs, privateKey, ncs)
cs.v2Credential = handshake.NewCredential(v2, v2hs, classicalKey, ncs)
if cs.initiatingVersion == 0 {
cs.initiatingVersion = cert.Version2
+2 -2
View File
@@ -119,7 +119,7 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hh *Handshak
InitiatorRelayIndex: idx,
}
switch relayHostInfo.GetCert().Certificate.Version() {
switch protocolVersion(relayHostInfo.GetCert().Certificate.Version()) {
case cert.Version1:
if !f.myVpnAddrs[0].Is4() {
hl.Error("can not establish v1 relay with a v6 network because the relay is not running a current nebula version")
@@ -178,7 +178,7 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hh *Handshak
InitiatorRelayIndex: existingRelay.LocalIndex,
}
switch relayHostInfo.GetCert().Certificate.Version() {
switch protocolVersion(relayHostInfo.GetCert().Certificate.Version()) {
case cert.Version1:
if !f.myVpnAddrs[0].Is4() {
hl.Error("can not establish v1 relay with a v6 network because the relay is not running a current nebula version")