mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 12:36:52 +01:00
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:
1 parent
ab89556c5c
commit
5c5254dc18
6 files changed
+156
-14
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user