Ver código fonte

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

JackDoan 6 dias atrás
pai
commit
5c5254dc18
6 arquivos alterados com 156 adições e 14 exclusões
  1. 1 1
      connection_manager.go
  2. 8 0
      firewall.go
  3. 58 1
      firewall_test.go
  4. 2 2
      lighthouse.go
  5. 85 8
      pki.go
  6. 2 2
      relay_manager.go

+ 1 - 1
connection_manager.go

@@ -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 - 0
firewall.go

@@ -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
firewall_test.go

@@ -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
lighthouse.go

@@ -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
pki.go

@@ -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
relay_manager.go

@@ -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")