Skip to content

Commit

Permalink
Get ready for a new certificate version by using an interface type
Browse files Browse the repository at this point in the history
  • Loading branch information
nbrownus committed Sep 6, 2024
1 parent aba4b09 commit fc81d9a
Show file tree
Hide file tree
Showing 29 changed files with 1,158 additions and 961 deletions.
129 changes: 105 additions & 24 deletions cert/ca_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import (
)

type CAPool struct {
CAs map[string]*NebulaCertificate
CAs map[string]*CachedCertificate
certBlocklist map[string]struct{}
}

// NewCAPool creates a CAPool
func NewCAPool() *CAPool {
ca := CAPool{
CAs: make(map[string]*NebulaCertificate),
CAs: make(map[string]*CachedCertificate),
certBlocklist: make(map[string]struct{}),
}

Expand Down Expand Up @@ -60,25 +60,46 @@ func (ncp *CAPool) AddCAFromPEM(pemBytes []byte) ([]byte, error) {
return pemBytes, err
}

if !c.Details.IsCA {
return pemBytes, fmt.Errorf("%s: %w", c.Details.Name, ErrNotCA)
err = ncp.AddCA(c)
if err != nil {
return pemBytes, err
}

return pemBytes, nil
}

// TODO:
func (ncp *CAPool) AddCA(c Certificate) error {
if !c.IsCA() {
return fmt.Errorf("%s: %w", c.Name(), ErrNotCA)
}

if !c.CheckSignature(c.Details.PublicKey) {
return pemBytes, fmt.Errorf("%s: %w", c.Details.Name, ErrNotSelfSigned)
if !c.CheckSignature(c.PublicKey()) {
return fmt.Errorf("%s: %w", c.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)
return fmt.Errorf("could not calculate shasum for provided CA; error: %s; %s", err, c.Name())
}

cc := &CachedCertificate{
Certificate: c,
ShaSum: sum,
InvertedGroups: make(map[string]struct{}),
}

ncp.CAs[sum] = c
for _, g := range c.Groups() {
cc.InvertedGroups[g] = struct{}{}
}

ncp.CAs[sum] = cc

if c.Expired(time.Now()) {
return pemBytes, fmt.Errorf("%s: %w", c.Details.Name, ErrExpired)
return fmt.Errorf("%s: %w", c.Name(), ErrExpired)
}

return pemBytes, nil
return nil
}

// BlocklistFingerprint adds a cert fingerprint to the blocklist
Expand All @@ -91,34 +112,94 @@ func (ncp *CAPool) 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 *CAPool) IsBlocklisted(c *NebulaCertificate) bool {
return ncp.isBlocklistedWithCache(c, false)
// TODO:
func (ncp *CAPool) IsBlocklisted(sha string) bool {
if _, ok := ncp.certBlocklist[sha]; ok {
return true
}

return false
}

// IsBlocklisted returns true if the fingerprint fails to generate or has been explicitly blocklisted
func (ncp *CAPool) isBlocklistedWithCache(c *NebulaCertificate, useCache bool) bool {
h, err := c.sha256SumWithCache(useCache)
// 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) {
sha, err := c.Sha256Sum()
if err != nil {
return true
return nil, fmt.Errorf("could not calculate shasum to verify: %w", err)
}

if _, ok := ncp.certBlocklist[h]; ok {
return true
signer, err := ncp.verify(c, now, sha, "")
if err != nil {
return nil, err
}

return false
cc := CachedCertificate{
Certificate: c,
InvertedGroups: make(map[string]struct{}),
ShaSum: sha,
signerShaSum: signer.ShaSum,
}

for _, g := range c.Groups() {
cc.InvertedGroups[g] = struct{}{}
}

return &cc, nil
}

func (ncp *CAPool) VerifyCachedCertificate(now time.Time, c *CachedCertificate) error {
_, err := ncp.verify(c.Certificate, now, c.ShaSum, c.signerShaSum)
return err
}

func (ncp *CAPool) verify(c Certificate, now time.Time, certSha string, signerSha string) (*CachedCertificate, error) {
if ncp.IsBlocklisted(certSha) {
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(signerSha) > 0 {
if signerSha != signer.ShaSum {
return nil, ErrSignatureMismatch
}
return signer, nil
}
if !c.CheckSignature(signer.Certificate.PublicKey()) {
return nil, ErrSignatureMismatch
}

err = c.CheckRootConstraints(signer.Certificate)
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 *NebulaCertificate) (*NebulaCertificate, error) {
if c.Details.Issuer == "" {
func (ncp *CAPool) GetCAForCert(c Certificate) (*CachedCertificate, error) {
if c.Issuer() == "" {
return nil, fmt.Errorf("no issuer in certificate")
}

signer, ok := ncp.CAs[c.Details.Issuer]
signer, ok := ncp.CAs[c.Issuer()]
if ok {
return signer, nil
}
Expand Down
30 changes: 15 additions & 15 deletions cert/ca_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,49 +61,49 @@ IBNWYMep3ysx9zCgknfG5dKtwGTaqF++BWKDYdyl34KX
-----END NEBULA CERTIFICATE-----
`

rootCA := NebulaCertificate{
Details: NebulaCertificateDetails{
rootCA := certificateV1{
details: detailsV1{
Name: "nebula root ca",
},
}

rootCA01 := NebulaCertificate{
Details: NebulaCertificateDetails{
rootCA01 := certificateV1{
details: detailsV1{
Name: "nebula root ca 01",
},
}

rootCAP256 := NebulaCertificate{
Details: NebulaCertificateDetails{
rootCAP256 := certificateV1{
details: detailsV1{
Name: "nebula P256 test",
},
}

p, err := NewCAPoolFromPEM([]byte(noNewLines))
assert.Nil(t, err)
assert.Equal(t, p.CAs[string("c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522")].Details.Name, rootCA.Details.Name)
assert.Equal(t, p.CAs[string("5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd")].Details.Name, rootCA01.Details.Name)
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")].Details.Name, rootCA.Details.Name)
assert.Equal(t, pp.CAs[string("5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd")].Details.Name, rootCA01.Details.Name)
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")].Details.Name, "expired")
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")].Details.Name, rootCA.Details.Name)
assert.Equal(t, pppp.CAs[string("5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd")].Details.Name, rootCA01.Details.Name)
assert.Equal(t, pppp.CAs[string("152070be6bb19bc9e3bde4c2f0e7d8f4ff5448b4c9856b8eccb314fade0229b0")].Details.Name, "expired")
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")].Details.Name, rootCAP256.Details.Name)
assert.Equal(t, ppppp.CAs[string("a7938893ec8c4ef769b06d7f425e5e46f7a7f5ffa49c3bcf4a86b608caba9159")].Certificate.Name(), rootCAP256.details.Name)
assert.Equal(t, len(ppppp.CAs), 1)
}
132 changes: 132 additions & 0 deletions cert/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package cert

import (
"net/netip"
"time"
)

type Version int

const (
Version1 Version = 1
Version2 Version = 2
)

type Certificate interface {
//TODO: describe this
Version() Version

// Name is the human-readable name that identifies this certificate.
Name() string

// 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

// 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

// 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

// 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

// 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

// 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

// Issuer is the fingerprint of the CA that signed this certificate.
// If IsCA is true then this will be empty.
Issuer() string //TODO: string or bytes?

// PublicKey is the raw bytes to be used in asymmetric cryptographic operations.
PublicKey() []byte

// Curve identifies which curve was used for the PublicKey and Signature.
Curve() Curve

// 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 //TODO: string or bytes?

// 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

// Sha256Sum returns the hex encoded sha256 sum of the certificate.
// This acts as a unique fingerprint and can be used to blocklist certificates.
Sha256Sum() (string, error)

// Expired tests if the certificate is valid for the provided time.
Expired(t time.Time) bool

// CheckRootConstraints tests if the certificate meets all constraints in the
// signing certificate, returning the first violated constraint or nil if the
// certificate conforms to all constraints.
//TODO: feels better to have this on the CAPool I think
CheckRootConstraints(signer Certificate) error

//TODO
VerifyPrivateKey(curve Curve, privateKey []byte) error

// Marshal will return the byte representation of this certificate
// This is primarily the format transmitted on the wire.
Marshal() ([]byte, error)

// MarshalForHandshakes prepares the bytes needed to use directly in a handshake
MarshalForHandshakes() ([]byte, error)

// MarshalToPEM will return a PEM encoded representation of this certificate
// This is primarily the format stored on disk
//TODO: MarshalPEM?
MarshalToPEM() ([]byte, error)

// MarshalJSON will return the json representation of this certificate
MarshalJSON() ([]byte, error)

// String will return a human-readable representation of this certificate
String() string

//TODO
Copy() Certificate
}

// CachedCertificate represents a verified certificate with some cached fields to improve
// performance.
type CachedCertificate struct {
Certificate Certificate
InvertedGroups map[string]struct{}
ShaSum string
signerShaSum string
}

// TODO:
func UnmarshalCertificate(b []byte) (Certificate, error) {
c, err := unmarshalCertificateV1(b, true)
if err != nil {
return nil, err
}
return c, nil
}

// TODO:
func UnmarshalCertificateFromHandshake(b []byte, publicKey []byte) (Certificate, error) {
c, err := unmarshalCertificateV1(b, false)
if err != nil {
return nil, err
}
c.details.PublicKey = publicKey
return c, nil
}
Loading

0 comments on commit fc81d9a

Please sign in to comment.