Nate Brown пре 11 месеци
родитељ
комит
08ac65362e

+ 1 - 1
cert/Makefile

@@ -1,7 +1,7 @@
 GO111MODULE = on
 export GO111MODULE
 
-cert.pb.go: cert.proto .FORCE
+cert_v1.pb.go: cert_v1.proto .FORCE
 	go build google.golang.org/protobuf/cmd/protoc-gen-go
 	PATH="$(CURDIR):$(PATH)" protoc --go_out=. --go_opt=paths=source_relative $<
 	rm protoc-gen-go

+ 0 - 140
cert/ca.go

@@ -1,140 +0,0 @@
-package cert
-
-import (
-	"errors"
-	"fmt"
-	"strings"
-	"time"
-)
-
-type NebulaCAPool struct {
-	CAs           map[string]*NebulaCertificate
-	certBlocklist map[string]struct{}
-}
-
-// NewCAPool creates a CAPool
-func NewCAPool() *NebulaCAPool {
-	ca := NebulaCAPool{
-		CAs:           make(map[string]*NebulaCertificate),
-		certBlocklist: make(map[string]struct{}),
-	}
-
-	return &ca
-}
-
-// NewCAPoolFromBytes will create a new CA pool from the provided
-// input bytes, which must be a PEM-encoded set of nebula certificates.
-// If the pool contains any expired certificates, an ErrExpired will be
-// returned along with the pool. The caller must handle any such errors.
-func NewCAPoolFromBytes(caPEMs []byte) (*NebulaCAPool, error) {
-	pool := NewCAPool()
-	var err error
-	var expired bool
-	for {
-		caPEMs, err = pool.AddCACertificate(caPEMs)
-		if errors.Is(err, ErrExpired) {
-			expired = true
-			err = nil
-		}
-		if err != nil {
-			return nil, err
-		}
-		if len(caPEMs) == 0 || strings.TrimSpace(string(caPEMs)) == "" {
-			break
-		}
-	}
-
-	if expired {
-		return pool, ErrExpired
-	}
-
-	return pool, nil
-}
-
-// AddCACertificate verifies a Nebula CA certificate and adds it to the pool
-// Only the first pem encoded object will be consumed, any remaining bytes are returned.
-// Parsed certificates will be verified and must be a CA
-func (ncp *NebulaCAPool) AddCACertificate(pemBytes []byte) ([]byte, error) {
-	c, pemBytes, err := UnmarshalNebulaCertificateFromPEM(pemBytes)
-	if err != nil {
-		return pemBytes, err
-	}
-
-	if !c.Details.IsCA {
-		return pemBytes, fmt.Errorf("%s: %w", c.Details.Name, ErrNotCA)
-	}
-
-	if !c.CheckSignature(c.Details.PublicKey) {
-		return pemBytes, fmt.Errorf("%s: %w", c.Details.Name, ErrNotSelfSigned)
-	}
-
-	sum, err := c.Sha256Sum()
-	if err != nil {
-		return pemBytes, fmt.Errorf("could not calculate shasum for provided CA; error: %s; %s", err, c.Details.Name)
-	}
-
-	ncp.CAs[sum] = c
-	if c.Expired(time.Now()) {
-		return pemBytes, fmt.Errorf("%s: %w", c.Details.Name, ErrExpired)
-	}
-
-	return pemBytes, nil
-}
-
-// BlocklistFingerprint adds a cert fingerprint to the blocklist
-func (ncp *NebulaCAPool) BlocklistFingerprint(f string) {
-	ncp.certBlocklist[f] = struct{}{}
-}
-
-// ResetCertBlocklist removes all previously blocklisted cert fingerprints
-func (ncp *NebulaCAPool) ResetCertBlocklist() {
-	ncp.certBlocklist = make(map[string]struct{})
-}
-
-// NOTE: This uses an internal cache for Sha256Sum() that will not be invalidated
-// automatically if you manually change any fields in the NebulaCertificate.
-func (ncp *NebulaCAPool) IsBlocklisted(c *NebulaCertificate) bool {
-	return ncp.isBlocklistedWithCache(c, false)
-}
-
-// IsBlocklisted returns true if the fingerprint fails to generate or has been explicitly blocklisted
-func (ncp *NebulaCAPool) isBlocklistedWithCache(c *NebulaCertificate, useCache bool) bool {
-	h, err := c.sha256SumWithCache(useCache)
-	if err != nil {
-		return true
-	}
-
-	if _, ok := ncp.certBlocklist[h]; ok {
-		return true
-	}
-
-	return false
-}
-
-// GetCAForCert attempts to return the signing certificate for the provided certificate.
-// No signature validation is performed
-func (ncp *NebulaCAPool) GetCAForCert(c *NebulaCertificate) (*NebulaCertificate, error) {
-	if c.Details.Issuer == "" {
-		return nil, fmt.Errorf("no issuer in certificate")
-	}
-
-	signer, ok := ncp.CAs[c.Details.Issuer]
-	if ok {
-		return signer, nil
-	}
-
-	return nil, fmt.Errorf("could not find ca for the certificate")
-}
-
-// GetFingerprints returns an array of trusted CA fingerprints
-func (ncp *NebulaCAPool) GetFingerprints() []string {
-	fp := make([]string, len(ncp.CAs))
-
-	i := 0
-	for k := range ncp.CAs {
-		fp[i] = k
-		i++
-	}
-
-	return fp
-}

+ 296 - 0
cert/ca_pool.go

@@ -0,0 +1,296 @@
+package cert
+
+import (
+	"errors"
+	"fmt"
+	"net/netip"
+	"slices"
+	"strings"
+	"time"
+)
+
+type CAPool struct {
+	CAs           map[string]*CachedCertificate
+	certBlocklist map[string]struct{}
+}
+
+// NewCAPool creates an empty CAPool
+func NewCAPool() *CAPool {
+	ca := CAPool{
+		CAs:           make(map[string]*CachedCertificate),
+		certBlocklist: make(map[string]struct{}),
+	}
+
+	return &ca
+}
+
+// NewCAPoolFromPEM will create a new CA pool from the provided
+// input bytes, which must be a PEM-encoded set of nebula certificates.
+// If the pool contains any expired certificates, an ErrExpired will be
+// returned along with the pool. The caller must handle any such errors.
+func NewCAPoolFromPEM(caPEMs []byte) (*CAPool, error) {
+	pool := NewCAPool()
+	var err error
+	var expired bool
+	for {
+		caPEMs, err = pool.AddCAFromPEM(caPEMs)
+		if errors.Is(err, ErrExpired) {
+			expired = true
+			err = nil
+		}
+		if err != nil {
+			return nil, err
+		}
+		if len(caPEMs) == 0 || strings.TrimSpace(string(caPEMs)) == "" {
+			break
+		}
+	}
+
+	if expired {
+		return pool, ErrExpired
+	}
+
+	return pool, nil
+}
+
+// AddCAFromPEM verifies a Nebula CA certificate and adds it to the pool.
+// Only the first pem encoded object will be consumed, any remaining bytes are returned.
+// Parsed certificates will be verified and must be a CA
+func (ncp *CAPool) AddCAFromPEM(pemBytes []byte) ([]byte, error) {
+	c, pemBytes, err := UnmarshalCertificateFromPEM(pemBytes)
+	if err != nil {
+		return pemBytes, err
+	}
+
+	err = ncp.AddCA(c)
+	if err != nil {
+		return pemBytes, err
+	}
+
+	return pemBytes, nil
+}
+
+// AddCA verifies a Nebula CA certificate and adds it to the pool.
+func (ncp *CAPool) AddCA(c Certificate) error {
+	if !c.IsCA() {
+		return fmt.Errorf("%s: %w", c.Name(), ErrNotCA)
+	}
+
+	if !c.CheckSignature(c.PublicKey()) {
+		return fmt.Errorf("%s: %w", c.Name(), ErrNotSelfSigned)
+	}
+
+	sum, err := c.Fingerprint()
+	if err != nil {
+		return fmt.Errorf("could not calculate fingerprint for provided CA; error: %w; %s", err, c.Name())
+	}
+
+	cc := &CachedCertificate{
+		Certificate:    c,
+		Fingerprint:    sum,
+		InvertedGroups: make(map[string]struct{}),
+	}
+
+	for _, g := range c.Groups() {
+		cc.InvertedGroups[g] = struct{}{}
+	}
+
+	ncp.CAs[sum] = cc
+
+	if c.Expired(time.Now()) {
+		return fmt.Errorf("%s: %w", c.Name(), ErrExpired)
+	}
+
+	return nil
+}
+
+// BlocklistFingerprint adds a cert fingerprint to the blocklist
+func (ncp *CAPool) BlocklistFingerprint(f string) {
+	ncp.certBlocklist[f] = struct{}{}
+}
+
+// ResetCertBlocklist removes all previously blocklisted cert fingerprints
+func (ncp *CAPool) ResetCertBlocklist() {
+	ncp.certBlocklist = make(map[string]struct{})
+}
+
+// IsBlocklisted tests the provided fingerprint against the pools blocklist.
+// Returns true if the fingerprint is blocked.
+func (ncp *CAPool) IsBlocklisted(fingerprint string) bool {
+	if _, ok := ncp.certBlocklist[fingerprint]; ok {
+		return true
+	}
+
+	return false
+}
+
+// VerifyCertificate verifies the certificate is valid and is signed by a trusted CA in the pool.
+// If the certificate is valid then the returned CachedCertificate can be used in subsequent verification attempts
+// to increase performance.
+func (ncp *CAPool) VerifyCertificate(now time.Time, c Certificate) (*CachedCertificate, error) {
+	if c == nil {
+		return nil, fmt.Errorf("no certificate")
+	}
+	fp, err := c.Fingerprint()
+	if err != nil {
+		return nil, fmt.Errorf("could not calculate fingerprint to verify: %w", err)
+	}
+
+	signer, err := ncp.verify(c, now, fp, "")
+	if err != nil {
+		return nil, err
+	}
+
+	cc := CachedCertificate{
+		Certificate:       c,
+		InvertedGroups:    make(map[string]struct{}),
+		Fingerprint:       fp,
+		signerFingerprint: signer.Fingerprint,
+	}
+
+	for _, g := range c.Groups() {
+		cc.InvertedGroups[g] = struct{}{}
+	}
+
+	return &cc, nil
+}
+
+// VerifyCachedCertificate is the same as VerifyCertificate other than it operates on a pre-verified structure and
+// is a cheaper operation to perform as a result.
+func (ncp *CAPool) VerifyCachedCertificate(now time.Time, c *CachedCertificate) error {
+	_, err := ncp.verify(c.Certificate, now, c.Fingerprint, c.signerFingerprint)
+	return err
+}
+
+func (ncp *CAPool) verify(c Certificate, now time.Time, certFp string, signerFp string) (*CachedCertificate, error) {
+	if ncp.IsBlocklisted(certFp) {
+		return nil, ErrBlockListed
+	}
+
+	signer, err := ncp.GetCAForCert(c)
+	if err != nil {
+		return nil, err
+	}
+
+	if signer.Certificate.Expired(now) {
+		return nil, ErrRootExpired
+	}
+
+	if c.Expired(now) {
+		return nil, ErrExpired
+	}
+
+	// If we are checking a cached certificate then we can bail early here
+	// Either the root is no longer trusted or everything is fine
+	if len(signerFp) > 0 {
+		if signerFp != signer.Fingerprint {
+			return nil, ErrFingerprintMismatch
+		}
+		return signer, nil
+	}
+	if !c.CheckSignature(signer.Certificate.PublicKey()) {
+		return nil, ErrSignatureMismatch
+	}
+
+	err = CheckCAConstraints(signer.Certificate, c)
+	if err != nil {
+		return nil, err
+	}
+
+	return signer, nil
+}
+
+// GetCAForCert attempts to return the signing certificate for the provided certificate.
+// No signature validation is performed
+func (ncp *CAPool) GetCAForCert(c Certificate) (*CachedCertificate, error) {
+	issuer := c.Issuer()
+	if issuer == "" {
+		return nil, fmt.Errorf("no issuer in certificate")
+	}
+
+	signer, ok := ncp.CAs[issuer]
+	if ok {
+		return signer, nil
+	}
+
+	return nil, fmt.Errorf("could not find ca for the certificate")
+}
+
+// GetFingerprints returns an array of trusted CA fingerprints
+func (ncp *CAPool) GetFingerprints() []string {
+	fp := make([]string, len(ncp.CAs))
+
+	i := 0
+	for k := range ncp.CAs {
+		fp[i] = k
+		i++
+	}
+
+	return fp
+}
+
+// CheckCAConstraints returns an error if the sub certificate violates constraints present in the signer certificate.
+func CheckCAConstraints(signer Certificate, sub Certificate) error {
+	return checkCAConstraints(signer, sub.NotBefore(), sub.NotAfter(), sub.Groups(), sub.Networks(), sub.UnsafeNetworks())
+}
+
+// checkCAConstraints is a very generic function allowing both Certificates and TBSCertificates to be tested.
+func checkCAConstraints(signer Certificate, notBefore, notAfter time.Time, groups []string, networks, unsafeNetworks []netip.Prefix) error {
+	// Make sure this cert isn't valid after the root
+	if notAfter.After(signer.NotAfter()) {
+		return fmt.Errorf("certificate expires after signing certificate")
+	}
+
+	// Make sure this cert wasn't valid before the root
+	if notBefore.Before(signer.NotBefore()) {
+		return fmt.Errorf("certificate is valid before the signing certificate")
+	}
+
+	// If the signer has a limited set of groups make sure the cert only contains a subset
+	signerGroups := signer.Groups()
+	if len(signerGroups) > 0 {
+		for _, g := range groups {
+			if !slices.Contains(signerGroups, g) {
+				return fmt.Errorf("certificate contained a group not present on the signing ca: %s", g)
+			}
+		}
+	}
+
+	// If the signer has a limited set of ip ranges to issue from make sure the cert only contains a subset
+	signingNetworks := signer.Networks()
+	if len(signingNetworks) > 0 {
+		for _, certNetwork := range networks {
+			found := false
+			for _, signingNetwork := range signingNetworks {
+				if signingNetwork.Contains(certNetwork.Addr()) && signingNetwork.Bits() <= certNetwork.Bits() {
+					found = true
+					break
+				}
+			}
+
+			if !found {
+				return fmt.Errorf("certificate contained a network assignment outside the limitations of the signing ca: %s", certNetwork.String())
+			}
+		}
+	}
+
+	// If the signer has a limited set of subnet ranges to issue from make sure the cert only contains a subset
+	signingUnsafeNetworks := signer.UnsafeNetworks()
+	if len(signingUnsafeNetworks) > 0 {
+		for _, certUnsafeNetwork := range unsafeNetworks {
+			found := false
+			for _, caNetwork := range signingUnsafeNetworks {
+				if caNetwork.Contains(certUnsafeNetwork.Addr()) && caNetwork.Bits() <= certUnsafeNetwork.Bits() {
+					found = true
+					break
+				}
+			}
+
+			if !found {
+				return fmt.Errorf("certificate contained an unsafe network assignment outside the limitations of the signing ca: %s", certUnsafeNetwork.String())
+			}
+		}
+	}
+
+	return nil
+}

+ 109 - 0
cert/ca_pool_test.go

@@ -0,0 +1,109 @@
+package cert
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewCAPoolFromBytes(t *testing.T) {
+	noNewLines := `
+# Current provisional, Remove once everything moves over to the real root.
+-----BEGIN NEBULA CERTIFICATE-----
+CkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL
+vcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv
+bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
+-----END NEBULA CERTIFICATE-----
+# root-ca01
+-----BEGIN NEBULA CERTIFICATE-----
+CkMKEW5lYnVsYSByb290IGNhIDAxKJL2u9EFMJL86+cGOiDPXMH4oU6HZTk/CqTG
+BVG+oJpAoqokUBbI4U0N8CSfpUABEkB/Pm5A2xyH/nc8mg/wvGUWG3pZ7nHzaDMf
+8/phAUt+FLzqTECzQKisYswKvE3pl9mbEYKbOdIHrxdIp95mo4sF
+-----END NEBULA CERTIFICATE-----
+`
+
+	withNewLines := `
+# Current provisional, Remove once everything moves over to the real root.
+
+-----BEGIN NEBULA CERTIFICATE-----
+CkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL
+vcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv
+bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
+-----END NEBULA CERTIFICATE-----
+
+# root-ca01
+
+
+-----BEGIN NEBULA CERTIFICATE-----
+CkMKEW5lYnVsYSByb290IGNhIDAxKJL2u9EFMJL86+cGOiDPXMH4oU6HZTk/CqTG
+BVG+oJpAoqokUBbI4U0N8CSfpUABEkB/Pm5A2xyH/nc8mg/wvGUWG3pZ7nHzaDMf
+8/phAUt+FLzqTECzQKisYswKvE3pl9mbEYKbOdIHrxdIp95mo4sF
+-----END NEBULA CERTIFICATE-----
+
+`
+
+	expired := `
+# expired certificate
+-----BEGIN NEBULA CERTIFICATE-----
+CjkKB2V4cGlyZWQouPmWjQYwufmWjQY6ILCRaoCkJlqHgv5jfDN4lzLHBvDzaQm4
+vZxfu144hmgjQAESQG4qlnZi8DncvD/LDZnLgJHOaX1DWCHHEh59epVsC+BNgTie
+WH1M9n4O7cFtGlM6sJJOS+rCVVEJ3ABS7+MPdQs=
+-----END NEBULA CERTIFICATE-----
+`
+
+	p256 := `
+# p256 certificate
+-----BEGIN NEBULA CERTIFICATE-----
+CmYKEG5lYnVsYSBQMjU2IHRlc3Qo4s+7mgYw4tXrsAc6QQRkaW2jFmllYvN4+/k2
+6tctO9sPT3jOx8ES6M1nIqOhpTmZeabF/4rELDqPV4aH5jfJut798DUXql0FlF8H
+76gvQAGgBgESRzBFAiEAib0/te6eMiZOKD8gdDeloMTS0wGuX2t0C7TFdUhAQzgC
+IBNWYMep3ysx9zCgknfG5dKtwGTaqF++BWKDYdyl34KX
+-----END NEBULA CERTIFICATE-----
+`
+
+	rootCA := certificateV1{
+		details: detailsV1{
+			Name: "nebula root ca",
+		},
+	}
+
+	rootCA01 := certificateV1{
+		details: detailsV1{
+			Name: "nebula root ca 01",
+		},
+	}
+
+	rootCAP256 := certificateV1{
+		details: detailsV1{
+			Name: "nebula P256 test",
+		},
+	}
+
+	p, err := NewCAPoolFromPEM([]byte(noNewLines))
+	assert.Nil(t, err)
+	assert.Equal(t, p.CAs[string("c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522")].Certificate.Name(), rootCA.details.Name)
+	assert.Equal(t, p.CAs[string("5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd")].Certificate.Name(), rootCA01.details.Name)
+
+	pp, err := NewCAPoolFromPEM([]byte(withNewLines))
+	assert.Nil(t, err)
+	assert.Equal(t, pp.CAs[string("c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522")].Certificate.Name(), rootCA.details.Name)
+	assert.Equal(t, pp.CAs[string("5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd")].Certificate.Name(), rootCA01.details.Name)
+
+	// expired cert, no valid certs
+	ppp, err := NewCAPoolFromPEM([]byte(expired))
+	assert.Equal(t, ErrExpired, err)
+	assert.Equal(t, ppp.CAs[string("152070be6bb19bc9e3bde4c2f0e7d8f4ff5448b4c9856b8eccb314fade0229b0")].Certificate.Name(), "expired")
+
+	// expired cert, with valid certs
+	pppp, err := NewCAPoolFromPEM(append([]byte(expired), noNewLines...))
+	assert.Equal(t, ErrExpired, err)
+	assert.Equal(t, pppp.CAs[string("c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522")].Certificate.Name(), rootCA.details.Name)
+	assert.Equal(t, pppp.CAs[string("5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd")].Certificate.Name(), rootCA01.details.Name)
+	assert.Equal(t, pppp.CAs[string("152070be6bb19bc9e3bde4c2f0e7d8f4ff5448b4c9856b8eccb314fade0229b0")].Certificate.Name(), "expired")
+	assert.Equal(t, len(pppp.CAs), 3)
+
+	ppppp, err := NewCAPoolFromPEM([]byte(p256))
+	assert.Nil(t, err)
+	assert.Equal(t, ppppp.CAs[string("a7938893ec8c4ef769b06d7f425e5e46f7a7f5ffa49c3bcf4a86b608caba9159")].Certificate.Name(), rootCAP256.details.Name)
+	assert.Equal(t, len(ppppp.CAs), 1)
+}

+ 85 - 1018
cert/cert.go

@@ -1,1062 +1,129 @@
 package cert
 
 import (
-	"bytes"
-	"crypto/ecdh"
-	"crypto/ecdsa"
-	"crypto/ed25519"
-	"crypto/elliptic"
-	"crypto/rand"
-	"crypto/sha256"
-	"encoding/binary"
-	"encoding/hex"
-	"encoding/json"
-	"encoding/pem"
-	"errors"
-	"fmt"
-	"math"
-	"math/big"
-	"net"
-	"sync/atomic"
+	"net/netip"
 	"time"
-
-	"github.com/slackhq/nebula/pkclient"
-	"golang.org/x/crypto/curve25519"
-	"google.golang.org/protobuf/proto"
 )
 
-const publicKeyLen = 32
+type Version int
 
 const (
-	CertBanner                       = "NEBULA CERTIFICATE"
-	X25519PrivateKeyBanner           = "NEBULA X25519 PRIVATE KEY"
-	X25519PublicKeyBanner            = "NEBULA X25519 PUBLIC KEY"
-	EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY"
-	Ed25519PrivateKeyBanner          = "NEBULA ED25519 PRIVATE KEY"
-	Ed25519PublicKeyBanner           = "NEBULA ED25519 PUBLIC KEY"
-
-	P256PrivateKeyBanner               = "NEBULA P256 PRIVATE KEY"
-	P256PublicKeyBanner                = "NEBULA P256 PUBLIC KEY"
-	EncryptedECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 ENCRYPTED PRIVATE KEY"
-	ECDSAP256PrivateKeyBanner          = "NEBULA ECDSA P256 PRIVATE KEY"
+	Version1 Version = 1
+	Version2 Version = 2
 )
 
-type NebulaCertificate struct {
-	Details      NebulaCertificateDetails
-	Pkcs11Backed bool
-	Signature    []byte
-
-	// the cached hex string of the calculated sha256sum
-	// for VerifyWithCache
-	sha256sum atomic.Pointer[string]
-
-	// the cached public key bytes if they were verified as the signer
-	// for VerifyWithCache
-	signatureVerified atomic.Pointer[[]byte]
-}
-
-type NebulaCertificateDetails struct {
-	Name      string
-	Ips       []*net.IPNet
-	Subnets   []*net.IPNet
-	Groups    []string
-	NotBefore time.Time
-	NotAfter  time.Time
-	PublicKey []byte
-	IsCA      bool
-	Issuer    string
-
-	// Map of groups for faster lookup
-	InvertedGroups map[string]struct{}
-
-	Curve Curve
-}
+type Certificate interface {
+	// Version defines the underlying certificate structure and wire protocol version
+	// Version1 certificates are ipv4 only and uses protobuf serialization
+	// Version2 certificates are ipv4 or ipv6 and uses asn.1 serialization
+	Version() Version
 
-type NebulaEncryptedData struct {
-	EncryptionMetadata NebulaEncryptionMetadata
-	Ciphertext         []byte
-}
+	// Name is the human-readable name that identifies this certificate.
+	Name() string
 
-type NebulaEncryptionMetadata struct {
-	EncryptionAlgorithm string
-	Argon2Parameters    Argon2Parameters
-}
+	// Networks is a list of ip addresses and network sizes assigned to this certificate.
+	// If IsCA is true then certificates signed by this CA can only have ip addresses and
+	// networks that are contained by an entry in this list.
+	Networks() []netip.Prefix
 
-type m map[string]interface{}
+	// UnsafeNetworks is a list of networks that this host can act as an unsafe router for.
+	// If IsCA is true then certificates signed by this CA can only have networks that are
+	// contained by an entry in this list.
+	UnsafeNetworks() []netip.Prefix
 
-// Returned if we try to unmarshal an encrypted private key without a passphrase
-var ErrPrivateKeyEncrypted = errors.New("private key must be decrypted")
+	// Groups is a list of identities that can be used to write more general firewall rule
+	// definitions.
+	// If IsCA is true then certificates signed by this CA can only use groups that are
+	// in this list.
+	Groups() []string
 
-// UnmarshalNebulaCertificate will unmarshal a protobuf byte representation of a nebula cert
-func UnmarshalNebulaCertificate(b []byte) (*NebulaCertificate, error) {
-	if len(b) == 0 {
-		return nil, fmt.Errorf("nil byte array")
-	}
-	var rc RawNebulaCertificate
-	err := proto.Unmarshal(b, &rc)
-	if err != nil {
-		return nil, err
-	}
+	// IsCA signifies if this is a certificate authority (true) or a host certificate (false).
+	// It is invalid to use a CA certificate as a host certificate.
+	IsCA() bool
 
-	if rc.Details == nil {
-		return nil, fmt.Errorf("encoded Details was nil")
-	}
+	// NotBefore is the time at which this certificate becomes valid.
+	// If IsCA is true then certificate signed by this CA can not have a time before this.
+	NotBefore() time.Time
 
-	if len(rc.Details.Ips)%2 != 0 {
-		return nil, fmt.Errorf("encoded IPs should be in pairs, an odd number was found")
-	}
+	// NotAfter is the time at which this certificate becomes invalid.
+	// If IsCA is true then certificate signed by this CA can not have a time after this.
+	NotAfter() time.Time
 
-	if len(rc.Details.Subnets)%2 != 0 {
-		return nil, fmt.Errorf("encoded Subnets should be in pairs, an odd number was found")
-	}
+	// Issuer is the fingerprint of the CA that signed this certificate.
+	// If IsCA is true then this will be empty.
+	Issuer() string
 
-	nc := NebulaCertificate{
-		Details: NebulaCertificateDetails{
-			Name:           rc.Details.Name,
-			Groups:         make([]string, len(rc.Details.Groups)),
-			Ips:            make([]*net.IPNet, len(rc.Details.Ips)/2),
-			Subnets:        make([]*net.IPNet, len(rc.Details.Subnets)/2),
-			NotBefore:      time.Unix(rc.Details.NotBefore, 0),
-			NotAfter:       time.Unix(rc.Details.NotAfter, 0),
-			PublicKey:      make([]byte, len(rc.Details.PublicKey)),
-			IsCA:           rc.Details.IsCA,
-			InvertedGroups: make(map[string]struct{}),
-			Curve:          rc.Details.Curve,
-		},
-		Signature: make([]byte, len(rc.Signature)),
-	}
+	// PublicKey is the raw bytes to be used in asymmetric cryptographic operations.
+	PublicKey() []byte
 
-	copy(nc.Signature, rc.Signature)
-	copy(nc.Details.Groups, rc.Details.Groups)
-	nc.Details.Issuer = hex.EncodeToString(rc.Details.Issuer)
+	// Curve identifies which curve was used for the PublicKey and Signature.
+	Curve() Curve
 
-	if len(rc.Details.PublicKey) < publicKeyLen {
-		return nil, fmt.Errorf("Public key was fewer than 32 bytes; %v", len(rc.Details.PublicKey))
-	}
-	copy(nc.Details.PublicKey, rc.Details.PublicKey)
+	// Signature is the cryptographic seal for all the details of this certificate.
+	// CheckSignature can be used to verify that the details of this certificate are valid.
+	Signature() []byte
 
-	for i, rawIp := range rc.Details.Ips {
-		if i%2 == 0 {
-			nc.Details.Ips[i/2] = &net.IPNet{IP: int2ip(rawIp)}
-		} else {
-			nc.Details.Ips[i/2].Mask = net.IPMask(int2ip(rawIp))
-		}
-	}
+	// CheckSignature will check that the certificate Signature() matches the
+	// computed signature. A true result means this certificate has not been tampered with.
+	CheckSignature(signingPublicKey []byte) bool
 
-	for i, rawIp := range rc.Details.Subnets {
-		if i%2 == 0 {
-			nc.Details.Subnets[i/2] = &net.IPNet{IP: int2ip(rawIp)}
-		} else {
-			nc.Details.Subnets[i/2].Mask = net.IPMask(int2ip(rawIp))
-		}
-	}
+	// Fingerprint returns the hex encoded sha256 sum of the certificate.
+	// This acts as a unique fingerprint and can be used to blocklist certificates.
+	Fingerprint() (string, error)
 
-	for _, g := range rc.Details.Groups {
-		nc.Details.InvertedGroups[g] = struct{}{}
-	}
+	// Expired tests if the certificate is valid for the provided time.
+	Expired(t time.Time) bool
 
-	return &nc, nil
-}
+	// VerifyPrivateKey returns an error if the private key is not a pair with the certificates public key.
+	VerifyPrivateKey(curve Curve, privateKey []byte) error
 
-// UnmarshalNebulaCertificateFromPEM will unmarshal the first pem block in a byte array, returning any non consumed data
-// or an error on failure
-func UnmarshalNebulaCertificateFromPEM(b []byte) (*NebulaCertificate, []byte, error) {
-	p, r := pem.Decode(b)
-	if p == nil {
-		return nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
-	}
-	if p.Type != CertBanner {
-		return nil, r, fmt.Errorf("bytes did not contain a proper nebula certificate banner")
-	}
-	nc, err := UnmarshalNebulaCertificate(p.Bytes)
-	return nc, r, err
-}
+	// Marshal will return the byte representation of this certificate
+	// This is primarily the format transmitted on the wire.
+	Marshal() ([]byte, error)
 
-func MarshalPrivateKey(curve Curve, b []byte) []byte {
-	switch curve {
-	case Curve_CURVE25519:
-		return pem.EncodeToMemory(&pem.Block{Type: X25519PrivateKeyBanner, Bytes: b})
-	case Curve_P256:
-		return pem.EncodeToMemory(&pem.Block{Type: P256PrivateKeyBanner, Bytes: b})
-	default:
-		return nil
-	}
-}
+	// MarshalForHandshakes prepares the bytes needed to use directly in a handshake
+	MarshalForHandshakes() ([]byte, error)
 
-func MarshalSigningPrivateKey(curve Curve, b []byte) []byte {
-	switch curve {
-	case Curve_CURVE25519:
-		return pem.EncodeToMemory(&pem.Block{Type: Ed25519PrivateKeyBanner, Bytes: b})
-	case Curve_P256:
-		return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256PrivateKeyBanner, Bytes: b})
-	default:
-		return nil
-	}
-}
+	// MarshalPEM will return a PEM encoded representation of this certificate
+	// This is primarily the format stored on disk
+	MarshalPEM() ([]byte, error)
 
-// MarshalX25519PrivateKey is a simple helper to PEM encode an X25519 private key
-func MarshalX25519PrivateKey(b []byte) []byte {
-	return pem.EncodeToMemory(&pem.Block{Type: X25519PrivateKeyBanner, Bytes: b})
-}
+	// MarshalJSON will return the json representation of this certificate
+	MarshalJSON() ([]byte, error)
 
-// MarshalEd25519PrivateKey is a simple helper to PEM encode an Ed25519 private key
-func MarshalEd25519PrivateKey(key ed25519.PrivateKey) []byte {
-	return pem.EncodeToMemory(&pem.Block{Type: Ed25519PrivateKeyBanner, Bytes: key})
-}
+	// String will return a human-readable representation of this certificate
+	String() string
 
-func UnmarshalPrivateKey(b []byte) ([]byte, []byte, Curve, error) {
-	k, r := pem.Decode(b)
-	if k == nil {
-		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
-	}
-	var expectedLen int
-	var curve Curve
-	switch k.Type {
-	case X25519PrivateKeyBanner:
-		expectedLen = 32
-		curve = Curve_CURVE25519
-	case P256PrivateKeyBanner:
-		expectedLen = 32
-		curve = Curve_P256
-	default:
-		return nil, r, 0, fmt.Errorf("bytes did not contain a proper nebula private key banner")
-	}
-	if len(k.Bytes) != expectedLen {
-		return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s private key", expectedLen, curve)
-	}
-	return k.Bytes, r, curve, nil
+	// Copy creates a copy of the certificate
+	Copy() Certificate
 }
 
-func UnmarshalSigningPrivateKey(b []byte) ([]byte, []byte, Curve, error) {
-	k, r := pem.Decode(b)
-	if k == nil {
-		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
-	}
-	var curve Curve
-	switch k.Type {
-	case EncryptedEd25519PrivateKeyBanner:
-		return nil, nil, Curve_CURVE25519, ErrPrivateKeyEncrypted
-	case EncryptedECDSAP256PrivateKeyBanner:
-		return nil, nil, Curve_P256, ErrPrivateKeyEncrypted
-	case Ed25519PrivateKeyBanner:
-		curve = Curve_CURVE25519
-		if len(k.Bytes) != ed25519.PrivateKeySize {
-			return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid Ed25519 private key", ed25519.PrivateKeySize)
-		}
-	case ECDSAP256PrivateKeyBanner:
-		curve = Curve_P256
-		if len(k.Bytes) != 32 {
-			return nil, r, 0, fmt.Errorf("key was not 32 bytes, is invalid ECDSA P256 private key")
-		}
-	default:
-		return nil, r, 0, fmt.Errorf("bytes did not contain a proper nebula Ed25519/ECDSA private key banner")
-	}
-	return k.Bytes, r, curve, nil
+// CachedCertificate represents a verified certificate with some cached fields to improve
+// performance.
+type CachedCertificate struct {
+	Certificate       Certificate
+	InvertedGroups    map[string]struct{}
+	Fingerprint       string
+	signerFingerprint string
 }
 
-// EncryptAndMarshalSigningPrivateKey is a simple helper to encrypt and PEM encode a private key
-func EncryptAndMarshalSigningPrivateKey(curve Curve, b []byte, passphrase []byte, kdfParams *Argon2Parameters) ([]byte, error) {
-	ciphertext, err := aes256Encrypt(passphrase, kdfParams, b)
+// UnmarshalCertificate will attempt to unmarshal a wire protocol level certificate.
+func UnmarshalCertificate(b []byte) (Certificate, error) {
+	c, err := unmarshalCertificateV1(b, true)
 	if err != nil {
 		return nil, err
 	}
-
-	b, err = proto.Marshal(&RawNebulaEncryptedData{
-		EncryptionMetadata: &RawNebulaEncryptionMetadata{
-			EncryptionAlgorithm: "AES-256-GCM",
-			Argon2Parameters: &RawNebulaArgon2Parameters{
-				Version:     kdfParams.version,
-				Memory:      kdfParams.Memory,
-				Parallelism: uint32(kdfParams.Parallelism),
-				Iterations:  kdfParams.Iterations,
-				Salt:        kdfParams.salt,
-			},
-		},
-		Ciphertext: ciphertext,
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	switch curve {
-	case Curve_CURVE25519:
-		return pem.EncodeToMemory(&pem.Block{Type: EncryptedEd25519PrivateKeyBanner, Bytes: b}), nil
-	case Curve_P256:
-		return pem.EncodeToMemory(&pem.Block{Type: EncryptedECDSAP256PrivateKeyBanner, Bytes: b}), nil
-	default:
-		return nil, fmt.Errorf("invalid curve: %v", curve)
-	}
-}
-
-// UnmarshalX25519PrivateKey will try to pem decode an X25519 private key, returning any other bytes b
-// or an error on failure
-func UnmarshalX25519PrivateKey(b []byte) ([]byte, []byte, error) {
-	k, r := pem.Decode(b)
-	if k == nil {
-		return nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
-	}
-	if k.Type != X25519PrivateKeyBanner {
-		return nil, r, fmt.Errorf("bytes did not contain a proper nebula X25519 private key banner")
-	}
-	if len(k.Bytes) != publicKeyLen {
-		return nil, r, fmt.Errorf("key was not 32 bytes, is invalid X25519 private key")
-	}
-
-	return k.Bytes, r, nil
-}
-
-// UnmarshalEd25519PrivateKey will try to pem decode an Ed25519 private key, returning any other bytes b
-// or an error on failure
-func UnmarshalEd25519PrivateKey(b []byte) (ed25519.PrivateKey, []byte, error) {
-	k, r := pem.Decode(b)
-	if k == nil {
-		return nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
-	}
-
-	if k.Type == EncryptedEd25519PrivateKeyBanner {
-		return nil, r, ErrPrivateKeyEncrypted
-	} else if k.Type != Ed25519PrivateKeyBanner {
-		return nil, r, fmt.Errorf("bytes did not contain a proper nebula Ed25519 private key banner")
-	}
-
-	if len(k.Bytes) != ed25519.PrivateKeySize {
-		return nil, r, fmt.Errorf("key was not 64 bytes, is invalid ed25519 private key")
-	}
-
-	return k.Bytes, r, nil
+	return c, nil
 }
 
-// UnmarshalNebulaEncryptedData will unmarshal a protobuf byte representation of a nebula cert into its
-// protobuf-generated struct.
-func UnmarshalNebulaEncryptedData(b []byte) (*NebulaEncryptedData, error) {
-	if len(b) == 0 {
-		return nil, fmt.Errorf("nil byte array")
-	}
-	var rned RawNebulaEncryptedData
-	err := proto.Unmarshal(b, &rned)
-	if err != nil {
-		return nil, err
-	}
-
-	if rned.EncryptionMetadata == nil {
-		return nil, fmt.Errorf("encoded EncryptionMetadata was nil")
-	}
-
-	if rned.EncryptionMetadata.Argon2Parameters == nil {
-		return nil, fmt.Errorf("encoded Argon2Parameters was nil")
-	}
-
-	params, err := unmarshalArgon2Parameters(rned.EncryptionMetadata.Argon2Parameters)
+// UnmarshalCertificateFromHandshake will attempt to unmarshal a certificate received in a handshake.
+// Handshakes save space by placing the peers public key in a different part of the packet, we have to
+// reassemble the actual certificate structure with that in mind.
+func UnmarshalCertificateFromHandshake(b []byte, publicKey []byte) (Certificate, error) {
+	c, err := unmarshalCertificateV1(b, false)
 	if err != nil {
 		return nil, err
 	}
-
-	ned := NebulaEncryptedData{
-		EncryptionMetadata: NebulaEncryptionMetadata{
-			EncryptionAlgorithm: rned.EncryptionMetadata.EncryptionAlgorithm,
-			Argon2Parameters:    *params,
-		},
-		Ciphertext: rned.Ciphertext,
-	}
-
-	return &ned, nil
-}
-
-func unmarshalArgon2Parameters(params *RawNebulaArgon2Parameters) (*Argon2Parameters, error) {
-	if params.Version < math.MinInt32 || params.Version > math.MaxInt32 {
-		return nil, fmt.Errorf("Argon2Parameters Version must be at least %d and no more than %d", math.MinInt32, math.MaxInt32)
-	}
-	if params.Memory <= 0 || params.Memory > math.MaxUint32 {
-		return nil, fmt.Errorf("Argon2Parameters Memory must be be greater than 0 and no more than %d KiB", uint32(math.MaxUint32))
-	}
-	if params.Parallelism <= 0 || params.Parallelism > math.MaxUint8 {
-		return nil, fmt.Errorf("Argon2Parameters Parallelism must be be greater than 0 and no more than %d", math.MaxUint8)
-	}
-	if params.Iterations <= 0 || params.Iterations > math.MaxUint32 {
-		return nil, fmt.Errorf("-argon-iterations must be be greater than 0 and no more than %d", uint32(math.MaxUint32))
-	}
-
-	return &Argon2Parameters{
-		version:     rune(params.Version),
-		Memory:      uint32(params.Memory),
-		Parallelism: uint8(params.Parallelism),
-		Iterations:  uint32(params.Iterations),
-		salt:        params.Salt,
-	}, nil
-
-}
-
-// DecryptAndUnmarshalSigningPrivateKey will try to pem decode and decrypt an Ed25519/ECDSA private key with
-// the given passphrase, returning any other bytes b or an error on failure
-func DecryptAndUnmarshalSigningPrivateKey(passphrase, b []byte) (Curve, []byte, []byte, error) {
-	var curve Curve
-
-	k, r := pem.Decode(b)
-	if k == nil {
-		return curve, nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
-	}
-
-	switch k.Type {
-	case EncryptedEd25519PrivateKeyBanner:
-		curve = Curve_CURVE25519
-	case EncryptedECDSAP256PrivateKeyBanner:
-		curve = Curve_P256
-	default:
-		return curve, nil, r, fmt.Errorf("bytes did not contain a proper nebula encrypted Ed25519/ECDSA private key banner")
-	}
-
-	ned, err := UnmarshalNebulaEncryptedData(k.Bytes)
-	if err != nil {
-		return curve, nil, r, err
-	}
-
-	var bytes []byte
-	switch ned.EncryptionMetadata.EncryptionAlgorithm {
-	case "AES-256-GCM":
-		bytes, err = aes256Decrypt(passphrase, &ned.EncryptionMetadata.Argon2Parameters, ned.Ciphertext)
-		if err != nil {
-			return curve, nil, r, err
-		}
-	default:
-		return curve, nil, r, fmt.Errorf("unsupported encryption algorithm: %s", ned.EncryptionMetadata.EncryptionAlgorithm)
-	}
-
-	switch curve {
-	case Curve_CURVE25519:
-		if len(bytes) != ed25519.PrivateKeySize {
-			return curve, nil, r, fmt.Errorf("key was not %d bytes, is invalid ed25519 private key", ed25519.PrivateKeySize)
-		}
-	case Curve_P256:
-		if len(bytes) != 32 {
-			return curve, nil, r, fmt.Errorf("key was not 32 bytes, is invalid ECDSA P256 private key")
-		}
-	}
-
-	return curve, bytes, r, nil
-}
-
-func MarshalPublicKey(curve Curve, b []byte) []byte {
-	switch curve {
-	case Curve_CURVE25519:
-		return pem.EncodeToMemory(&pem.Block{Type: X25519PublicKeyBanner, Bytes: b})
-	case Curve_P256:
-		return pem.EncodeToMemory(&pem.Block{Type: P256PublicKeyBanner, Bytes: b})
-	default:
-		return nil
-	}
-}
-
-// MarshalX25519PublicKey is a simple helper to PEM encode an X25519 public key
-func MarshalX25519PublicKey(b []byte) []byte {
-	return pem.EncodeToMemory(&pem.Block{Type: X25519PublicKeyBanner, Bytes: b})
-}
-
-// MarshalEd25519PublicKey is a simple helper to PEM encode an Ed25519 public key
-func MarshalEd25519PublicKey(key ed25519.PublicKey) []byte {
-	return pem.EncodeToMemory(&pem.Block{Type: Ed25519PublicKeyBanner, Bytes: key})
-}
-
-func UnmarshalPublicKey(b []byte) ([]byte, []byte, Curve, error) {
-	k, r := pem.Decode(b)
-	if k == nil {
-		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
-	}
-	var expectedLen int
-	var curve Curve
-	switch k.Type {
-	case X25519PublicKeyBanner:
-		expectedLen = 32
-		curve = Curve_CURVE25519
-	case P256PublicKeyBanner:
-		// Uncompressed
-		expectedLen = 65
-		curve = Curve_P256
-	default:
-		return nil, r, 0, fmt.Errorf("bytes did not contain a proper nebula public key banner")
-	}
-	if len(k.Bytes) != expectedLen {
-		return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s public key", expectedLen, curve)
-	}
-	return k.Bytes, r, curve, nil
-}
-
-// UnmarshalX25519PublicKey will try to pem decode an X25519 public key, returning any other bytes b
-// or an error on failure
-func UnmarshalX25519PublicKey(b []byte) ([]byte, []byte, error) {
-	k, r := pem.Decode(b)
-	if k == nil {
-		return nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
-	}
-	if k.Type != X25519PublicKeyBanner {
-		return nil, r, fmt.Errorf("bytes did not contain a proper nebula X25519 public key banner")
-	}
-	if len(k.Bytes) != publicKeyLen {
-		return nil, r, fmt.Errorf("key was not 32 bytes, is invalid X25519 public key")
-	}
-
-	return k.Bytes, r, nil
-}
-
-// UnmarshalEd25519PublicKey will try to pem decode an Ed25519 public key, returning any other bytes b
-// or an error on failure
-func UnmarshalEd25519PublicKey(b []byte) (ed25519.PublicKey, []byte, error) {
-	k, r := pem.Decode(b)
-	if k == nil {
-		return nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
-	}
-	if k.Type != Ed25519PublicKeyBanner {
-		return nil, r, fmt.Errorf("bytes did not contain a proper nebula Ed25519 public key banner")
-	}
-	if len(k.Bytes) != ed25519.PublicKeySize {
-		return nil, r, fmt.Errorf("key was not 32 bytes, is invalid ed25519 public key")
-	}
-
-	return k.Bytes, r, nil
-}
-
-// Sign signs a nebula cert with the provided private key
-func (nc *NebulaCertificate) Sign(curve Curve, key []byte) error {
-	if curve != nc.Details.Curve {
-		return fmt.Errorf("curve in cert and private key supplied don't match")
-	}
-
-	b, err := proto.Marshal(nc.getRawDetails())
-	if err != nil {
-		return err
-	}
-
-	var sig []byte
-
-	switch curve {
-	case Curve_CURVE25519:
-		signer := ed25519.PrivateKey(key)
-		sig = ed25519.Sign(signer, b)
-	case Curve_P256:
-		signer := &ecdsa.PrivateKey{
-			PublicKey: ecdsa.PublicKey{
-				Curve: elliptic.P256(),
-			},
-			// ref: https://github.com/golang/go/blob/go1.19/src/crypto/x509/sec1.go#L95
-			D: new(big.Int).SetBytes(key),
-		}
-		// ref: https://github.com/golang/go/blob/go1.19/src/crypto/x509/sec1.go#L119
-		signer.X, signer.Y = signer.Curve.ScalarBaseMult(key)
-
-		// We need to hash first for ECDSA
-		// - https://pkg.go.dev/crypto/ecdsa#SignASN1
-		hashed := sha256.Sum256(b)
-		sig, err = ecdsa.SignASN1(rand.Reader, signer, hashed[:])
-		if err != nil {
-			return err
-		}
-	default:
-		return fmt.Errorf("invalid curve: %s", nc.Details.Curve)
-	}
-
-	nc.Signature = sig
-	return nil
-}
-
-// SignPkcs11 signs a nebula cert with the provided private key
-func (nc *NebulaCertificate) SignPkcs11(curve Curve, client *pkclient.PKClient) error {
-	if !nc.Pkcs11Backed {
-		return fmt.Errorf("certificate is not PKCS#11 backed")
-	}
-
-	if curve != nc.Details.Curve {
-		return fmt.Errorf("curve in cert and private key supplied don't match")
-	}
-
-	if curve != Curve_P256 {
-		return fmt.Errorf("only P256 is supported by PKCS#11")
-	}
-
-	b, err := proto.Marshal(nc.getRawDetails())
-	if err != nil {
-		return err
-	}
-
-	sig, err := client.SignASN1(b)
-	if err != nil {
-		return err
-	}
-
-	nc.Signature = sig
-	return nil
-}
-
-// CheckSignature verifies the signature against the provided public key
-func (nc *NebulaCertificate) CheckSignature(key []byte) bool {
-	b, err := proto.Marshal(nc.getRawDetails())
-	if err != nil {
-		return false
-	}
-	switch nc.Details.Curve {
-	case Curve_CURVE25519:
-		return ed25519.Verify(ed25519.PublicKey(key), b, nc.Signature)
-	case Curve_P256:
-		x, y := elliptic.Unmarshal(elliptic.P256(), key)
-		pubKey := &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}
-		hashed := sha256.Sum256(b)
-		return ecdsa.VerifyASN1(pubKey, hashed[:], nc.Signature)
-	default:
-		return false
-	}
-}
-
-// NOTE: This uses an internal cache that will not be invalidated automatically
-// if you manually change any fields in the NebulaCertificate.
-func (nc *NebulaCertificate) checkSignatureWithCache(key []byte, useCache bool) bool {
-	if !useCache {
-		return nc.CheckSignature(key)
-	}
-
-	if v := nc.signatureVerified.Load(); v != nil {
-		return bytes.Equal(*v, key)
-	}
-
-	verified := nc.CheckSignature(key)
-	if verified {
-		keyCopy := make([]byte, len(key))
-		copy(keyCopy, key)
-		nc.signatureVerified.Store(&keyCopy)
-	}
-
-	return verified
-}
-
-// Expired will return true if the nebula cert is too young or too old compared to the provided time, otherwise false
-func (nc *NebulaCertificate) Expired(t time.Time) bool {
-	return nc.Details.NotBefore.After(t) || nc.Details.NotAfter.Before(t)
-}
-
-// Verify will ensure a certificate is good in all respects (expiry, group membership, signature, cert blocklist, etc)
-func (nc *NebulaCertificate) Verify(t time.Time, ncp *NebulaCAPool) (bool, error) {
-	return nc.verify(t, ncp, false)
-}
-
-// VerifyWithCache will ensure a certificate is good in all respects (expiry, group membership, signature, cert blocklist, etc)
-//
-// NOTE: This uses an internal cache that will not be invalidated automatically
-// if you manually change any fields in the NebulaCertificate.
-func (nc *NebulaCertificate) VerifyWithCache(t time.Time, ncp *NebulaCAPool) (bool, error) {
-	return nc.verify(t, ncp, true)
-}
-
-// ResetCache resets the cache used by VerifyWithCache.
-func (nc *NebulaCertificate) ResetCache() {
-	nc.sha256sum.Store(nil)
-	nc.signatureVerified.Store(nil)
-}
-
-// Verify will ensure a certificate is good in all respects (expiry, group membership, signature, cert blocklist, etc)
-func (nc *NebulaCertificate) verify(t time.Time, ncp *NebulaCAPool, useCache bool) (bool, error) {
-	if ncp.isBlocklistedWithCache(nc, useCache) {
-		return false, ErrBlockListed
-	}
-
-	signer, err := ncp.GetCAForCert(nc)
-	if err != nil {
-		return false, err
-	}
-
-	if signer.Expired(t) {
-		return false, ErrRootExpired
-	}
-
-	if nc.Expired(t) {
-		return false, ErrExpired
-	}
-
-	if !nc.checkSignatureWithCache(signer.Details.PublicKey, useCache) {
-		return false, ErrSignatureMismatch
-	}
-
-	if err := nc.CheckRootConstrains(signer); err != nil {
-		return false, err
-	}
-
-	return true, nil
-}
-
-// CheckRootConstrains returns an error if the certificate violates constraints set on the root (groups, ips, subnets)
-func (nc *NebulaCertificate) CheckRootConstrains(signer *NebulaCertificate) error {
-	// Make sure this cert wasn't valid before the root
-	if signer.Details.NotAfter.Before(nc.Details.NotAfter) {
-		return fmt.Errorf("certificate expires after signing certificate")
-	}
-
-	// Make sure this cert isn't valid after the root
-	if signer.Details.NotBefore.After(nc.Details.NotBefore) {
-		return fmt.Errorf("certificate is valid before the signing certificate")
-	}
-
-	// If the signer has a limited set of groups make sure the cert only contains a subset
-	if len(signer.Details.InvertedGroups) > 0 {
-		for _, g := range nc.Details.Groups {
-			if _, ok := signer.Details.InvertedGroups[g]; !ok {
-				return fmt.Errorf("certificate contained a group not present on the signing ca: %s", g)
-			}
-		}
-	}
-
-	// If the signer has a limited set of ip ranges to issue from make sure the cert only contains a subset
-	if len(signer.Details.Ips) > 0 {
-		for _, ip := range nc.Details.Ips {
-			if !netMatch(ip, signer.Details.Ips) {
-				return fmt.Errorf("certificate contained an ip assignment outside the limitations of the signing ca: %s", ip.String())
-			}
-		}
-	}
-
-	// If the signer has a limited set of subnet ranges to issue from make sure the cert only contains a subset
-	if len(signer.Details.Subnets) > 0 {
-		for _, subnet := range nc.Details.Subnets {
-			if !netMatch(subnet, signer.Details.Subnets) {
-				return fmt.Errorf("certificate contained a subnet assignment outside the limitations of the signing ca: %s", subnet)
-			}
-		}
-	}
-
-	return nil
-}
-
-// VerifyPrivateKey checks that the public key in the Nebula certificate and a supplied private key match
-func (nc *NebulaCertificate) VerifyPrivateKey(curve Curve, key []byte) error {
-	if nc.Pkcs11Backed {
-		return nil //todo!
-	}
-	if curve != nc.Details.Curve {
-		return fmt.Errorf("curve in cert and private key supplied don't match")
-	}
-	if nc.Details.IsCA {
-		switch curve {
-		case Curve_CURVE25519:
-			// the call to PublicKey below will panic slice bounds out of range otherwise
-			if len(key) != ed25519.PrivateKeySize {
-				return fmt.Errorf("key was not 64 bytes, is invalid ed25519 private key")
-			}
-
-			if !ed25519.PublicKey(nc.Details.PublicKey).Equal(ed25519.PrivateKey(key).Public()) {
-				return fmt.Errorf("public key in cert and private key supplied don't match")
-			}
-		case Curve_P256:
-			privkey, err := ecdh.P256().NewPrivateKey(key)
-			if err != nil {
-				return fmt.Errorf("cannot parse private key as P256")
-			}
-			pub := privkey.PublicKey().Bytes()
-			if !bytes.Equal(pub, nc.Details.PublicKey) {
-				return fmt.Errorf("public key in cert and private key supplied don't match")
-			}
-		default:
-			return fmt.Errorf("invalid curve: %s", curve)
-		}
-		return nil
-	}
-
-	var pub []byte
-	switch curve {
-	case Curve_CURVE25519:
-		var err error
-		pub, err = curve25519.X25519(key, curve25519.Basepoint)
-		if err != nil {
-			return err
-		}
-	case Curve_P256:
-		privkey, err := ecdh.P256().NewPrivateKey(key)
-		if err != nil {
-			return err
-		}
-		pub = privkey.PublicKey().Bytes()
-	default:
-		return fmt.Errorf("invalid curve: %s", curve)
-	}
-	if !bytes.Equal(pub, nc.Details.PublicKey) {
-		return fmt.Errorf("public key in cert and private key supplied don't match")
-	}
-
-	return nil
-}
-
-// String will return a pretty printed representation of a nebula cert
-func (nc *NebulaCertificate) String() string {
-	if nc == nil {
-		return "NebulaCertificate {}\n"
-	}
-
-	s := "NebulaCertificate {\n"
-	s += "\tDetails {\n"
-	s += fmt.Sprintf("\t\tName: %v\n", nc.Details.Name)
-
-	if len(nc.Details.Ips) > 0 {
-		s += "\t\tIps: [\n"
-		for _, ip := range nc.Details.Ips {
-			s += fmt.Sprintf("\t\t\t%v\n", ip.String())
-		}
-		s += "\t\t]\n"
-	} else {
-		s += "\t\tIps: []\n"
-	}
-
-	if len(nc.Details.Subnets) > 0 {
-		s += "\t\tSubnets: [\n"
-		for _, ip := range nc.Details.Subnets {
-			s += fmt.Sprintf("\t\t\t%v\n", ip.String())
-		}
-		s += "\t\t]\n"
-	} else {
-		s += "\t\tSubnets: []\n"
-	}
-
-	if len(nc.Details.Groups) > 0 {
-		s += "\t\tGroups: [\n"
-		for _, g := range nc.Details.Groups {
-			s += fmt.Sprintf("\t\t\t\"%v\"\n", g)
-		}
-		s += "\t\t]\n"
-	} else {
-		s += "\t\tGroups: []\n"
-	}
-
-	s += fmt.Sprintf("\t\tNot before: %v\n", nc.Details.NotBefore)
-	s += fmt.Sprintf("\t\tNot After: %v\n", nc.Details.NotAfter)
-	s += fmt.Sprintf("\t\tIs CA: %v\n", nc.Details.IsCA)
-	s += fmt.Sprintf("\t\tIssuer: %s\n", nc.Details.Issuer)
-	s += fmt.Sprintf("\t\tPublic key: %x\n", nc.Details.PublicKey)
-	s += fmt.Sprintf("\t\tCurve: %s\n", nc.Details.Curve)
-	s += "\t}\n"
-	fp, err := nc.Sha256Sum()
-	if err == nil {
-		s += fmt.Sprintf("\tFingerprint: %s\n", fp)
-	}
-	s += fmt.Sprintf("\tSignature: %x\n", nc.Signature)
-	s += "}"
-
-	return s
-}
-
-// getRawDetails marshals the raw details into protobuf ready struct
-func (nc *NebulaCertificate) getRawDetails() *RawNebulaCertificateDetails {
-	rd := &RawNebulaCertificateDetails{
-		Name:      nc.Details.Name,
-		Groups:    nc.Details.Groups,
-		NotBefore: nc.Details.NotBefore.Unix(),
-		NotAfter:  nc.Details.NotAfter.Unix(),
-		PublicKey: make([]byte, len(nc.Details.PublicKey)),
-		IsCA:      nc.Details.IsCA,
-		Curve:     nc.Details.Curve,
-	}
-
-	for _, ipNet := range nc.Details.Ips {
-		rd.Ips = append(rd.Ips, ip2int(ipNet.IP), ip2int(ipNet.Mask))
-	}
-
-	for _, ipNet := range nc.Details.Subnets {
-		rd.Subnets = append(rd.Subnets, ip2int(ipNet.IP), ip2int(ipNet.Mask))
-	}
-
-	copy(rd.PublicKey, nc.Details.PublicKey[:])
-
-	// I know, this is terrible
-	rd.Issuer, _ = hex.DecodeString(nc.Details.Issuer)
-
-	return rd
-}
-
-// Marshal will marshal a nebula cert into a protobuf byte array
-func (nc *NebulaCertificate) Marshal() ([]byte, error) {
-	rc := RawNebulaCertificate{
-		Details:   nc.getRawDetails(),
-		Signature: nc.Signature,
-	}
-
-	return proto.Marshal(&rc)
-}
-
-// MarshalToPEM will marshal a nebula cert into a protobuf byte array and pem encode the result
-func (nc *NebulaCertificate) MarshalToPEM() ([]byte, error) {
-	b, err := nc.Marshal()
-	if err != nil {
-		return nil, err
-	}
-	return pem.EncodeToMemory(&pem.Block{Type: CertBanner, Bytes: b}), nil
-}
-
-// Sha256Sum calculates a sha-256 sum of the marshaled certificate
-func (nc *NebulaCertificate) Sha256Sum() (string, error) {
-	b, err := nc.Marshal()
-	if err != nil {
-		return "", err
-	}
-
-	sum := sha256.Sum256(b)
-	return hex.EncodeToString(sum[:]), nil
-}
-
-// NOTE: This uses an internal cache that will not be invalidated automatically
-// if you manually change any fields in the NebulaCertificate.
-func (nc *NebulaCertificate) sha256SumWithCache(useCache bool) (string, error) {
-	if !useCache {
-		return nc.Sha256Sum()
-	}
-
-	if s := nc.sha256sum.Load(); s != nil {
-		return *s, nil
-	}
-	s, err := nc.Sha256Sum()
-	if err != nil {
-		return s, err
-	}
-
-	nc.sha256sum.Store(&s)
-	return s, nil
-}
-
-func (nc *NebulaCertificate) MarshalJSON() ([]byte, error) {
-	toString := func(ips []*net.IPNet) []string {
-		s := []string{}
-		for _, ip := range ips {
-			s = append(s, ip.String())
-		}
-		return s
-	}
-
-	fp, _ := nc.Sha256Sum()
-	jc := m{
-		"details": m{
-			"name":      nc.Details.Name,
-			"ips":       toString(nc.Details.Ips),
-			"subnets":   toString(nc.Details.Subnets),
-			"groups":    nc.Details.Groups,
-			"notBefore": nc.Details.NotBefore,
-			"notAfter":  nc.Details.NotAfter,
-			"publicKey": fmt.Sprintf("%x", nc.Details.PublicKey),
-			"isCa":      nc.Details.IsCA,
-			"issuer":    nc.Details.Issuer,
-			"curve":     nc.Details.Curve.String(),
-		},
-		"fingerprint": fp,
-		"signature":   fmt.Sprintf("%x", nc.Signature),
-	}
-	return json.Marshal(jc)
-}
-
-//func (nc *NebulaCertificate) Copy() *NebulaCertificate {
-//	r, err := nc.Marshal()
-//	if err != nil {
-//		//TODO
-//		return nil
-//	}
-//
-//	c, err := UnmarshalNebulaCertificate(r)
-//	return c
-//}
-
-func (nc *NebulaCertificate) Copy() *NebulaCertificate {
-	c := &NebulaCertificate{
-		Details: NebulaCertificateDetails{
-			Name:           nc.Details.Name,
-			Groups:         make([]string, len(nc.Details.Groups)),
-			Ips:            make([]*net.IPNet, len(nc.Details.Ips)),
-			Subnets:        make([]*net.IPNet, len(nc.Details.Subnets)),
-			NotBefore:      nc.Details.NotBefore,
-			NotAfter:       nc.Details.NotAfter,
-			PublicKey:      make([]byte, len(nc.Details.PublicKey)),
-			IsCA:           nc.Details.IsCA,
-			Issuer:         nc.Details.Issuer,
-			InvertedGroups: make(map[string]struct{}, len(nc.Details.InvertedGroups)),
-		},
-		Signature: make([]byte, len(nc.Signature)),
-	}
-
-	copy(c.Signature, nc.Signature)
-	copy(c.Details.Groups, nc.Details.Groups)
-	copy(c.Details.PublicKey, nc.Details.PublicKey)
-
-	for i, p := range nc.Details.Ips {
-		c.Details.Ips[i] = &net.IPNet{
-			IP:   make(net.IP, len(p.IP)),
-			Mask: make(net.IPMask, len(p.Mask)),
-		}
-		copy(c.Details.Ips[i].IP, p.IP)
-		copy(c.Details.Ips[i].Mask, p.Mask)
-	}
-
-	for i, p := range nc.Details.Subnets {
-		c.Details.Subnets[i] = &net.IPNet{
-			IP:   make(net.IP, len(p.IP)),
-			Mask: make(net.IPMask, len(p.Mask)),
-		}
-		copy(c.Details.Subnets[i].IP, p.IP)
-		copy(c.Details.Subnets[i].Mask, p.Mask)
-	}
-
-	for g := range nc.Details.InvertedGroups {
-		c.Details.InvertedGroups[g] = struct{}{}
-	}
-
-	return c
-}
-
-func netMatch(certIp *net.IPNet, rootIps []*net.IPNet) bool {
-	for _, net := range rootIps {
-		if net.Contains(certIp.IP) && maskContains(net.Mask, certIp.Mask) {
-			return true
-		}
-	}
-
-	return false
-}
-
-func maskContains(caMask, certMask net.IPMask) bool {
-	caM := maskTo4(caMask)
-	cM := maskTo4(certMask)
-	// Make sure forcing to ipv4 didn't nuke us
-	if caM == nil || cM == nil {
-		return false
-	}
-
-	// Make sure the cert mask is not greater than the ca mask
-	for i := 0; i < len(caMask); i++ {
-		if caM[i] > cM[i] {
-			return false
-		}
-	}
-
-	return true
-}
-
-func maskTo4(ip net.IPMask) net.IPMask {
-	if len(ip) == net.IPv4len {
-		return ip
-	}
-
-	if len(ip) == net.IPv6len && isZeros(ip[0:10]) && ip[10] == 0xff && ip[11] == 0xff {
-		return ip[12:16]
-	}
-
-	return nil
-}
-
-func isZeros(b []byte) bool {
-	for i := 0; i < len(b); i++ {
-		if b[i] != 0 {
-			return false
-		}
-	}
-	return true
-}
-
-func ip2int(ip []byte) uint32 {
-	if len(ip) == 16 {
-		return binary.BigEndian.Uint32(ip[12:16])
-	}
-	return binary.BigEndian.Uint32(ip)
-}
-
-func int2ip(nn uint32) net.IP {
-	ip := make(net.IP, net.IPv4len)
-	binary.BigEndian.PutUint32(ip, nn)
-	return ip
+	c.details.PublicKey = publicKey
+	return c, nil
 }

Разлика између датотеке није приказан због своје велике величине
+ 264 - 790
cert/cert_test.go


+ 496 - 0
cert/cert_v1.go

@@ -0,0 +1,496 @@
+package cert
+
+import (
+	"bytes"
+	"crypto/ecdh"
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/sha256"
+	"encoding/binary"
+	"encoding/hex"
+	"encoding/json"
+	"encoding/pem"
+	"fmt"
+	"math/big"
+	"net"
+	"net/netip"
+	"time"
+
+	"github.com/slackhq/nebula/pkclient"
+	"golang.org/x/crypto/curve25519"
+	"google.golang.org/protobuf/proto"
+)
+
+const publicKeyLen = 32
+
+type certificateV1 struct {
+	details   detailsV1
+	signature []byte
+}
+
+type detailsV1 struct {
+	Name      string
+	Ips       []netip.Prefix
+	Subnets   []netip.Prefix
+	Groups    []string
+	NotBefore time.Time
+	NotAfter  time.Time
+	PublicKey []byte
+	IsCA      bool
+	Issuer    string
+
+	Curve Curve
+}
+
+type m map[string]interface{}
+
+func (nc *certificateV1) Version() Version {
+	return Version1
+}
+
+func (nc *certificateV1) Curve() Curve {
+	return nc.details.Curve
+}
+
+func (nc *certificateV1) Groups() []string {
+	return nc.details.Groups
+}
+
+func (nc *certificateV1) IsCA() bool {
+	return nc.details.IsCA
+}
+
+func (nc *certificateV1) Issuer() string {
+	return nc.details.Issuer
+}
+
+func (nc *certificateV1) Name() string {
+	return nc.details.Name
+}
+
+func (nc *certificateV1) Networks() []netip.Prefix {
+	return nc.details.Ips
+}
+
+func (nc *certificateV1) NotAfter() time.Time {
+	return nc.details.NotAfter
+}
+
+func (nc *certificateV1) NotBefore() time.Time {
+	return nc.details.NotBefore
+}
+
+func (nc *certificateV1) PublicKey() []byte {
+	return nc.details.PublicKey
+}
+
+func (nc *certificateV1) Signature() []byte {
+	return nc.signature
+}
+
+func (nc *certificateV1) UnsafeNetworks() []netip.Prefix {
+	return nc.details.Subnets
+}
+
+func (nc *certificateV1) Fingerprint() (string, error) {
+	b, err := nc.Marshal()
+	if err != nil {
+		return "", err
+	}
+
+	sum := sha256.Sum256(b)
+	return hex.EncodeToString(sum[:]), nil
+}
+
+func (nc *certificateV1) CheckSignature(key []byte) bool {
+	b, err := proto.Marshal(nc.getRawDetails())
+	if err != nil {
+		return false
+	}
+	switch nc.details.Curve {
+	case Curve_CURVE25519:
+		return ed25519.Verify(key, b, nc.signature)
+	case Curve_P256:
+		x, y := elliptic.Unmarshal(elliptic.P256(), key)
+		pubKey := &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}
+		hashed := sha256.Sum256(b)
+		return ecdsa.VerifyASN1(pubKey, hashed[:], nc.signature)
+	default:
+		return false
+	}
+}
+
+func (nc *certificateV1) Expired(t time.Time) bool {
+	return nc.details.NotBefore.After(t) || nc.details.NotAfter.Before(t)
+}
+
+func (nc *certificateV1) VerifyPrivateKey(curve Curve, key []byte) error {
+	if curve != nc.details.Curve {
+		return fmt.Errorf("curve in cert and private key supplied don't match")
+	}
+	if nc.details.IsCA {
+		switch curve {
+		case Curve_CURVE25519:
+			// the call to PublicKey below will panic slice bounds out of range otherwise
+			if len(key) != ed25519.PrivateKeySize {
+				return fmt.Errorf("key was not 64 bytes, is invalid ed25519 private key")
+			}
+
+			if !ed25519.PublicKey(nc.details.PublicKey).Equal(ed25519.PrivateKey(key).Public()) {
+				return fmt.Errorf("public key in cert and private key supplied don't match")
+			}
+		case Curve_P256:
+			privkey, err := ecdh.P256().NewPrivateKey(key)
+			if err != nil {
+				return fmt.Errorf("cannot parse private key as P256: %w", err)
+			}
+			pub := privkey.PublicKey().Bytes()
+			if !bytes.Equal(pub, nc.details.PublicKey) {
+				return fmt.Errorf("public key in cert and private key supplied don't match")
+			}
+		default:
+			return fmt.Errorf("invalid curve: %s", curve)
+		}
+		return nil
+	}
+
+	var pub []byte
+	switch curve {
+	case Curve_CURVE25519:
+		var err error
+		pub, err = curve25519.X25519(key, curve25519.Basepoint)
+		if err != nil {
+			return err
+		}
+	case Curve_P256:
+		privkey, err := ecdh.P256().NewPrivateKey(key)
+		if err != nil {
+			return err
+		}
+		pub = privkey.PublicKey().Bytes()
+	default:
+		return fmt.Errorf("invalid curve: %s", curve)
+	}
+	if !bytes.Equal(pub, nc.details.PublicKey) {
+		return fmt.Errorf("public key in cert and private key supplied don't match")
+	}
+
+	return nil
+}
+
+// getRawDetails marshals the raw details into protobuf ready struct
+func (nc *certificateV1) getRawDetails() *RawNebulaCertificateDetails {
+	rd := &RawNebulaCertificateDetails{
+		Name:      nc.details.Name,
+		Groups:    nc.details.Groups,
+		NotBefore: nc.details.NotBefore.Unix(),
+		NotAfter:  nc.details.NotAfter.Unix(),
+		PublicKey: make([]byte, len(nc.details.PublicKey)),
+		IsCA:      nc.details.IsCA,
+		Curve:     nc.details.Curve,
+	}
+
+	for _, ipNet := range nc.details.Ips {
+		mask := net.CIDRMask(ipNet.Bits(), ipNet.Addr().BitLen())
+		rd.Ips = append(rd.Ips, addr2int(ipNet.Addr()), ip2int(mask))
+	}
+
+	for _, ipNet := range nc.details.Subnets {
+		mask := net.CIDRMask(ipNet.Bits(), ipNet.Addr().BitLen())
+		rd.Subnets = append(rd.Subnets, addr2int(ipNet.Addr()), ip2int(mask))
+	}
+
+	copy(rd.PublicKey, nc.details.PublicKey[:])
+
+	// I know, this is terrible
+	rd.Issuer, _ = hex.DecodeString(nc.details.Issuer)
+
+	return rd
+}
+
+func (nc *certificateV1) String() string {
+	if nc == nil {
+		return "Certificate {}\n"
+	}
+
+	s := "NebulaCertificate {\n"
+	s += "\tDetails {\n"
+	s += fmt.Sprintf("\t\tName: %v\n", nc.details.Name)
+
+	if len(nc.details.Ips) > 0 {
+		s += "\t\tIps: [\n"
+		for _, ip := range nc.details.Ips {
+			s += fmt.Sprintf("\t\t\t%v\n", ip.String())
+		}
+		s += "\t\t]\n"
+	} else {
+		s += "\t\tIps: []\n"
+	}
+
+	if len(nc.details.Subnets) > 0 {
+		s += "\t\tSubnets: [\n"
+		for _, ip := range nc.details.Subnets {
+			s += fmt.Sprintf("\t\t\t%v\n", ip.String())
+		}
+		s += "\t\t]\n"
+	} else {
+		s += "\t\tSubnets: []\n"
+	}
+
+	if len(nc.details.Groups) > 0 {
+		s += "\t\tGroups: [\n"
+		for _, g := range nc.details.Groups {
+			s += fmt.Sprintf("\t\t\t\"%v\"\n", g)
+		}
+		s += "\t\t]\n"
+	} else {
+		s += "\t\tGroups: []\n"
+	}
+
+	s += fmt.Sprintf("\t\tNot before: %v\n", nc.details.NotBefore)
+	s += fmt.Sprintf("\t\tNot After: %v\n", nc.details.NotAfter)
+	s += fmt.Sprintf("\t\tIs CA: %v\n", nc.details.IsCA)
+	s += fmt.Sprintf("\t\tIssuer: %s\n", nc.details.Issuer)
+	s += fmt.Sprintf("\t\tPublic key: %x\n", nc.details.PublicKey)
+	s += fmt.Sprintf("\t\tCurve: %s\n", nc.details.Curve)
+	s += "\t}\n"
+	fp, err := nc.Fingerprint()
+	if err == nil {
+		s += fmt.Sprintf("\tFingerprint: %s\n", fp)
+	}
+	s += fmt.Sprintf("\tSignature: %x\n", nc.Signature())
+	s += "}"
+
+	return s
+}
+
+func (nc *certificateV1) MarshalForHandshakes() ([]byte, error) {
+	pubKey := nc.details.PublicKey
+	nc.details.PublicKey = nil
+	rawCertNoKey, err := nc.Marshal()
+	if err != nil {
+		return nil, err
+	}
+	nc.details.PublicKey = pubKey
+	return rawCertNoKey, nil
+}
+
+func (nc *certificateV1) Marshal() ([]byte, error) {
+	rc := RawNebulaCertificate{
+		Details:   nc.getRawDetails(),
+		Signature: nc.signature,
+	}
+
+	return proto.Marshal(&rc)
+}
+
+func (nc *certificateV1) MarshalPEM() ([]byte, error) {
+	b, err := nc.Marshal()
+	if err != nil {
+		return nil, err
+	}
+	return pem.EncodeToMemory(&pem.Block{Type: CertificateBanner, Bytes: b}), nil
+}
+
+func (nc *certificateV1) MarshalJSON() ([]byte, error) {
+	fp, _ := nc.Fingerprint()
+	jc := m{
+		"details": m{
+			"name":      nc.details.Name,
+			"ips":       nc.details.Ips,
+			"subnets":   nc.details.Subnets,
+			"groups":    nc.details.Groups,
+			"notBefore": nc.details.NotBefore,
+			"notAfter":  nc.details.NotAfter,
+			"publicKey": fmt.Sprintf("%x", nc.details.PublicKey),
+			"isCa":      nc.details.IsCA,
+			"issuer":    nc.details.Issuer,
+			"curve":     nc.details.Curve.String(),
+		},
+		"fingerprint": fp,
+		"signature":   fmt.Sprintf("%x", nc.Signature()),
+	}
+	return json.Marshal(jc)
+}
+
+func (nc *certificateV1) Copy() Certificate {
+	c := &certificateV1{
+		details: detailsV1{
+			Name:      nc.details.Name,
+			Groups:    make([]string, len(nc.details.Groups)),
+			Ips:       make([]netip.Prefix, len(nc.details.Ips)),
+			Subnets:   make([]netip.Prefix, len(nc.details.Subnets)),
+			NotBefore: nc.details.NotBefore,
+			NotAfter:  nc.details.NotAfter,
+			PublicKey: make([]byte, len(nc.details.PublicKey)),
+			IsCA:      nc.details.IsCA,
+			Issuer:    nc.details.Issuer,
+		},
+		signature: make([]byte, len(nc.signature)),
+	}
+
+	copy(c.signature, nc.signature)
+	copy(c.details.Groups, nc.details.Groups)
+	copy(c.details.PublicKey, nc.details.PublicKey)
+
+	for i, p := range nc.details.Ips {
+		c.details.Ips[i] = p
+	}
+
+	for i, p := range nc.details.Subnets {
+		c.details.Subnets[i] = p
+	}
+
+	return c
+}
+
+// unmarshalCertificateV1 will unmarshal a protobuf byte representation of a nebula cert
+func unmarshalCertificateV1(b []byte, assertPublicKey bool) (*certificateV1, error) {
+	if len(b) == 0 {
+		return nil, fmt.Errorf("nil byte array")
+	}
+	var rc RawNebulaCertificate
+	err := proto.Unmarshal(b, &rc)
+	if err != nil {
+		return nil, err
+	}
+
+	if rc.Details == nil {
+		return nil, fmt.Errorf("encoded Details was nil")
+	}
+
+	if len(rc.Details.Ips)%2 != 0 {
+		return nil, fmt.Errorf("encoded IPs should be in pairs, an odd number was found")
+	}
+
+	if len(rc.Details.Subnets)%2 != 0 {
+		return nil, fmt.Errorf("encoded Subnets should be in pairs, an odd number was found")
+	}
+
+	nc := certificateV1{
+		details: detailsV1{
+			Name:      rc.Details.Name,
+			Groups:    make([]string, len(rc.Details.Groups)),
+			Ips:       make([]netip.Prefix, len(rc.Details.Ips)/2),
+			Subnets:   make([]netip.Prefix, len(rc.Details.Subnets)/2),
+			NotBefore: time.Unix(rc.Details.NotBefore, 0),
+			NotAfter:  time.Unix(rc.Details.NotAfter, 0),
+			PublicKey: make([]byte, len(rc.Details.PublicKey)),
+			IsCA:      rc.Details.IsCA,
+			Curve:     rc.Details.Curve,
+		},
+		signature: make([]byte, len(rc.Signature)),
+	}
+
+	copy(nc.signature, rc.Signature)
+	copy(nc.details.Groups, rc.Details.Groups)
+	nc.details.Issuer = hex.EncodeToString(rc.Details.Issuer)
+
+	if len(rc.Details.PublicKey) < publicKeyLen && assertPublicKey {
+		return nil, fmt.Errorf("public key was fewer than 32 bytes; %v", len(rc.Details.PublicKey))
+	}
+	copy(nc.details.PublicKey, rc.Details.PublicKey)
+
+	var ip netip.Addr
+	for i, rawIp := range rc.Details.Ips {
+		if i%2 == 0 {
+			ip = int2addr(rawIp)
+		} else {
+			ones, _ := net.IPMask(int2ip(rawIp)).Size()
+			nc.details.Ips[i/2] = netip.PrefixFrom(ip, ones)
+		}
+	}
+
+	for i, rawIp := range rc.Details.Subnets {
+		if i%2 == 0 {
+			ip = int2addr(rawIp)
+		} else {
+			ones, _ := net.IPMask(int2ip(rawIp)).Size()
+			nc.details.Subnets[i/2] = netip.PrefixFrom(ip, ones)
+		}
+	}
+
+	return &nc, nil
+}
+
+func signV1(t *TBSCertificate, curve Curve, key []byte, client *pkclient.PKClient) (*certificateV1, error) {
+	c := &certificateV1{
+		details: detailsV1{
+			Name:      t.Name,
+			Ips:       t.Networks,
+			Subnets:   t.UnsafeNetworks,
+			Groups:    t.Groups,
+			NotBefore: t.NotBefore,
+			NotAfter:  t.NotAfter,
+			PublicKey: t.PublicKey,
+			IsCA:      t.IsCA,
+			Curve:     t.Curve,
+			Issuer:    t.issuer,
+		},
+	}
+	b, err := proto.Marshal(c.getRawDetails())
+	if err != nil {
+		return nil, err
+	}
+
+	var sig []byte
+
+	switch curve {
+	case Curve_CURVE25519:
+		signer := ed25519.PrivateKey(key)
+		sig = ed25519.Sign(signer, b)
+	case Curve_P256:
+		if client != nil {
+			sig, err = client.SignASN1(b)
+		} else {
+			signer := &ecdsa.PrivateKey{
+				PublicKey: ecdsa.PublicKey{
+					Curve: elliptic.P256(),
+				},
+				// ref: https://github.com/golang/go/blob/go1.19/src/crypto/x509/sec1.go#L95
+				D: new(big.Int).SetBytes(key),
+			}
+			// ref: https://github.com/golang/go/blob/go1.19/src/crypto/x509/sec1.go#L119
+			signer.X, signer.Y = signer.Curve.ScalarBaseMult(key)
+
+			// We need to hash first for ECDSA
+			// - https://pkg.go.dev/crypto/ecdsa#SignASN1
+			hashed := sha256.Sum256(b)
+			sig, err = ecdsa.SignASN1(rand.Reader, signer, hashed[:])
+			if err != nil {
+				return nil, err
+			}
+		}
+	default:
+		return nil, fmt.Errorf("invalid curve: %s", c.details.Curve)
+	}
+
+	c.signature = sig
+	return c, nil
+}
+
+func ip2int(ip []byte) uint32 {
+	if len(ip) == 16 {
+		return binary.BigEndian.Uint32(ip[12:16])
+	}
+	return binary.BigEndian.Uint32(ip)
+}
+
+func int2ip(nn uint32) net.IP {
+	ip := make(net.IP, net.IPv4len)
+	binary.BigEndian.PutUint32(ip, nn)
+	return ip
+}
+
+func addr2int(addr netip.Addr) uint32 {
+	b := addr.Unmap().As4()
+	return binary.BigEndian.Uint32(b[:])
+}
+
+func int2addr(nn uint32) netip.Addr {
+	ip := [4]byte{}
+	binary.BigEndian.PutUint32(ip[:], nn)
+	return netip.AddrFrom4(ip).Unmap()
+}

+ 111 - 111
cert/cert.pb.go → cert/cert_v1.pb.go

@@ -1,8 +1,8 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.30.0
+// 	protoc-gen-go v1.34.2
 // 	protoc        v3.21.5
-// source: cert.proto
+// source: cert_v1.proto
 
 package cert
 
@@ -50,11 +50,11 @@ func (x Curve) String() string {
 }
 
 func (Curve) Descriptor() protoreflect.EnumDescriptor {
-	return file_cert_proto_enumTypes[0].Descriptor()
+	return file_cert_v1_proto_enumTypes[0].Descriptor()
 }
 
 func (Curve) Type() protoreflect.EnumType {
-	return &file_cert_proto_enumTypes[0]
+	return &file_cert_v1_proto_enumTypes[0]
 }
 
 func (x Curve) Number() protoreflect.EnumNumber {
@@ -63,7 +63,7 @@ func (x Curve) Number() protoreflect.EnumNumber {
 
 // Deprecated: Use Curve.Descriptor instead.
 func (Curve) EnumDescriptor() ([]byte, []int) {
-	return file_cert_proto_rawDescGZIP(), []int{0}
+	return file_cert_v1_proto_rawDescGZIP(), []int{0}
 }
 
 type RawNebulaCertificate struct {
@@ -78,7 +78,7 @@ type RawNebulaCertificate struct {
 func (x *RawNebulaCertificate) Reset() {
 	*x = RawNebulaCertificate{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_cert_proto_msgTypes[0]
+		mi := &file_cert_v1_proto_msgTypes[0]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -91,7 +91,7 @@ func (x *RawNebulaCertificate) String() string {
 func (*RawNebulaCertificate) ProtoMessage() {}
 
 func (x *RawNebulaCertificate) ProtoReflect() protoreflect.Message {
-	mi := &file_cert_proto_msgTypes[0]
+	mi := &file_cert_v1_proto_msgTypes[0]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -104,7 +104,7 @@ func (x *RawNebulaCertificate) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use RawNebulaCertificate.ProtoReflect.Descriptor instead.
 func (*RawNebulaCertificate) Descriptor() ([]byte, []int) {
-	return file_cert_proto_rawDescGZIP(), []int{0}
+	return file_cert_v1_proto_rawDescGZIP(), []int{0}
 }
 
 func (x *RawNebulaCertificate) GetDetails() *RawNebulaCertificateDetails {
@@ -143,7 +143,7 @@ type RawNebulaCertificateDetails struct {
 func (x *RawNebulaCertificateDetails) Reset() {
 	*x = RawNebulaCertificateDetails{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_cert_proto_msgTypes[1]
+		mi := &file_cert_v1_proto_msgTypes[1]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -156,7 +156,7 @@ func (x *RawNebulaCertificateDetails) String() string {
 func (*RawNebulaCertificateDetails) ProtoMessage() {}
 
 func (x *RawNebulaCertificateDetails) ProtoReflect() protoreflect.Message {
-	mi := &file_cert_proto_msgTypes[1]
+	mi := &file_cert_v1_proto_msgTypes[1]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -169,7 +169,7 @@ func (x *RawNebulaCertificateDetails) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use RawNebulaCertificateDetails.ProtoReflect.Descriptor instead.
 func (*RawNebulaCertificateDetails) Descriptor() ([]byte, []int) {
-	return file_cert_proto_rawDescGZIP(), []int{1}
+	return file_cert_v1_proto_rawDescGZIP(), []int{1}
 }
 
 func (x *RawNebulaCertificateDetails) GetName() string {
@@ -254,7 +254,7 @@ type RawNebulaEncryptedData struct {
 func (x *RawNebulaEncryptedData) Reset() {
 	*x = RawNebulaEncryptedData{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_cert_proto_msgTypes[2]
+		mi := &file_cert_v1_proto_msgTypes[2]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -267,7 +267,7 @@ func (x *RawNebulaEncryptedData) String() string {
 func (*RawNebulaEncryptedData) ProtoMessage() {}
 
 func (x *RawNebulaEncryptedData) ProtoReflect() protoreflect.Message {
-	mi := &file_cert_proto_msgTypes[2]
+	mi := &file_cert_v1_proto_msgTypes[2]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -280,7 +280,7 @@ func (x *RawNebulaEncryptedData) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use RawNebulaEncryptedData.ProtoReflect.Descriptor instead.
 func (*RawNebulaEncryptedData) Descriptor() ([]byte, []int) {
-	return file_cert_proto_rawDescGZIP(), []int{2}
+	return file_cert_v1_proto_rawDescGZIP(), []int{2}
 }
 
 func (x *RawNebulaEncryptedData) GetEncryptionMetadata() *RawNebulaEncryptionMetadata {
@@ -309,7 +309,7 @@ type RawNebulaEncryptionMetadata struct {
 func (x *RawNebulaEncryptionMetadata) Reset() {
 	*x = RawNebulaEncryptionMetadata{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_cert_proto_msgTypes[3]
+		mi := &file_cert_v1_proto_msgTypes[3]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -322,7 +322,7 @@ func (x *RawNebulaEncryptionMetadata) String() string {
 func (*RawNebulaEncryptionMetadata) ProtoMessage() {}
 
 func (x *RawNebulaEncryptionMetadata) ProtoReflect() protoreflect.Message {
-	mi := &file_cert_proto_msgTypes[3]
+	mi := &file_cert_v1_proto_msgTypes[3]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -335,7 +335,7 @@ func (x *RawNebulaEncryptionMetadata) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use RawNebulaEncryptionMetadata.ProtoReflect.Descriptor instead.
 func (*RawNebulaEncryptionMetadata) Descriptor() ([]byte, []int) {
-	return file_cert_proto_rawDescGZIP(), []int{3}
+	return file_cert_v1_proto_rawDescGZIP(), []int{3}
 }
 
 func (x *RawNebulaEncryptionMetadata) GetEncryptionAlgorithm() string {
@@ -367,7 +367,7 @@ type RawNebulaArgon2Parameters struct {
 func (x *RawNebulaArgon2Parameters) Reset() {
 	*x = RawNebulaArgon2Parameters{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_cert_proto_msgTypes[4]
+		mi := &file_cert_v1_proto_msgTypes[4]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -380,7 +380,7 @@ func (x *RawNebulaArgon2Parameters) String() string {
 func (*RawNebulaArgon2Parameters) ProtoMessage() {}
 
 func (x *RawNebulaArgon2Parameters) ProtoReflect() protoreflect.Message {
-	mi := &file_cert_proto_msgTypes[4]
+	mi := &file_cert_v1_proto_msgTypes[4]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -393,7 +393,7 @@ func (x *RawNebulaArgon2Parameters) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use RawNebulaArgon2Parameters.ProtoReflect.Descriptor instead.
 func (*RawNebulaArgon2Parameters) Descriptor() ([]byte, []int) {
-	return file_cert_proto_rawDescGZIP(), []int{4}
+	return file_cert_v1_proto_rawDescGZIP(), []int{4}
 }
 
 func (x *RawNebulaArgon2Parameters) GetVersion() int32 {
@@ -431,87 +431,87 @@ func (x *RawNebulaArgon2Parameters) GetSalt() []byte {
 	return nil
 }
 
-var File_cert_proto protoreflect.FileDescriptor
-
-var file_cert_proto_rawDesc = []byte{
-	0x0a, 0x0a, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x63, 0x65,
-	0x72, 0x74, 0x22, 0x71, 0x0a, 0x14, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x43,
-	0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a, 0x07, 0x44, 0x65,
-	0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x65,
-	0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x43, 0x65, 0x72, 0x74,
-	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07,
-	0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61,
-	0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x53, 0x69, 0x67, 0x6e,
-	0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9c, 0x02, 0x0a, 0x1b, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62,
-	0x75, 0x6c, 0x61, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x44, 0x65,
-	0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x49, 0x70, 0x73,
-	0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x49, 0x70, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x53,
-	0x75, 0x62, 0x6e, 0x65, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x53, 0x75,
-	0x62, 0x6e, 0x65, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18,
-	0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x1c, 0x0a,
-	0x09, 0x4e, 0x6f, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03,
-	0x52, 0x09, 0x4e, 0x6f, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x4e,
-	0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x4e,
-	0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69,
-	0x63, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x50, 0x75, 0x62, 0x6c,
-	0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x49, 0x73, 0x43, 0x41, 0x18, 0x08, 0x20,
-	0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x73, 0x43, 0x41, 0x12, 0x16, 0x0a, 0x06, 0x49, 0x73, 0x73,
-	0x75, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x49, 0x73, 0x73, 0x75, 0x65,
-	0x72, 0x12, 0x21, 0x0a, 0x05, 0x63, 0x75, 0x72, 0x76, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0e,
-	0x32, 0x0b, 0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x43, 0x75, 0x72, 0x76, 0x65, 0x52, 0x05, 0x63,
-	0x75, 0x72, 0x76, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x16, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75,
-	0x6c, 0x61, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12,
-	0x51, 0x0a, 0x12, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74,
-	0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x65,
-	0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x45, 0x6e, 0x63, 0x72,
-	0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x12,
-	0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
-	0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65,
-	0x78, 0x74, 0x22, 0x9c, 0x01, 0x0a, 0x1b, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61,
-	0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
-	0x74, 0x61, 0x12, 0x30, 0x0a, 0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e,
-	0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x67, 0x6f, 0x72,
-	0x69, 0x74, 0x68, 0x6d, 0x12, 0x4b, 0x0a, 0x10, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61,
-	0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f,
-	0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x41,
-	0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x52,
-	0x10, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72,
-	0x73, 0x22, 0xa3, 0x01, 0x0a, 0x19, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x41,
-	0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12,
-	0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05,
-	0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d,
-	0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72,
-	0x79, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x69, 0x73, 0x6d,
-	0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c,
-	0x69, 0x73, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
-	0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69,
-	0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28,
-	0x0c, 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x2a, 0x21, 0x0a, 0x05, 0x43, 0x75, 0x72, 0x76, 0x65,
-	0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x55, 0x52, 0x56, 0x45, 0x32, 0x35, 0x35, 0x31, 0x39, 0x10, 0x00,
-	0x12, 0x08, 0x0a, 0x04, 0x50, 0x32, 0x35, 0x36, 0x10, 0x01, 0x42, 0x20, 0x5a, 0x1e, 0x67, 0x69,
-	0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6c, 0x61, 0x63, 0x6b, 0x68, 0x71,
-	0x2f, 0x6e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x62, 0x06, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x33,
+var File_cert_v1_proto protoreflect.FileDescriptor
+
+var file_cert_v1_proto_rawDesc = []byte{
+	0x0a, 0x0d, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x76, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
+	0x04, 0x63, 0x65, 0x72, 0x74, 0x22, 0x71, 0x0a, 0x14, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75,
+	0x6c, 0x61, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a,
+	0x07, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21,
+	0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x43,
+	0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c,
+	0x73, 0x52, 0x07, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x69,
+	0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x53,
+	0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9c, 0x02, 0x0a, 0x1b, 0x52, 0x61, 0x77,
+	0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
+	0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03,
+	0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x49, 0x70, 0x73, 0x12, 0x18,
+	0x0a, 0x07, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, 0x52,
+	0x07, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x47, 0x72, 0x6f, 0x75,
+	0x70, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73,
+	0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x6f, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x05, 0x20,
+	0x01, 0x28, 0x03, 0x52, 0x09, 0x4e, 0x6f, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x1a,
+	0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03,
+	0x52, 0x08, 0x4e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x75,
+	0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x50,
+	0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x49, 0x73, 0x43, 0x41,
+	0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x73, 0x43, 0x41, 0x12, 0x16, 0x0a, 0x06,
+	0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x49, 0x73,
+	0x73, 0x75, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x05, 0x63, 0x75, 0x72, 0x76, 0x65, 0x18, 0x64, 0x20,
+	0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x43, 0x75, 0x72, 0x76, 0x65,
+	0x52, 0x05, 0x63, 0x75, 0x72, 0x76, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x16, 0x52, 0x61, 0x77, 0x4e,
+	0x65, 0x62, 0x75, 0x6c, 0x61, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61,
+	0x74, 0x61, 0x12, 0x51, 0x0a, 0x12, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+	0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21,
+	0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x45,
+	0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+	0x61, 0x52, 0x12, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74,
+	0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74,
+	0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x43, 0x69, 0x70, 0x68, 0x65,
+	0x72, 0x74, 0x65, 0x78, 0x74, 0x22, 0x9c, 0x01, 0x0a, 0x1b, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62,
+	0x75, 0x6c, 0x61, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74,
+	0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x30, 0x0a, 0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
+	0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c,
+	0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x4b, 0x0a, 0x10, 0x41, 0x72, 0x67, 0x6f, 0x6e,
+	0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75,
+	0x6c, 0x61, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65,
+	0x72, 0x73, 0x52, 0x10, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65,
+	0x74, 0x65, 0x72, 0x73, 0x22, 0xa3, 0x01, 0x0a, 0x19, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75,
+	0x6c, 0x61, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65,
+	0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06,
+	0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6d, 0x65,
+	0x6d, 0x6f, 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c,
+	0x69, 0x73, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c,
+	0x6c, 0x65, 0x6c, 0x69, 0x73, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74,
+	0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x69, 0x74, 0x65, 0x72,
+	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x05,
+	0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x2a, 0x21, 0x0a, 0x05, 0x43, 0x75,
+	0x72, 0x76, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x55, 0x52, 0x56, 0x45, 0x32, 0x35, 0x35, 0x31,
+	0x39, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x32, 0x35, 0x36, 0x10, 0x01, 0x42, 0x20, 0x5a,
+	0x1e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6c, 0x61, 0x63,
+	0x6b, 0x68, 0x71, 0x2f, 0x6e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x62,
+	0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
-	file_cert_proto_rawDescOnce sync.Once
-	file_cert_proto_rawDescData = file_cert_proto_rawDesc
+	file_cert_v1_proto_rawDescOnce sync.Once
+	file_cert_v1_proto_rawDescData = file_cert_v1_proto_rawDesc
 )
 
-func file_cert_proto_rawDescGZIP() []byte {
-	file_cert_proto_rawDescOnce.Do(func() {
-		file_cert_proto_rawDescData = protoimpl.X.CompressGZIP(file_cert_proto_rawDescData)
+func file_cert_v1_proto_rawDescGZIP() []byte {
+	file_cert_v1_proto_rawDescOnce.Do(func() {
+		file_cert_v1_proto_rawDescData = protoimpl.X.CompressGZIP(file_cert_v1_proto_rawDescData)
 	})
-	return file_cert_proto_rawDescData
+	return file_cert_v1_proto_rawDescData
 }
 
-var file_cert_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_cert_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
-var file_cert_proto_goTypes = []interface{}{
+var file_cert_v1_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_cert_v1_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
+var file_cert_v1_proto_goTypes = []any{
 	(Curve)(0),                          // 0: cert.Curve
 	(*RawNebulaCertificate)(nil),        // 1: cert.RawNebulaCertificate
 	(*RawNebulaCertificateDetails)(nil), // 2: cert.RawNebulaCertificateDetails
@@ -519,7 +519,7 @@ var file_cert_proto_goTypes = []interface{}{
 	(*RawNebulaEncryptionMetadata)(nil), // 4: cert.RawNebulaEncryptionMetadata
 	(*RawNebulaArgon2Parameters)(nil),   // 5: cert.RawNebulaArgon2Parameters
 }
-var file_cert_proto_depIdxs = []int32{
+var file_cert_v1_proto_depIdxs = []int32{
 	2, // 0: cert.RawNebulaCertificate.Details:type_name -> cert.RawNebulaCertificateDetails
 	0, // 1: cert.RawNebulaCertificateDetails.curve:type_name -> cert.Curve
 	4, // 2: cert.RawNebulaEncryptedData.EncryptionMetadata:type_name -> cert.RawNebulaEncryptionMetadata
@@ -531,13 +531,13 @@ var file_cert_proto_depIdxs = []int32{
 	0, // [0:4] is the sub-list for field type_name
 }
 
-func init() { file_cert_proto_init() }
-func file_cert_proto_init() {
-	if File_cert_proto != nil {
+func init() { file_cert_v1_proto_init() }
+func file_cert_v1_proto_init() {
+	if File_cert_v1_proto != nil {
 		return
 	}
 	if !protoimpl.UnsafeEnabled {
-		file_cert_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+		file_cert_v1_proto_msgTypes[0].Exporter = func(v any, i int) any {
 			switch v := v.(*RawNebulaCertificate); i {
 			case 0:
 				return &v.state
@@ -549,7 +549,7 @@ func file_cert_proto_init() {
 				return nil
 			}
 		}
-		file_cert_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+		file_cert_v1_proto_msgTypes[1].Exporter = func(v any, i int) any {
 			switch v := v.(*RawNebulaCertificateDetails); i {
 			case 0:
 				return &v.state
@@ -561,7 +561,7 @@ func file_cert_proto_init() {
 				return nil
 			}
 		}
-		file_cert_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+		file_cert_v1_proto_msgTypes[2].Exporter = func(v any, i int) any {
 			switch v := v.(*RawNebulaEncryptedData); i {
 			case 0:
 				return &v.state
@@ -573,7 +573,7 @@ func file_cert_proto_init() {
 				return nil
 			}
 		}
-		file_cert_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+		file_cert_v1_proto_msgTypes[3].Exporter = func(v any, i int) any {
 			switch v := v.(*RawNebulaEncryptionMetadata); i {
 			case 0:
 				return &v.state
@@ -585,7 +585,7 @@ func file_cert_proto_init() {
 				return nil
 			}
 		}
-		file_cert_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+		file_cert_v1_proto_msgTypes[4].Exporter = func(v any, i int) any {
 			switch v := v.(*RawNebulaArgon2Parameters); i {
 			case 0:
 				return &v.state
@@ -602,19 +602,19 @@ func file_cert_proto_init() {
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: file_cert_proto_rawDesc,
+			RawDescriptor: file_cert_v1_proto_rawDesc,
 			NumEnums:      1,
 			NumMessages:   5,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
-		GoTypes:           file_cert_proto_goTypes,
-		DependencyIndexes: file_cert_proto_depIdxs,
-		EnumInfos:         file_cert_proto_enumTypes,
-		MessageInfos:      file_cert_proto_msgTypes,
+		GoTypes:           file_cert_v1_proto_goTypes,
+		DependencyIndexes: file_cert_v1_proto_depIdxs,
+		EnumInfos:         file_cert_v1_proto_enumTypes,
+		MessageInfos:      file_cert_v1_proto_msgTypes,
 	}.Build()
-	File_cert_proto = out.File
-	file_cert_proto_rawDesc = nil
-	file_cert_proto_goTypes = nil
-	file_cert_proto_depIdxs = nil
+	File_cert_v1_proto = out.File
+	file_cert_v1_proto_rawDesc = nil
+	file_cert_v1_proto_goTypes = nil
+	file_cert_v1_proto_depIdxs = nil
 }

+ 0 - 0
cert/cert.proto → cert/cert_v1.proto


+ 159 - 2
cert/crypto.go

@@ -3,14 +3,28 @@ package cert
 import (
 	"crypto/aes"
 	"crypto/cipher"
+	"crypto/ed25519"
 	"crypto/rand"
+	"encoding/pem"
 	"fmt"
 	"io"
+	"math"
 
 	"golang.org/x/crypto/argon2"
+	"google.golang.org/protobuf/proto"
 )
 
-// KDF factors
+type NebulaEncryptedData struct {
+	EncryptionMetadata NebulaEncryptionMetadata
+	Ciphertext         []byte
+}
+
+type NebulaEncryptionMetadata struct {
+	EncryptionAlgorithm string
+	Argon2Parameters    Argon2Parameters
+}
+
+// Argon2Parameters KDF factors
 type Argon2Parameters struct {
 	version     rune
 	Memory      uint32 // KiB
@@ -19,7 +33,7 @@ type Argon2Parameters struct {
 	salt        []byte
 }
 
-// Returns a new Argon2Parameters object with current version set
+// NewArgon2Parameters Returns a new Argon2Parameters object with current version set
 func NewArgon2Parameters(memory uint32, parallelism uint8, iterations uint32) *Argon2Parameters {
 	return &Argon2Parameters{
 		version:     argon2.Version,
@@ -141,3 +155,146 @@ func splitNonceCiphertext(blob []byte, nonceSize int) ([]byte, []byte, error) {
 
 	return blob[:nonceSize], blob[nonceSize:], nil
 }
+
+// EncryptAndMarshalSigningPrivateKey is a simple helper to encrypt and PEM encode a private key
+func EncryptAndMarshalSigningPrivateKey(curve Curve, b []byte, passphrase []byte, kdfParams *Argon2Parameters) ([]byte, error) {
+	ciphertext, err := aes256Encrypt(passphrase, kdfParams, b)
+	if err != nil {
+		return nil, err
+	}
+
+	b, err = proto.Marshal(&RawNebulaEncryptedData{
+		EncryptionMetadata: &RawNebulaEncryptionMetadata{
+			EncryptionAlgorithm: "AES-256-GCM",
+			Argon2Parameters: &RawNebulaArgon2Parameters{
+				Version:     kdfParams.version,
+				Memory:      kdfParams.Memory,
+				Parallelism: uint32(kdfParams.Parallelism),
+				Iterations:  kdfParams.Iterations,
+				Salt:        kdfParams.salt,
+			},
+		},
+		Ciphertext: ciphertext,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	switch curve {
+	case Curve_CURVE25519:
+		return pem.EncodeToMemory(&pem.Block{Type: EncryptedEd25519PrivateKeyBanner, Bytes: b}), nil
+	case Curve_P256:
+		return pem.EncodeToMemory(&pem.Block{Type: EncryptedECDSAP256PrivateKeyBanner, Bytes: b}), nil
+	default:
+		return nil, fmt.Errorf("invalid curve: %v", curve)
+	}
+}
+
+// UnmarshalNebulaEncryptedData will unmarshal a protobuf byte representation of a nebula cert into its
+// protobuf-generated struct.
+func UnmarshalNebulaEncryptedData(b []byte) (*NebulaEncryptedData, error) {
+	if len(b) == 0 {
+		return nil, fmt.Errorf("nil byte array")
+	}
+	var rned RawNebulaEncryptedData
+	err := proto.Unmarshal(b, &rned)
+	if err != nil {
+		return nil, err
+	}
+
+	if rned.EncryptionMetadata == nil {
+		return nil, fmt.Errorf("encoded EncryptionMetadata was nil")
+	}
+
+	if rned.EncryptionMetadata.Argon2Parameters == nil {
+		return nil, fmt.Errorf("encoded Argon2Parameters was nil")
+	}
+
+	params, err := unmarshalArgon2Parameters(rned.EncryptionMetadata.Argon2Parameters)
+	if err != nil {
+		return nil, err
+	}
+
+	ned := NebulaEncryptedData{
+		EncryptionMetadata: NebulaEncryptionMetadata{
+			EncryptionAlgorithm: rned.EncryptionMetadata.EncryptionAlgorithm,
+			Argon2Parameters:    *params,
+		},
+		Ciphertext: rned.Ciphertext,
+	}
+
+	return &ned, nil
+}
+
+func unmarshalArgon2Parameters(params *RawNebulaArgon2Parameters) (*Argon2Parameters, error) {
+	if params.Version < math.MinInt32 || params.Version > math.MaxInt32 {
+		return nil, fmt.Errorf("Argon2Parameters Version must be at least %d and no more than %d", math.MinInt32, math.MaxInt32)
+	}
+	if params.Memory <= 0 || params.Memory > math.MaxUint32 {
+		return nil, fmt.Errorf("Argon2Parameters Memory must be be greater than 0 and no more than %d KiB", uint32(math.MaxUint32))
+	}
+	if params.Parallelism <= 0 || params.Parallelism > math.MaxUint8 {
+		return nil, fmt.Errorf("Argon2Parameters Parallelism must be be greater than 0 and no more than %d", math.MaxUint8)
+	}
+	if params.Iterations <= 0 || params.Iterations > math.MaxUint32 {
+		return nil, fmt.Errorf("-argon-iterations must be be greater than 0 and no more than %d", uint32(math.MaxUint32))
+	}
+
+	return &Argon2Parameters{
+		version:     params.Version,
+		Memory:      params.Memory,
+		Parallelism: uint8(params.Parallelism),
+		Iterations:  params.Iterations,
+		salt:        params.Salt,
+	}, nil
+
+}
+
+// DecryptAndUnmarshalSigningPrivateKey will try to pem decode and decrypt an Ed25519/ECDSA private key with
+// the given passphrase, returning any other bytes b or an error on failure
+func DecryptAndUnmarshalSigningPrivateKey(passphrase, b []byte) (Curve, []byte, []byte, error) {
+	var curve Curve
+
+	k, r := pem.Decode(b)
+	if k == nil {
+		return curve, nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
+	}
+
+	switch k.Type {
+	case EncryptedEd25519PrivateKeyBanner:
+		curve = Curve_CURVE25519
+	case EncryptedECDSAP256PrivateKeyBanner:
+		curve = Curve_P256
+	default:
+		return curve, nil, r, fmt.Errorf("bytes did not contain a proper nebula encrypted Ed25519/ECDSA private key banner")
+	}
+
+	ned, err := UnmarshalNebulaEncryptedData(k.Bytes)
+	if err != nil {
+		return curve, nil, r, err
+	}
+
+	var bytes []byte
+	switch ned.EncryptionMetadata.EncryptionAlgorithm {
+	case "AES-256-GCM":
+		bytes, err = aes256Decrypt(passphrase, &ned.EncryptionMetadata.Argon2Parameters, ned.Ciphertext)
+		if err != nil {
+			return curve, nil, r, err
+		}
+	default:
+		return curve, nil, r, fmt.Errorf("unsupported encryption algorithm: %s", ned.EncryptionMetadata.EncryptionAlgorithm)
+	}
+
+	switch curve {
+	case Curve_CURVE25519:
+		if len(bytes) != ed25519.PrivateKeySize {
+			return curve, nil, r, fmt.Errorf("key was not %d bytes, is invalid ed25519 private key", ed25519.PrivateKeySize)
+		}
+	case Curve_P256:
+		if len(bytes) != 32 {
+			return curve, nil, r, fmt.Errorf("key was not 32 bytes, is invalid ECDSA P256 private key")
+		}
+	}
+
+	return curve, bytes, r, nil
+}

+ 87 - 0
cert/crypto_test.go

@@ -23,3 +23,90 @@ func TestNewArgon2Parameters(t *testing.T) {
 		Iterations:  1,
 	}, p)
 }
+
+func TestDecryptAndUnmarshalSigningPrivateKey(t *testing.T) {
+	passphrase := []byte("DO NOT USE THIS KEY")
+	privKey := []byte(`# A good key
+-----BEGIN NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
+CjwKC0FFUy0yNTYtR0NNEi0IExCAgIABGAEgBCognnjujd67Vsv99p22wfAjQaDT
+oCMW1mdjkU3gACKNW4MSXOWR9Sts4C81yk1RUku2gvGKs3TB9LYoklLsIizSYOLl
++Vs//O1T0I1Xbml2XBAROsb/VSoDln/6LMqR4B6fn6B3GOsLBBqRI8daDl9lRMPB
+qrlJ69wer3ZUHFXA
+-----END NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
+`)
+	shortKey := []byte(`# A key which, once decrypted, is too short
+-----BEGIN NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
+CjwKC0FFUy0yNTYtR0NNEi0IExCAgIABGAEgBCoga5h8owMEBWRSMMJKzuUvWce7
+k0qlBkQmCxiuLh80MuASW70YcKt8jeEIS2axo2V6zAKA9TSMcCsJW1kDDXEtL/xe
+GLF5T7sDl5COp4LU3pGxpV+KoeQ/S3gQCAAcnaOtnJQX+aSDnbO3jCHyP7U9CHbs
+rQr3bdH3Oy/WiYU=
+-----END NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
+`)
+	invalidBanner := []byte(`# Invalid banner (not encrypted)
+-----BEGIN NEBULA ED25519 PRIVATE KEY-----
+bWRp2CTVFhW9HD/qCd28ltDgK3w8VXSeaEYczDWos8sMUBqDb9jP3+NYwcS4lURG
+XgLvodMXZJuaFPssp+WwtA==
+-----END NEBULA ED25519 PRIVATE KEY-----
+`)
+	invalidPem := []byte(`# Not a valid PEM format
+-BEGIN NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
+CjwKC0FFUy0yNTYtR0NNEi0IExCAgIABGAEgBCognnjujd67Vsv99p22wfAjQaDT
+oCMW1mdjkU3gACKNW4MSXOWR9Sts4C81yk1RUku2gvGKs3TB9LYoklLsIizSYOLl
++Vs//O1T0I1Xbml2XBAROsb/VSoDln/6LMqR4B6fn6B3GOsLBBqRI8daDl9lRMPB
+qrlJ69wer3ZUHFXA
+-END NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
+`)
+
+	keyBundle := appendByteSlices(privKey, shortKey, invalidBanner, invalidPem)
+
+	// Success test case
+	curve, k, rest, err := DecryptAndUnmarshalSigningPrivateKey(passphrase, keyBundle)
+	assert.Nil(t, err)
+	assert.Equal(t, Curve_CURVE25519, curve)
+	assert.Len(t, k, 64)
+	assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
+
+	// Fail due to short key
+	curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey(passphrase, rest)
+	assert.EqualError(t, err, "key was not 64 bytes, is invalid ed25519 private key")
+	assert.Nil(t, k)
+	assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
+
+	// Fail due to invalid banner
+	curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey(passphrase, rest)
+	assert.EqualError(t, err, "bytes did not contain a proper nebula encrypted Ed25519/ECDSA private key banner")
+	assert.Nil(t, k)
+	assert.Equal(t, rest, invalidPem)
+
+	// Fail due to ivalid PEM format, because
+	// it's missing the requisite pre-encapsulation boundary.
+	curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey(passphrase, rest)
+	assert.EqualError(t, err, "input did not contain a valid PEM encoded block")
+	assert.Nil(t, k)
+	assert.Equal(t, rest, invalidPem)
+
+	// Fail due to invalid passphrase
+	curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey([]byte("invalid passphrase"), privKey)
+	assert.EqualError(t, err, "invalid passphrase or corrupt private key")
+	assert.Nil(t, k)
+	assert.Equal(t, rest, []byte{})
+}
+
+func TestEncryptAndMarshalSigningPrivateKey(t *testing.T) {
+	// Having proved that decryption works correctly above, we can test the
+	// encryption function produces a value which can be decrypted
+	passphrase := []byte("passphrase")
+	bytes := []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
+	kdfParams := NewArgon2Parameters(64*1024, 4, 3)
+	key, err := EncryptAndMarshalSigningPrivateKey(Curve_CURVE25519, bytes, passphrase, kdfParams)
+	assert.Nil(t, err)
+
+	// Verify the "key" can be decrypted successfully
+	curve, k, rest, err := DecryptAndUnmarshalSigningPrivateKey(passphrase, key)
+	assert.Len(t, k, 64)
+	assert.Equal(t, Curve_CURVE25519, curve)
+	assert.Equal(t, rest, []byte{})
+	assert.Nil(t, err)
+
+	// EncryptAndMarshalEd25519PrivateKey does not create any errors itself
+}

+ 19 - 6
cert/errors.go

@@ -5,10 +5,23 @@ import (
 )
 
 var (
-	ErrRootExpired       = errors.New("root certificate is expired")
-	ErrExpired           = errors.New("certificate is expired")
-	ErrNotCA             = errors.New("certificate is not a CA")
-	ErrNotSelfSigned     = errors.New("certificate is not self-signed")
-	ErrBlockListed       = errors.New("certificate is in the block list")
-	ErrSignatureMismatch = errors.New("certificate signature did not match")
+	ErrBadFormat               = errors.New("bad wire format")
+	ErrRootExpired             = errors.New("root certificate is expired")
+	ErrExpired                 = errors.New("certificate is expired")
+	ErrNotCA                   = errors.New("certificate is not a CA")
+	ErrNotSelfSigned           = errors.New("certificate is not self-signed")
+	ErrBlockListed             = errors.New("certificate is in the block list")
+	ErrFingerprintMismatch     = errors.New("certificate fingerprint did not match")
+	ErrSignatureMismatch       = errors.New("certificate signature did not match")
+	ErrInvalidPublicKeyLength  = errors.New("invalid public key length")
+	ErrInvalidPrivateKeyLength = errors.New("invalid private key length")
+
+	ErrPrivateKeyEncrypted = errors.New("private key must be decrypted")
+
+	ErrInvalidPEMBlock                   = errors.New("input did not contain a valid PEM encoded block")
+	ErrInvalidPEMCertificateBanner       = errors.New("bytes did not contain a proper certificate banner")
+	ErrInvalidPEMX25519PublicKeyBanner   = errors.New("bytes did not contain a proper X25519 public key banner")
+	ErrInvalidPEMX25519PrivateKeyBanner  = errors.New("bytes did not contain a proper X25519 private key banner")
+	ErrInvalidPEMEd25519PublicKeyBanner  = errors.New("bytes did not contain a proper Ed25519 public key banner")
+	ErrInvalidPEMEd25519PrivateKeyBanner = errors.New("bytes did not contain a proper Ed25519 private key banner")
 )

+ 155 - 0
cert/pem.go

@@ -0,0 +1,155 @@
+package cert
+
+import (
+	"encoding/pem"
+	"fmt"
+
+	"golang.org/x/crypto/ed25519"
+)
+
+const (
+	CertificateBanner                = "NEBULA CERTIFICATE"
+	CertificateV2Banner              = "NEBULA CERTIFICATE V2"
+	X25519PrivateKeyBanner           = "NEBULA X25519 PRIVATE KEY"
+	X25519PublicKeyBanner            = "NEBULA X25519 PUBLIC KEY"
+	EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY"
+	Ed25519PrivateKeyBanner          = "NEBULA ED25519 PRIVATE KEY"
+	Ed25519PublicKeyBanner           = "NEBULA ED25519 PUBLIC KEY"
+
+	P256PrivateKeyBanner               = "NEBULA P256 PRIVATE KEY"
+	P256PublicKeyBanner                = "NEBULA P256 PUBLIC KEY"
+	EncryptedECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 ENCRYPTED PRIVATE KEY"
+	ECDSAP256PrivateKeyBanner          = "NEBULA ECDSA P256 PRIVATE KEY"
+)
+
+// UnmarshalCertificateFromPEM will try to unmarshal the first pem block in a byte array, returning any non consumed
+// data or an error on failure
+func UnmarshalCertificateFromPEM(b []byte) (Certificate, []byte, error) {
+	p, r := pem.Decode(b)
+	if p == nil {
+		return nil, r, ErrInvalidPEMBlock
+	}
+
+	switch p.Type {
+	case CertificateBanner:
+		c, err := unmarshalCertificateV1(p.Bytes, true)
+		if err != nil {
+			return nil, nil, err
+		}
+		return c, r, nil
+	case CertificateV2Banner:
+		//TODO
+		panic("TODO")
+	default:
+		return nil, r, ErrInvalidPEMCertificateBanner
+	}
+}
+
+func MarshalPublicKeyToPEM(curve Curve, b []byte) []byte {
+	switch curve {
+	case Curve_CURVE25519:
+		return pem.EncodeToMemory(&pem.Block{Type: X25519PublicKeyBanner, Bytes: b})
+	case Curve_P256:
+		return pem.EncodeToMemory(&pem.Block{Type: P256PublicKeyBanner, Bytes: b})
+	default:
+		return nil
+	}
+}
+
+func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
+	k, r := pem.Decode(b)
+	if k == nil {
+		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
+	}
+	var expectedLen int
+	var curve Curve
+	switch k.Type {
+	case X25519PublicKeyBanner, Ed25519PublicKeyBanner:
+		expectedLen = 32
+		curve = Curve_CURVE25519
+	case P256PublicKeyBanner:
+		// Uncompressed
+		expectedLen = 65
+		curve = Curve_P256
+	default:
+		return nil, r, 0, fmt.Errorf("bytes did not contain a proper public key banner")
+	}
+	if len(k.Bytes) != expectedLen {
+		return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s public key", expectedLen, curve)
+	}
+	return k.Bytes, r, curve, nil
+}
+
+func MarshalPrivateKeyToPEM(curve Curve, b []byte) []byte {
+	switch curve {
+	case Curve_CURVE25519:
+		return pem.EncodeToMemory(&pem.Block{Type: X25519PrivateKeyBanner, Bytes: b})
+	case Curve_P256:
+		return pem.EncodeToMemory(&pem.Block{Type: P256PrivateKeyBanner, Bytes: b})
+	default:
+		return nil
+	}
+}
+
+func MarshalSigningPrivateKeyToPEM(curve Curve, b []byte) []byte {
+	switch curve {
+	case Curve_CURVE25519:
+		return pem.EncodeToMemory(&pem.Block{Type: Ed25519PrivateKeyBanner, Bytes: b})
+	case Curve_P256:
+		return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256PrivateKeyBanner, Bytes: b})
+	default:
+		return nil
+	}
+}
+
+// UnmarshalPrivateKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
+// consumed data or an error on failure
+func UnmarshalPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
+	k, r := pem.Decode(b)
+	if k == nil {
+		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
+	}
+	var expectedLen int
+	var curve Curve
+	switch k.Type {
+	case X25519PrivateKeyBanner:
+		expectedLen = 32
+		curve = Curve_CURVE25519
+	case P256PrivateKeyBanner:
+		expectedLen = 32
+		curve = Curve_P256
+	default:
+		return nil, r, 0, fmt.Errorf("bytes did not contain a proper private key banner")
+	}
+	if len(k.Bytes) != expectedLen {
+		return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s private key", expectedLen, curve)
+	}
+	return k.Bytes, r, curve, nil
+}
+
+func UnmarshalSigningPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
+	k, r := pem.Decode(b)
+	if k == nil {
+		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
+	}
+	var curve Curve
+	switch k.Type {
+	case EncryptedEd25519PrivateKeyBanner:
+		return nil, nil, Curve_CURVE25519, ErrPrivateKeyEncrypted
+	case EncryptedECDSAP256PrivateKeyBanner:
+		return nil, nil, Curve_P256, ErrPrivateKeyEncrypted
+	case Ed25519PrivateKeyBanner:
+		curve = Curve_CURVE25519
+		if len(k.Bytes) != ed25519.PrivateKeySize {
+			return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid Ed25519 private key", ed25519.PrivateKeySize)
+		}
+	case ECDSAP256PrivateKeyBanner:
+		curve = Curve_P256
+		if len(k.Bytes) != 32 {
+			return nil, r, 0, fmt.Errorf("key was not 32 bytes, is invalid ECDSA P256 private key")
+		}
+	default:
+		return nil, r, 0, fmt.Errorf("bytes did not contain a proper Ed25519/ECDSA private key banner")
+	}
+	return k.Bytes, r, curve, nil
+}

+ 292 - 0
cert/pem_test.go

@@ -0,0 +1,292 @@
+package cert
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestUnmarshalCertificateFromPEM(t *testing.T) {
+	goodCert := []byte(`
+# A good cert
+-----BEGIN NEBULA CERTIFICATE-----
+CkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL
+vcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv
+bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
+-----END NEBULA CERTIFICATE-----
+`)
+	badBanner := []byte(`# A bad banner
+-----BEGIN NOT A NEBULA CERTIFICATE-----
+CkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL
+vcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv
+bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
+-----END NOT A NEBULA CERTIFICATE-----
+`)
+	invalidPem := []byte(`# Not a valid PEM format
+-BEGIN NEBULA CERTIFICATE-----
+CkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL
+vcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv
+bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
+-END NEBULA CERTIFICATE----`)
+
+	certBundle := appendByteSlices(goodCert, badBanner, invalidPem)
+
+	// Success test case
+	cert, rest, err := UnmarshalCertificateFromPEM(certBundle)
+	assert.NotNil(t, cert)
+	assert.Equal(t, rest, append(badBanner, invalidPem...))
+	assert.Nil(t, err)
+
+	// Fail due to invalid banner.
+	cert, rest, err = UnmarshalCertificateFromPEM(rest)
+	assert.Nil(t, cert)
+	assert.Equal(t, rest, invalidPem)
+	assert.EqualError(t, err, "bytes did not contain a proper certificate banner")
+
+	// Fail due to ivalid PEM format, because
+	// it's missing the requisite pre-encapsulation boundary.
+	cert, rest, err = UnmarshalCertificateFromPEM(rest)
+	assert.Nil(t, cert)
+	assert.Equal(t, rest, invalidPem)
+	assert.EqualError(t, err, "input did not contain a valid PEM encoded block")
+}
+
+func TestUnmarshalSigningPrivateKeyFromPEM(t *testing.T) {
+	privKey := []byte(`# A good key
+-----BEGIN NEBULA ED25519 PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
+-----END NEBULA ED25519 PRIVATE KEY-----
+`)
+	privP256Key := []byte(`# A good key
+-----BEGIN NEBULA ECDSA P256 PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-----END NEBULA ECDSA P256 PRIVATE KEY-----
+`)
+	shortKey := []byte(`# A short key
+-----BEGIN NEBULA ED25519 PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+-----END NEBULA ED25519 PRIVATE KEY-----
+`)
+	invalidBanner := []byte(`# Invalid banner
+-----BEGIN NOT A NEBULA PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
+-----END NOT A NEBULA PRIVATE KEY-----
+`)
+	invalidPem := []byte(`# Not a valid PEM format
+-BEGIN NEBULA ED25519 PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
+-END NEBULA ED25519 PRIVATE KEY-----`)
+
+	keyBundle := appendByteSlices(privKey, privP256Key, shortKey, invalidBanner, invalidPem)
+
+	// Success test case
+	k, rest, curve, err := UnmarshalSigningPrivateKeyFromPEM(keyBundle)
+	assert.Len(t, k, 64)
+	assert.Equal(t, rest, appendByteSlices(privP256Key, shortKey, invalidBanner, invalidPem))
+	assert.Equal(t, Curve_CURVE25519, curve)
+	assert.Nil(t, err)
+
+	// Success test case
+	k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
+	assert.Len(t, k, 32)
+	assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
+	assert.Equal(t, Curve_P256, curve)
+	assert.Nil(t, err)
+
+	// Fail due to short key
+	k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
+	assert.EqualError(t, err, "key was not 64 bytes, is invalid Ed25519 private key")
+
+	// Fail due to invalid banner
+	k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, rest, invalidPem)
+	assert.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA private key banner")
+
+	// Fail due to ivalid PEM format, because
+	// it's missing the requisite pre-encapsulation boundary.
+	k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, rest, invalidPem)
+	assert.EqualError(t, err, "input did not contain a valid PEM encoded block")
+}
+
+func TestUnmarshalPrivateKeyFromPEM(t *testing.T) {
+	privKey := []byte(`# A good key
+-----BEGIN NEBULA X25519 PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-----END NEBULA X25519 PRIVATE KEY-----
+`)
+	privP256Key := []byte(`# A good key
+-----BEGIN NEBULA P256 PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-----END NEBULA P256 PRIVATE KEY-----
+`)
+	shortKey := []byte(`# A short key
+-----BEGIN NEBULA X25519 PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
+-----END NEBULA X25519 PRIVATE KEY-----
+`)
+	invalidBanner := []byte(`# Invalid banner
+-----BEGIN NOT A NEBULA PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-----END NOT A NEBULA PRIVATE KEY-----
+`)
+	invalidPem := []byte(`# Not a valid PEM format
+-BEGIN NEBULA X25519 PRIVATE KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-END NEBULA X25519 PRIVATE KEY-----`)
+
+	keyBundle := appendByteSlices(privKey, privP256Key, shortKey, invalidBanner, invalidPem)
+
+	// Success test case
+	k, rest, curve, err := UnmarshalPrivateKeyFromPEM(keyBundle)
+	assert.Len(t, k, 32)
+	assert.Equal(t, rest, appendByteSlices(privP256Key, shortKey, invalidBanner, invalidPem))
+	assert.Equal(t, Curve_CURVE25519, curve)
+	assert.Nil(t, err)
+
+	// Success test case
+	k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
+	assert.Len(t, k, 32)
+	assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
+	assert.Equal(t, Curve_P256, curve)
+	assert.Nil(t, err)
+
+	// Fail due to short key
+	k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
+	assert.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 private key")
+
+	// Fail due to invalid banner
+	k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, rest, invalidPem)
+	assert.EqualError(t, err, "bytes did not contain a proper private key banner")
+
+	// Fail due to ivalid PEM format, because
+	// it's missing the requisite pre-encapsulation boundary.
+	k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, rest, invalidPem)
+	assert.EqualError(t, err, "input did not contain a valid PEM encoded block")
+}
+
+func TestUnmarshalPublicKeyFromPEM(t *testing.T) {
+	pubKey := []byte(`# A good key
+-----BEGIN NEBULA ED25519 PUBLIC KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-----END NEBULA ED25519 PUBLIC KEY-----
+`)
+	shortKey := []byte(`# A short key
+-----BEGIN NEBULA ED25519 PUBLIC KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
+-----END NEBULA ED25519 PUBLIC KEY-----
+`)
+	invalidBanner := []byte(`# Invalid banner
+-----BEGIN NOT A NEBULA PUBLIC KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-----END NOT A NEBULA PUBLIC KEY-----
+`)
+	invalidPem := []byte(`# Not a valid PEM format
+-BEGIN NEBULA ED25519 PUBLIC KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-END NEBULA ED25519 PUBLIC KEY-----`)
+
+	keyBundle := appendByteSlices(pubKey, shortKey, invalidBanner, invalidPem)
+
+	// Success test case
+	k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
+	assert.Equal(t, 32, len(k))
+	assert.Equal(t, Curve_CURVE25519, curve)
+	assert.Nil(t, err)
+	assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
+
+	// Fail due to short key
+	k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, Curve_CURVE25519, curve)
+	assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
+	assert.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
+
+	// Fail due to invalid banner
+	k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, Curve_CURVE25519, curve)
+	assert.EqualError(t, err, "bytes did not contain a proper public key banner")
+	assert.Equal(t, rest, invalidPem)
+
+	// Fail due to ivalid PEM format, because
+	// it's missing the requisite pre-encapsulation boundary.
+	k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, Curve_CURVE25519, curve)
+	assert.Equal(t, rest, invalidPem)
+	assert.EqualError(t, err, "input did not contain a valid PEM encoded block")
+}
+
+func TestUnmarshalX25519PublicKey(t *testing.T) {
+	pubKey := []byte(`# A good key
+-----BEGIN NEBULA X25519 PUBLIC KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-----END NEBULA X25519 PUBLIC KEY-----
+`)
+	pubP256Key := []byte(`# A good key
+-----BEGIN NEBULA P256 PUBLIC KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAA=
+-----END NEBULA P256 PUBLIC KEY-----
+`)
+	shortKey := []byte(`# A short key
+-----BEGIN NEBULA X25519 PUBLIC KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
+-----END NEBULA X25519 PUBLIC KEY-----
+`)
+	invalidBanner := []byte(`# Invalid banner
+-----BEGIN NOT A NEBULA PUBLIC KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-----END NOT A NEBULA PUBLIC KEY-----
+`)
+	invalidPem := []byte(`# Not a valid PEM format
+-BEGIN NEBULA X25519 PUBLIC KEY-----
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+-END NEBULA X25519 PUBLIC KEY-----`)
+
+	keyBundle := appendByteSlices(pubKey, pubP256Key, shortKey, invalidBanner, invalidPem)
+
+	// Success test case
+	k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
+	assert.Equal(t, 32, len(k))
+	assert.Nil(t, err)
+	assert.Equal(t, rest, appendByteSlices(pubP256Key, shortKey, invalidBanner, invalidPem))
+	assert.Equal(t, Curve_CURVE25519, curve)
+
+	// Success test case
+	k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
+	assert.Equal(t, 65, len(k))
+	assert.Nil(t, err)
+	assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
+	assert.Equal(t, Curve_P256, curve)
+
+	// Fail due to short key
+	k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
+	assert.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
+
+	// Fail due to invalid banner
+	k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.EqualError(t, err, "bytes did not contain a proper public key banner")
+	assert.Equal(t, rest, invalidPem)
+
+	// Fail due to ivalid PEM format, because
+	// it's missing the requisite pre-encapsulation boundary.
+	k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
+	assert.Nil(t, k)
+	assert.Equal(t, rest, invalidPem)
+	assert.EqualError(t, err, "input did not contain a valid PEM encoded block")
+}

+ 76 - 0
cert/sign.go

@@ -0,0 +1,76 @@
+package cert
+
+import (
+	"fmt"
+	"net/netip"
+	"time"
+
+	"github.com/slackhq/nebula/pkclient"
+)
+
+// TBSCertificate represents a certificate intended to be signed.
+// It is invalid to use this structure as a Certificate.
+type TBSCertificate struct {
+	Version        Version
+	Name           string
+	Networks       []netip.Prefix
+	UnsafeNetworks []netip.Prefix
+	Groups         []string
+	IsCA           bool
+	NotBefore      time.Time
+	NotAfter       time.Time
+	PublicKey      []byte
+	Curve          Curve
+	issuer         string
+}
+
+// Sign will create a sealed certificate using details provided by the TBSCertificate as long as those
+// details do not violate constraints of the signing certificate.
+// If the TBSCertificate is a CA then signer must be nil.
+func (t *TBSCertificate) Sign(signer Certificate, curve Curve, key []byte) (Certificate, error) {
+	return t.sign(signer, curve, key, nil)
+}
+
+func (t *TBSCertificate) SignPkcs11(signer Certificate, curve Curve, client *pkclient.PKClient) (Certificate, error) {
+	if curve != Curve_P256 {
+		return nil, fmt.Errorf("only P256 is supported by PKCS#11")
+	}
+
+	return t.sign(signer, curve, nil, client)
+}
+
+func (t *TBSCertificate) sign(signer Certificate, curve Curve, key []byte, client *pkclient.PKClient) (Certificate, error) {
+	if curve != t.Curve {
+		return nil, fmt.Errorf("curve in cert and private key supplied don't match")
+	}
+
+	//TODO: make sure we have all minimum properties to sign, like a public key
+
+	if signer != nil {
+		if t.IsCA {
+			return nil, fmt.Errorf("can not sign a CA certificate with another")
+		}
+
+		err := checkCAConstraints(signer, t.NotBefore, t.NotAfter, t.Groups, t.Networks, t.UnsafeNetworks)
+		if err != nil {
+			return nil, err
+		}
+
+		issuer, err := signer.Fingerprint()
+		if err != nil {
+			return nil, fmt.Errorf("error computing issuer: %v", err)
+		}
+		t.issuer = issuer
+	} else {
+		if !t.IsCA {
+			return nil, fmt.Errorf("self signed certificates must have IsCA set to true")
+		}
+	}
+
+	switch t.Version {
+	case Version1:
+		return signV1(t, curve, key, client)
+	default:
+		return nil, fmt.Errorf("unknown cert version %d", t.Version)
+	}
+}

+ 25 - 32
cmd/nebula-cert/ca.go

@@ -4,12 +4,11 @@ import (
 	"crypto/ecdsa"
 	"crypto/elliptic"
 	"crypto/rand"
-
 	"flag"
 	"fmt"
 	"io"
 	"math"
-	"net"
+	"net/netip"
 	"os"
 	"strings"
 	"time"
@@ -114,38 +113,36 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
 		}
 	}
 
-	var ips []*net.IPNet
+	var ips []netip.Prefix
 	if *cf.ips != "" {
 		for _, rs := range strings.Split(*cf.ips, ",") {
 			rs := strings.Trim(rs, " ")
 			if rs != "" {
-				ip, ipNet, err := net.ParseCIDR(rs)
+				n, err := netip.ParsePrefix(rs)
 				if err != nil {
 					return newHelpErrorf("invalid ip definition: %s", err)
 				}
-				if ip.To4() == nil {
+				if !n.Addr().Is4() {
 					return newHelpErrorf("invalid ip definition: can only be ipv4, have %s", rs)
 				}
-
-				ipNet.IP = ip
-				ips = append(ips, ipNet)
+				ips = append(ips, n)
 			}
 		}
 	}
 
-	var subnets []*net.IPNet
+	var subnets []netip.Prefix
 	if *cf.subnets != "" {
 		for _, rs := range strings.Split(*cf.subnets, ",") {
 			rs := strings.Trim(rs, " ")
 			if rs != "" {
-				_, s, err := net.ParseCIDR(rs)
+				n, err := netip.ParsePrefix(rs)
 				if err != nil {
 					return newHelpErrorf("invalid subnet definition: %s", err)
 				}
-				if s.IP.To4() == nil {
+				if !n.Addr().Is4() {
 					return newHelpErrorf("invalid subnet definition: can only be ipv4, have %s", rs)
 				}
-				subnets = append(subnets, s)
+				subnets = append(subnets, n)
 			}
 		}
 	}
@@ -224,19 +221,17 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
 		}
 	}
 
-	nc := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      *cf.name,
-			Groups:    groups,
-			Ips:       ips,
-			Subnets:   subnets,
-			NotBefore: time.Now(),
-			NotAfter:  time.Now().Add(*cf.duration),
-			PublicKey: pub,
-			IsCA:      true,
-			Curve:     curve,
-		},
-		Pkcs11Backed: isP11,
+	t := &cert.TBSCertificate{
+		Version:        cert.Version1,
+		Name:           *cf.name,
+		Groups:         groups,
+		Networks:       ips,
+		UnsafeNetworks: subnets,
+		NotBefore:      time.Now(),
+		NotAfter:       time.Now().Add(*cf.duration),
+		PublicKey:      pub,
+		IsCA:           true,
+		Curve:          curve,
 	}
 
 	if !isP11 {
@@ -249,15 +244,16 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
 		return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
 	}
 
+	var c cert.Certificate
 	var b []byte
 
 	if isP11 {
-		err = nc.SignPkcs11(curve, p11Client)
+		c, err = t.SignPkcs11(nil, curve, p11Client)
 		if err != nil {
 			return fmt.Errorf("error while signing with PKCS#11: %w", err)
 		}
 	} else {
-		err = nc.Sign(curve, rawPriv)
+		c, err = t.Sign(nil, curve, rawPriv)
 		if err != nil {
 			return fmt.Errorf("error while signing: %s", err)
 		}
@@ -268,19 +264,16 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
 				return fmt.Errorf("error while encrypting out-key: %s", err)
 			}
 		} else {
-			b = cert.MarshalSigningPrivateKey(curve, rawPriv)
+			b = cert.MarshalSigningPrivateKeyToPEM(curve, rawPriv)
 		}
 
 		err = os.WriteFile(*cf.outKeyPath, b, 0600)
 		if err != nil {
 			return fmt.Errorf("error while writing out-key: %s", err)
 		}
-		if _, err := os.Stat(*cf.outCertPath); err == nil {
-			return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
-		}
 	}
 
-	b, err = nc.MarshalToPEM()
+	b, err = c.MarshalPEM()
 	if err != nil {
 		return fmt.Errorf("error while marshalling certificate: %s", err)
 	}

+ 15 - 14
cmd/nebula-cert/ca_test.go

@@ -109,7 +109,7 @@ func Test_ca(t *testing.T) {
 	// create temp key file
 	keyF, err := os.CreateTemp("", "test.key")
 	assert.Nil(t, err)
-	os.Remove(keyF.Name())
+	assert.Nil(t, os.Remove(keyF.Name()))
 
 	// failed cert write
 	ob.Reset()
@@ -122,8 +122,8 @@ func Test_ca(t *testing.T) {
 	// create temp cert file
 	crtF, err := os.CreateTemp("", "test.crt")
 	assert.Nil(t, err)
-	os.Remove(crtF.Name())
-	os.Remove(keyF.Name())
+	assert.Nil(t, os.Remove(crtF.Name()))
+	assert.Nil(t, os.Remove(keyF.Name()))
 
 	// test proper cert with removed empty groups and subnets
 	ob.Reset()
@@ -135,25 +135,26 @@ func Test_ca(t *testing.T) {
 
 	// read cert and key files
 	rb, _ := os.ReadFile(keyF.Name())
-	lKey, b, err := cert.UnmarshalEd25519PrivateKey(rb)
+	lKey, b, c, err := cert.UnmarshalSigningPrivateKeyFromPEM(rb)
+	assert.Equal(t, cert.Curve_CURVE25519, c)
 	assert.Len(t, b, 0)
 	assert.Nil(t, err)
 	assert.Len(t, lKey, 64)
 
 	rb, _ = os.ReadFile(crtF.Name())
-	lCrt, b, err := cert.UnmarshalNebulaCertificateFromPEM(rb)
+	lCrt, b, err := cert.UnmarshalCertificateFromPEM(rb)
 	assert.Len(t, b, 0)
 	assert.Nil(t, err)
 
-	assert.Equal(t, "test", lCrt.Details.Name)
-	assert.Len(t, lCrt.Details.Ips, 0)
-	assert.True(t, lCrt.Details.IsCA)
-	assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Details.Groups)
-	assert.Len(t, lCrt.Details.Subnets, 0)
-	assert.Len(t, lCrt.Details.PublicKey, 32)
-	assert.Equal(t, time.Duration(time.Minute*100), lCrt.Details.NotAfter.Sub(lCrt.Details.NotBefore))
-	assert.Equal(t, "", lCrt.Details.Issuer)
-	assert.True(t, lCrt.CheckSignature(lCrt.Details.PublicKey))
+	assert.Equal(t, "test", lCrt.Name())
+	assert.Len(t, lCrt.Networks(), 0)
+	assert.True(t, lCrt.IsCA())
+	assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Groups())
+	assert.Len(t, lCrt.UnsafeNetworks(), 0)
+	assert.Len(t, lCrt.PublicKey(), 32)
+	assert.Equal(t, time.Duration(time.Minute*100), lCrt.NotAfter().Sub(lCrt.NotBefore()))
+	assert.Equal(t, "", lCrt.Issuer())
+	assert.True(t, lCrt.CheckSignature(lCrt.PublicKey()))
 
 	// test encrypted key
 	os.Remove(keyF.Name())

+ 2 - 2
cmd/nebula-cert/keygen.go

@@ -82,12 +82,12 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
 			return fmt.Errorf("error while getting public key: %w", err)
 		}
 	} else {
-		err = os.WriteFile(*cf.outKeyPath, cert.MarshalPrivateKey(curve, rawPriv), 0600)
+		err = os.WriteFile(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600)
 		if err != nil {
 			return fmt.Errorf("error while writing out-key: %s", err)
 		}
 	}
-	err = os.WriteFile(*cf.outPubPath, cert.MarshalPublicKey(curve, pub), 0600)
+	err = os.WriteFile(*cf.outPubPath, cert.MarshalPublicKeyToPEM(curve, pub), 0600)
 	if err != nil {
 		return fmt.Errorf("error while writing out-pub: %s", err)
 	}

+ 4 - 2
cmd/nebula-cert/keygen_test.go

@@ -81,13 +81,15 @@ func Test_keygen(t *testing.T) {
 
 	// read cert and key files
 	rb, _ := os.ReadFile(keyF.Name())
-	lKey, b, err := cert.UnmarshalX25519PrivateKey(rb)
+	lKey, b, curve, err := cert.UnmarshalPrivateKeyFromPEM(rb)
+	assert.Equal(t, cert.Curve_CURVE25519, curve)
 	assert.Len(t, b, 0)
 	assert.Nil(t, err)
 	assert.Len(t, lKey, 32)
 
 	rb, _ = os.ReadFile(pubF.Name())
-	lPub, b, err := cert.UnmarshalX25519PublicKey(rb)
+	lPub, b, curve, err := cert.UnmarshalPublicKeyFromPEM(rb)
+	assert.Equal(t, cert.Curve_CURVE25519, curve)
 	assert.Len(t, b, 0)
 	assert.Nil(t, err)
 	assert.Len(t, lPub, 32)

+ 3 - 3
cmd/nebula-cert/print.go

@@ -45,12 +45,12 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
 		return fmt.Errorf("unable to read cert; %s", err)
 	}
 
-	var c *cert.NebulaCertificate
+	var c cert.Certificate
 	var qrBytes []byte
 	part := 0
 
 	for {
-		c, rawCert, err = cert.UnmarshalNebulaCertificateFromPEM(rawCert)
+		c, rawCert, err = cert.UnmarshalCertificateFromPEM(rawCert)
 		if err != nil {
 			return fmt.Errorf("error while unmarshaling cert: %s", err)
 		}
@@ -66,7 +66,7 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
 		}
 
 		if *pf.outQRPath != "" {
-			b, err := c.MarshalToPEM()
+			b, err := c.MarshalPEM()
 			if err != nil {
 				return fmt.Errorf("error while marshalling cert to PEM: %s", err)
 			}

+ 75 - 21
cmd/nebula-cert/print_test.go

@@ -2,6 +2,10 @@ package main
 
 import (
 	"bytes"
+	"crypto/ed25519"
+	"crypto/rand"
+	"encoding/hex"
+	"net/netip"
 	"os"
 	"testing"
 	"time"
@@ -68,25 +72,22 @@ func Test_printCert(t *testing.T) {
 	eb.Reset()
 	tf.Truncate(0)
 	tf.Seek(0, 0)
-	c := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      "test",
-			Groups:    []string{"hi"},
-			PublicKey: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2},
-		},
-		Signature: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2},
-	}
+	ca, caKey := NewTestCaCert("test ca", nil, nil, time.Time{}, time.Time{}, nil, nil, nil)
+	c, _ := NewTestCert(ca, caKey, "test", time.Time{}, time.Time{}, nil, nil, []string{"hi"})
 
-	p, _ := c.MarshalToPEM()
+	p, _ := c.MarshalPEM()
 	tf.Write(p)
 	tf.Write(p)
 	tf.Write(p)
 
 	err = printCert([]string{"-path", tf.Name()}, ob, eb)
+	fp, _ := c.Fingerprint()
+	pk := hex.EncodeToString(c.PublicKey())
+	sig := hex.EncodeToString(c.Signature())
 	assert.Nil(t, err)
 	assert.Equal(
 		t,
-		"NebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: \n\t\tPublic key: 0102030405060708090001020304050607080900010203040506070809000102\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\n\tSignature: 0102030405060708090001020304050607080900010203040506070809000102\n}\nNebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: \n\t\tPublic key: 0102030405060708090001020304050607080900010203040506070809000102\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\n\tSignature: 0102030405060708090001020304050607080900010203040506070809000102\n}\nNebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: \n\t\tPublic key: 0102030405060708090001020304050607080900010203040506070809000102\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\n\tSignature: 0102030405060708090001020304050607080900010203040506070809000102\n}\n",
+		"NebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: "+c.Issuer()+"\n\t\tPublic key: "+pk+"\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: "+fp+"\n\tSignature: "+sig+"\n}\nNebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: "+c.Issuer()+"\n\t\tPublic key: "+pk+"\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: "+fp+"\n\tSignature: "+sig+"\n}\nNebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: "+c.Issuer()+"\n\t\tPublic key: "+pk+"\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: "+fp+"\n\tSignature: "+sig+"\n}\n",
 		ob.String(),
 	)
 	assert.Equal(t, "", eb.String())
@@ -96,26 +97,79 @@ func Test_printCert(t *testing.T) {
 	eb.Reset()
 	tf.Truncate(0)
 	tf.Seek(0, 0)
-	c = cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      "test",
-			Groups:    []string{"hi"},
-			PublicKey: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2},
-		},
-		Signature: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2},
-	}
-
-	p, _ = c.MarshalToPEM()
 	tf.Write(p)
 	tf.Write(p)
 	tf.Write(p)
 
 	err = printCert([]string{"-json", "-path", tf.Name()}, ob, eb)
+	fp, _ = c.Fingerprint()
+	pk = hex.EncodeToString(c.PublicKey())
+	sig = hex.EncodeToString(c.Signature())
 	assert.Nil(t, err)
 	assert.Equal(
 		t,
-		"{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"hi\"],\"ips\":[],\"isCa\":false,\"issuer\":\"\",\"name\":\"test\",\"notAfter\":\"0001-01-01T00:00:00Z\",\"notBefore\":\"0001-01-01T00:00:00Z\",\"publicKey\":\"0102030405060708090001020304050607080900010203040506070809000102\",\"subnets\":[]},\"fingerprint\":\"cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\",\"signature\":\"0102030405060708090001020304050607080900010203040506070809000102\"}\n{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"hi\"],\"ips\":[],\"isCa\":false,\"issuer\":\"\",\"name\":\"test\",\"notAfter\":\"0001-01-01T00:00:00Z\",\"notBefore\":\"0001-01-01T00:00:00Z\",\"publicKey\":\"0102030405060708090001020304050607080900010203040506070809000102\",\"subnets\":[]},\"fingerprint\":\"cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\",\"signature\":\"0102030405060708090001020304050607080900010203040506070809000102\"}\n{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"hi\"],\"ips\":[],\"isCa\":false,\"issuer\":\"\",\"name\":\"test\",\"notAfter\":\"0001-01-01T00:00:00Z\",\"notBefore\":\"0001-01-01T00:00:00Z\",\"publicKey\":\"0102030405060708090001020304050607080900010203040506070809000102\",\"subnets\":[]},\"fingerprint\":\"cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\",\"signature\":\"0102030405060708090001020304050607080900010203040506070809000102\"}\n",
+		"{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"hi\"],\"ips\":[],\"isCa\":false,\"issuer\":\""+c.Issuer()+"\",\"name\":\"test\",\"notAfter\":\"0001-01-01T00:00:00Z\",\"notBefore\":\"0001-01-01T00:00:00Z\",\"publicKey\":\""+pk+"\",\"subnets\":[]},\"fingerprint\":\""+fp+"\",\"signature\":\""+sig+"\"}\n{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"hi\"],\"ips\":[],\"isCa\":false,\"issuer\":\""+c.Issuer()+"\",\"name\":\"test\",\"notAfter\":\"0001-01-01T00:00:00Z\",\"notBefore\":\"0001-01-01T00:00:00Z\",\"publicKey\":\""+pk+"\",\"subnets\":[]},\"fingerprint\":\""+fp+"\",\"signature\":\""+sig+"\"}\n{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"hi\"],\"ips\":[],\"isCa\":false,\"issuer\":\""+c.Issuer()+"\",\"name\":\"test\",\"notAfter\":\"0001-01-01T00:00:00Z\",\"notBefore\":\"0001-01-01T00:00:00Z\",\"publicKey\":\""+pk+"\",\"subnets\":[]},\"fingerprint\":\""+fp+"\",\"signature\":\""+sig+"\"}\n",
 		ob.String(),
 	)
 	assert.Equal(t, "", eb.String())
 }
+
+// NewTestCaCert will generate a CA cert
+func NewTestCaCert(name string, pubKey, privKey []byte, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte) {
+	var err error
+	if pubKey == nil || privKey == nil {
+		pubKey, privKey, err = ed25519.GenerateKey(rand.Reader)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	t := &cert.TBSCertificate{
+		Version:        cert.Version1,
+		Name:           name,
+		NotBefore:      time.Unix(before.Unix(), 0),
+		NotAfter:       time.Unix(after.Unix(), 0),
+		PublicKey:      pubKey,
+		Networks:       networks,
+		UnsafeNetworks: unsafeNetworks,
+		Groups:         groups,
+		IsCA:           true,
+	}
+
+	c, err := t.Sign(nil, cert.Curve_CURVE25519, privKey)
+	if err != nil {
+		panic(err)
+	}
+
+	return c, privKey
+}
+
+func NewTestCert(ca cert.Certificate, signerKey []byte, name string, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte) {
+	if before.IsZero() {
+		before = ca.NotBefore()
+	}
+
+	if after.IsZero() {
+		after = ca.NotAfter()
+	}
+
+	pub, rawPriv := x25519Keypair()
+	nc := &cert.TBSCertificate{
+		Version:        cert.Version1,
+		Name:           name,
+		Networks:       networks,
+		UnsafeNetworks: unsafeNetworks,
+		Groups:         groups,
+		NotBefore:      time.Unix(before.Unix(), 0),
+		NotAfter:       time.Unix(after.Unix(), 0),
+		PublicKey:      pub,
+		IsCA:           false,
+	}
+
+	c, err := nc.Sign(ca, ca.Curve(), signerKey)
+	if err != nil {
+		panic(err)
+	}
+
+	return c, rawPriv
+}

+ 43 - 51
cmd/nebula-cert/sign.go

@@ -6,7 +6,7 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"net"
+	"net/netip"
 	"os"
 	"strings"
 	"time"
@@ -80,15 +80,17 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
 
 	var curve cert.Curve
 	var caKey []byte
+
 	if !isP11 {
 		var rawCAKey []byte
 		rawCAKey, err := os.ReadFile(*sf.caKeyPath)
+
 		if err != nil {
 			return fmt.Errorf("error while reading ca-key: %s", err)
 		}
 
 		// naively attempt to decode the private key as though it is not encrypted
-		caKey, _, curve, err = cert.UnmarshalSigningPrivateKey(rawCAKey)
+		caKey, _, curve, err = cert.UnmarshalSigningPrivateKeyFromPEM(rawCAKey)
 		if err == cert.ErrPrivateKeyEncrypted {
 			// ask for a passphrase until we get one
 			var passphrase []byte
@@ -124,7 +126,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
 		return fmt.Errorf("error while reading ca-crt: %s", err)
 	}
 
-	caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM(rawCACert)
+	caCert, _, err := cert.UnmarshalCertificateFromPEM(rawCACert)
 	if err != nil {
 		return fmt.Errorf("error while parsing ca-crt: %s", err)
 	}
@@ -135,30 +137,24 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
 		}
 	}
 
-	issuer, err := caCert.Sha256Sum()
-	if err != nil {
-		return fmt.Errorf("error while getting -ca-crt fingerprint: %s", err)
-	}
-
 	if caCert.Expired(time.Now()) {
 		return fmt.Errorf("ca certificate is expired")
 	}
 
 	// if no duration is given, expire one second before the root expires
 	if *sf.duration <= 0 {
-		*sf.duration = time.Until(caCert.Details.NotAfter) - time.Second*1
+		*sf.duration = time.Until(caCert.NotAfter()) - time.Second*1
 	}
 
-	ip, ipNet, err := net.ParseCIDR(*sf.ip)
+	network, err := netip.ParsePrefix(*sf.ip)
 	if err != nil {
-		return newHelpErrorf("invalid ip definition: %s", err)
+		return newHelpErrorf("invalid ip definition: %s", *sf.ip)
 	}
-	if ip.To4() == nil {
+	if !network.Addr().Is4() {
 		return newHelpErrorf("invalid ip definition: can only be ipv4, have %s", *sf.ip)
 	}
-	ipNet.IP = ip
 
-	groups := []string{}
+	var groups []string
 	if *sf.groups != "" {
 		for _, rg := range strings.Split(*sf.groups, ",") {
 			g := strings.TrimSpace(rg)
@@ -168,16 +164,16 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
 		}
 	}
 
-	subnets := []*net.IPNet{}
+	var subnets []netip.Prefix
 	if *sf.subnets != "" {
 		for _, rs := range strings.Split(*sf.subnets, ",") {
 			rs := strings.Trim(rs, " ")
 			if rs != "" {
-				_, s, err := net.ParseCIDR(rs)
+				s, err := netip.ParsePrefix(rs)
 				if err != nil {
-					return newHelpErrorf("invalid subnet definition: %s", err)
+					return newHelpErrorf("invalid subnet definition: %s", rs)
 				}
-				if s.IP.To4() == nil {
+				if !s.Addr().Is4() {
 					return newHelpErrorf("invalid subnet definition: can only be ipv4, have %s", rs)
 				}
 				subnets = append(subnets, s)
@@ -205,7 +201,8 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
 		if err != nil {
 			return fmt.Errorf("error while reading in-pub: %s", err)
 		}
-		pub, _, pubCurve, err = cert.UnmarshalPublicKey(rawPub)
+
+		pub, _, pubCurve, err = cert.UnmarshalPublicKeyFromPEM(rawPub)
 		if err != nil {
 			return fmt.Errorf("error while parsing in-pub: %s", err)
 		}
@@ -221,36 +218,17 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
 		pub, rawPriv = newKeypair(curve)
 	}
 
-	nc := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      *sf.name,
-			Ips:       []*net.IPNet{ipNet},
-			Groups:    groups,
-			Subnets:   subnets,
-			NotBefore: time.Now(),
-			NotAfter:  time.Now().Add(*sf.duration),
-			PublicKey: pub,
-			IsCA:      false,
-			Issuer:    issuer,
-			Curve:     curve,
-		},
-		Pkcs11Backed: isP11,
-	}
-
-	if p11Client == nil {
-		err = nc.Sign(curve, caKey)
-		if err != nil {
-			return fmt.Errorf("error while signing: %w", err)
-		}
-	} else {
-		err = nc.SignPkcs11(curve, p11Client)
-		if err != nil {
-			return fmt.Errorf("error while signing with PKCS#11: %w", err)
-		}
-	}
-
-	if err := nc.CheckRootConstrains(caCert); err != nil {
-		return fmt.Errorf("refusing to sign, root certificate constraints violated: %s", err)
+	t := &cert.TBSCertificate{
+		Version:        cert.Version1,
+		Name:           *sf.name,
+		Networks:       []netip.Prefix{network},
+		Groups:         groups,
+		UnsafeNetworks: subnets,
+		NotBefore:      time.Now(),
+		NotAfter:       time.Now().Add(*sf.duration),
+		PublicKey:      pub,
+		IsCA:           false,
+		Curve:          curve,
 	}
 
 	if *sf.outKeyPath == "" {
@@ -265,18 +243,32 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
 		return fmt.Errorf("refusing to overwrite existing cert: %s", *sf.outCertPath)
 	}
 
+	var c cert.Certificate
+
+	if p11Client == nil {
+		c, err = t.Sign(caCert, curve, caKey)
+		if err != nil {
+			return fmt.Errorf("error while signing: %w", err)
+		}
+	} else {
+		c, err = t.SignPkcs11(caCert, curve, p11Client)
+		if err != nil {
+			return fmt.Errorf("error while signing with PKCS#11: %w", err)
+		}
+	}
+
 	if !isP11 && *sf.inPubPath == "" {
 		if _, err := os.Stat(*sf.outKeyPath); err == nil {
 			return fmt.Errorf("refusing to overwrite existing key: %s", *sf.outKeyPath)
 		}
 
-		err = os.WriteFile(*sf.outKeyPath, cert.MarshalPrivateKey(curve, rawPriv), 0600)
+		err = os.WriteFile(*sf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600)
 		if err != nil {
 			return fmt.Errorf("error while writing out-key: %s", err)
 		}
 	}
 
-	b, err := nc.MarshalToPEM()
+	b, err := c.MarshalPEM()
 	if err != nil {
 		return fmt.Errorf("error while marshalling certificate: %s", err)
 	}

+ 28 - 41
cmd/nebula-cert/sign_test.go

@@ -117,7 +117,7 @@ func Test_signCert(t *testing.T) {
 	ob.Reset()
 	eb.Reset()
 	caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
-	caKeyF.Write(cert.MarshalEd25519PrivateKey(caPriv))
+	caKeyF.Write(cert.MarshalSigningPrivateKeyToPEM(cert.Curve_CURVE25519, caPriv))
 
 	// failed to read cert
 	args = []string{"-ca-crt", "./nope", "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
@@ -138,16 +138,8 @@ func Test_signCert(t *testing.T) {
 	assert.Empty(t, eb.String())
 
 	// write a proper ca cert for later
-	ca := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      "ca",
-			NotBefore: time.Now(),
-			NotAfter:  time.Now().Add(time.Minute * 200),
-			PublicKey: caPub,
-			IsCA:      true,
-		},
-	}
-	b, _ := ca.MarshalToPEM()
+	ca, _ := NewTestCaCert("ca", caPub, caPriv, time.Now(), time.Now().Add(time.Minute*200), nil, nil, nil)
+	b, _ := ca.MarshalPEM()
 	caCrtF.Write(b)
 
 	// failed to read pub
@@ -172,13 +164,13 @@ func Test_signCert(t *testing.T) {
 	ob.Reset()
 	eb.Reset()
 	inPub, _ := x25519Keypair()
-	inPubF.Write(cert.MarshalX25519PublicKey(inPub))
+	inPubF.Write(cert.MarshalPublicKeyToPEM(cert.Curve_CURVE25519, inPub))
 
 	// bad ip cidr
 	ob.Reset()
 	eb.Reset()
 	args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "a1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
-	assertHelpError(t, signCert(args, ob, eb, nopw), "invalid ip definition: invalid CIDR address: a1.1.1.1/24")
+	assertHelpError(t, signCert(args, ob, eb, nopw), "invalid ip definition: a1.1.1.1/24")
 	assert.Empty(t, ob.String())
 	assert.Empty(t, eb.String())
 
@@ -193,7 +185,7 @@ func Test_signCert(t *testing.T) {
 	ob.Reset()
 	eb.Reset()
 	args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m", "-subnets", "a"}
-	assertHelpError(t, signCert(args, ob, eb, nopw), "invalid subnet definition: invalid CIDR address: a")
+	assertHelpError(t, signCert(args, ob, eb, nopw), "invalid subnet definition: a")
 	assert.Empty(t, ob.String())
 	assert.Empty(t, eb.String())
 
@@ -209,7 +201,7 @@ func Test_signCert(t *testing.T) {
 	caKeyF2, err := os.CreateTemp("", "sign-cert-2.key")
 	assert.Nil(t, err)
 	defer os.Remove(caKeyF2.Name())
-	caKeyF2.Write(cert.MarshalEd25519PrivateKey(caPriv2))
+	caKeyF2.Write(cert.MarshalSigningPrivateKeyToPEM(cert.Curve_CURVE25519, caPriv2))
 
 	ob.Reset()
 	eb.Reset()
@@ -255,33 +247,34 @@ func Test_signCert(t *testing.T) {
 
 	// read cert and key files
 	rb, _ := os.ReadFile(keyF.Name())
-	lKey, b, err := cert.UnmarshalX25519PrivateKey(rb)
+	lKey, b, curve, err := cert.UnmarshalPrivateKeyFromPEM(rb)
+	assert.Equal(t, cert.Curve_CURVE25519, curve)
 	assert.Len(t, b, 0)
 	assert.Nil(t, err)
 	assert.Len(t, lKey, 32)
 
 	rb, _ = os.ReadFile(crtF.Name())
-	lCrt, b, err := cert.UnmarshalNebulaCertificateFromPEM(rb)
+	lCrt, b, err := cert.UnmarshalCertificateFromPEM(rb)
 	assert.Len(t, b, 0)
 	assert.Nil(t, err)
 
-	assert.Equal(t, "test", lCrt.Details.Name)
-	assert.Equal(t, "1.1.1.1/24", lCrt.Details.Ips[0].String())
-	assert.Len(t, lCrt.Details.Ips, 1)
-	assert.False(t, lCrt.Details.IsCA)
-	assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Details.Groups)
-	assert.Len(t, lCrt.Details.Subnets, 3)
-	assert.Len(t, lCrt.Details.PublicKey, 32)
-	assert.Equal(t, time.Duration(time.Minute*100), lCrt.Details.NotAfter.Sub(lCrt.Details.NotBefore))
+	assert.Equal(t, "test", lCrt.Name())
+	assert.Equal(t, "1.1.1.1/24", lCrt.Networks()[0].String())
+	assert.Len(t, lCrt.Networks(), 1)
+	assert.False(t, lCrt.IsCA())
+	assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Groups())
+	assert.Len(t, lCrt.UnsafeNetworks(), 3)
+	assert.Len(t, lCrt.PublicKey(), 32)
+	assert.Equal(t, time.Duration(time.Minute*100), lCrt.NotAfter().Sub(lCrt.NotBefore()))
 
 	sns := []string{}
-	for _, sn := range lCrt.Details.Subnets {
+	for _, sn := range lCrt.UnsafeNetworks() {
 		sns = append(sns, sn.String())
 	}
 	assert.Equal(t, []string{"10.1.1.1/32", "10.2.2.2/32", "10.5.5.5/32"}, sns)
 
-	issuer, _ := ca.Sha256Sum()
-	assert.Equal(t, issuer, lCrt.Details.Issuer)
+	issuer, _ := ca.Fingerprint()
+	assert.Equal(t, issuer, lCrt.Issuer())
 
 	assert.True(t, lCrt.CheckSignature(caPub))
 
@@ -297,16 +290,18 @@ func Test_signCert(t *testing.T) {
 
 	// read cert file and check pub key matches in-pub
 	rb, _ = os.ReadFile(crtF.Name())
-	lCrt, b, err = cert.UnmarshalNebulaCertificateFromPEM(rb)
+	lCrt, b, err = cert.UnmarshalCertificateFromPEM(rb)
 	assert.Len(t, b, 0)
 	assert.Nil(t, err)
-	assert.Equal(t, lCrt.Details.PublicKey, inPub)
+	assert.Equal(t, lCrt.PublicKey(), inPub)
 
 	// test refuse to sign cert with duration beyond root
 	ob.Reset()
 	eb.Reset()
+	os.Remove(keyF.Name())
+	os.Remove(crtF.Name())
 	args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "1000m", "-subnets", "10.1.1.1/32, ,   10.2.2.2/32   ,   ,  ,, 10.5.5.5/32", "-groups", "1,,   2    ,        ,,,3,4,5"}
-	assert.EqualError(t, signCert(args, ob, eb, nopw), "refusing to sign, root certificate constraints violated: certificate expires after signing certificate")
+	assert.EqualError(t, signCert(args, ob, eb, nopw), "error while signing: certificate expires after signing certificate")
 	assert.Empty(t, ob.String())
 	assert.Empty(t, eb.String())
 
@@ -362,16 +357,8 @@ func Test_signCert(t *testing.T) {
 	b, _ = cert.EncryptAndMarshalSigningPrivateKey(cert.Curve_CURVE25519, caPriv, passphrase, kdfParams)
 	caKeyF.Write(b)
 
-	ca = cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      "ca",
-			NotBefore: time.Now(),
-			NotAfter:  time.Now().Add(time.Minute * 200),
-			PublicKey: caPub,
-			IsCA:      true,
-		},
-	}
-	b, _ = ca.MarshalToPEM()
+	ca, _ = NewTestCaCert("ca", caPub, caPriv, time.Now(), time.Now().Add(time.Minute*200), nil, nil, nil)
+	b, _ = ca.MarshalPEM()
 	caCrtF.Write(b)
 
 	// test with the proper password

+ 4 - 4
cmd/nebula-cert/verify.go

@@ -46,7 +46,7 @@ func verify(args []string, out io.Writer, errOut io.Writer) error {
 
 	caPool := cert.NewCAPool()
 	for {
-		rawCACert, err = caPool.AddCACertificate(rawCACert)
+		rawCACert, err = caPool.AddCAFromPEM(rawCACert)
 		if err != nil {
 			return fmt.Errorf("error while adding ca cert to pool: %s", err)
 		}
@@ -61,13 +61,13 @@ func verify(args []string, out io.Writer, errOut io.Writer) error {
 		return fmt.Errorf("unable to read crt; %s", err)
 	}
 
-	c, _, err := cert.UnmarshalNebulaCertificateFromPEM(rawCert)
+	c, _, err := cert.UnmarshalCertificateFromPEM(rawCert)
 	if err != nil {
 		return fmt.Errorf("error while parsing crt: %s", err)
 	}
 
-	good, err := c.Verify(time.Now(), caPool)
-	if !good {
+	_, err = caPool.VerifyCertificate(time.Now(), c)
+	if err != nil {
 		return err
 	}
 

+ 10 - 29
cmd/nebula-cert/verify_test.go

@@ -7,7 +7,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/slackhq/nebula/cert"
 	"github.com/stretchr/testify/assert"
 	"golang.org/x/crypto/ed25519"
 )
@@ -67,17 +66,8 @@ func Test_verify(t *testing.T) {
 
 	// make a ca for later
 	caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
-	ca := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      "test-ca",
-			NotBefore: time.Now().Add(time.Hour * -1),
-			NotAfter:  time.Now().Add(time.Hour * 2),
-			PublicKey: caPub,
-			IsCA:      true,
-		},
-	}
-	ca.Sign(cert.Curve_CURVE25519, caPriv)
-	b, _ := ca.MarshalToPEM()
+	ca, _ := NewTestCaCert("test-ca", caPub, caPriv, time.Now().Add(time.Hour*-1), time.Now().Add(time.Hour*2), nil, nil, nil)
+	b, _ := ca.MarshalPEM()
 	caFile.Truncate(0)
 	caFile.Seek(0, 0)
 	caFile.Write(b)
@@ -102,22 +92,13 @@ func Test_verify(t *testing.T) {
 	assert.EqualError(t, err, "error while parsing crt: input did not contain a valid PEM encoded block")
 
 	// unverifiable cert at path
-	_, badPriv, _ := ed25519.GenerateKey(rand.Reader)
-	certPub, _ := x25519Keypair()
-	signer, _ := ca.Sha256Sum()
-	crt := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      "test-cert",
-			NotBefore: time.Now().Add(time.Hour * -1),
-			NotAfter:  time.Now().Add(time.Hour),
-			PublicKey: certPub,
-			IsCA:      false,
-			Issuer:    signer,
-		},
+	crt, _ := NewTestCert(ca, caPriv, "test-cert", time.Now().Add(time.Hour*-1), time.Now().Add(time.Hour), nil, nil, nil)
+	// Slightly evil hack to modify the certificate after it was sealed to generate an invalid signature
+	pub := crt.PublicKey()
+	for i, _ := range pub {
+		pub[i] = 0
 	}
-
-	crt.Sign(cert.Curve_CURVE25519, badPriv)
-	b, _ = crt.MarshalToPEM()
+	b, _ = crt.MarshalPEM()
 	certFile.Truncate(0)
 	certFile.Seek(0, 0)
 	certFile.Write(b)
@@ -128,8 +109,8 @@ func Test_verify(t *testing.T) {
 	assert.EqualError(t, err, "certificate signature did not match")
 
 	// verified cert at path
-	crt.Sign(cert.Curve_CURVE25519, caPriv)
-	b, _ = crt.MarshalToPEM()
+	crt, _ = NewTestCert(ca, caPriv, "test-cert", time.Now().Add(time.Hour*-1), time.Now().Add(time.Hour), nil, nil, nil)
+	b, _ = crt.MarshalPEM()
 	certFile.Truncate(0)
 	certFile.Seek(0, 0)
 	certFile.Write(b)

+ 6 - 6
connection_manager.go

@@ -415,7 +415,7 @@ func (n *connectionManager) shouldSwapPrimary(current, primary *HostInfo) bool {
 	}
 
 	certState := n.intf.pki.GetCertState()
-	return bytes.Equal(current.ConnectionState.myCert.Signature, certState.Certificate.Signature)
+	return bytes.Equal(current.ConnectionState.myCert.Signature(), certState.Certificate.Signature())
 }
 
 func (n *connectionManager) swapPrimary(current, primary *HostInfo) {
@@ -436,8 +436,9 @@ func (n *connectionManager) isInvalidCertificate(now time.Time, hostinfo *HostIn
 		return false
 	}
 
-	valid, err := remoteCert.VerifyWithCache(now, n.intf.pki.GetCAPool())
-	if valid {
+	caPool := n.intf.pki.GetCAPool()
+	err := caPool.VerifyCachedCertificate(now, remoteCert)
+	if err == nil {
 		return false
 	}
 
@@ -446,9 +447,8 @@ func (n *connectionManager) isInvalidCertificate(now time.Time, hostinfo *HostIn
 		return false
 	}
 
-	fingerprint, _ := remoteCert.Sha256Sum()
 	hostinfo.logger(n.l).WithError(err).
-		WithField("fingerprint", fingerprint).
+		WithField("fingerprint", remoteCert.Fingerprint).
 		Info("Remote certificate is no longer valid, tearing down the tunnel")
 
 	return true
@@ -474,7 +474,7 @@ func (n *connectionManager) sendPunch(hostinfo *HostInfo) {
 
 func (n *connectionManager) tryRehandshake(hostinfo *HostInfo) {
 	certState := n.intf.pki.GetCertState()
-	if bytes.Equal(hostinfo.ConnectionState.myCert.Signature, certState.Certificate.Signature) {
+	if bytes.Equal(hostinfo.ConnectionState.myCert.Signature(), certState.Certificate.Signature()) {
 		return
 	}
 

+ 141 - 37
connection_manager_test.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"crypto/ed25519"
 	"crypto/rand"
-	"net"
 	"net/netip"
 	"testing"
 	"time"
@@ -47,7 +46,7 @@ func Test_NewConnectionManagerTest(t *testing.T) {
 	cs := &CertState{
 		RawCertificate:      []byte{},
 		PrivateKey:          []byte{},
-		Certificate:         &cert.NebulaCertificate{},
+		Certificate:         &dummyCert{},
 		RawCertificateNoKey: []byte{},
 	}
 
@@ -80,7 +79,7 @@ func Test_NewConnectionManagerTest(t *testing.T) {
 		remoteIndexId: 9901,
 	}
 	hostinfo.ConnectionState = &ConnectionState{
-		myCert: &cert.NebulaCertificate{},
+		myCert: &dummyCert{},
 		H:      &noise.HandshakeState{},
 	}
 	nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
@@ -130,7 +129,7 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
 	cs := &CertState{
 		RawCertificate:      []byte{},
 		PrivateKey:          []byte{},
-		Certificate:         &cert.NebulaCertificate{},
+		Certificate:         &dummyCert{},
 		RawCertificateNoKey: []byte{},
 	}
 
@@ -163,7 +162,7 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
 		remoteIndexId: 9901,
 	}
 	hostinfo.ConnectionState = &ConnectionState{
-		myCert: &cert.NebulaCertificate{},
+		myCert: &dummyCert{},
 		H:      &noise.HandshakeState{},
 	}
 	nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
@@ -206,10 +205,7 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
 func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
 	now := time.Now()
 	l := test.NewLogger()
-	ipNet := net.IPNet{
-		IP:   net.IPv4(172, 1, 1, 2),
-		Mask: net.IPMask{255, 255, 255, 0},
-	}
+
 	vpncidr := netip.MustParsePrefix("172.1.1.1/24")
 	localrange := netip.MustParsePrefix("10.1.1.1/24")
 	vpnIp := netip.MustParseAddr("172.1.1.2")
@@ -219,41 +215,38 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
 
 	// Generate keys for CA and peer's cert.
 	pubCA, privCA, _ := ed25519.GenerateKey(rand.Reader)
-	caCert := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      "ca",
-			NotBefore: now,
-			NotAfter:  now.Add(1 * time.Hour),
-			IsCA:      true,
-			PublicKey: pubCA,
-		},
+	tbs := &cert.TBSCertificate{
+		Version:   1,
+		Name:      "ca",
+		IsCA:      true,
+		NotBefore: now,
+		NotAfter:  now.Add(1 * time.Hour),
+		PublicKey: pubCA,
 	}
 
-	assert.NoError(t, caCert.Sign(cert.Curve_CURVE25519, privCA))
-	ncp := &cert.NebulaCAPool{
-		CAs: cert.NewCAPool().CAs,
-	}
-	ncp.CAs["ca"] = &caCert
+	caCert, err := tbs.Sign(nil, cert.Curve_CURVE25519, privCA)
+	assert.NoError(t, err)
+	ncp := cert.NewCAPool()
+	assert.NoError(t, ncp.AddCA(caCert))
 
 	pubCrt, _, _ := ed25519.GenerateKey(rand.Reader)
-	peerCert := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:      "host",
-			Ips:       []*net.IPNet{&ipNet},
-			Subnets:   []*net.IPNet{},
-			NotBefore: now,
-			NotAfter:  now.Add(60 * time.Second),
-			PublicKey: pubCrt,
-			IsCA:      false,
-			Issuer:    "ca",
-		},
+	tbs = &cert.TBSCertificate{
+		Version:   1,
+		Name:      "host",
+		Networks:  []netip.Prefix{vpncidr},
+		NotBefore: now,
+		NotAfter:  now.Add(60 * time.Second),
+		PublicKey: pubCrt,
 	}
-	assert.NoError(t, peerCert.Sign(cert.Curve_CURVE25519, privCA))
+	peerCert, err := tbs.Sign(caCert, cert.Curve_CURVE25519, privCA)
+	assert.NoError(t, err)
+
+	cachedPeerCert, err := ncp.VerifyCertificate(now.Add(time.Second), peerCert)
 
 	cs := &CertState{
 		RawCertificate:      []byte{},
 		PrivateKey:          []byte{},
-		Certificate:         &cert.NebulaCertificate{},
+		Certificate:         &dummyCert{},
 		RawCertificateNoKey: []byte{},
 	}
 
@@ -282,8 +275,8 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
 	hostinfo := &HostInfo{
 		vpnIp: vpnIp,
 		ConnectionState: &ConnectionState{
-			myCert:   &cert.NebulaCertificate{},
-			peerCert: &peerCert,
+			myCert:   &dummyCert{},
+			peerCert: cachedPeerCert,
 			H:        &noise.HandshakeState{},
 		},
 	}
@@ -303,3 +296,114 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
 	invalid = nc.isInvalidCertificate(nextTick, hostinfo)
 	assert.True(t, invalid)
 }
+
+type dummyCert struct {
+	version        cert.Version
+	curve          cert.Curve
+	groups         []string
+	isCa           bool
+	issuer         string
+	name           string
+	networks       []netip.Prefix
+	notAfter       time.Time
+	notBefore      time.Time
+	publicKey      []byte
+	signature      []byte
+	unsafeNetworks []netip.Prefix
+}
+
+func (d *dummyCert) Version() cert.Version {
+	return d.version
+}
+
+func (d *dummyCert) Curve() cert.Curve {
+	return d.curve
+}
+
+func (d *dummyCert) Groups() []string {
+	return d.groups
+}
+
+func (d *dummyCert) IsCA() bool {
+	return d.isCa
+}
+
+func (d *dummyCert) Issuer() string {
+	return d.issuer
+}
+
+func (d *dummyCert) Name() string {
+	return d.name
+}
+
+func (d *dummyCert) Networks() []netip.Prefix {
+	return d.networks
+}
+
+func (d *dummyCert) NotAfter() time.Time {
+	return d.notAfter
+}
+
+func (d *dummyCert) NotBefore() time.Time {
+	return d.notBefore
+}
+
+func (d *dummyCert) PublicKey() []byte {
+	return d.publicKey
+}
+
+func (d *dummyCert) Signature() []byte {
+	return d.signature
+}
+
+func (d *dummyCert) UnsafeNetworks() []netip.Prefix {
+	return d.unsafeNetworks
+}
+
+func (d *dummyCert) MarshalForHandshakes() ([]byte, error) {
+	return nil, nil
+}
+
+func (d *dummyCert) Sign(curve cert.Curve, key []byte) error {
+	return nil
+}
+
+func (d *dummyCert) CheckSignature(key []byte) bool {
+	return true
+}
+
+func (d *dummyCert) Expired(t time.Time) bool {
+	return false
+}
+
+func (d *dummyCert) CheckRootConstraints(signer cert.Certificate) error {
+	return nil
+}
+
+func (d *dummyCert) VerifyPrivateKey(curve cert.Curve, key []byte) error {
+	return nil
+}
+
+func (d *dummyCert) String() string {
+	return ""
+}
+
+func (d *dummyCert) Marshal() ([]byte, error) {
+	return nil, nil
+}
+
+func (d *dummyCert) MarshalPEM() ([]byte, error) {
+	return nil, nil
+}
+
+func (d *dummyCert) Fingerprint() (string, error) {
+	return "", nil
+}
+
+func (d *dummyCert) MarshalJSON() ([]byte, error) {
+	return nil, nil
+}
+
+func (d *dummyCert) Copy() cert.Certificate {
+	return d
+}

+ 5 - 5
connection_state.go

@@ -18,8 +18,8 @@ type ConnectionState struct {
 	eKey           *NebulaCipherState
 	dKey           *NebulaCipherState
 	H              *noise.HandshakeState
-	myCert         *cert.NebulaCertificate
-	peerCert       *cert.NebulaCertificate
+	myCert         cert.Certificate
+	peerCert       *cert.CachedCertificate
 	initiator      bool
 	messageCounter atomic.Uint64
 	window         *Bits
@@ -28,17 +28,17 @@ type ConnectionState struct {
 
 func NewConnectionState(l *logrus.Logger, cipher string, certState *CertState, initiator bool, pattern noise.HandshakePattern, psk []byte, pskStage int) *ConnectionState {
 	var dhFunc noise.DHFunc
-	switch certState.Certificate.Details.Curve {
+	switch certState.Certificate.Curve() {
 	case cert.Curve_CURVE25519:
 		dhFunc = noise.DH25519
 	case cert.Curve_P256:
-		if certState.Certificate.Pkcs11Backed {
+		if certState.pkcs11Backed {
 			dhFunc = noiseutil.DHP256PKCS11
 		} else {
 			dhFunc = noiseutil.DHP256
 		}
 	default:
-		l.Errorf("invalid curve: %s", certState.Certificate.Details.Curve)
+		l.Errorf("invalid curve: %s", certState.Certificate.Curve())
 		return nil
 	}
 

+ 13 - 13
control.go

@@ -37,15 +37,15 @@ type Control struct {
 }
 
 type ControlHostInfo struct {
-	VpnIp                  netip.Addr              `json:"vpnIp"`
-	LocalIndex             uint32                  `json:"localIndex"`
-	RemoteIndex            uint32                  `json:"remoteIndex"`
-	RemoteAddrs            []netip.AddrPort        `json:"remoteAddrs"`
-	Cert                   *cert.NebulaCertificate `json:"cert"`
-	MessageCounter         uint64                  `json:"messageCounter"`
-	CurrentRemote          netip.AddrPort          `json:"currentRemote"`
-	CurrentRelaysToMe      []netip.Addr            `json:"currentRelaysToMe"`
-	CurrentRelaysThroughMe []netip.Addr            `json:"currentRelaysThroughMe"`
+	VpnIp                  netip.Addr       `json:"vpnIp"`
+	LocalIndex             uint32           `json:"localIndex"`
+	RemoteIndex            uint32           `json:"remoteIndex"`
+	RemoteAddrs            []netip.AddrPort `json:"remoteAddrs"`
+	Cert                   cert.Certificate `json:"cert"`
+	MessageCounter         uint64           `json:"messageCounter"`
+	CurrentRemote          netip.AddrPort   `json:"currentRemote"`
+	CurrentRelaysToMe      []netip.Addr     `json:"currentRelaysToMe"`
+	CurrentRelaysThroughMe []netip.Addr     `json:"currentRelaysThroughMe"`
 }
 
 // Start actually runs nebula, this is a nonblocking call. To block use Control.ShutdownBlock()
@@ -130,15 +130,15 @@ func (c *Control) ListHostmapIndexes(pendingMap bool) []ControlHostInfo {
 }
 
 // GetCertByVpnIp returns the authenticated certificate of the given vpn IP, or nil if not found
-func (c *Control) GetCertByVpnIp(vpnIp netip.Addr) *cert.NebulaCertificate {
+func (c *Control) GetCertByVpnIp(vpnIp netip.Addr) cert.Certificate {
 	if c.f.myVpnNet.Addr() == vpnIp {
-		return c.f.pki.GetCertState().Certificate
+		return c.f.pki.GetCertState().Certificate.Copy()
 	}
 	hi := c.f.hostMap.QueryVpnIp(vpnIp)
 	if hi == nil {
 		return nil
 	}
-	return hi.GetCert()
+	return hi.GetCert().Certificate.Copy()
 }
 
 // CreateTunnel creates a new tunnel to the given vpn ip.
@@ -290,7 +290,7 @@ func copyHostInfo(h *HostInfo, preferredRanges []netip.Prefix) ControlHostInfo {
 	}
 
 	if c := h.GetCert(); c != nil {
-		chi.Cert = c.Copy()
+		chi.Cert = c.Certificate.Copy()
 	}
 
 	return chi

+ 6 - 20
control_test.go

@@ -5,7 +5,6 @@ import (
 	"net/netip"
 	"reflect"
 	"testing"
-	"time"
 
 	"github.com/sirupsen/logrus"
 	"github.com/slackhq/nebula/cert"
@@ -14,6 +13,9 @@ import (
 )
 
 func TestControl_GetHostInfoByVpnIp(t *testing.T) {
+	//TODO: with multiple certificate versions we have a problem with this test
+	// Some certs versions have different characteristics and each version implements their own Copy() func
+	// which means this is not a good place to test for exposing memory
 	l := test.NewLogger()
 	// Special care must be taken to re-use all objects provided to the hostmap and certificate in the expectedInfo object
 	// To properly ensure we are not exposing core memory to the caller
@@ -33,22 +35,6 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
 		Mask: net.IPMask{255, 255, 255, 0},
 	}
 
-	crt := &cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:           "test",
-			Ips:            []*net.IPNet{&ipNet},
-			Subnets:        []*net.IPNet{},
-			Groups:         []string{"default-group"},
-			NotBefore:      time.Unix(1, 0),
-			NotAfter:       time.Unix(2, 0),
-			PublicKey:      []byte{5, 6, 7, 8},
-			IsCA:           false,
-			Issuer:         "the-issuer",
-			InvertedGroups: map[string]struct{}{"default-group": {}},
-		},
-		Signature: []byte{1, 2, 1, 2, 1, 3},
-	}
-
 	remotes := NewRemoteList(nil)
 	remotes.unlockedPrependV4(netip.IPv4Unspecified(), NewIp4AndPortFromNetIP(remote1.Addr(), remote1.Port()))
 	remotes.unlockedPrependV6(netip.IPv4Unspecified(), NewIp6AndPortFromNetIP(remote2.Addr(), remote2.Port()))
@@ -56,11 +42,12 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
 	vpnIp, ok := netip.AddrFromSlice(ipNet.IP)
 	assert.True(t, ok)
 
+	crt := &dummyCert{}
 	hm.unlockedAddHostInfo(&HostInfo{
 		remote:  remote1,
 		remotes: remotes,
 		ConnectionState: &ConnectionState{
-			peerCert: crt,
+			peerCert: &cert.CachedCertificate{Certificate: crt},
 		},
 		remoteIndexId: 200,
 		localIndexId:  201,
@@ -115,8 +102,7 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
 	// Make sure we don't have any unexpected fields
 	assertFields(t, []string{"VpnIp", "LocalIndex", "RemoteIndex", "RemoteAddrs", "Cert", "MessageCounter", "CurrentRemote", "CurrentRelaysToMe", "CurrentRelaysThroughMe"}, thi)
 	assert.EqualValues(t, &expectedInfo, thi)
-	//TODO: netip.Addr reuses global memory for zone identifiers which breaks our "no reused memory check" here
-	//test.AssertDeepCopyEqual(t, &expectedInfo, thi)
+	test.AssertDeepCopyEqual(t, &expectedInfo, thi)
 
 	// Make sure we don't panic if the host info doesn't have a cert yet
 	assert.NotPanics(t, func() {

+ 1 - 1
control_tester.go

@@ -153,7 +153,7 @@ func (c *Control) GetHostmap() *HostMap {
 	return c.f.hostMap
 }
 
-func (c *Control) GetCert() *cert.NebulaCertificate {
+func (c *Control) GetCert() cert.Certificate {
 	return c.f.pki.GetCertState().Certificate
 }
 

+ 5 - 3
dns_server.go

@@ -57,9 +57,11 @@ func (d *dnsRecords) QueryCert(data string) string {
 		return ""
 	}
 
-	cert := q.Details
-	c := fmt.Sprintf("\"Name: %s\" \"Ips: %s\" \"Subnets %s\" \"Groups %s\" \"NotBefore %s\" \"NotAfter %s\" \"PublicKey %x\" \"IsCA %t\" \"Issuer %s\"", cert.Name, cert.Ips, cert.Subnets, cert.Groups, cert.NotBefore, cert.NotAfter, cert.PublicKey, cert.IsCA, cert.Issuer)
-	return c
+	b, err := q.Certificate.MarshalJSON()
+	if err != nil {
+		return ""
+	}
+	return string(b)
 }
 
 func (d *dnsRecords) Add(host, data string) {

+ 18 - 17
e2e/handshakes_test.go

@@ -6,6 +6,7 @@ package e2e
 import (
 	"fmt"
 	"net/netip"
+	"slices"
 	"testing"
 	"time"
 
@@ -538,9 +539,9 @@ func TestRehandshakingRelays(t *testing.T) {
 	// When I update the certificate for the relay, both me and them will have 2 host infos for the relay,
 	// and the main host infos will not have any relay state to handle the me<->relay<->them tunnel.
 	r.Log("Renew relay certificate and spin until me and them sees it")
-	_, _, myNextPrivKey, myNextPEM := NewTestCert(ca, caKey, "relay", time.Now(), time.Now().Add(5*time.Minute), relayVpnIpNet, nil, []string{"new group"})
+	_, _, myNextPrivKey, myNextPEM := NewTestCert(ca, caKey, "relay", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{relayVpnIpNet}, nil, []string{"new group"})
 
-	caB, err := ca.MarshalToPEM()
+	caB, err := ca.MarshalPEM()
 	if err != nil {
 		panic(err)
 	}
@@ -558,7 +559,7 @@ func TestRehandshakingRelays(t *testing.T) {
 		r.Log("Assert the tunnel works between myVpnIpNet and relayVpnIpNet")
 		assertTunnel(t, myVpnIpNet.Addr(), relayVpnIpNet.Addr(), myControl, relayControl, r)
 		c := myControl.GetHostInfoByVpnIp(relayVpnIpNet.Addr(), false)
-		if len(c.Cert.Details.Groups) != 0 {
+		if len(c.Cert.Groups()) != 0 {
 			// We have a new certificate now
 			r.Log("Certificate between my and relay is updated!")
 			break
@@ -571,7 +572,7 @@ func TestRehandshakingRelays(t *testing.T) {
 		r.Log("Assert the tunnel works between theirVpnIpNet and relayVpnIpNet")
 		assertTunnel(t, theirVpnIpNet.Addr(), relayVpnIpNet.Addr(), theirControl, relayControl, r)
 		c := theirControl.GetHostInfoByVpnIp(relayVpnIpNet.Addr(), false)
-		if len(c.Cert.Details.Groups) != 0 {
+		if len(c.Cert.Groups()) != 0 {
 			// We have a new certificate now
 			r.Log("Certificate between their and relay is updated!")
 			break
@@ -642,9 +643,9 @@ func TestRehandshakingRelaysPrimary(t *testing.T) {
 	// When I update the certificate for the relay, both me and them will have 2 host infos for the relay,
 	// and the main host infos will not have any relay state to handle the me<->relay<->them tunnel.
 	r.Log("Renew relay certificate and spin until me and them sees it")
-	_, _, myNextPrivKey, myNextPEM := NewTestCert(ca, caKey, "relay", time.Now(), time.Now().Add(5*time.Minute), relayVpnIpNet, nil, []string{"new group"})
+	_, _, myNextPrivKey, myNextPEM := NewTestCert(ca, caKey, "relay", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{relayVpnIpNet}, nil, []string{"new group"})
 
-	caB, err := ca.MarshalToPEM()
+	caB, err := ca.MarshalPEM()
 	if err != nil {
 		panic(err)
 	}
@@ -662,7 +663,7 @@ func TestRehandshakingRelaysPrimary(t *testing.T) {
 		r.Log("Assert the tunnel works between myVpnIpNet and relayVpnIpNet")
 		assertTunnel(t, myVpnIpNet.Addr(), relayVpnIpNet.Addr(), myControl, relayControl, r)
 		c := myControl.GetHostInfoByVpnIp(relayVpnIpNet.Addr(), false)
-		if len(c.Cert.Details.Groups) != 0 {
+		if len(c.Cert.Groups()) != 0 {
 			// We have a new certificate now
 			r.Log("Certificate between my and relay is updated!")
 			break
@@ -675,7 +676,7 @@ func TestRehandshakingRelaysPrimary(t *testing.T) {
 		r.Log("Assert the tunnel works between theirVpnIpNet and relayVpnIpNet")
 		assertTunnel(t, theirVpnIpNet.Addr(), relayVpnIpNet.Addr(), theirControl, relayControl, r)
 		c := theirControl.GetHostInfoByVpnIp(relayVpnIpNet.Addr(), false)
-		if len(c.Cert.Details.Groups) != 0 {
+		if len(c.Cert.Groups()) != 0 {
 			// We have a new certificate now
 			r.Log("Certificate between their and relay is updated!")
 			break
@@ -737,9 +738,9 @@ func TestRehandshaking(t *testing.T) {
 	r.RenderHostmaps("Starting hostmaps", myControl, theirControl)
 
 	r.Log("Renew my certificate and spin until their sees it")
-	_, _, myNextPrivKey, myNextPEM := NewTestCert(ca, caKey, "me", time.Now(), time.Now().Add(5*time.Minute), myVpnIpNet, nil, []string{"new group"})
+	_, _, myNextPrivKey, myNextPEM := NewTestCert(ca, caKey, "me", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{myVpnIpNet}, nil, []string{"new group"})
 
-	caB, err := ca.MarshalToPEM()
+	caB, err := ca.MarshalPEM()
 	if err != nil {
 		panic(err)
 	}
@@ -756,7 +757,7 @@ func TestRehandshaking(t *testing.T) {
 	for {
 		assertTunnel(t, myVpnIpNet.Addr(), theirVpnIpNet.Addr(), myControl, theirControl, r)
 		c := theirControl.GetHostInfoByVpnIp(myVpnIpNet.Addr(), false)
-		if len(c.Cert.Details.Groups) != 0 {
+		if len(c.Cert.Groups()) != 0 {
 			// We have a new certificate now
 			break
 		}
@@ -764,6 +765,7 @@ func TestRehandshaking(t *testing.T) {
 		time.Sleep(time.Second)
 	}
 
+	r.Log("Got the new cert")
 	// Flip their firewall to only allowing the new group to catch the tunnels reverting incorrectly
 	rc, err = yaml.Marshal(theirConfig.Settings)
 	assert.NoError(t, err)
@@ -794,7 +796,7 @@ func TestRehandshaking(t *testing.T) {
 
 	// Make sure the correct tunnel won
 	c := theirControl.GetHostInfoByVpnIp(myVpnIpNet.Addr(), false)
-	assert.Contains(t, c.Cert.Details.Groups, "new group")
+	assert.Contains(t, c.Cert.Groups(), "new group")
 
 	// We should only have a single tunnel now on both sides
 	assert.Len(t, myFinalHostmapHosts, 1)
@@ -837,9 +839,9 @@ func TestRehandshakingLoser(t *testing.T) {
 	r.RenderHostmaps("Starting hostmaps", myControl, theirControl)
 
 	r.Log("Renew their certificate and spin until mine sees it")
-	_, _, theirNextPrivKey, theirNextPEM := NewTestCert(ca, caKey, "them", time.Now(), time.Now().Add(5*time.Minute), theirVpnIpNet, nil, []string{"their new group"})
+	_, _, theirNextPrivKey, theirNextPEM := NewTestCert(ca, caKey, "them", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{theirVpnIpNet}, nil, []string{"their new group"})
 
-	caB, err := ca.MarshalToPEM()
+	caB, err := ca.MarshalPEM()
 	if err != nil {
 		panic(err)
 	}
@@ -857,8 +859,7 @@ func TestRehandshakingLoser(t *testing.T) {
 		assertTunnel(t, myVpnIpNet.Addr(), theirVpnIpNet.Addr(), myControl, theirControl, r)
 		theirCertInMe := myControl.GetHostInfoByVpnIp(theirVpnIpNet.Addr(), false)
 
-		_, theirNewGroup := theirCertInMe.Cert.Details.InvertedGroups["their new group"]
-		if theirNewGroup {
+		if slices.Contains(theirCertInMe.Cert.Groups(), "their new group") {
 			break
 		}
 
@@ -895,7 +896,7 @@ func TestRehandshakingLoser(t *testing.T) {
 
 	// Make sure the correct tunnel won
 	theirCertInMe := myControl.GetHostInfoByVpnIp(theirVpnIpNet.Addr(), false)
-	assert.Contains(t, theirCertInMe.Cert.Details.Groups, "their new group")
+	assert.Contains(t, theirCertInMe.Cert.Groups(), "their new group")
 
 	// We should only have a single tunnel now on both sides
 	assert.Len(t, myFinalHostmapHosts, 1)

+ 30 - 57
e2e/helpers.go

@@ -3,7 +3,6 @@ package e2e
 import (
 	"crypto/rand"
 	"io"
-	"net"
 	"net/netip"
 	"time"
 
@@ -13,7 +12,7 @@ import (
 )
 
 // NewTestCaCert will generate a CA cert
-func NewTestCaCert(before, after time.Time, ips, subnets []netip.Prefix, groups []string) (*cert.NebulaCertificate, []byte, []byte, []byte) {
+func NewTestCaCert(before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte, []byte, []byte) {
 	pub, priv, err := ed25519.GenerateKey(rand.Reader)
 	if before.IsZero() {
 		before = time.Now().Add(time.Second * -60).Round(time.Second)
@@ -22,56 +21,34 @@ func NewTestCaCert(before, after time.Time, ips, subnets []netip.Prefix, groups
 		after = time.Now().Add(time.Second * 60).Round(time.Second)
 	}
 
-	nc := &cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:           "test ca",
-			NotBefore:      time.Unix(before.Unix(), 0),
-			NotAfter:       time.Unix(after.Unix(), 0),
-			PublicKey:      pub,
-			IsCA:           true,
-			InvertedGroups: make(map[string]struct{}),
-		},
+	t := &cert.TBSCertificate{
+		Version:        cert.Version1,
+		Name:           "test ca",
+		NotBefore:      time.Unix(before.Unix(), 0),
+		NotAfter:       time.Unix(after.Unix(), 0),
+		PublicKey:      pub,
+		Networks:       networks,
+		UnsafeNetworks: unsafeNetworks,
+		Groups:         groups,
+		IsCA:           true,
 	}
 
-	if len(ips) > 0 {
-		nc.Details.Ips = make([]*net.IPNet, len(ips))
-		for i, ip := range ips {
-			nc.Details.Ips[i] = &net.IPNet{IP: ip.Addr().AsSlice(), Mask: net.CIDRMask(ip.Bits(), ip.Addr().BitLen())}
-		}
-	}
-
-	if len(subnets) > 0 {
-		nc.Details.Subnets = make([]*net.IPNet, len(subnets))
-		for i, ip := range subnets {
-			nc.Details.Ips[i] = &net.IPNet{IP: ip.Addr().AsSlice(), Mask: net.CIDRMask(ip.Bits(), ip.Addr().BitLen())}
-		}
-	}
-
-	if len(groups) > 0 {
-		nc.Details.Groups = groups
-	}
-
-	err = nc.Sign(cert.Curve_CURVE25519, priv)
+	c, err := t.Sign(nil, cert.Curve_CURVE25519, priv)
 	if err != nil {
 		panic(err)
 	}
 
-	pem, err := nc.MarshalToPEM()
+	pem, err := c.MarshalPEM()
 	if err != nil {
 		panic(err)
 	}
 
-	return nc, pub, priv, pem
+	return c, pub, priv, pem
 }
 
 // NewTestCert will generate a signed certificate with the provided details.
 // Expiry times are defaulted if you do not pass them in
-func NewTestCert(ca *cert.NebulaCertificate, key []byte, name string, before, after time.Time, ip netip.Prefix, subnets []netip.Prefix, groups []string) (*cert.NebulaCertificate, []byte, []byte, []byte) {
-	issuer, err := ca.Sha256Sum()
-	if err != nil {
-		panic(err)
-	}
-
+func NewTestCert(ca cert.Certificate, key []byte, name string, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte, []byte, []byte) {
 	if before.IsZero() {
 		before = time.Now().Add(time.Second * -60).Round(time.Second)
 	}
@@ -81,33 +58,29 @@ func NewTestCert(ca *cert.NebulaCertificate, key []byte, name string, before, af
 	}
 
 	pub, rawPriv := x25519Keypair()
-	ipb := ip.Addr().AsSlice()
-	nc := &cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name: name,
-			Ips:  []*net.IPNet{{IP: ipb[:], Mask: net.CIDRMask(ip.Bits(), ip.Addr().BitLen())}},
-			//Subnets:        subnets,
-			Groups:         groups,
-			NotBefore:      time.Unix(before.Unix(), 0),
-			NotAfter:       time.Unix(after.Unix(), 0),
-			PublicKey:      pub,
-			IsCA:           false,
-			Issuer:         issuer,
-			InvertedGroups: make(map[string]struct{}),
-		},
-	}
-
-	err = nc.Sign(ca.Details.Curve, key)
+	nc := &cert.TBSCertificate{
+		Version:        cert.Version1,
+		Name:           name,
+		Networks:       networks,
+		UnsafeNetworks: unsafeNetworks,
+		Groups:         groups,
+		NotBefore:      time.Unix(before.Unix(), 0),
+		NotAfter:       time.Unix(after.Unix(), 0),
+		PublicKey:      pub,
+		IsCA:           false,
+	}
+
+	c, err := nc.Sign(ca, ca.Curve(), key)
 	if err != nil {
 		panic(err)
 	}
 
-	pem, err := nc.MarshalToPEM()
+	pem, err := c.MarshalPEM()
 	if err != nil {
 		panic(err)
 	}
 
-	return nc, pub, cert.MarshalX25519PrivateKey(rawPriv), pem
+	return c, pub, cert.MarshalPrivateKeyToPEM(cert.Curve_CURVE25519, rawPriv), pem
 }
 
 func x25519Keypair() ([]byte, []byte) {

+ 3 - 3
e2e/helpers_test.go

@@ -26,7 +26,7 @@ import (
 type m map[string]interface{}
 
 // newSimpleServer creates a nebula instance with many assumptions
-func newSimpleServer(caCrt *cert.NebulaCertificate, caKey []byte, name string, sVpnIpNet string, overrides m) (*nebula.Control, netip.Prefix, netip.AddrPort, *config.C) {
+func newSimpleServer(caCrt cert.Certificate, caKey []byte, name string, sVpnIpNet string, overrides m) (*nebula.Control, netip.Prefix, netip.AddrPort, *config.C) {
 	l := NewTestLogger()
 
 	vpnIpNet, err := netip.ParsePrefix(sVpnIpNet)
@@ -44,9 +44,9 @@ func newSimpleServer(caCrt *cert.NebulaCertificate, caKey []byte, name string, s
 		budpIp[13] -= 128
 		udpAddr = netip.AddrPortFrom(netip.AddrFrom16(budpIp), 4242)
 	}
-	_, _, myPrivKey, myPEM := NewTestCert(caCrt, caKey, name, time.Now(), time.Now().Add(5*time.Minute), vpnIpNet, nil, []string{})
+	_, _, myPrivKey, myPEM := NewTestCert(caCrt, caKey, name, time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{vpnIpNet}, nil, []string{})
 
-	caB, err := caCrt.MarshalToPEM()
+	caB, err := caCrt.MarshalPEM()
 	if err != nil {
 		panic(err)
 	}

+ 3 - 3
e2e/router/hostmap.go

@@ -58,8 +58,8 @@ func renderHostmap(c *nebula.Control) (string, []*edge) {
 	var lines []string
 	var globalLines []*edge
 
-	clusterName := strings.Trim(c.GetCert().Details.Name, " ")
-	clusterVpnIp := c.GetCert().Details.Ips[0].IP
+	clusterName := strings.Trim(c.GetCert().Name(), " ")
+	clusterVpnIp := c.GetCert().Networks()[0].Addr()
 	r := fmt.Sprintf("\tsubgraph %s[\"%s (%s)\"]\n", clusterName, clusterName, clusterVpnIp)
 
 	hm := c.GetHostmap()
@@ -102,7 +102,7 @@ func renderHostmap(c *nebula.Control) (string, []*edge) {
 		hi, ok := hm.Indexes[idx]
 		if ok {
 			r += fmt.Sprintf("\t\t\t%v.%v[\"%v (%v)\"]\n", clusterName, idx, idx, hi.GetVpnIp())
-			remoteClusterName := strings.Trim(hi.GetCert().Details.Name, " ")
+			remoteClusterName := strings.Trim(hi.GetCert().Certificate.Name(), " ")
 			globalLines = append(globalLines, &edge{from: fmt.Sprintf("%v.%v", clusterName, idx), to: fmt.Sprintf("%v.%v", remoteClusterName, hi.GetRemoteIndex())})
 			_ = hi
 		}

+ 33 - 37
firewall.go

@@ -52,9 +52,9 @@ type Firewall struct {
 	DefaultTimeout time.Duration //linux: 600s
 
 	// Used to ensure we don't emit local packets for ips we don't own
-	localIps     *bart.Table[struct{}]
-	assignedCIDR netip.Prefix
-	hasSubnets   bool
+	localIps          *bart.Table[struct{}]
+	assignedCIDR      netip.Prefix
+	hasUnsafeNetworks bool
 
 	rules        string
 	rulesVersion uint16
@@ -126,7 +126,7 @@ type firewallLocalCIDR struct {
 }
 
 // NewFirewall creates a new Firewall object. A TimerWheel is created for you from the provided timeouts.
-func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Duration, c *cert.NebulaCertificate) *Firewall {
+func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Duration, c cert.Certificate) *Firewall {
 	//TODO: error on 0 duration
 	var min, max time.Duration
 
@@ -147,11 +147,8 @@ func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.D
 	localIps := new(bart.Table[struct{}])
 	var assignedCIDR netip.Prefix
 	var assignedSet bool
-	for _, ip := range c.Details.Ips {
-		//TODO: IPV6-WORK the unmap is a bit unfortunate
-		nip, _ := netip.AddrFromSlice(ip.IP)
-		nip = nip.Unmap()
-		nprefix := netip.PrefixFrom(nip, nip.BitLen())
+	for _, network := range c.Networks() {
+		nprefix := netip.PrefixFrom(network.Addr(), network.Addr().BitLen())
 		localIps.Insert(nprefix, struct{}{})
 
 		if !assignedSet {
@@ -161,11 +158,10 @@ func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.D
 		}
 	}
 
-	for _, n := range c.Details.Subnets {
-		nip, _ := netip.AddrFromSlice(n.IP)
-		ones, _ := n.Mask.Size()
-		nip = nip.Unmap()
-		localIps.Insert(netip.PrefixFrom(nip, ones), struct{}{})
+	hasUnsafeNetworks := false
+	for _, n := range c.UnsafeNetworks() {
+		localIps.Insert(n, struct{}{})
+		hasUnsafeNetworks = true
 	}
 
 	return &Firewall{
@@ -173,15 +169,15 @@ func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.D
 			Conns:      make(map[firewall.Packet]*conn),
 			TimerWheel: NewTimerWheel[firewall.Packet](min, max),
 		},
-		InRules:        newFirewallTable(),
-		OutRules:       newFirewallTable(),
-		TCPTimeout:     tcpTimeout,
-		UDPTimeout:     UDPTimeout,
-		DefaultTimeout: defaultTimeout,
-		localIps:       localIps,
-		assignedCIDR:   assignedCIDR,
-		hasSubnets:     len(c.Details.Subnets) > 0,
-		l:              l,
+		InRules:           newFirewallTable(),
+		OutRules:          newFirewallTable(),
+		TCPTimeout:        tcpTimeout,
+		UDPTimeout:        UDPTimeout,
+		DefaultTimeout:    defaultTimeout,
+		localIps:          localIps,
+		assignedCIDR:      assignedCIDR,
+		hasUnsafeNetworks: hasUnsafeNetworks,
+		l:                 l,
 
 		incomingMetrics: firewallMetrics{
 			droppedLocalIP:  metrics.GetOrRegisterCounter("firewall.incoming.dropped.local_ip", nil),
@@ -196,7 +192,7 @@ func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.D
 	}
 }
 
-func NewFirewallFromConfig(l *logrus.Logger, nc *cert.NebulaCertificate, c *config.C) (*Firewall, error) {
+func NewFirewallFromConfig(l *logrus.Logger, nc cert.Certificate, c *config.C) (*Firewall, error) {
 	fw := NewFirewall(
 		l,
 		c.GetDuration("firewall.conntrack.tcp_timeout", time.Minute*12),
@@ -421,7 +417,7 @@ var ErrNoMatchingRule = errors.New("no matching rule in firewall table")
 
 // Drop returns an error if the packet should be dropped, explaining why. It
 // returns nil if the packet should not be dropped.
-func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *cert.NebulaCAPool, localCache firewall.ConntrackCache) error {
+func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) error {
 	// Check if we spoke to this tuple, if we did then allow this packet
 	if f.inConns(fp, h, caPool, localCache) {
 		return nil
@@ -492,7 +488,7 @@ func (f *Firewall) EmitStats() {
 	metrics.GetOrRegisterGauge("firewall.rules.hash", nil).Update(int64(f.GetRuleHashFNV()))
 }
 
-func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.NebulaCAPool, localCache firewall.ConntrackCache) bool {
+func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) bool {
 	if localCache != nil {
 		if _, ok := localCache[fp]; ok {
 			return true
@@ -619,7 +615,7 @@ func (f *Firewall) evict(p firewall.Packet) {
 	delete(conntrack.Conns, p)
 }
 
-func (ft *FirewallTable) match(p firewall.Packet, incoming bool, c *cert.NebulaCertificate, caPool *cert.NebulaCAPool) bool {
+func (ft *FirewallTable) match(p firewall.Packet, incoming bool, c *cert.CachedCertificate, caPool *cert.CAPool) bool {
 	if ft.AnyProto.match(p, incoming, c, caPool) {
 		return true
 	}
@@ -663,7 +659,7 @@ func (fp firewallPort) addRule(f *Firewall, startPort int32, endPort int32, grou
 	return nil
 }
 
-func (fp firewallPort) match(p firewall.Packet, incoming bool, c *cert.NebulaCertificate, caPool *cert.NebulaCAPool) bool {
+func (fp firewallPort) match(p firewall.Packet, incoming bool, c *cert.CachedCertificate, caPool *cert.CAPool) bool {
 	// We don't have any allowed ports, bail
 	if fp == nil {
 		return false
@@ -726,7 +722,7 @@ func (fc *FirewallCA) addRule(f *Firewall, groups []string, host string, ip, loc
 	return nil
 }
 
-func (fc *FirewallCA) match(p firewall.Packet, c *cert.NebulaCertificate, caPool *cert.NebulaCAPool) bool {
+func (fc *FirewallCA) match(p firewall.Packet, c *cert.CachedCertificate, caPool *cert.CAPool) bool {
 	if fc == nil {
 		return false
 	}
@@ -735,18 +731,18 @@ func (fc *FirewallCA) match(p firewall.Packet, c *cert.NebulaCertificate, caPool
 		return true
 	}
 
-	if t, ok := fc.CAShas[c.Details.Issuer]; ok {
+	if t, ok := fc.CAShas[c.Certificate.Issuer()]; ok {
 		if t.match(p, c) {
 			return true
 		}
 	}
 
-	s, err := caPool.GetCAForCert(c)
+	s, err := caPool.GetCAForCert(c.Certificate)
 	if err != nil {
 		return false
 	}
 
-	return fc.CANames[s.Details.Name].match(p, c)
+	return fc.CANames[s.Certificate.Name()].match(p, c)
 }
 
 func (fr *FirewallRule) addRule(f *Firewall, groups []string, host string, ip, localCIDR netip.Prefix) error {
@@ -826,7 +822,7 @@ func (fr *FirewallRule) isAny(groups []string, host string, ip netip.Prefix) boo
 	return false
 }
 
-func (fr *FirewallRule) match(p firewall.Packet, c *cert.NebulaCertificate) bool {
+func (fr *FirewallRule) match(p firewall.Packet, c *cert.CachedCertificate) bool {
 	if fr == nil {
 		return false
 	}
@@ -841,7 +837,7 @@ func (fr *FirewallRule) match(p firewall.Packet, c *cert.NebulaCertificate) bool
 		found := false
 
 		for _, g := range sg.Groups {
-			if _, ok := c.Details.InvertedGroups[g]; !ok {
+			if _, ok := c.InvertedGroups[g]; !ok {
 				found = false
 				break
 			}
@@ -855,7 +851,7 @@ func (fr *FirewallRule) match(p firewall.Packet, c *cert.NebulaCertificate) bool
 	}
 
 	if fr.Hosts != nil {
-		if flc, ok := fr.Hosts[c.Details.Name]; ok {
+		if flc, ok := fr.Hosts[c.Certificate.Name()]; ok {
 			if flc.match(p, c) {
 				return true
 			}
@@ -876,7 +872,7 @@ func (fr *FirewallRule) match(p firewall.Packet, c *cert.NebulaCertificate) bool
 
 func (flc *firewallLocalCIDR) addRule(f *Firewall, localIp netip.Prefix) error {
 	if !localIp.IsValid() {
-		if !f.hasSubnets || f.defaultLocalCIDRAny {
+		if !f.hasUnsafeNetworks || f.defaultLocalCIDRAny {
 			flc.Any = true
 			return nil
 		}
@@ -890,7 +886,7 @@ func (flc *firewallLocalCIDR) addRule(f *Firewall, localIp netip.Prefix) error {
 	return nil
 }
 
-func (flc *firewallLocalCIDR) match(p firewall.Packet, c *cert.NebulaCertificate) bool {
+func (flc *firewallLocalCIDR) match(p firewall.Packet, c *cert.CachedCertificate) bool {
 	if flc == nil {
 		return false
 	}

+ 101 - 168
firewall_test.go

@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"errors"
 	"math"
-	"net"
 	"net/netip"
 	"testing"
 	"time"
@@ -18,7 +17,7 @@ import (
 
 func TestNewFirewall(t *testing.T) {
 	l := test.NewLogger()
-	c := &cert.NebulaCertificate{}
+	c := &dummyCert{}
 	fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
 	conntrack := fw.Conntrack
 	assert.NotNil(t, conntrack)
@@ -60,7 +59,7 @@ func TestFirewall_AddRule(t *testing.T) {
 	ob := &bytes.Buffer{}
 	l.SetOutput(ob)
 
-	c := &cert.NebulaCertificate{}
+	c := &dummyCert{}
 	fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
 	assert.NotNil(t, fw.InRules)
 	assert.NotNil(t, fw.OutRules)
@@ -137,23 +136,18 @@ func TestFirewall_Drop(t *testing.T) {
 		Fragment:   false,
 	}
 
-	ipNet := net.IPNet{
-		IP:   net.IPv4(1, 2, 3, 4),
-		Mask: net.IPMask{255, 255, 255, 0},
-	}
-
-	c := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:           "host1",
-			Ips:            []*net.IPNet{&ipNet},
-			Groups:         []string{"default-group"},
-			InvertedGroups: map[string]struct{}{"default-group": {}},
-			Issuer:         "signer-shasum",
-		},
+	c := dummyCert{
+		name:     "host1",
+		networks: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/24")},
+		groups:   []string{"default-group"},
+		issuer:   "signer-shasum",
 	}
 	h := HostInfo{
 		ConnectionState: &ConnectionState{
-			peerCert: &c,
+			peerCert: &cert.CachedCertificate{
+				Certificate:    &c,
+				InvertedGroups: map[string]struct{}{"default-group": {}},
+			},
 		},
 		vpnIp: netip.MustParseAddr("1.2.3.4"),
 	}
@@ -190,14 +184,14 @@ func TestFirewall_Drop(t *testing.T) {
 	assert.NoError(t, fw.Drop(p, true, &h, cp, nil))
 
 	// ensure ca name doesn't get in the way of group checks
-	cp.CAs["signer-shasum"] = &cert.NebulaCertificate{Details: cert.NebulaCertificateDetails{Name: "ca-good"}}
+	cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
 	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", netip.Prefix{}, netip.Prefix{}, "ca-good", ""))
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", netip.Prefix{}, netip.Prefix{}, "ca-good-bad", ""))
 	assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule)
 
 	// test caName doesn't drop on match
-	cp.CAs["signer-shasum"] = &cert.NebulaCertificate{Details: cert.NebulaCertificateDetails{Name: "ca-good"}}
+	cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
 	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", netip.Prefix{}, netip.Prefix{}, "ca-good-bad", ""))
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", netip.Prefix{}, netip.Prefix{}, "ca-good", ""))
@@ -217,7 +211,9 @@ func BenchmarkFirewallTable_match(b *testing.B) {
 
 	b.Run("fail on proto", func(b *testing.B) {
 		// This benchmark is showing us the cost of failing to match the protocol
-		c := &cert.NebulaCertificate{}
+		c := &cert.CachedCertificate{
+			Certificate: &dummyCert{},
+		}
 		for n := 0; n < b.N; n++ {
 			assert.False(b, ft.match(firewall.Packet{Protocol: firewall.ProtoUDP}, true, c, cp))
 		}
@@ -225,14 +221,18 @@ func BenchmarkFirewallTable_match(b *testing.B) {
 
 	b.Run("pass proto, fail on port", func(b *testing.B) {
 		// This benchmark is showing us the cost of matching a specific protocol but failing to match the port
-		c := &cert.NebulaCertificate{}
+		c := &cert.CachedCertificate{
+			Certificate: &dummyCert{},
+		}
 		for n := 0; n < b.N; n++ {
 			assert.False(b, ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 1}, true, c, cp))
 		}
 	})
 
 	b.Run("pass proto, port, fail on local CIDR", func(b *testing.B) {
-		c := &cert.NebulaCertificate{}
+		c := &cert.CachedCertificate{
+			Certificate: &dummyCert{},
+		}
 		ip := netip.MustParsePrefix("9.254.254.254/32")
 		for n := 0; n < b.N; n++ {
 			assert.False(b, ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 100, LocalIP: ip.Addr()}, true, c, cp))
@@ -240,13 +240,12 @@ func BenchmarkFirewallTable_match(b *testing.B) {
 	})
 
 	b.Run("pass proto, port, any local CIDR, fail all group, name, and cidr", func(b *testing.B) {
-		_, ip, _ := net.ParseCIDR("9.254.254.254/32")
-		c := &cert.NebulaCertificate{
-			Details: cert.NebulaCertificateDetails{
-				InvertedGroups: map[string]struct{}{"nope": {}},
-				Name:           "nope",
-				Ips:            []*net.IPNet{ip},
+		c := &cert.CachedCertificate{
+			Certificate: &dummyCert{
+				name:     "nope",
+				networks: []netip.Prefix{netip.MustParsePrefix("9.254.254.245/32")},
 			},
+			InvertedGroups: map[string]struct{}{"nope": {}},
 		}
 		for n := 0; n < b.N; n++ {
 			assert.False(b, ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 10}, true, c, cp))
@@ -254,13 +253,12 @@ func BenchmarkFirewallTable_match(b *testing.B) {
 	})
 
 	b.Run("pass proto, port, specific local CIDR, fail all group, name, and cidr", func(b *testing.B) {
-		_, ip, _ := net.ParseCIDR("9.254.254.254/32")
-		c := &cert.NebulaCertificate{
-			Details: cert.NebulaCertificateDetails{
-				InvertedGroups: map[string]struct{}{"nope": {}},
-				Name:           "nope",
-				Ips:            []*net.IPNet{ip},
+		c := &cert.CachedCertificate{
+			Certificate: &dummyCert{
+				name:     "nope",
+				networks: []netip.Prefix{netip.MustParsePrefix("9.254.254.245/32")},
 			},
+			InvertedGroups: map[string]struct{}{"nope": {}},
 		}
 		for n := 0; n < b.N; n++ {
 			assert.False(b, ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 100, LocalIP: pfix.Addr()}, true, c, cp))
@@ -268,11 +266,11 @@ func BenchmarkFirewallTable_match(b *testing.B) {
 	})
 
 	b.Run("pass on group on any local cidr", func(b *testing.B) {
-		c := &cert.NebulaCertificate{
-			Details: cert.NebulaCertificateDetails{
-				InvertedGroups: map[string]struct{}{"good-group": {}},
-				Name:           "nope",
+		c := &cert.CachedCertificate{
+			Certificate: &dummyCert{
+				name: "nope",
 			},
+			InvertedGroups: map[string]struct{}{"good-group": {}},
 		}
 		for n := 0; n < b.N; n++ {
 			assert.True(b, ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 10}, true, c, cp))
@@ -280,11 +278,11 @@ func BenchmarkFirewallTable_match(b *testing.B) {
 	})
 
 	b.Run("pass on group on specific local cidr", func(b *testing.B) {
-		c := &cert.NebulaCertificate{
-			Details: cert.NebulaCertificateDetails{
-				InvertedGroups: map[string]struct{}{"good-group": {}},
-				Name:           "nope",
+		c := &cert.CachedCertificate{
+			Certificate: &dummyCert{
+				name: "nope",
 			},
+			InvertedGroups: map[string]struct{}{"good-group": {}},
 		}
 		for n := 0; n < b.N; n++ {
 			assert.True(b, ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 100, LocalIP: pfix.Addr()}, true, c, cp))
@@ -292,70 +290,16 @@ func BenchmarkFirewallTable_match(b *testing.B) {
 	})
 
 	b.Run("pass on name", func(b *testing.B) {
-		c := &cert.NebulaCertificate{
-			Details: cert.NebulaCertificateDetails{
-				InvertedGroups: map[string]struct{}{"nope": {}},
-				Name:           "good-host",
+		c := &cert.CachedCertificate{
+			Certificate: &dummyCert{
+				name: "good-host",
 			},
+			InvertedGroups: map[string]struct{}{"nope": {}},
 		}
 		for n := 0; n < b.N; n++ {
 			ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 10}, true, c, cp)
 		}
 	})
-	//
-	//b.Run("pass on ip", func(b *testing.B) {
-	//	ip := iputil.Ip2VpnIp(net.IPv4(172, 1, 1, 1))
-	//	c := &cert.NebulaCertificate{
-	//		Details: cert.NebulaCertificateDetails{
-	//			InvertedGroups: map[string]struct{}{"nope": {}},
-	//			Name:           "good-host",
-	//		},
-	//	}
-	//	for n := 0; n < b.N; n++ {
-	//		ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 10, RemoteIP: ip}, true, c, cp)
-	//	}
-	//})
-	//
-	//b.Run("pass on local ip", func(b *testing.B) {
-	//	ip := iputil.Ip2VpnIp(net.IPv4(172, 1, 1, 1))
-	//	c := &cert.NebulaCertificate{
-	//		Details: cert.NebulaCertificateDetails{
-	//			InvertedGroups: map[string]struct{}{"nope": {}},
-	//			Name:           "good-host",
-	//		},
-	//	}
-	//	for n := 0; n < b.N; n++ {
-	//		ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 10, LocalIP: ip}, true, c, cp)
-	//	}
-	//})
-	//
-	//_ = ft.TCP.addRule(0, 0, []string{"good-group"}, "good-host", n, n, "", "")
-	//
-	//b.Run("pass on ip with any port", func(b *testing.B) {
-	//	ip := iputil.Ip2VpnIp(net.IPv4(172, 1, 1, 1))
-	//	c := &cert.NebulaCertificate{
-	//		Details: cert.NebulaCertificateDetails{
-	//			InvertedGroups: map[string]struct{}{"nope": {}},
-	//			Name:           "good-host",
-	//		},
-	//	}
-	//	for n := 0; n < b.N; n++ {
-	//		ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 100, RemoteIP: ip}, true, c, cp)
-	//	}
-	//})
-	//
-	//b.Run("pass on local ip with any port", func(b *testing.B) {
-	//	ip := iputil.Ip2VpnIp(net.IPv4(172, 1, 1, 1))
-	//	c := &cert.NebulaCertificate{
-	//		Details: cert.NebulaCertificateDetails{
-	//			InvertedGroups: map[string]struct{}{"nope": {}},
-	//			Name:           "good-host",
-	//		},
-	//	}
-	//	for n := 0; n < b.N; n++ {
-	//		ft.match(firewall.Packet{Protocol: firewall.ProtoTCP, LocalPort: 100, LocalIP: ip}, true, c, cp)
-	//	}
-	//})
 }
 
 func TestFirewall_Drop2(t *testing.T) {
@@ -372,41 +316,38 @@ func TestFirewall_Drop2(t *testing.T) {
 		Fragment:   false,
 	}
 
-	ipNet := net.IPNet{
-		IP:   net.IPv4(1, 2, 3, 4),
-		Mask: net.IPMask{255, 255, 255, 0},
-	}
+	network := netip.MustParsePrefix("1.2.3.4/24")
 
-	c := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:           "host1",
-			Ips:            []*net.IPNet{&ipNet},
-			InvertedGroups: map[string]struct{}{"default-group": {}, "test-group": {}},
+	c := cert.CachedCertificate{
+		Certificate: &dummyCert{
+			name:     "host1",
+			networks: []netip.Prefix{network},
 		},
+		InvertedGroups: map[string]struct{}{"default-group": {}, "test-group": {}},
 	}
 	h := HostInfo{
 		ConnectionState: &ConnectionState{
 			peerCert: &c,
 		},
-		vpnIp: netip.MustParseAddr(ipNet.IP.String()),
+		vpnIp: network.Addr(),
 	}
-	h.CreateRemoteCIDR(&c)
+	h.CreateRemoteCIDR(c.Certificate)
 
-	c1 := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:           "host1",
-			Ips:            []*net.IPNet{&ipNet},
-			InvertedGroups: map[string]struct{}{"default-group": {}, "test-group-not": {}},
+	c1 := cert.CachedCertificate{
+		Certificate: &dummyCert{
+			name:     "host1",
+			networks: []netip.Prefix{network},
 		},
+		InvertedGroups: map[string]struct{}{"default-group": {}, "test-group-not": {}},
 	}
 	h1 := HostInfo{
 		ConnectionState: &ConnectionState{
 			peerCert: &c1,
 		},
 	}
-	h1.CreateRemoteCIDR(&c1)
+	h1.CreateRemoteCIDR(c1.Certificate)
 
-	fw := NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
+	fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group", "test-group"}, "", netip.Prefix{}, netip.Prefix{}, "", ""))
 	cp := cert.NewCAPool()
 
@@ -431,64 +372,60 @@ func TestFirewall_Drop3(t *testing.T) {
 		Fragment:   false,
 	}
 
-	ipNet := net.IPNet{
-		IP:   net.IPv4(1, 2, 3, 4),
-		Mask: net.IPMask{255, 255, 255, 0},
-	}
-
-	c := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name: "host-owner",
-			Ips:  []*net.IPNet{&ipNet},
+	network := netip.MustParsePrefix("1.2.3.4/24")
+	c := cert.CachedCertificate{
+		Certificate: &dummyCert{
+			name:     "host-owner",
+			networks: []netip.Prefix{network},
 		},
 	}
 
-	c1 := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:   "host1",
-			Ips:    []*net.IPNet{&ipNet},
-			Issuer: "signer-sha-bad",
+	c1 := cert.CachedCertificate{
+		Certificate: &dummyCert{
+			name:     "host1",
+			networks: []netip.Prefix{network},
+			issuer:   "signer-sha-bad",
 		},
 	}
 	h1 := HostInfo{
 		ConnectionState: &ConnectionState{
 			peerCert: &c1,
 		},
-		vpnIp: netip.MustParseAddr(ipNet.IP.String()),
+		vpnIp: network.Addr(),
 	}
-	h1.CreateRemoteCIDR(&c1)
+	h1.CreateRemoteCIDR(c1.Certificate)
 
-	c2 := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:   "host2",
-			Ips:    []*net.IPNet{&ipNet},
-			Issuer: "signer-sha",
+	c2 := cert.CachedCertificate{
+		Certificate: &dummyCert{
+			name:     "host2",
+			networks: []netip.Prefix{network},
+			issuer:   "signer-sha",
 		},
 	}
 	h2 := HostInfo{
 		ConnectionState: &ConnectionState{
 			peerCert: &c2,
 		},
-		vpnIp: netip.MustParseAddr(ipNet.IP.String()),
+		vpnIp: network.Addr(),
 	}
-	h2.CreateRemoteCIDR(&c2)
+	h2.CreateRemoteCIDR(c2.Certificate)
 
-	c3 := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:   "host3",
-			Ips:    []*net.IPNet{&ipNet},
-			Issuer: "signer-sha-bad",
+	c3 := cert.CachedCertificate{
+		Certificate: &dummyCert{
+			name:     "host3",
+			networks: []netip.Prefix{network},
+			issuer:   "signer-sha-bad",
 		},
 	}
 	h3 := HostInfo{
 		ConnectionState: &ConnectionState{
 			peerCert: &c3,
 		},
-		vpnIp: netip.MustParseAddr(ipNet.IP.String()),
+		vpnIp: network.Addr(),
 	}
-	h3.CreateRemoteCIDR(&c3)
+	h3.CreateRemoteCIDR(c3.Certificate)
 
-	fw := NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
+	fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "host1", netip.Prefix{}, netip.Prefix{}, "", ""))
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", netip.Prefix{}, netip.Prefix{}, "", "signer-sha"))
 	cp := cert.NewCAPool()
@@ -516,30 +453,26 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
 		Protocol:   firewall.ProtoUDP,
 		Fragment:   false,
 	}
-
-	ipNet := net.IPNet{
-		IP:   net.IPv4(1, 2, 3, 4),
-		Mask: net.IPMask{255, 255, 255, 0},
-	}
-
-	c := cert.NebulaCertificate{
-		Details: cert.NebulaCertificateDetails{
-			Name:           "host1",
-			Ips:            []*net.IPNet{&ipNet},
-			Groups:         []string{"default-group"},
-			InvertedGroups: map[string]struct{}{"default-group": {}},
-			Issuer:         "signer-shasum",
+	network := netip.MustParsePrefix("1.2.3.4/24")
+
+	c := cert.CachedCertificate{
+		Certificate: &dummyCert{
+			name:     "host1",
+			networks: []netip.Prefix{network},
+			groups:   []string{"default-group"},
+			issuer:   "signer-shasum",
 		},
+		InvertedGroups: map[string]struct{}{"default-group": {}},
 	}
 	h := HostInfo{
 		ConnectionState: &ConnectionState{
 			peerCert: &c,
 		},
-		vpnIp: netip.MustParseAddr(ipNet.IP.String()),
+		vpnIp: network.Addr(),
 	}
-	h.CreateRemoteCIDR(&c)
+	h.CreateRemoteCIDR(c.Certificate)
 
-	fw := NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
+	fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", netip.Prefix{}, netip.Prefix{}, "", ""))
 	cp := cert.NewCAPool()
 
@@ -552,7 +485,7 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
 	assert.NoError(t, fw.Drop(p, false, &h, cp, nil))
 
 	oldFw := fw
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
+	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 10, 10, []string{"any"}, "", netip.Prefix{}, netip.Prefix{}, "", ""))
 	fw.Conntrack = oldFw.Conntrack
 	fw.rulesVersion = oldFw.rulesVersion + 1
@@ -561,7 +494,7 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
 	assert.NoError(t, fw.Drop(p, false, &h, cp, nil))
 
 	oldFw = fw
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
+	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
 	assert.Nil(t, fw.AddRule(true, firewall.ProtoAny, 11, 11, []string{"any"}, "", netip.Prefix{}, netip.Prefix{}, "", ""))
 	fw.Conntrack = oldFw.Conntrack
 	fw.rulesVersion = oldFw.rulesVersion + 1
@@ -688,7 +621,7 @@ func Test_parsePort(t *testing.T) {
 func TestNewFirewallFromConfig(t *testing.T) {
 	l := test.NewLogger()
 	// Test a bad rule definition
-	c := &cert.NebulaCertificate{}
+	c := &dummyCert{}
 	conf := config.NewC(l)
 	conf.Settings["firewall"] = map[interface{}]interface{}{"outbound": "asdf"}
 	_, err := NewFirewallFromConfig(l, c, conf)

+ 12 - 14
handshake_ix.go

@@ -99,8 +99,7 @@ func ixHandshakeStage1(f *Interface, addr netip.AddrPort, via *ViaSender, packet
 		return
 	}
 
-	vpnIp, ok := netip.AddrFromSlice(remoteCert.Details.Ips[0].IP)
-	if !ok {
+	if len(remoteCert.Certificate.Networks()) == 0 {
 		e := f.l.WithError(err).WithField("udpAddr", addr).
 			WithField("handshake", m{"stage": 1, "style": "ix_psk0"})
 
@@ -112,10 +111,10 @@ func ixHandshakeStage1(f *Interface, addr netip.AddrPort, via *ViaSender, packet
 		return
 	}
 
-	vpnIp = vpnIp.Unmap()
-	certName := remoteCert.Details.Name
-	fingerprint, _ := remoteCert.Sha256Sum()
-	issuer := remoteCert.Details.Issuer
+	vpnIp := remoteCert.Certificate.Networks()[0].Addr().Unmap()
+	certName := remoteCert.Certificate.Name()
+	fingerprint := remoteCert.Fingerprint
+	issuer := remoteCert.Certificate.Issuer()
 
 	if vpnIp == f.myVpnNet.Addr() {
 		f.l.WithField("vpnIp", vpnIp).WithField("udpAddr", addr).
@@ -216,7 +215,7 @@ func ixHandshakeStage1(f *Interface, addr netip.AddrPort, via *ViaSender, packet
 
 	hostinfo.remotes = f.lightHouse.QueryCache(vpnIp)
 	hostinfo.SetRemote(addr)
-	hostinfo.CreateRemoteCIDR(remoteCert)
+	hostinfo.CreateRemoteCIDR(remoteCert.Certificate)
 
 	existing, err := f.handshakeManager.CheckAndComplete(hostinfo, 0, f)
 	if err != nil {
@@ -402,8 +401,7 @@ func ixHandshakeStage2(f *Interface, addr netip.AddrPort, via *ViaSender, hh *Ha
 		return true
 	}
 
-	vpnIp, ok := netip.AddrFromSlice(remoteCert.Details.Ips[0].IP)
-	if !ok {
+	if len(remoteCert.Certificate.Networks()) == 0 {
 		e := f.l.WithError(err).WithField("udpAddr", addr).
 			WithField("handshake", m{"stage": 2, "style": "ix_psk0"})
 
@@ -415,10 +413,10 @@ func ixHandshakeStage2(f *Interface, addr netip.AddrPort, via *ViaSender, hh *Ha
 		return true
 	}
 
-	vpnIp = vpnIp.Unmap()
-	certName := remoteCert.Details.Name
-	fingerprint, _ := remoteCert.Sha256Sum()
-	issuer := remoteCert.Details.Issuer
+	vpnIp := remoteCert.Certificate.Networks()[0].Addr().Unmap()
+	certName := remoteCert.Certificate.Name()
+	fingerprint := remoteCert.Fingerprint
+	issuer := remoteCert.Certificate.Issuer()
 
 	// Ensure the right host responded
 	if vpnIp != hostinfo.vpnIp {
@@ -486,7 +484,7 @@ func ixHandshakeStage2(f *Interface, addr netip.AddrPort, via *ViaSender, hh *Ha
 	}
 
 	// Build up the radix for the firewall if we have subnets in the cert
-	hostinfo.CreateRemoteCIDR(remoteCert)
+	hostinfo.CreateRemoteCIDR(remoteCert.Certificate)
 
 	// Complete our handshake and update metrics, this will replace any existing tunnels for this vpnIp
 	f.handshakeManager.Complete(hostinfo, f)

+ 1 - 1
handshake_manager.go

@@ -7,6 +7,7 @@ import (
 	"encoding/binary"
 	"errors"
 	"net/netip"
+	"slices"
 	"sync"
 	"time"
 
@@ -14,7 +15,6 @@ import (
 	"github.com/sirupsen/logrus"
 	"github.com/slackhq/nebula/header"
 	"github.com/slackhq/nebula/udp"
-	"golang.org/x/exp/slices"
 )
 
 const (

+ 1 - 2
handshake_manager_test.go

@@ -5,7 +5,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/slackhq/nebula/cert"
 	"github.com/slackhq/nebula/header"
 	"github.com/slackhq/nebula/test"
 	"github.com/slackhq/nebula/udp"
@@ -27,7 +26,7 @@ func Test_NewHandshakeManagerVpnIp(t *testing.T) {
 	cs := &CertState{
 		RawCertificate:      []byte{},
 		PrivateKey:          []byte{},
-		Certificate:         &cert.NebulaCertificate{},
+		Certificate:         &dummyCert{},
 		RawCertificateNoKey: []byte{},
 	}
 

+ 9 - 17
hostmap.go

@@ -491,7 +491,7 @@ func (hm *HostMap) queryVpnIp(vpnIp netip.Addr, promoteIfce *Interface) *HostInf
 func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) {
 	if f.serveDns {
 		remoteCert := hostinfo.ConnectionState.peerCert
-		dnsR.Add(remoteCert.Details.Name+".", remoteCert.Details.Ips[0].IP.String())
+		dnsR.Add(remoteCert.Certificate.Name()+".", remoteCert.Certificate.Networks()[0].Addr().String())
 	}
 
 	existing := hm.Hosts[hostinfo.vpnIp]
@@ -585,7 +585,7 @@ func (i *HostInfo) TryPromoteBest(preferredRanges []netip.Prefix, ifce *Interfac
 	}
 }
 
-func (i *HostInfo) GetCert() *cert.NebulaCertificate {
+func (i *HostInfo) GetCert() *cert.CachedCertificate {
 	if i.ConnectionState != nil {
 		return i.ConnectionState.peerCert
 	}
@@ -647,27 +647,19 @@ func (i *HostInfo) RecvErrorExceeded() bool {
 	return true
 }
 
-func (i *HostInfo) CreateRemoteCIDR(c *cert.NebulaCertificate) {
-	if len(c.Details.Ips) == 1 && len(c.Details.Subnets) == 0 {
+func (i *HostInfo) CreateRemoteCIDR(c cert.Certificate) {
+	if len(c.Networks()) == 1 && len(c.UnsafeNetworks()) == 0 {
 		// Simple case, no CIDRTree needed
 		return
 	}
 
 	remoteCidr := new(bart.Table[struct{}])
-	for _, ip := range c.Details.Ips {
-		//TODO: IPV6-WORK what to do when ip is invalid?
-		nip, _ := netip.AddrFromSlice(ip.IP)
-		nip = nip.Unmap()
-		bits, _ := ip.Mask.Size()
-		remoteCidr.Insert(netip.PrefixFrom(nip, bits), struct{}{})
+	for _, network := range c.Networks() {
+		remoteCidr.Insert(network, struct{}{})
 	}
 
-	for _, n := range c.Details.Subnets {
-		//TODO: IPV6-WORK what to do when ip is invalid?
-		nip, _ := netip.AddrFromSlice(n.IP)
-		nip = nip.Unmap()
-		bits, _ := n.Mask.Size()
-		remoteCidr.Insert(netip.PrefixFrom(nip, bits), struct{}{})
+	for _, network := range c.UnsafeNetworks() {
+		remoteCidr.Insert(network, struct{}{})
 	}
 	i.remoteCidr = remoteCidr
 }
@@ -683,7 +675,7 @@ func (i *HostInfo) logger(l *logrus.Logger) *logrus.Entry {
 
 	if connState := i.ConnectionState; connState != nil {
 		if peerCert := connState.peerCert; peerCert != nil {
-			li = li.WithField("certName", peerCert.Details.Name)
+			li = li.WithField("certName", peerCert.Certificate.Name())
 		}
 	}
 

+ 8 - 25
interface.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"net"
 	"net/netip"
 	"os"
 	"runtime"
@@ -157,26 +158,6 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
 
 	certificate := c.pki.GetCertState().Certificate
 
-	myVpnAddr, ok := netip.AddrFromSlice(certificate.Details.Ips[0].IP)
-	if !ok {
-		return nil, fmt.Errorf("invalid ip address in certificate: %s", certificate.Details.Ips[0].IP)
-	}
-
-	myVpnMask, ok := netip.AddrFromSlice(certificate.Details.Ips[0].Mask)
-	if !ok {
-		return nil, fmt.Errorf("invalid ip mask in certificate: %s", certificate.Details.Ips[0].Mask)
-	}
-
-	myVpnAddr = myVpnAddr.Unmap()
-	myVpnMask = myVpnMask.Unmap()
-
-	if myVpnAddr.BitLen() != myVpnMask.BitLen() {
-		return nil, fmt.Errorf("ip address and mask are different lengths in certificate")
-	}
-
-	ones, _ := certificate.Details.Ips[0].Mask.Size()
-	myVpnNet := netip.PrefixFrom(myVpnAddr, ones)
-
 	ifce := &Interface{
 		pki:                c.pki,
 		hostMap:            c.HostMap,
@@ -194,7 +175,7 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
 		version:            c.version,
 		writers:            make([]udp.Conn, c.routines),
 		readers:            make([]io.ReadWriteCloser, c.routines),
-		myVpnNet:           myVpnNet,
+		myVpnNet:           certificate.Networks()[0],
 		relayManager:       c.relayManager,
 
 		conntrackCacheTimeout: c.ConntrackCacheTimeout,
@@ -209,9 +190,11 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
 		l: c.l,
 	}
 
-	if myVpnAddr.Is4() {
-		addr := myVpnNet.Masked().Addr().As4()
-		binary.BigEndian.PutUint32(addr[:], binary.BigEndian.Uint32(addr[:])|^binary.BigEndian.Uint32(certificate.Details.Ips[0].Mask))
+	if ifce.myVpnNet.Addr().Is4() {
+		maskedAddr := certificate.Networks()[0].Masked()
+		addr := maskedAddr.Addr().As4()
+		mask := net.CIDRMask(maskedAddr.Bits(), maskedAddr.Addr().BitLen())
+		binary.BigEndian.PutUint32(addr[:], binary.BigEndian.Uint32(addr[:])|^binary.BigEndian.Uint32(mask))
 		ifce.myBroadcastAddr = netip.AddrFrom4(addr)
 	}
 
@@ -434,7 +417,7 @@ func (f *Interface) emitStats(ctx context.Context, i time.Duration) {
 			f.firewall.EmitStats()
 			f.handshakeManager.EmitStats()
 			udpStats()
-			certExpirationGauge.Update(int64(f.pki.GetCertState().Certificate.Details.NotAfter.Sub(time.Now()) / time.Second))
+			certExpirationGauge.Update(int64(f.pki.GetCertState().Certificate.NotAfter().Sub(time.Now()) / time.Second))
 		}
 	}
 }

+ 1 - 11
main.go

@@ -68,17 +68,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
 	}
 	l.WithField("firewallHashes", fw.GetRuleHashes()).Info("Firewall started")
 
-	ones, _ := certificate.Details.Ips[0].Mask.Size()
-	addr, ok := netip.AddrFromSlice(certificate.Details.Ips[0].IP)
-	if !ok {
-		err = util.NewContextualError(
-			"Invalid ip address in certificate",
-			m{"vpnIp": certificate.Details.Ips[0].IP},
-			nil,
-		)
-		return nil, err
-	}
-	tunCidr := netip.PrefixFrom(addr, ones)
+	tunCidr := certificate.Networks()[0]
 
 	ssh, err := sshd.NewSSHServer(l.WithField("subsystem", "sshd"))
 	if err != nil {

+ 6 - 23
outside.go

@@ -14,7 +14,6 @@ import (
 	"github.com/slackhq/nebula/header"
 	"github.com/slackhq/nebula/udp"
 	"golang.org/x/net/ipv4"
-	"google.golang.org/protobuf/proto"
 )
 
 const (
@@ -494,7 +493,7 @@ func (f *Interface) sendMeta(ci *ConnectionState, endpoint *net.UDPAddr, meta *N
 }
 */
 
-func RecombineCertAndValidate(h *noise.HandshakeState, rawCertBytes []byte, caPool *cert.NebulaCAPool) (*cert.NebulaCertificate, error) {
+func RecombineCertAndValidate(h *noise.HandshakeState, rawCertBytes []byte, caPool *cert.CAPool) (*cert.CachedCertificate, error) {
 	pk := h.PeerStatic()
 
 	if pk == nil {
@@ -505,31 +504,15 @@ func RecombineCertAndValidate(h *noise.HandshakeState, rawCertBytes []byte, caPo
 		return nil, errors.New("provided payload was empty")
 	}
 
-	r := &cert.RawNebulaCertificate{}
-	err := proto.Unmarshal(rawCertBytes, r)
+	c, err := cert.UnmarshalCertificateFromHandshake(rawCertBytes, pk)
 	if err != nil {
-		return nil, fmt.Errorf("error unmarshaling cert: %s", err)
+		return nil, fmt.Errorf("error unmarshaling cert: %w", err)
 	}
 
-	// If the Details are nil, just exit to avoid crashing
-	if r.Details == nil {
-		return nil, fmt.Errorf("certificate did not contain any details")
-	}
-
-	r.Details.PublicKey = pk
-	recombined, err := proto.Marshal(r)
-	if err != nil {
-		return nil, fmt.Errorf("error while recombining certificate: %s", err)
-	}
-
-	c, _ := cert.UnmarshalNebulaCertificate(recombined)
-	isValid, err := c.Verify(time.Now(), caPool)
+	cc, err := caPool.VerifyCertificate(time.Now(), c)
 	if err != nil {
-		return c, fmt.Errorf("certificate validation failed: %s", err)
-	} else if !isValid {
-		// This case should never happen but here's to defensive programming!
-		return c, errors.New("certificate validation failed but did not return an error")
+		return nil, fmt.Errorf("certificate validation failed: %w", err)
 	}
 
-	return c, nil
+	return cc, nil
 }

+ 23 - 23
pki.go

@@ -16,16 +16,17 @@ import (
 
 type PKI struct {
 	cs     atomic.Pointer[CertState]
-	caPool atomic.Pointer[cert.NebulaCAPool]
+	caPool atomic.Pointer[cert.CAPool]
 	l      *logrus.Logger
 }
 
 type CertState struct {
-	Certificate         *cert.NebulaCertificate
+	Certificate         cert.Certificate
 	RawCertificate      []byte
 	RawCertificateNoKey []byte
 	PublicKey           []byte
 	PrivateKey          []byte
+	pkcs11Backed        bool
 }
 
 func NewPKIFromConfig(l *logrus.Logger, c *config.C) (*PKI, error) {
@@ -49,7 +50,7 @@ func (p *PKI) GetCertState() *CertState {
 	return p.cs.Load()
 }
 
-func (p *PKI) GetCAPool() *cert.NebulaCAPool {
+func (p *PKI) GetCAPool() *cert.CAPool {
 	return p.caPool.Load()
 }
 
@@ -84,12 +85,12 @@ func (p *PKI) reloadCert(c *config.C, initial bool) *util.ContextualError {
 
 		// did IP in cert change? if so, don't set
 		currentCert := p.cs.Load().Certificate
-		oldIPs := currentCert.Details.Ips
-		newIPs := cs.Certificate.Details.Ips
+		oldIPs := currentCert.Networks()
+		newIPs := cs.Certificate.Networks()
 		if len(oldIPs) > 0 && len(newIPs) > 0 && oldIPs[0].String() != newIPs[0].String() {
 			return util.NewContextualError(
-				"IP in new cert was different from old",
-				m{"new_ip": newIPs[0], "old_ip": oldIPs[0]},
+				"Networks in new cert was different from old",
+				m{"new_network": newIPs[0], "old_network": oldIPs[0]},
 				nil,
 			)
 		}
@@ -115,29 +116,28 @@ func (p *PKI) reloadCAPool(c *config.C) *util.ContextualError {
 	return nil
 }
 
-func newCertState(certificate *cert.NebulaCertificate, privateKey []byte) (*CertState, error) {
+func newCertState(certificate cert.Certificate, pkcs11backed bool, privateKey []byte) (*CertState, error) {
 	// Marshal the certificate to ensure it is valid
 	rawCertificate, err := certificate.Marshal()
 	if err != nil {
 		return nil, fmt.Errorf("invalid nebula certificate on interface: %s", err)
 	}
 
-	publicKey := certificate.Details.PublicKey
+	publicKey := certificate.PublicKey()
 	cs := &CertState{
 		RawCertificate: rawCertificate,
 		Certificate:    certificate,
 		PrivateKey:     privateKey,
 		PublicKey:      publicKey,
+		pkcs11Backed:   pkcs11backed,
 	}
 
-	cs.Certificate.Details.PublicKey = nil
-	rawCertNoKey, err := cs.Certificate.Marshal()
+	rawCertNoKey, err := cs.Certificate.MarshalForHandshakes()
 	if err != nil {
 		return nil, fmt.Errorf("error marshalling certificate no key: %s", err)
 	}
 	cs.RawCertificateNoKey = rawCertNoKey
-	// put public key back
-	cs.Certificate.Details.PublicKey = cs.PublicKey
+
 	return cs, nil
 }
 
@@ -146,7 +146,7 @@ func loadPrivateKey(privPathOrPEM string) (rawKey []byte, curve cert.Curve, isPk
 	if strings.Contains(privPathOrPEM, "-----BEGIN") {
 		pemPrivateKey = []byte(privPathOrPEM)
 		privPathOrPEM = "<inline>"
-		rawKey, _, curve, err = cert.UnmarshalPrivateKey(pemPrivateKey)
+		rawKey, _, curve, err = cert.UnmarshalPrivateKeyFromPEM(pemPrivateKey)
 		if err != nil {
 			return nil, curve, false, fmt.Errorf("error while unmarshaling pki.key %s: %s", privPathOrPEM, err)
 		}
@@ -158,7 +158,7 @@ func loadPrivateKey(privPathOrPEM string) (rawKey []byte, curve cert.Curve, isPk
 		if err != nil {
 			return nil, curve, false, fmt.Errorf("unable to read pki.key file %s: %s", privPathOrPEM, err)
 		}
-		rawKey, _, curve, err = cert.UnmarshalPrivateKey(pemPrivateKey)
+		rawKey, _, curve, err = cert.UnmarshalPrivateKeyFromPEM(pemPrivateKey)
 		if err != nil {
 			return nil, curve, false, fmt.Errorf("error while unmarshaling pki.key %s: %s", privPathOrPEM, err)
 		}
@@ -198,27 +198,27 @@ func newCertStateFromConfig(c *config.C) (*CertState, error) {
 		}
 	}
 
-	nebulaCert, _, err := cert.UnmarshalNebulaCertificateFromPEM(rawCert)
+	nebulaCert, _, err := cert.UnmarshalCertificateFromPEM(rawCert)
 	if err != nil {
 		return nil, fmt.Errorf("error while unmarshaling pki.cert %s: %s", pubPathOrPEM, err)
 	}
-	nebulaCert.Pkcs11Backed = isPkcs11
+
 	if nebulaCert.Expired(time.Now()) {
 		return nil, fmt.Errorf("nebula certificate for this host is expired")
 	}
 
-	if len(nebulaCert.Details.Ips) == 0 {
-		return nil, fmt.Errorf("no IPs encoded in certificate")
+	if len(nebulaCert.Networks()) == 0 {
+		return nil, fmt.Errorf("no networks encoded in certificate")
 	}
 
 	if err = nebulaCert.VerifyPrivateKey(curve, rawKey); err != nil {
 		return nil, fmt.Errorf("private key is not a pair with public key in nebula cert")
 	}
 
-	return newCertState(nebulaCert, rawKey)
+	return newCertState(nebulaCert, isPkcs11, rawKey)
 }
 
-func loadCAPoolFromConfig(l *logrus.Logger, c *config.C) (*cert.NebulaCAPool, error) {
+func loadCAPoolFromConfig(l *logrus.Logger, c *config.C) (*cert.CAPool, error) {
 	var rawCA []byte
 	var err error
 
@@ -237,11 +237,11 @@ func loadCAPoolFromConfig(l *logrus.Logger, c *config.C) (*cert.NebulaCAPool, er
 		}
 	}
 
-	caPool, err := cert.NewCAPoolFromBytes(rawCA)
+	caPool, err := cert.NewCAPoolFromPEM(rawCA)
 	if errors.Is(err, cert.ErrExpired) {
 		var expired int
 		for _, crt := range caPool.CAs {
-			if crt.Expired(time.Now()) {
+			if crt.Certificate.Expired(time.Now()) {
 				expired++
 				l.WithField("cert", crt).Warn("expired certificate present in CA pool")
 			}

+ 3 - 3
service/service_test.go

@@ -18,9 +18,9 @@ import (
 
 type m map[string]interface{}
 
-func newSimpleService(caCrt *cert.NebulaCertificate, caKey []byte, name string, udpIp netip.Addr, overrides m) *Service {
-	_, _, myPrivKey, myPEM := e2e.NewTestCert(caCrt, caKey, "a", time.Now(), time.Now().Add(5*time.Minute), netip.PrefixFrom(udpIp, 24), nil, []string{})
-	caB, err := caCrt.MarshalToPEM()
+func newSimpleService(caCrt cert.Certificate, caKey []byte, name string, udpIp netip.Addr, overrides m) *Service {
+	_, _, myPrivKey, myPEM := e2e.NewTestCert(caCrt, caKey, "a", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{netip.PrefixFrom(udpIp, 24)}, nil, []string{})
+	caB, err := caCrt.MarshalPEM()
 	if err != nil {
 		panic(err)
 	}

+ 2 - 2
ssh.go

@@ -801,7 +801,7 @@ func sshPrintCert(ifce *Interface, fs interface{}, a []string, w sshd.StringWrit
 			return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn ip: %v", a[0]))
 		}
 
-		cert = hostInfo.GetCert()
+		cert = hostInfo.GetCert().Certificate
 	}
 
 	if args.Json || args.Pretty {
@@ -825,7 +825,7 @@ func sshPrintCert(ifce *Interface, fs interface{}, a []string, w sshd.StringWrit
 	}
 
 	if args.Raw {
-		b, err := cert.MarshalToPEM()
+		b, err := cert.MarshalPEM()
 		if err != nil {
 			//TODO: handle it
 			return nil

+ 6 - 0
test/assert.go

@@ -2,6 +2,7 @@ package test
 
 import (
 	"fmt"
+	"net/netip"
 	"reflect"
 	"testing"
 	"time"
@@ -24,6 +25,11 @@ func AssertDeepCopyEqual(t *testing.T, a interface{}, b interface{}) {
 }
 
 func traverseDeepCopy(t *testing.T, v1 reflect.Value, v2 reflect.Value, name string) bool {
+	if v1.Type() == v2.Type() && v1.Type() == reflect.TypeOf(netip.Addr{}) {
+		// Ignore netip.Addr types since they reuse an interned global value
+		return false
+	}
+
 	switch v1.Kind() {
 	case reflect.Array:
 		for i := 0; i < v1.Len(); i++ {

Неке датотеке нису приказане због велике количине промена