diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65a6e3e86..2b27f52f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,6 +67,24 @@ jobs: - name: End 2 end run: make e2evv GOEXPERIMENT=boringcrypto CGO_ENABLED=1 + test-linux-pkcs11: + name: Build and test on linux with pkcs11 + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + check-latest: true + + - name: Build + run: make bin-pkcs11 + + - name: Test + run: make test-pkcs11 + test: name: Build and test on ${{ matrix.os }} runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 0efb96756..0bffc857c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,6 @@ **.crt **.key **.pem +**.pub !/examples/quickstart-vagrant/ansible/roles/nebula/files/vagrant-test-ca.key !/examples/quickstart-vagrant/ansible/roles/nebula/files/vagrant-test-ca.crt diff --git a/CHANGELOG.md b/CHANGELOG.md index f763b69aa..ad1714780 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.9.4] - 2024-09-09 + +### Added + +- Support UDP dialing with gVisor. (#1181) + +### Changed + +- Make some Nebula state programmatically available via control object. (#1188) +- Switch internal representation of IPs to netip, to prepare for IPv6 support + in the overlay. (#1173) +- Minor build and cleanup changes. (#1171, #1164, #1162) +- Various dependency updates. (#1195, #1190, #1174, #1168, #1167, #1161, #1147, #1146) + +### Fixed + +- Fix a bug on big endian hosts, like mips. (#1194) +- Fix a rare panic if a local index collision happens. (#1191) +- Fix integer wraparound in the calculation of handshake timeouts on 32-bit targets. (#1185) + ## [1.9.3] - 2024-06-06 ### Fixed @@ -644,7 +664,8 @@ created.) - Initial public release. -[Unreleased]: https://github.com/slackhq/nebula/compare/v1.9.3...HEAD +[Unreleased]: https://github.com/slackhq/nebula/compare/v1.9.4...HEAD +[1.9.4]: https://github.com/slackhq/nebula/releases/tag/v1.9.4 [1.9.3]: https://github.com/slackhq/nebula/releases/tag/v1.9.3 [1.9.2]: https://github.com/slackhq/nebula/releases/tag/v1.9.2 [1.9.1]: https://github.com/slackhq/nebula/releases/tag/v1.9.1 diff --git a/Makefile b/Makefile index 0d0943f0a..6922cc3e8 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ ALL_LINUX = linux-amd64 \ linux-mips64le \ linux-mips-softfloat \ linux-riscv64 \ - linux-loong64 + linux-loong64 ALL_FREEBSD = freebsd-amd64 \ freebsd-arm64 @@ -63,7 +63,7 @@ ALL = $(ALL_LINUX) \ e2e: $(TEST_ENV) go test -tags=e2e_testing -count=1 $(TEST_FLAGS) ./e2e -e2ev: TEST_FLAGS = -v +e2ev: TEST_FLAGS += -v e2ev: e2e e2evv: TEST_ENV += TEST_LOGS=1 @@ -96,7 +96,7 @@ release-netbsd: $(ALL_NETBSD:%=build/nebula-%.tar.gz) release-boringcrypto: build/nebula-linux-$(shell go env GOARCH)-boringcrypto.tar.gz -BUILD_ARGS = -trimpath +BUILD_ARGS += -trimpath bin-windows: build/windows-amd64/nebula.exe build/windows-amd64/nebula-cert.exe mv $? . @@ -116,6 +116,10 @@ bin-freebsd-arm64: build/freebsd-arm64/nebula build/freebsd-arm64/nebula-cert bin-boringcrypto: build/linux-$(shell go env GOARCH)-boringcrypto/nebula build/linux-$(shell go env GOARCH)-boringcrypto/nebula-cert mv $? . +bin-pkcs11: BUILD_ARGS += -tags pkcs11 +bin-pkcs11: CGO_ENABLED = 1 +bin-pkcs11: bin + bin: go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula${NEBULA_CMD_SUFFIX} ${NEBULA_CMD_PATH} go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula-cert${NEBULA_CMD_SUFFIX} ./cmd/nebula-cert @@ -168,6 +172,9 @@ test: test-boringcrypto: GOEXPERIMENT=boringcrypto CGO_ENABLED=1 go test -v ./... +test-pkcs11: + CGO_ENABLED=1 go test -v -tags pkcs11 ./... + test-cov-html: go test -coverprofile=coverage.out go tool cover -html=coverage.out diff --git a/cert/Makefile b/cert/Makefile index 28170b622..311afc24e 100644 --- a/cert/Makefile +++ b/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 diff --git a/cert/ca.go b/cert/ca.go deleted file mode 100644 index 0ffbd8792..000000000 --- a/cert/ca.go +++ /dev/null @@ -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 -} diff --git a/cert/ca_pool.go b/cert/ca_pool.go new file mode 100644 index 000000000..d52583035 --- /dev/null +++ b/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 +} diff --git a/cert/ca_pool_test.go b/cert/ca_pool_test.go new file mode 100644 index 000000000..053640d98 --- /dev/null +++ b/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) +} diff --git a/cert/cert.go b/cert/cert.go index a0164f7bc..02c88777c 100644 --- a/cert/cert.go +++ b/cert/cert.go @@ -1,1029 +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" - - "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 - 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 NebulaEncryptedData struct { - EncryptionMetadata NebulaEncryptionMetadata - Ciphertext []byte -} - -type NebulaEncryptionMetadata struct { - EncryptionAlgorithm string - Argon2Parameters Argon2Parameters -} - -type m map[string]interface{} - -// Returned if we try to unmarshal an encrypted private key without a passphrase -var ErrPrivateKeyEncrypted = errors.New("private key must be decrypted") - -// 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 - } - - if rc.Details == nil { - return nil, fmt.Errorf("encoded Details was nil") - } +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 - if len(rc.Details.Ips)%2 != 0 { - return nil, fmt.Errorf("encoded IPs should be in pairs, an odd number was found") - } + // Name is the human-readable name that identifies this certificate. + Name() string - if len(rc.Details.Subnets)%2 != 0 { - return nil, fmt.Errorf("encoded Subnets should be in pairs, an odd number was found") - } + // 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 - 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)), - } + // 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 - copy(nc.Signature, rc.Signature) - copy(nc.Details.Groups, rc.Details.Groups) - nc.Details.Issuer = hex.EncodeToString(rc.Details.Issuer) + // 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 - 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) + // 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 - 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)) - } - } + // 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 - 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)) - } - } + // 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 - for _, g := range rc.Details.Groups { - nc.Details.InvertedGroups[g] = struct{}{} - } + // Issuer is the fingerprint of the CA that signed this certificate. + // If IsCA is true then this will be empty. + Issuer() string - return &nc, nil -} + // PublicKey is the raw bytes to be used in asymmetric cryptographic operations. + PublicKey() []byte -// 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 -} + // Curve identifies which curve was used for the PublicKey and Signature. + Curve() Curve -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 - } -} + // 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 -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 - } -} + // 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 -// 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}) -} + // 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) -// 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}) -} + // Expired tests if the certificate is valid for the provided time. + Expired(t time.Time) bool -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 -} + // VerifyPrivateKey returns an error if the private key is not a pair with the certificates public key. + VerifyPrivateKey(curve Curve, privateKey []byte) error -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 -} + // Marshal will return the byte representation of this certificate + // This is primarily the format transmitted on the wire. + Marshal() ([]byte, error) -// 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 - } + // MarshalForHandshakes prepares the bytes needed to use directly in a handshake + MarshalForHandshakes() ([]byte, error) - 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 - } + // MarshalPEM will return a PEM encoded representation of this certificate + // This is primarily the format stored on disk + MarshalPEM() ([]byte, error) - 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) - } -} + // MarshalJSON will return the json representation of this certificate + MarshalJSON() ([]byte, error) -// 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") - } + // String will return a human-readable representation of this certificate + String() string - return k.Bytes, r, nil + // Copy creates a copy of the certificate + Copy() Certificate } -// 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 +// 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 } -// 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) +// 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 } - - 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: 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 -} - -// 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) + return c, 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 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() +// 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 } - 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 } diff --git a/cert/cert_test.go b/cert/cert_test.go index 30e99eca1..12bbd9700 100644 --- a/cert/cert_test.go +++ b/cert/cert_test.go @@ -7,7 +7,7 @@ import ( "crypto/rand" "fmt" "io" - "net" + "net/netip" "testing" "time" @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/ed25519" - "google.golang.org/protobuf/proto" ) func TestMarshalingNebulaCertificate(t *testing.T) { @@ -23,18 +22,16 @@ func TestMarshalingNebulaCertificate(t *testing.T) { after := time.Now().Add(time.Second * 60).Round(time.Second) pubKey := []byte("1234567890abcedfghij1234567890ab") - nc := NebulaCertificate{ - Details: NebulaCertificateDetails{ + nc := certificateV1{ + details: detailsV1{ Name: "testing", - Ips: []*net.IPNet{ - {IP: net.ParseIP("10.1.1.1"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("10.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, - {IP: net.ParseIP("10.1.1.3"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, + Ips: []netip.Prefix{ + mustParsePrefixUnmapped("10.1.1.1/24"), + mustParsePrefixUnmapped("10.1.1.2/16"), }, - Subnets: []*net.IPNet{ - {IP: net.ParseIP("9.1.1.1"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, - {IP: net.ParseIP("9.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("9.1.1.3"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, + Subnets: []netip.Prefix{ + mustParsePrefixUnmapped("9.1.1.2/24"), + mustParsePrefixUnmapped("9.1.1.3/16"), }, Groups: []string{"test-group1", "test-group2", "test-group3"}, NotBefore: before, @@ -43,120 +40,116 @@ func TestMarshalingNebulaCertificate(t *testing.T) { IsCA: false, Issuer: "1234567890abcedfghij1234567890ab", }, - Signature: []byte("1234567890abcedfghij1234567890ab"), + signature: []byte("1234567890abcedfghij1234567890ab"), } b, err := nc.Marshal() assert.Nil(t, err) //t.Log("Cert size:", len(b)) - nc2, err := UnmarshalNebulaCertificate(b) + nc2, err := unmarshalCertificateV1(b, true) assert.Nil(t, err) - assert.Equal(t, nc.Signature, nc2.Signature) - assert.Equal(t, nc.Details.Name, nc2.Details.Name) - assert.Equal(t, nc.Details.NotBefore, nc2.Details.NotBefore) - assert.Equal(t, nc.Details.NotAfter, nc2.Details.NotAfter) - assert.Equal(t, nc.Details.PublicKey, nc2.Details.PublicKey) - assert.Equal(t, nc.Details.IsCA, nc2.Details.IsCA) - - // IP byte arrays can be 4 or 16 in length so we have to go this route - assert.Equal(t, len(nc.Details.Ips), len(nc2.Details.Ips)) - for i, wIp := range nc.Details.Ips { - assert.Equal(t, wIp.String(), nc2.Details.Ips[i].String()) - } - - assert.Equal(t, len(nc.Details.Subnets), len(nc2.Details.Subnets)) - for i, wIp := range nc.Details.Subnets { - assert.Equal(t, wIp.String(), nc2.Details.Subnets[i].String()) - } - - assert.EqualValues(t, nc.Details.Groups, nc2.Details.Groups) -} - -func TestNebulaCertificate_Sign(t *testing.T) { - before := time.Now().Add(time.Second * -60).Round(time.Second) - after := time.Now().Add(time.Second * 60).Round(time.Second) - pubKey := []byte("1234567890abcedfghij1234567890ab") - - nc := NebulaCertificate{ - Details: NebulaCertificateDetails{ - Name: "testing", - Ips: []*net.IPNet{ - {IP: net.ParseIP("10.1.1.1"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("10.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, - {IP: net.ParseIP("10.1.1.3"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, - }, - Subnets: []*net.IPNet{ - {IP: net.ParseIP("9.1.1.1"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, - {IP: net.ParseIP("9.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("9.1.1.3"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, - }, - Groups: []string{"test-group1", "test-group2", "test-group3"}, - NotBefore: before, - NotAfter: after, - PublicKey: pubKey, - IsCA: false, - Issuer: "1234567890abcedfghij1234567890ab", - }, - } + assert.Equal(t, nc.signature, nc2.Signature()) + assert.Equal(t, nc.details.Name, nc2.Name()) + assert.Equal(t, nc.details.NotBefore, nc2.NotBefore()) + assert.Equal(t, nc.details.NotAfter, nc2.NotAfter()) + assert.Equal(t, nc.details.PublicKey, nc2.PublicKey()) + assert.Equal(t, nc.details.IsCA, nc2.IsCA()) - pub, priv, err := ed25519.GenerateKey(rand.Reader) - assert.Nil(t, err) - assert.False(t, nc.CheckSignature(pub)) - assert.Nil(t, nc.Sign(Curve_CURVE25519, priv)) - assert.True(t, nc.CheckSignature(pub)) + assert.Equal(t, nc.details.Ips, nc2.Networks()) + assert.Equal(t, nc.details.Subnets, nc2.UnsafeNetworks()) - _, err = nc.Marshal() - assert.Nil(t, err) - //t.Log("Cert size:", len(b)) + assert.Equal(t, nc.details.Groups, nc2.Groups()) } -func TestNebulaCertificate_SignP256(t *testing.T) { - before := time.Now().Add(time.Second * -60).Round(time.Second) - after := time.Now().Add(time.Second * 60).Round(time.Second) - pubKey := []byte("01234567890abcedfghij1234567890ab1234567890abcedfghij1234567890ab") - - nc := NebulaCertificate{ - Details: NebulaCertificateDetails{ - Name: "testing", - Ips: []*net.IPNet{ - {IP: net.ParseIP("10.1.1.1"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("10.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, - {IP: net.ParseIP("10.1.1.3"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, - }, - Subnets: []*net.IPNet{ - {IP: net.ParseIP("9.1.1.1"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, - {IP: net.ParseIP("9.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("9.1.1.3"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, - }, - Groups: []string{"test-group1", "test-group2", "test-group3"}, - NotBefore: before, - NotAfter: after, - PublicKey: pubKey, - IsCA: false, - Curve: Curve_P256, - Issuer: "1234567890abcedfghij1234567890ab", - }, - } - - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - pub := elliptic.Marshal(elliptic.P256(), priv.PublicKey.X, priv.PublicKey.Y) - rawPriv := priv.D.FillBytes(make([]byte, 32)) - - assert.Nil(t, err) - assert.False(t, nc.CheckSignature(pub)) - assert.Nil(t, nc.Sign(Curve_P256, rawPriv)) - assert.True(t, nc.CheckSignature(pub)) - - _, err = nc.Marshal() - assert.Nil(t, err) - //t.Log("Cert size:", len(b)) -} +//func TestNebulaCertificate_Sign(t *testing.T) { +// before := time.Now().Add(time.Second * -60).Round(time.Second) +// after := time.Now().Add(time.Second * 60).Round(time.Second) +// pubKey := []byte("1234567890abcedfghij1234567890ab") +// +// nc := certificateV1{ +// details: detailsV1{ +// Name: "testing", +// Ips: []netip.Prefix{ +// mustParsePrefixUnmapped("10.1.1.1/24"), +// mustParsePrefixUnmapped("10.1.1.2/16"), +// //TODO: netip cant do it +// //{IP: net.ParseIP("10.1.1.3"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, +// }, +// Subnets: []netip.Prefix{ +// //TODO: netip cant do it +// //{IP: net.ParseIP("9.1.1.1"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, +// mustParsePrefixUnmapped("9.1.1.2/24"), +// mustParsePrefixUnmapped("9.1.1.3/24"), +// }, +// Groups: []string{"test-group1", "test-group2", "test-group3"}, +// NotBefore: before, +// NotAfter: after, +// PublicKey: pubKey, +// IsCA: false, +// Issuer: "1234567890abcedfghij1234567890ab", +// }, +// } +// +// pub, priv, err := ed25519.GenerateKey(rand.Reader) +// assert.Nil(t, err) +// assert.False(t, nc.CheckSignature(pub)) +// assert.Nil(t, nc.Sign(Curve_CURVE25519, priv)) +// assert.True(t, nc.CheckSignature(pub)) +// +// _, err = nc.Marshal() +// assert.Nil(t, err) +// //t.Log("Cert size:", len(b)) +//} + +//func TestNebulaCertificate_SignP256(t *testing.T) { +// before := time.Now().Add(time.Second * -60).Round(time.Second) +// after := time.Now().Add(time.Second * 60).Round(time.Second) +// pubKey := []byte("01234567890abcedfghij1234567890ab1234567890abcedfghij1234567890ab") +// +// nc := certificateV1{ +// details: detailsV1{ +// Name: "testing", +// Ips: []netip.Prefix{ +// mustParsePrefixUnmapped("10.1.1.1/24"), +// mustParsePrefixUnmapped("10.1.1.2/16"), +// //TODO: netip no can do +// //{IP: net.ParseIP("10.1.1.3"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, +// }, +// Subnets: []netip.Prefix{ +// //TODO: netip bad +// //{IP: net.ParseIP("9.1.1.1"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, +// mustParsePrefixUnmapped("9.1.1.2/24"), +// mustParsePrefixUnmapped("9.1.1.3/16"), +// }, +// Groups: []string{"test-group1", "test-group2", "test-group3"}, +// NotBefore: before, +// NotAfter: after, +// PublicKey: pubKey, +// IsCA: false, +// Curve: Curve_P256, +// Issuer: "1234567890abcedfghij1234567890ab", +// }, +// } +// +// priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +// pub := elliptic.Marshal(elliptic.P256(), priv.PublicKey.X, priv.PublicKey.Y) +// rawPriv := priv.D.FillBytes(make([]byte, 32)) +// +// assert.Nil(t, err) +// assert.False(t, nc.CheckSignature(pub)) +// assert.Nil(t, nc.Sign(Curve_P256, rawPriv)) +// assert.True(t, nc.CheckSignature(pub)) +// +// _, err = nc.Marshal() +// assert.Nil(t, err) +// //t.Log("Cert size:", len(b)) +//} func TestNebulaCertificate_Expired(t *testing.T) { - nc := NebulaCertificate{ - Details: NebulaCertificateDetails{ + nc := certificateV1{ + details: detailsV1{ NotBefore: time.Now().Add(time.Second * -60).Round(time.Second), NotAfter: time.Now().Add(time.Second * 60).Round(time.Second), }, @@ -171,18 +164,16 @@ func TestNebulaCertificate_MarshalJSON(t *testing.T) { time.Local = time.UTC pubKey := []byte("1234567890abcedfghij1234567890ab") - nc := NebulaCertificate{ - Details: NebulaCertificateDetails{ + nc := certificateV1{ + details: detailsV1{ Name: "testing", - Ips: []*net.IPNet{ - {IP: net.ParseIP("10.1.1.1"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("10.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, - {IP: net.ParseIP("10.1.1.3"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, + Ips: []netip.Prefix{ + mustParsePrefixUnmapped("10.1.1.1/24"), + mustParsePrefixUnmapped("10.1.1.2/16"), }, - Subnets: []*net.IPNet{ - {IP: net.ParseIP("9.1.1.1"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, - {IP: net.ParseIP("9.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("9.1.1.3"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, + Subnets: []netip.Prefix{ + mustParsePrefixUnmapped("9.1.1.2/24"), + mustParsePrefixUnmapped("9.1.1.3/16"), }, Groups: []string{"test-group1", "test-group2", "test-group3"}, NotBefore: time.Date(1, 0, 0, 1, 0, 0, 0, time.UTC), @@ -191,306 +182,256 @@ func TestNebulaCertificate_MarshalJSON(t *testing.T) { IsCA: false, Issuer: "1234567890abcedfghij1234567890ab", }, - Signature: []byte("1234567890abcedfghij1234567890ab"), + signature: []byte("1234567890abcedfghij1234567890ab"), } b, err := nc.MarshalJSON() assert.Nil(t, err) assert.Equal( t, - "{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"test-group1\",\"test-group2\",\"test-group3\"],\"ips\":[\"10.1.1.1/24\",\"10.1.1.2/16\",\"10.1.1.3/ff00ff00\"],\"isCa\":false,\"issuer\":\"1234567890abcedfghij1234567890ab\",\"name\":\"testing\",\"notAfter\":\"0000-11-30T02:00:00Z\",\"notBefore\":\"0000-11-30T01:00:00Z\",\"publicKey\":\"313233343536373839306162636564666768696a313233343536373839306162\",\"subnets\":[\"9.1.1.1/ff00ff00\",\"9.1.1.2/24\",\"9.1.1.3/16\"]},\"fingerprint\":\"26cb1c30ad7872c804c166b5150fa372f437aa3856b04edb4334b4470ec728e4\",\"signature\":\"313233343536373839306162636564666768696a313233343536373839306162\"}", + "{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"test-group1\",\"test-group2\",\"test-group3\"],\"ips\":[\"10.1.1.1/24\",\"10.1.1.2/16\"],\"isCa\":false,\"issuer\":\"1234567890abcedfghij1234567890ab\",\"name\":\"testing\",\"notAfter\":\"0000-11-30T02:00:00Z\",\"notBefore\":\"0000-11-30T01:00:00Z\",\"publicKey\":\"313233343536373839306162636564666768696a313233343536373839306162\",\"subnets\":[\"9.1.1.2/24\",\"9.1.1.3/16\"]},\"fingerprint\":\"3944c53d4267a229295b56cb2d27d459164c010ac97d655063ba421e0670f4ba\",\"signature\":\"313233343536373839306162636564666768696a313233343536373839306162\"}", string(b), ) } func TestNebulaCertificate_Verify(t *testing.T) { - ca, _, caKey, err := newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{}) + ca, _, caKey, err := newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil) assert.Nil(t, err) - c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{}) - assert.Nil(t, err) - - h, err := ca.Sha256Sum() + c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil) assert.Nil(t, err) caPool := NewCAPool() - caPool.CAs[h] = ca + assert.NoError(t, caPool.AddCA(ca)) - f, err := c.Sha256Sum() + f, err := c.Fingerprint() assert.Nil(t, err) caPool.BlocklistFingerprint(f) - v, err := c.Verify(time.Now(), caPool) - assert.False(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.EqualError(t, err, "certificate is in the block list") caPool.ResetCertBlocklist() - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) - v, err = c.Verify(time.Now().Add(time.Hour*1000), caPool) - assert.False(t, v) + _, err = caPool.VerifyCertificate(time.Now().Add(time.Hour*1000), c) assert.EqualError(t, err, "root certificate is expired") - c, _, _, err = newTestCert(ca, caKey, time.Time{}, time.Time{}, []*net.IPNet{}, []*net.IPNet{}, []string{}) - assert.Nil(t, err) - v, err = c.Verify(time.Now().Add(time.Minute*6), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate is expired") + c, _, _, err = newTestCert(ca, caKey, time.Time{}, time.Time{}, nil, nil, nil) + assert.EqualError(t, err, "certificate is valid before the signing certificate") // Test group assertion - ca, _, caKey, err = newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{"test1", "test2"}) + ca, _, caKey, err = newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{"test1", "test2"}) assert.Nil(t, err) - caPem, err := ca.MarshalToPEM() + caPem, err := ca.MarshalPEM() assert.Nil(t, err) caPool = NewCAPool() - caPool.AddCACertificate(caPem) + b, err := caPool.AddCAFromPEM(caPem) + assert.NoError(t, err) + assert.Empty(t, b) - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{"test1", "bad"}) - assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.False(t, v) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1", "bad"}) assert.EqualError(t, err, "certificate contained a group not present on the signing ca: bad") - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{"test1"}) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) } func TestNebulaCertificate_VerifyP256(t *testing.T) { - ca, _, caKey, err := newTestCaCertP256(time.Now(), time.Now().Add(10*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{}) + ca, _, caKey, err := newTestCaCertP256(time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil) assert.Nil(t, err) - c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{}) - assert.Nil(t, err) - - h, err := ca.Sha256Sum() + c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil) assert.Nil(t, err) caPool := NewCAPool() - caPool.CAs[h] = ca + assert.NoError(t, caPool.AddCA(ca)) - f, err := c.Sha256Sum() + f, err := c.Fingerprint() assert.Nil(t, err) caPool.BlocklistFingerprint(f) - v, err := c.Verify(time.Now(), caPool) - assert.False(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.EqualError(t, err, "certificate is in the block list") caPool.ResetCertBlocklist() - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) - v, err = c.Verify(time.Now().Add(time.Hour*1000), caPool) - assert.False(t, v) + _, err = caPool.VerifyCertificate(time.Now().Add(time.Hour*1000), c) assert.EqualError(t, err, "root certificate is expired") - c, _, _, err = newTestCert(ca, caKey, time.Time{}, time.Time{}, []*net.IPNet{}, []*net.IPNet{}, []string{}) - assert.Nil(t, err) - v, err = c.Verify(time.Now().Add(time.Minute*6), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate is expired") + c, _, _, err = newTestCert(ca, caKey, time.Time{}, time.Time{}, nil, nil, nil) + assert.EqualError(t, err, "certificate is valid before the signing certificate") // Test group assertion - ca, _, caKey, err = newTestCaCertP256(time.Now(), time.Now().Add(10*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{"test1", "test2"}) + ca, _, caKey, err = newTestCaCertP256(time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{"test1", "test2"}) assert.Nil(t, err) - caPem, err := ca.MarshalToPEM() + caPem, err := ca.MarshalPEM() assert.Nil(t, err) caPool = NewCAPool() - caPool.AddCACertificate(caPem) + b, err := caPool.AddCAFromPEM(caPem) + assert.NoError(t, err) + assert.Empty(t, b) - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{"test1", "bad"}) - assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.False(t, v) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1", "bad"}) assert.EqualError(t, err, "certificate contained a group not present on the signing ca: bad") - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{"test1"}) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) } func TestNebulaCertificate_Verify_IPs(t *testing.T) { - _, caIp1, _ := net.ParseCIDR("10.0.0.0/16") - _, caIp2, _ := net.ParseCIDR("192.168.0.0/24") - ca, _, caKey, err := newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), []*net.IPNet{caIp1, caIp2}, []*net.IPNet{}, []string{"test"}) + caIp1 := mustParsePrefixUnmapped("10.0.0.0/16") + caIp2 := mustParsePrefixUnmapped("192.168.0.0/24") + ca, _, caKey, err := newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), []netip.Prefix{caIp1, caIp2}, nil, []string{"test"}) assert.Nil(t, err) - caPem, err := ca.MarshalToPEM() + caPem, err := ca.MarshalPEM() assert.Nil(t, err) caPool := NewCAPool() - caPool.AddCACertificate(caPem) + b, err := caPool.AddCAFromPEM(caPem) + assert.NoError(t, err) + assert.Empty(t, b) // ip is outside the network - cIp1 := &net.IPNet{IP: net.ParseIP("10.1.0.0"), Mask: []byte{255, 255, 255, 0}} - cIp2 := &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 0, 0}} - c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{cIp1, cIp2}, []*net.IPNet{}, []string{"test"}) - assert.Nil(t, err) - v, err := c.Verify(time.Now(), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate contained an ip assignment outside the limitations of the signing ca: 10.1.0.0/24") + cIp1 := mustParsePrefixUnmapped("10.1.0.0/24") + cIp2 := mustParsePrefixUnmapped("192.168.0.1/16") + c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"}) + assert.EqualError(t, err, "certificate contained a network assignment outside the limitations of the signing ca: 10.1.0.0/24") // ip is outside the network reversed order of above - cIp1 = &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 255, 0}} - cIp2 = &net.IPNet{IP: net.ParseIP("10.1.0.0"), Mask: []byte{255, 255, 255, 0}} - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{cIp1, cIp2}, []*net.IPNet{}, []string{"test"}) - assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate contained an ip assignment outside the limitations of the signing ca: 10.1.0.0/24") + cIp1 = mustParsePrefixUnmapped("192.168.0.1/24") + cIp2 = mustParsePrefixUnmapped("10.1.0.0/24") + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"}) + assert.EqualError(t, err, "certificate contained a network assignment outside the limitations of the signing ca: 10.1.0.0/24") // ip is within the network but mask is outside - cIp1 = &net.IPNet{IP: net.ParseIP("10.0.1.0"), Mask: []byte{255, 254, 0, 0}} - cIp2 = &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 255, 0}} - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{cIp1, cIp2}, []*net.IPNet{}, []string{"test"}) - assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate contained an ip assignment outside the limitations of the signing ca: 10.0.1.0/15") + cIp1 = mustParsePrefixUnmapped("10.0.1.0/15") + cIp2 = mustParsePrefixUnmapped("192.168.0.1/24") + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"}) + assert.EqualError(t, err, "certificate contained a network assignment outside the limitations of the signing ca: 10.0.1.0/15") // ip is within the network but mask is outside reversed order of above - cIp1 = &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 255, 0}} - cIp2 = &net.IPNet{IP: net.ParseIP("10.0.1.0"), Mask: []byte{255, 254, 0, 0}} - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{cIp1, cIp2}, []*net.IPNet{}, []string{"test"}) - assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate contained an ip assignment outside the limitations of the signing ca: 10.0.1.0/15") + cIp1 = mustParsePrefixUnmapped("192.168.0.1/24") + cIp2 = mustParsePrefixUnmapped("10.0.1.0/15") + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"}) + assert.EqualError(t, err, "certificate contained a network assignment outside the limitations of the signing ca: 10.0.1.0/15") // ip and mask are within the network - cIp1 = &net.IPNet{IP: net.ParseIP("10.0.1.0"), Mask: []byte{255, 255, 0, 0}} - cIp2 = &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 255, 128}} - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{cIp1, cIp2}, []*net.IPNet{}, []string{"test"}) + cIp1 = mustParsePrefixUnmapped("10.0.1.0/16") + cIp2 = mustParsePrefixUnmapped("192.168.0.1/25") + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) // Exact matches - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{caIp1, caIp2}, []*net.IPNet{}, []string{"test"}) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{caIp1, caIp2}, nil, []string{"test"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) // Exact matches reversed - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{caIp2, caIp1}, []*net.IPNet{}, []string{"test"}) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{caIp2, caIp1}, nil, []string{"test"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) // Exact matches reversed with just 1 - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{caIp1}, []*net.IPNet{}, []string{"test"}) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{caIp1}, nil, []string{"test"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) } func TestNebulaCertificate_Verify_Subnets(t *testing.T) { - _, caIp1, _ := net.ParseCIDR("10.0.0.0/16") - _, caIp2, _ := net.ParseCIDR("192.168.0.0/24") - ca, _, caKey, err := newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), []*net.IPNet{}, []*net.IPNet{caIp1, caIp2}, []string{"test"}) + caIp1 := mustParsePrefixUnmapped("10.0.0.0/16") + caIp2 := mustParsePrefixUnmapped("192.168.0.0/24") + ca, _, caKey, err := newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), nil, []netip.Prefix{caIp1, caIp2}, []string{"test"}) assert.Nil(t, err) - caPem, err := ca.MarshalToPEM() + caPem, err := ca.MarshalPEM() assert.Nil(t, err) caPool := NewCAPool() - caPool.AddCACertificate(caPem) + b, err := caPool.AddCAFromPEM(caPem) + assert.NoError(t, err) + assert.Empty(t, b) // ip is outside the network - cIp1 := &net.IPNet{IP: net.ParseIP("10.1.0.0"), Mask: []byte{255, 255, 255, 0}} - cIp2 := &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 0, 0}} - c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{cIp1, cIp2}, []string{"test"}) - assert.Nil(t, err) - v, err := c.Verify(time.Now(), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate contained a subnet assignment outside the limitations of the signing ca: 10.1.0.0/24") + cIp1 := mustParsePrefixUnmapped("10.1.0.0/24") + cIp2 := mustParsePrefixUnmapped("192.168.0.1/16") + c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"}) + assert.EqualError(t, err, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.1.0.0/24") // ip is outside the network reversed order of above - cIp1 = &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 255, 0}} - cIp2 = &net.IPNet{IP: net.ParseIP("10.1.0.0"), Mask: []byte{255, 255, 255, 0}} - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{cIp1, cIp2}, []string{"test"}) - assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate contained a subnet assignment outside the limitations of the signing ca: 10.1.0.0/24") + cIp1 = mustParsePrefixUnmapped("192.168.0.1/24") + cIp2 = mustParsePrefixUnmapped("10.1.0.0/24") + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"}) + assert.EqualError(t, err, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.1.0.0/24") // ip is within the network but mask is outside - cIp1 = &net.IPNet{IP: net.ParseIP("10.0.1.0"), Mask: []byte{255, 254, 0, 0}} - cIp2 = &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 255, 0}} - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{cIp1, cIp2}, []string{"test"}) - assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate contained a subnet assignment outside the limitations of the signing ca: 10.0.1.0/15") + cIp1 = mustParsePrefixUnmapped("10.0.1.0/15") + cIp2 = mustParsePrefixUnmapped("192.168.0.1/24") + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"}) + assert.EqualError(t, err, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.0.1.0/15") // ip is within the network but mask is outside reversed order of above - cIp1 = &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 255, 0}} - cIp2 = &net.IPNet{IP: net.ParseIP("10.0.1.0"), Mask: []byte{255, 254, 0, 0}} - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{cIp1, cIp2}, []string{"test"}) - assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.False(t, v) - assert.EqualError(t, err, "certificate contained a subnet assignment outside the limitations of the signing ca: 10.0.1.0/15") + cIp1 = mustParsePrefixUnmapped("192.168.0.1/24") + cIp2 = mustParsePrefixUnmapped("10.0.1.0/15") + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"}) + assert.EqualError(t, err, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.0.1.0/15") // ip and mask are within the network - cIp1 = &net.IPNet{IP: net.ParseIP("10.0.1.0"), Mask: []byte{255, 255, 0, 0}} - cIp2 = &net.IPNet{IP: net.ParseIP("192.168.0.1"), Mask: []byte{255, 255, 255, 128}} - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{cIp1, cIp2}, []string{"test"}) + cIp1 = mustParsePrefixUnmapped("10.0.1.0/16") + cIp2 = mustParsePrefixUnmapped("192.168.0.1/25") + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) // Exact matches - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{caIp1, caIp2}, []string{"test"}) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{caIp1, caIp2}, []string{"test"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) // Exact matches reversed - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{caIp2, caIp1}, []string{"test"}) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{caIp2, caIp1}, []string{"test"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) // Exact matches reversed with just 1 - c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{caIp1}, []string{"test"}) + c, _, _, err = newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{caIp1}, []string{"test"}) assert.Nil(t, err) - v, err = c.Verify(time.Now(), caPool) - assert.True(t, v) + _, err = caPool.VerifyCertificate(time.Now(), c) assert.Nil(t, err) } func TestNebulaCertificate_VerifyPrivateKey(t *testing.T) { - ca, _, caKey, err := newTestCaCert(time.Time{}, time.Time{}, []*net.IPNet{}, []*net.IPNet{}, []string{}) + ca, _, caKey, err := newTestCaCert(time.Time{}, time.Time{}, nil, nil, nil) assert.Nil(t, err) err = ca.VerifyPrivateKey(Curve_CURVE25519, caKey) assert.Nil(t, err) - _, _, caKey2, err := newTestCaCert(time.Time{}, time.Time{}, []*net.IPNet{}, []*net.IPNet{}, []string{}) + _, _, caKey2, err := newTestCaCert(time.Time{}, time.Time{}, nil, nil, nil) assert.Nil(t, err) err = ca.VerifyPrivateKey(Curve_CURVE25519, caKey2) assert.NotNil(t, err) - c, _, priv, err := newTestCert(ca, caKey, time.Time{}, time.Time{}, []*net.IPNet{}, []*net.IPNet{}, []string{}) + c, _, priv, err := newTestCert(ca, caKey, time.Time{}, time.Time{}, nil, nil, nil) err = c.VerifyPrivateKey(Curve_CURVE25519, priv) assert.Nil(t, err) @@ -500,17 +441,17 @@ func TestNebulaCertificate_VerifyPrivateKey(t *testing.T) { } func TestNebulaCertificate_VerifyPrivateKeyP256(t *testing.T) { - ca, _, caKey, err := newTestCaCertP256(time.Time{}, time.Time{}, []*net.IPNet{}, []*net.IPNet{}, []string{}) + ca, _, caKey, err := newTestCaCertP256(time.Time{}, time.Time{}, nil, nil, nil) assert.Nil(t, err) err = ca.VerifyPrivateKey(Curve_P256, caKey) assert.Nil(t, err) - _, _, caKey2, err := newTestCaCertP256(time.Time{}, time.Time{}, []*net.IPNet{}, []*net.IPNet{}, []string{}) + _, _, caKey2, err := newTestCaCertP256(time.Time{}, time.Time{}, nil, nil, nil) assert.Nil(t, err) err = ca.VerifyPrivateKey(Curve_P256, caKey2) assert.NotNil(t, err) - c, _, priv, err := newTestCert(ca, caKey, time.Time{}, time.Time{}, []*net.IPNet{}, []*net.IPNet{}, []string{}) + c, _, priv, err := newTestCert(ca, caKey, time.Time{}, time.Time{}, nil, nil, nil) err = c.VerifyPrivateKey(Curve_P256, priv) assert.Nil(t, err) @@ -519,108 +460,6 @@ func TestNebulaCertificate_VerifyPrivateKeyP256(t *testing.T) { assert.NotNil(t, err) } -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 := NebulaCertificate{ - Details: NebulaCertificateDetails{ - Name: "nebula root ca", - }, - } - - rootCA01 := NebulaCertificate{ - Details: NebulaCertificateDetails{ - Name: "nebula root ca 01", - }, - } - - rootCAP256 := NebulaCertificate{ - Details: NebulaCertificateDetails{ - Name: "nebula P256 test", - }, - } - - p, err := NewCAPoolFromBytes([]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) - - pp, err := NewCAPoolFromBytes([]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) - - // expired cert, no valid certs - ppp, err := NewCAPoolFromBytes([]byte(expired)) - assert.Equal(t, ErrExpired, err) - assert.Equal(t, ppp.CAs[string("152070be6bb19bc9e3bde4c2f0e7d8f4ff5448b4c9856b8eccb314fade0229b0")].Details.Name, "expired") - - // expired cert, with valid certs - pppp, err := NewCAPoolFromBytes(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, len(pppp.CAs), 3) - - ppppp, err := NewCAPoolFromBytes([]byte(p256)) - assert.Nil(t, err) - assert.Equal(t, ppppp.CAs[string("a7938893ec8c4ef769b06d7f425e5e46f7a7f5ffa49c3bcf4a86b608caba9159")].Details.Name, rootCAP256.Details.Name) - assert.Equal(t, len(ppppp.CAs), 1) -} - func appendByteSlices(b ...[]byte) []byte { retSlice := []byte{} for _, v := range b { @@ -629,420 +468,55 @@ func appendByteSlices(b ...[]byte) []byte { return retSlice } -func TestUnmrshalCertPEM(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 := UnmarshalNebulaCertificateFromPEM(certBundle) - assert.NotNil(t, cert) - assert.Equal(t, rest, append(badBanner, invalidPem...)) - assert.Nil(t, err) - - // Fail due to invalid banner. - cert, rest, err = UnmarshalNebulaCertificateFromPEM(rest) - assert.Nil(t, cert) - assert.Equal(t, rest, invalidPem) - assert.EqualError(t, err, "bytes did not contain a proper nebula certificate banner") - - // Fail due to ivalid PEM format, because - // it's missing the requisite pre-encapsulation boundary. - cert, rest, err = UnmarshalNebulaCertificateFromPEM(rest) - assert.Nil(t, cert) - assert.Equal(t, rest, invalidPem) - assert.EqualError(t, err, "input did not contain a valid PEM encoded block") -} - -func TestUnmarshalSigningPrivateKey(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 := UnmarshalSigningPrivateKey(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 = UnmarshalSigningPrivateKey(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 = UnmarshalSigningPrivateKey(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 = UnmarshalSigningPrivateKey(rest) - assert.Nil(t, k) - assert.Equal(t, rest, invalidPem) - assert.EqualError(t, err, "bytes did not contain a proper nebula Ed25519/ECDSA private key banner") - - // Fail due to ivalid PEM format, because - // it's missing the requisite pre-encapsulation boundary. - k, rest, curve, err = UnmarshalSigningPrivateKey(rest) - assert.Nil(t, k) - assert.Equal(t, rest, invalidPem) - assert.EqualError(t, err, "input did not contain a valid PEM encoded block") -} - -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 -} - -func TestUnmarshalPrivateKey(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 := UnmarshalPrivateKey(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 = UnmarshalPrivateKey(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 = UnmarshalPrivateKey(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 = UnmarshalPrivateKey(rest) - assert.Nil(t, k) - assert.Equal(t, rest, invalidPem) - assert.EqualError(t, err, "bytes did not contain a proper nebula private key banner") - - // Fail due to ivalid PEM format, because - // it's missing the requisite pre-encapsulation boundary. - k, rest, curve, err = UnmarshalPrivateKey(rest) - assert.Nil(t, k) - assert.Equal(t, rest, invalidPem) - assert.EqualError(t, err, "input did not contain a valid PEM encoded block") -} - -func TestUnmarshalEd25519PublicKey(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, err := UnmarshalEd25519PublicKey(keyBundle) - assert.Equal(t, len(k), 32) - assert.Nil(t, err) - assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem)) - - // Fail due to short key - k, rest, err = UnmarshalEd25519PublicKey(rest) - assert.Nil(t, k) - assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem)) - assert.EqualError(t, err, "key was not 32 bytes, is invalid ed25519 public key") - - // Fail due to invalid banner - k, rest, err = UnmarshalEd25519PublicKey(rest) - assert.Nil(t, k) - assert.EqualError(t, err, "bytes did not contain a proper nebula Ed25519 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, err = UnmarshalEd25519PublicKey(rest) - assert.Nil(t, k) - 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 := UnmarshalPublicKey(keyBundle) - assert.Equal(t, len(k), 32) - 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 = UnmarshalPublicKey(rest) - assert.Equal(t, len(k), 65) - 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 = UnmarshalPublicKey(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 = UnmarshalPublicKey(rest) - assert.Nil(t, k) - assert.EqualError(t, err, "bytes did not contain a proper nebula 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 = UnmarshalPublicKey(rest) - assert.Nil(t, k) - assert.Equal(t, rest, invalidPem) - assert.EqualError(t, err, "input did not contain a valid PEM encoded block") -} - // Ensure that upgrading the protobuf library does not change how certificates // are marshalled, since this would break signature verification -func TestMarshalingNebulaCertificateConsistency(t *testing.T) { - before := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) - after := time.Date(2017, time.January, 18, 28, 40, 0, 0, time.UTC) - pubKey := []byte("1234567890abcedfghij1234567890ab") - - nc := NebulaCertificate{ - Details: NebulaCertificateDetails{ - Name: "testing", - Ips: []*net.IPNet{ - {IP: net.ParseIP("10.1.1.1"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("10.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, - {IP: net.ParseIP("10.1.1.3"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, - }, - Subnets: []*net.IPNet{ - {IP: net.ParseIP("9.1.1.1"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, - {IP: net.ParseIP("9.1.1.2"), Mask: net.IPMask(net.ParseIP("255.255.255.0"))}, - {IP: net.ParseIP("9.1.1.3"), Mask: net.IPMask(net.ParseIP("255.255.0.0"))}, - }, - Groups: []string{"test-group1", "test-group2", "test-group3"}, - NotBefore: before, - NotAfter: after, - PublicKey: pubKey, - IsCA: false, - Issuer: "1234567890abcedfghij1234567890ab", - }, - Signature: []byte("1234567890abcedfghij1234567890ab"), - } - - b, err := nc.Marshal() - assert.Nil(t, err) - //t.Log("Cert size:", len(b)) - assert.Equal(t, "0aa2010a0774657374696e67121b8182845080feffff0f828284508080fcff0f8382845080fe83f80f1a1b8182844880fe83f80f8282844880feffff0f838284488080fcff0f220b746573742d67726f757031220b746573742d67726f757032220b746573742d67726f75703328f0e0e7d70430a08681c4053a20313233343536373839306162636564666768696a3132333435363738393061624a081234567890abcedf1220313233343536373839306162636564666768696a313233343536373839306162", fmt.Sprintf("%x", b)) - - b, err = proto.Marshal(nc.getRawDetails()) - assert.Nil(t, err) - //t.Log("Raw cert size:", len(b)) - assert.Equal(t, "0a0774657374696e67121b8182845080feffff0f828284508080fcff0f8382845080fe83f80f1a1b8182844880fe83f80f8282844880feffff0f838284488080fcff0f220b746573742d67726f757031220b746573742d67726f757032220b746573742d67726f75703328f0e0e7d70430a08681c4053a20313233343536373839306162636564666768696a3132333435363738393061624a081234567890abcedf", fmt.Sprintf("%x", b)) -} +//TODO: since netip cant represent 255.0.255.0 netmask we can't verify the old certs are ok +//func TestMarshalingNebulaCertificateConsistency(t *testing.T) { +// before := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) +// after := time.Date(2017, time.January, 18, 28, 40, 0, 0, time.UTC) +// pubKey := []byte("1234567890abcedfghij1234567890ab") +// +// nc := certificateV1{ +// details: detailsV1{ +// Name: "testing", +// Ips: []netip.Prefix{ +// mustParsePrefixUnmapped("10.1.1.1/24"), +// mustParsePrefixUnmapped("10.1.1.2/16"), +// //TODO: netip bad +// //{IP: net.ParseIP("10.1.1.3"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, +// }, +// Subnets: []netip.Prefix{ +// //TODO: netip bad +// //{IP: net.ParseIP("9.1.1.1"), Mask: net.IPMask(net.ParseIP("255.0.255.0"))}, +// mustParsePrefixUnmapped("9.1.1.2/24"), +// mustParsePrefixUnmapped("9.1.1.3/16"), +// }, +// Groups: []string{"test-group1", "test-group2", "test-group3"}, +// NotBefore: before, +// NotAfter: after, +// PublicKey: pubKey, +// IsCA: false, +// Issuer: "1234567890abcedfghij1234567890ab", +// }, +// signature: []byte("1234567890abcedfghij1234567890ab"), +// } +// +// b, err := nc.Marshal() +// assert.Nil(t, err) +// //t.Log("Cert size:", len(b)) +// assert.Equal(t, "0aa2010a0774657374696e67121b8182845080feffff0f828284508080fcff0f8382845080fe83f80f1a1b8182844880fe83f80f8282844880feffff0f838284488080fcff0f220b746573742d67726f757031220b746573742d67726f757032220b746573742d67726f75703328f0e0e7d70430a08681c4053a20313233343536373839306162636564666768696a3132333435363738393061624a081234567890abcedf1220313233343536373839306162636564666768696a313233343536373839306162", fmt.Sprintf("%x", b)) +// +// b, err = proto.Marshal(nc.getRawDetails()) +// assert.Nil(t, err) +// //t.Log("Raw cert size:", len(b)) +// assert.Equal(t, "0a0774657374696e67121b8182845080feffff0f828284508080fcff0f8382845080fe83f80f1a1b8182844880fe83f80f8282844880feffff0f838284488080fcff0f220b746573742d67726f757031220b746573742d67726f757032220b746573742d67726f75703328f0e0e7d70430a08681c4053a20313233343536373839306162636564666768696a3132333435363738393061624a081234567890abcedf", fmt.Sprintf("%x", b)) +//} func TestNebulaCertificate_Copy(t *testing.T) { - ca, _, caKey, err := newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{}) + ca, _, caKey, err := newTestCaCert(time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil) assert.Nil(t, err) - c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), []*net.IPNet{}, []*net.IPNet{}, []string{}) + c, _, _, err := newTestCert(ca, caKey, time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil) assert.Nil(t, err) cc := c.Copy() @@ -1052,11 +526,11 @@ func TestNebulaCertificate_Copy(t *testing.T) { func TestUnmarshalNebulaCertificate(t *testing.T) { // Test that we don't panic with an invalid certificate (#332) data := []byte("\x98\x00\x00") - _, err := UnmarshalNebulaCertificate(data) + _, err := unmarshalCertificateV1(data, true) assert.EqualError(t, err, "encoded Details was nil") } -func newTestCaCert(before, after time.Time, ips, subnets []*net.IPNet, groups []string) (*NebulaCertificate, []byte, []byte, error) { +func newTestCaCert(before, after time.Time, ips, subnets []netip.Prefix, groups []string) (Certificate, []byte, []byte, error) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if before.IsZero() { before = time.Now().Add(time.Second * -60).Round(time.Second) @@ -1065,37 +539,35 @@ func newTestCaCert(before, after time.Time, ips, subnets []*net.IPNet, groups [] after = time.Now().Add(time.Second * 60).Round(time.Second) } - nc := &NebulaCertificate{ - Details: 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{}), - }, + tbs := &TBSCertificate{ + Version: Version1, + Name: "test ca", + IsCA: true, + NotBefore: time.Unix(before.Unix(), 0), + NotAfter: time.Unix(after.Unix(), 0), + PublicKey: pub, } if len(ips) > 0 { - nc.Details.Ips = ips + tbs.Networks = ips } if len(subnets) > 0 { - nc.Details.Subnets = subnets + tbs.UnsafeNetworks = subnets } if len(groups) > 0 { - nc.Details.Groups = groups + tbs.Groups = groups } - err = nc.Sign(Curve_CURVE25519, priv) + nc, err := tbs.Sign(nil, Curve_CURVE25519, priv) if err != nil { return nil, nil, nil, err } return nc, pub, priv, nil } -func newTestCaCertP256(before, after time.Time, ips, subnets []*net.IPNet, groups []string) (*NebulaCertificate, []byte, []byte, error) { +func newTestCaCertP256(before, after time.Time, ips, subnets []netip.Prefix, groups []string) (Certificate, []byte, []byte, error) { priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) pub := elliptic.Marshal(elliptic.P256(), priv.PublicKey.X, priv.PublicKey.Y) rawPriv := priv.D.FillBytes(make([]byte, 32)) @@ -1107,43 +579,36 @@ func newTestCaCertP256(before, after time.Time, ips, subnets []*net.IPNet, group after = time.Now().Add(time.Second * 60).Round(time.Second) } - nc := &NebulaCertificate{ - Details: NebulaCertificateDetails{ - Name: "test ca", - NotBefore: time.Unix(before.Unix(), 0), - NotAfter: time.Unix(after.Unix(), 0), - PublicKey: pub, - IsCA: true, - Curve: Curve_P256, - InvertedGroups: make(map[string]struct{}), - }, + tbs := &TBSCertificate{ + Version: Version1, + Name: "test ca", + IsCA: true, + NotBefore: time.Unix(before.Unix(), 0), + NotAfter: time.Unix(after.Unix(), 0), + PublicKey: pub, + Curve: Curve_P256, } if len(ips) > 0 { - nc.Details.Ips = ips + tbs.Networks = ips } if len(subnets) > 0 { - nc.Details.Subnets = subnets + tbs.UnsafeNetworks = subnets } if len(groups) > 0 { - nc.Details.Groups = groups + tbs.Groups = groups } - err = nc.Sign(Curve_P256, rawPriv) + nc, err := tbs.Sign(nil, Curve_P256, rawPriv) if err != nil { return nil, nil, nil, err } return nc, pub, rawPriv, nil } -func newTestCert(ca *NebulaCertificate, key []byte, before, after time.Time, ips, subnets []*net.IPNet, groups []string) (*NebulaCertificate, []byte, []byte, error) { - issuer, err := ca.Sha256Sum() - if err != nil { - return nil, nil, nil, err - } - +func newTestCert(ca Certificate, key []byte, before, after time.Time, ips, subnets []netip.Prefix, groups []string) (Certificate, []byte, []byte, error) { if before.IsZero() { before = time.Now().Add(time.Second * -60).Round(time.Second) } @@ -1156,49 +621,44 @@ func newTestCert(ca *NebulaCertificate, key []byte, before, after time.Time, ips } if len(ips) == 0 { - ips = []*net.IPNet{ - {IP: net.ParseIP("10.1.1.1").To4(), Mask: net.IPMask(net.ParseIP("255.255.255.0").To4())}, - {IP: net.ParseIP("10.1.1.2").To4(), Mask: net.IPMask(net.ParseIP("255.255.0.0").To4())}, - {IP: net.ParseIP("10.1.1.3").To4(), Mask: net.IPMask(net.ParseIP("255.0.255.0").To4())}, + ips = []netip.Prefix{ + mustParsePrefixUnmapped("10.1.1.1/24"), + mustParsePrefixUnmapped("10.1.1.2/16"), } } if len(subnets) == 0 { - subnets = []*net.IPNet{ - {IP: net.ParseIP("9.1.1.1").To4(), Mask: net.IPMask(net.ParseIP("255.0.255.0").To4())}, - {IP: net.ParseIP("9.1.1.2").To4(), Mask: net.IPMask(net.ParseIP("255.255.255.0").To4())}, - {IP: net.ParseIP("9.1.1.3").To4(), Mask: net.IPMask(net.ParseIP("255.255.0.0").To4())}, + subnets = []netip.Prefix{ + mustParsePrefixUnmapped("9.1.1.2/24"), + mustParsePrefixUnmapped("9.1.1.3/16"), } } var pub, rawPriv []byte - switch ca.Details.Curve { + switch ca.Curve() { case Curve_CURVE25519: pub, rawPriv = x25519Keypair() case Curve_P256: pub, rawPriv = p256Keypair() default: - return nil, nil, nil, fmt.Errorf("unknown curve: %v", ca.Details.Curve) + return nil, nil, nil, fmt.Errorf("unknown curve: %v", ca.Curve()) } - nc := &NebulaCertificate{ - Details: NebulaCertificateDetails{ - Name: "testing", - Ips: ips, - Subnets: subnets, - Groups: groups, - NotBefore: time.Unix(before.Unix(), 0), - NotAfter: time.Unix(after.Unix(), 0), - PublicKey: pub, - IsCA: false, - Curve: ca.Details.Curve, - Issuer: issuer, - InvertedGroups: make(map[string]struct{}), - }, + tbs := &TBSCertificate{ + Version: Version1, + Name: "testing", + Networks: ips, + UnsafeNetworks: subnets, + Groups: groups, + IsCA: false, + NotBefore: time.Unix(before.Unix(), 0), + NotAfter: time.Unix(after.Unix(), 0), + PublicKey: pub, + Curve: ca.Curve(), } - err = nc.Sign(ca.Details.Curve, key) + nc, err := tbs.Sign(ca, ca.Curve(), key) if err != nil { return nil, nil, nil, err } @@ -1228,3 +688,8 @@ func p256Keypair() ([]byte, []byte) { pubkey := privkey.PublicKey() return pubkey.Bytes(), privkey.Bytes() } + +func mustParsePrefixUnmapped(s string) netip.Prefix { + prefix := netip.MustParsePrefix(s) + return netip.PrefixFrom(prefix.Addr().Unmap(), prefix.Bits()) +} diff --git a/cert/cert_v1.go b/cert/cert_v1.go new file mode 100644 index 000000000..165e40951 --- /dev/null +++ b/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() +} diff --git a/cert/cert.pb.go b/cert/cert_v1.pb.go similarity index 62% rename from cert/cert.pb.go rename to cert/cert_v1.pb.go index 3570e0750..32de1a0dc 100644 --- a/cert/cert.pb.go +++ b/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 } diff --git a/cert/cert.proto b/cert/cert_v1.proto similarity index 100% rename from cert/cert.proto rename to cert/cert_v1.proto diff --git a/cert/crypto.go b/cert/crypto.go index 3558e1a54..4c236aeed 100644 --- a/cert/crypto.go +++ b/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 +} diff --git a/cert/crypto_test.go b/cert/crypto_test.go index c2e61df07..c9aba3e44 100644 --- a/cert/crypto_test.go +++ b/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 +} diff --git a/cert/errors.go b/cert/errors.go index 05b42d10c..da0d1be3f 100644 --- a/cert/errors.go +++ b/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") ) diff --git a/cert/pem.go b/cert/pem.go new file mode 100644 index 000000000..744ae2edf --- /dev/null +++ b/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 +} diff --git a/cert/pem_test.go b/cert/pem_test.go new file mode 100644 index 000000000..a0c6e7424 --- /dev/null +++ b/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") +} diff --git a/cert/sign.go b/cert/sign.go new file mode 100644 index 000000000..e446aa131 --- /dev/null +++ b/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) + } +} diff --git a/cmd/nebula-cert/ca.go b/cmd/nebula-cert/ca.go index 4e5d51d0c..90ea8ffa6 100644 --- a/cmd/nebula-cert/ca.go +++ b/cmd/nebula-cert/ca.go @@ -8,13 +8,14 @@ import ( "fmt" "io" "math" - "net" + "net/netip" "os" "strings" "time" "github.com/skip2/go-qrcode" "github.com/slackhq/nebula/cert" + "github.com/slackhq/nebula/pkclient" "golang.org/x/crypto/ed25519" ) @@ -33,7 +34,8 @@ type caFlags struct { argonParallelism *uint encryption *bool - curve *string + curve *string + p11url *string } func newCaFlags() *caFlags { @@ -52,6 +54,7 @@ func newCaFlags() *caFlags { cf.argonIterations = cf.set.Uint("argon-iterations", 1, "Optional: Argon2 iterations parameter used for encrypted private key passphrase") cf.encryption = cf.set.Bool("encrypt", false, "Optional: prompt for passphrase and write out-key in an encrypted format") cf.curve = cf.set.String("curve", "25519", "EdDSA/ECDSA Curve (25519, P256)") + cf.p11url = p11Flag(cf.set) return &cf } @@ -76,17 +79,21 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error return err } + isP11 := len(*cf.p11url) > 0 + if err := mustFlagString("name", cf.name); err != nil { return err } - if err := mustFlagString("out-key", cf.outKeyPath); err != nil { - return err + if !isP11 { + if err = mustFlagString("out-key", cf.outKeyPath); err != nil { + return err + } } if err := mustFlagString("out-crt", cf.outCertPath); err != nil { return err } var kdfParams *cert.Argon2Parameters - if *cf.encryption { + if !isP11 && *cf.encryption { if kdfParams, err = parseArgonParameters(*cf.argonMemory, *cf.argonParallelism, *cf.argonIterations); err != nil { return err } @@ -106,44 +113,42 @@ 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) } } } var passphrase []byte - if *cf.encryption { + if !isP11 && *cf.encryption { for i := 0; i < 5; i++ { out.Write([]byte("Enter passphrase: ")) passphrase, err = pr.ReadPassword() @@ -166,74 +171,109 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error var curve cert.Curve var pub, rawPriv []byte - switch *cf.curve { - case "25519", "X25519", "Curve25519", "CURVE25519": - curve = cert.Curve_CURVE25519 - pub, rawPriv, err = ed25519.GenerateKey(rand.Reader) + var p11Client *pkclient.PKClient + + if isP11 { + switch *cf.curve { + case "P256": + curve = cert.Curve_P256 + default: + return fmt.Errorf("invalid curve for PKCS#11: %s", *cf.curve) + } + + p11Client, err = pkclient.FromUrl(*cf.p11url) if err != nil { - return fmt.Errorf("error while generating ed25519 keys: %s", err) + return fmt.Errorf("error while creating PKCS#11 client: %w", err) } - case "P256": - var key *ecdsa.PrivateKey - curve = cert.Curve_P256 - key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + defer func(client *pkclient.PKClient) { + _ = client.Close() + }(p11Client) + pub, err = p11Client.GetPubKey() if err != nil { - return fmt.Errorf("error while generating ecdsa keys: %s", err) + return fmt.Errorf("error while getting public key with PKCS#11: %w", err) } + } else { + switch *cf.curve { + case "25519", "X25519", "Curve25519", "CURVE25519": + curve = cert.Curve_CURVE25519 + pub, rawPriv, err = ed25519.GenerateKey(rand.Reader) + if err != nil { + return fmt.Errorf("error while generating ed25519 keys: %s", err) + } + case "P256": + var key *ecdsa.PrivateKey + curve = cert.Curve_P256 + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return fmt.Errorf("error while generating ecdsa keys: %s", err) + } - // ecdh.PrivateKey lets us get at the encoded bytes, even though - // we aren't using ECDH here. - eKey, err := key.ECDH() - if err != nil { - return fmt.Errorf("error while converting ecdsa key: %s", err) + // ecdh.PrivateKey lets us get at the encoded bytes, even though + // we aren't using ECDH here. + eKey, err := key.ECDH() + if err != nil { + return fmt.Errorf("error while converting ecdsa key: %s", err) + } + rawPriv = eKey.Bytes() + pub = eKey.PublicKey().Bytes() + default: + return fmt.Errorf("invalid curve: %s", *cf.curve) } - rawPriv = eKey.Bytes() - pub = eKey.PublicKey().Bytes() } - 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, - }, + 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 _, err := os.Stat(*cf.outKeyPath); err == nil { - return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath) + if !isP11 { + if _, err := os.Stat(*cf.outKeyPath); err == nil { + return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath) + } } if _, err := os.Stat(*cf.outCertPath); err == nil { return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath) } - err = nc.Sign(curve, rawPriv) - if err != nil { - return fmt.Errorf("error while signing: %s", err) - } - + var c cert.Certificate var b []byte - if *cf.encryption { - b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, rawPriv, passphrase, kdfParams) + + if isP11 { + c, err = t.SignPkcs11(nil, curve, p11Client) if err != nil { - return fmt.Errorf("error while encrypting out-key: %s", err) + return fmt.Errorf("error while signing with PKCS#11: %w", err) } } else { - b = cert.MarshalSigningPrivateKey(curve, rawPriv) - } + c, err = t.Sign(nil, curve, rawPriv) + if err != nil { + return fmt.Errorf("error while signing: %s", err) + } - err = os.WriteFile(*cf.outKeyPath, b, 0600) - if err != nil { - return fmt.Errorf("error while writing out-key: %s", err) + if *cf.encryption { + b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, rawPriv, passphrase, kdfParams) + if err != nil { + return fmt.Errorf("error while encrypting out-key: %s", err) + } + } else { + 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) + } } - b, err = nc.MarshalToPEM() + b, err = c.MarshalPEM() if err != nil { return fmt.Errorf("error while marshalling certificate: %s", err) } diff --git a/cmd/nebula-cert/ca_test.go b/cmd/nebula-cert/ca_test.go index 3a534051d..06a24edd2 100644 --- a/cmd/nebula-cert/ca_test.go +++ b/cmd/nebula-cert/ca_test.go @@ -52,6 +52,7 @@ func Test_caHelp(t *testing.T) { " \tOptional: path to write the private key to (default \"ca.key\")\n"+ " -out-qr string\n"+ " \tOptional: output a qr code image (png) of the certificate\n"+ + optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+ " -subnets string\n"+ " \tOptional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use in subnets\n", ob.String(), @@ -108,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() @@ -121,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() @@ -134,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()) diff --git a/cmd/nebula-cert/keygen.go b/cmd/nebula-cert/keygen.go index d94cbf145..496f84c27 100644 --- a/cmd/nebula-cert/keygen.go +++ b/cmd/nebula-cert/keygen.go @@ -6,6 +6,8 @@ import ( "io" "os" + "github.com/slackhq/nebula/pkclient" + "github.com/slackhq/nebula/cert" ) @@ -13,8 +15,8 @@ type keygenFlags struct { set *flag.FlagSet outKeyPath *string outPubPath *string - - curve *string + curve *string + p11url *string } func newKeygenFlags() *keygenFlags { @@ -23,6 +25,7 @@ func newKeygenFlags() *keygenFlags { cf.outPubPath = cf.set.String("out-pub", "", "Required: path to write the public key to") cf.outKeyPath = cf.set.String("out-key", "", "Required: path to write the private key to") cf.curve = cf.set.String("curve", "25519", "ECDH Curve (25519, P256)") + cf.p11url = p11Flag(cf.set) return &cf } @@ -33,32 +36,58 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error { return err } - if err := mustFlagString("out-key", cf.outKeyPath); err != nil { - return err + isP11 := len(*cf.p11url) > 0 + + if !isP11 { + if err = mustFlagString("out-key", cf.outKeyPath); err != nil { + return err + } } - if err := mustFlagString("out-pub", cf.outPubPath); err != nil { + if err = mustFlagString("out-pub", cf.outPubPath); err != nil { return err } var pub, rawPriv []byte var curve cert.Curve - switch *cf.curve { - case "25519", "X25519", "Curve25519", "CURVE25519": - pub, rawPriv = x25519Keypair() - curve = cert.Curve_CURVE25519 - case "P256": - pub, rawPriv = p256Keypair() - curve = cert.Curve_P256 - default: - return fmt.Errorf("invalid curve: %s", *cf.curve) + if isP11 { + switch *cf.curve { + case "P256": + curve = cert.Curve_P256 + default: + return fmt.Errorf("invalid curve for PKCS#11: %s", *cf.curve) + } + } else { + switch *cf.curve { + case "25519", "X25519", "Curve25519", "CURVE25519": + pub, rawPriv = x25519Keypair() + curve = cert.Curve_CURVE25519 + case "P256": + pub, rawPriv = p256Keypair() + curve = cert.Curve_P256 + default: + return fmt.Errorf("invalid curve: %s", *cf.curve) + } } - err = os.WriteFile(*cf.outKeyPath, cert.MarshalPrivateKey(curve, rawPriv), 0600) - if err != nil { - return fmt.Errorf("error while writing out-key: %s", err) + if isP11 { + p11Client, err := pkclient.FromUrl(*cf.p11url) + if err != nil { + return fmt.Errorf("error while creating PKCS#11 client: %w", err) + } + defer func(client *pkclient.PKClient) { + _ = client.Close() + }(p11Client) + pub, err = p11Client.GetPubKey() + if err != nil { + return fmt.Errorf("error while getting public key: %w", err) + } + } else { + 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) } @@ -72,7 +101,7 @@ func keygenSummary() string { func keygenHelp(out io.Writer) { cf := newKeygenFlags() - out.Write([]byte("Usage of " + os.Args[0] + " " + keygenSummary() + "\n")) + _, _ = out.Write([]byte("Usage of " + os.Args[0] + " " + keygenSummary() + "\n")) cf.set.SetOutput(out) cf.set.PrintDefaults() } diff --git a/cmd/nebula-cert/keygen_test.go b/cmd/nebula-cert/keygen_test.go index 9a3b3f3bb..18ceb4b1f 100644 --- a/cmd/nebula-cert/keygen_test.go +++ b/cmd/nebula-cert/keygen_test.go @@ -26,7 +26,8 @@ func Test_keygenHelp(t *testing.T) { " -out-key string\n"+ " \tRequired: path to write the private key to\n"+ " -out-pub string\n"+ - " \tRequired: path to write the public key to\n", + " \tRequired: path to write the public key to\n"+ + optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n"), ob.String(), ) } @@ -80,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) diff --git a/cmd/nebula-cert/main_test.go b/cmd/nebula-cert/main_test.go index 3d0fa1b65..2502824b1 100644 --- a/cmd/nebula-cert/main_test.go +++ b/cmd/nebula-cert/main_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "errors" + "fmt" "io" "os" "testing" @@ -77,8 +78,16 @@ func assertHelpError(t *testing.T, err error, msg string) { case *helpError: // good default: - t.Fatal("err was not a helpError") + t.Fatal(fmt.Sprintf("err was not a helpError: %q, expected %q", err, msg)) } assert.EqualError(t, err, msg) } + +func optionalPkcs11String(msg string) string { + if p11Supported() { + return msg + } else { + return "" + } +} diff --git a/cmd/nebula-cert/p11_cgo.go b/cmd/nebula-cert/p11_cgo.go new file mode 100644 index 000000000..f1f1ec628 --- /dev/null +++ b/cmd/nebula-cert/p11_cgo.go @@ -0,0 +1,15 @@ +//go:build cgo && pkcs11 + +package main + +import ( + "flag" +) + +func p11Supported() bool { + return true +} + +func p11Flag(set *flag.FlagSet) *string { + return set.String("pkcs11", "", "Optional: PKCS#11 URI to an existing private key") +} diff --git a/cmd/nebula-cert/p11_stub.go b/cmd/nebula-cert/p11_stub.go new file mode 100644 index 000000000..5afeaeabe --- /dev/null +++ b/cmd/nebula-cert/p11_stub.go @@ -0,0 +1,16 @@ +//go:build !cgo || !pkcs11 + +package main + +import ( + "flag" +) + +func p11Supported() bool { + return false +} + +func p11Flag(set *flag.FlagSet) *string { + var ret = "" + return &ret +} diff --git a/cmd/nebula-cert/print.go b/cmd/nebula-cert/print.go index 746d6a3ab..a62c22338 100644 --- a/cmd/nebula-cert/print.go +++ b/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) } diff --git a/cmd/nebula-cert/print_test.go b/cmd/nebula-cert/print_test.go index 9fa8a5492..4c9a72db4 100644 --- a/cmd/nebula-cert/print_test.go +++ b/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 +} diff --git a/cmd/nebula-cert/sign.go b/cmd/nebula-cert/sign.go index 35d644689..13e807f3b 100644 --- a/cmd/nebula-cert/sign.go +++ b/cmd/nebula-cert/sign.go @@ -6,13 +6,14 @@ import ( "flag" "fmt" "io" - "net" + "net/netip" "os" "strings" "time" "github.com/skip2/go-qrcode" "github.com/slackhq/nebula/cert" + "github.com/slackhq/nebula/pkclient" "golang.org/x/crypto/curve25519" ) @@ -29,6 +30,7 @@ type signFlags struct { outQRPath *string groups *string subnets *string + p11url *string } func newSignFlags() *signFlags { @@ -45,8 +47,8 @@ func newSignFlags() *signFlags { sf.outQRPath = sf.set.String("out-qr", "", "Optional: output a qr code image (png) of the certificate") sf.groups = sf.set.String("groups", "", "Optional: comma separated list of groups") sf.subnets = sf.set.String("subnets", "", "Optional: comma separated list of ipv4 address and network in CIDR notation. Subnets this cert can serve for") + sf.p11url = p11Flag(sf.set) return &sf - } func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error { @@ -56,8 +58,12 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) return err } - if err := mustFlagString("ca-key", sf.caKeyPath); err != nil { - return err + isP11 := len(*sf.p11url) > 0 + + if !isP11 { + if err := mustFlagString("ca-key", sf.caKeyPath); err != nil { + return err + } } if err := mustFlagString("ca-crt", sf.caCertPath); err != nil { return err @@ -68,47 +74,51 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) if err := mustFlagString("ip", sf.ip); err != nil { return err } - if *sf.inPubPath != "" && *sf.outKeyPath != "" { + if !isP11 && *sf.inPubPath != "" && *sf.outKeyPath != "" { return newHelpErrorf("cannot set both -in-pub and -out-key") } - rawCAKey, err := os.ReadFile(*sf.caKeyPath) - if err != nil { - return fmt.Errorf("error while reading ca-key: %s", err) - } - var curve cert.Curve var caKey []byte - // naively attempt to decode the private key as though it is not encrypted - caKey, _, curve, err = cert.UnmarshalSigningPrivateKey(rawCAKey) - if err == cert.ErrPrivateKeyEncrypted { - // ask for a passphrase until we get one - var passphrase []byte - for i := 0; i < 5; i++ { - out.Write([]byte("Enter passphrase: ")) - passphrase, err = pr.ReadPassword() - - if err == ErrNoTerminal { - return fmt.Errorf("ca-key is encrypted and must be decrypted interactively") - } else if err != nil { - return fmt.Errorf("error reading password: %s", err) - } + if !isP11 { + var rawCAKey []byte + rawCAKey, err := os.ReadFile(*sf.caKeyPath) - if len(passphrase) > 0 { - break - } - } - if len(passphrase) == 0 { - return fmt.Errorf("cannot open encrypted ca-key without passphrase") + if err != nil { + return fmt.Errorf("error while reading ca-key: %s", err) } - curve, caKey, _, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rawCAKey) - if err != nil { - return fmt.Errorf("error while parsing encrypted ca-key: %s", err) + // naively attempt to decode the private key as though it is not encrypted + caKey, _, curve, err = cert.UnmarshalSigningPrivateKeyFromPEM(rawCAKey) + if err == cert.ErrPrivateKeyEncrypted { + // ask for a passphrase until we get one + var passphrase []byte + for i := 0; i < 5; i++ { + out.Write([]byte("Enter passphrase: ")) + passphrase, err = pr.ReadPassword() + + if err == ErrNoTerminal { + return fmt.Errorf("ca-key is encrypted and must be decrypted interactively") + } else if err != nil { + return fmt.Errorf("error reading password: %s", err) + } + + if len(passphrase) > 0 { + break + } + } + if len(passphrase) == 0 { + return fmt.Errorf("cannot open encrypted ca-key without passphrase") + } + + curve, caKey, _, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rawCAKey) + if err != nil { + return fmt.Errorf("error while parsing encrypted ca-key: %s", err) + } + } else if err != nil { + return fmt.Errorf("error while parsing ca-key: %s", err) } - } else if err != nil { - return fmt.Errorf("error while parsing ca-key: %s", err) } rawCACert, err := os.ReadFile(*sf.caCertPath) @@ -116,18 +126,15 @@ 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) } - if err := caCert.VerifyPrivateKey(curve, caKey); err != nil { - return fmt.Errorf("refusing to sign, root certificate does not match private key") - } - - issuer, err := caCert.Sha256Sum() - if err != nil { - return fmt.Errorf("error while getting -ca-crt fingerprint: %s", err) + if !isP11 { + if err := caCert.VerifyPrivateKey(curve, caKey); err != nil { + return fmt.Errorf("refusing to sign, root certificate does not match private key") + } } if caCert.Expired(time.Now()) { @@ -136,19 +143,18 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) // 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) @@ -158,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) @@ -176,40 +182,53 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) } var pub, rawPriv []byte + var p11Client *pkclient.PKClient + + if isP11 { + curve = cert.Curve_P256 + p11Client, err = pkclient.FromUrl(*sf.p11url) + if err != nil { + return fmt.Errorf("error while creating PKCS#11 client: %w", err) + } + defer func(client *pkclient.PKClient) { + _ = client.Close() + }(p11Client) + } + if *sf.inPubPath != "" { + var pubCurve cert.Curve rawPub, err := os.ReadFile(*sf.inPubPath) if err != nil { return fmt.Errorf("error while reading in-pub: %s", err) } - var pubCurve cert.Curve - 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) } if pubCurve != curve { return fmt.Errorf("curve of in-pub does not match ca") } + } else if isP11 { + pub, err = p11Client.GetPubKey() + if err != nil { + return fmt.Errorf("error while getting public key with PKCS#11: %w", err) + } } else { 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, - }, - } - - 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 == "" { @@ -224,23 +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) } - err = nc.Sign(curve, caKey) - if err != nil { - return fmt.Errorf("error while signing: %s", err) + 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 *sf.inPubPath == "" { + 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) } diff --git a/cmd/nebula-cert/sign_test.go b/cmd/nebula-cert/sign_test.go index adf83a267..b68434df7 100644 --- a/cmd/nebula-cert/sign_test.go +++ b/cmd/nebula-cert/sign_test.go @@ -48,6 +48,7 @@ func Test_signHelp(t *testing.T) { " \tOptional (if in-pub not set): path to write the private key to\n"+ " -out-qr string\n"+ " \tOptional: output a qr code image (png) of the certificate\n"+ + optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+ " -subnets string\n"+ " \tOptional: comma separated list of ipv4 address and network in CIDR notation. Subnets this cert can serve for\n", ob.String(), @@ -116,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"} @@ -137,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 @@ -171,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()) @@ -192,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()) @@ -208,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() @@ -254,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)) @@ -296,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()) @@ -361,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 diff --git a/cmd/nebula-cert/verify.go b/cmd/nebula-cert/verify.go index c9559136f..80cfef3c0 100644 --- a/cmd/nebula-cert/verify.go +++ b/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 } diff --git a/cmd/nebula-cert/verify_test.go b/cmd/nebula-cert/verify_test.go index f0f4c78dd..204ff09ff 100644 --- a/cmd/nebula-cert/verify_test.go +++ b/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) diff --git a/connection_manager.go b/connection_manager.go index 9a2d310d4..29e863156 100644 --- a/connection_manager.go +++ b/connection_manager.go @@ -420,7 +420,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) { @@ -441,8 +441,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 } @@ -451,9 +452,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 @@ -479,7 +479,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 } diff --git a/connection_manager_test.go b/connection_manager_test.go index 5f97cad9d..9f222c8b4 100644 --- a/connection_manager_test.go +++ b/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 +} diff --git a/connection_state.go b/connection_state.go index 1dd3c8cf5..bcc9e5d9a 100644 --- a/connection_state.go +++ b/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,13 +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: - dhFunc = noiseutil.DHP256 + 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 } diff --git a/control.go b/control.go index 3468b3536..26159845c 100644 --- a/control.go +++ b/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 diff --git a/control_test.go b/control_test.go index fbf29c060..fdfc0a57e 100644 --- a/control_test.go +++ b/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() { diff --git a/control_tester.go b/control_tester.go index d46540f04..fa87e5300 100644 --- a/control_tester.go +++ b/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 } diff --git a/dns_server.go b/dns_server.go index 5fea65c47..750123122 100644 --- a/dns_server.go +++ b/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) { diff --git a/e2e/handshakes_test.go b/e2e/handshakes_test.go index 3d42a560c..f6069bf46 100644 --- a/e2e/handshakes_test.go +++ b/e2e/handshakes_test.go @@ -6,6 +6,7 @@ package e2e import ( "fmt" "net/netip" + "slices" "testing" "time" @@ -96,17 +97,95 @@ func TestGoodHandshake(t *testing.T) { func TestWrongResponderHandshake(t *testing.T) { ca, _, caKey, _ := NewTestCaCert(time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{}) - // The IPs here are chosen on purpose: - // The current remote handling will sort by preference, public, and then lexically. - // So we need them to have a higher address than evil (we could apply a preference though) myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(ca, caKey, "me", "10.128.0.100/24", nil) theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(ca, caKey, "them", "10.128.0.99/24", nil) evilControl, evilVpnIp, evilUdpAddr, _ := newSimpleServer(ca, caKey, "evil", "10.128.0.2/24", nil) - // Add their real udp addr, which should be tried after evil. + // Put the evil udp addr in for their vpn Ip, this is a case of being lied to by the lighthouse. + myControl.InjectLightHouseAddr(theirVpnIpNet.Addr(), evilUdpAddr) + + // Build a router so we don't have to reason who gets which packet + r := router.NewR(t, myControl, theirControl, evilControl) + defer r.RenderFlow() + + // Start the servers + myControl.Start() + theirControl.Start() + evilControl.Start() + + t.Log("Start the handshake process, we will route until we see the evil tunnel closed") + myControl.InjectTunUDPPacket(theirVpnIpNet.Addr(), 80, 80, []byte("Hi from me")) + + h := &header.H{} + r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType { + err := h.Parse(p.Data) + if err != nil { + panic(err) + } + + if h.Type == header.CloseTunnel && p.To == evilUdpAddr { + return router.RouteAndExit + } + + return router.KeepRouting + }) + + t.Log("Evil tunnel is closed, inject the correct udp addr for them") myControl.InjectLightHouseAddr(theirVpnIpNet.Addr(), theirUdpAddr) + pendingHi := myControl.GetHostInfoByVpnIp(theirVpnIpNet.Addr(), true) + assert.NotContains(t, pendingHi.RemoteAddrs, evilUdpAddr) - // Put the evil udp addr in for their vpn Ip, this is a case of being lied to by the lighthouse. + t.Log("Route until we see the cached packet") + r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType { + err := h.Parse(p.Data) + if err != nil { + panic(err) + } + + if p.To == theirUdpAddr && h.Type == 1 { + return router.RouteAndExit + } + + return router.KeepRouting + }) + + //TODO: Assert pending hostmap - I should have a correct hostinfo for them now + + t.Log("My cached packet should be received by them") + myCachedPacket := theirControl.GetFromTun(true) + assertUdpPacket(t, []byte("Hi from me"), myCachedPacket, myVpnIpNet.Addr(), theirVpnIpNet.Addr(), 80, 80) + + t.Log("Test the tunnel with them") + assertHostInfoPair(t, myUdpAddr, theirUdpAddr, myVpnIpNet.Addr(), theirVpnIpNet.Addr(), myControl, theirControl) + assertTunnel(t, myVpnIpNet.Addr(), theirVpnIpNet.Addr(), myControl, theirControl, r) + + t.Log("Flush all packets from all controllers") + r.FlushAll() + + t.Log("Ensure ensure I don't have any hostinfo artifacts from evil") + assert.Nil(t, myControl.GetHostInfoByVpnIp(evilVpnIp.Addr(), true), "My pending hostmap should not contain evil") + assert.Nil(t, myControl.GetHostInfoByVpnIp(evilVpnIp.Addr(), false), "My main hostmap should not contain evil") + + //TODO: assert hostmaps for everyone + r.RenderHostmaps("Final hostmaps", myControl, theirControl, evilControl) + t.Log("Success!") + myControl.Stop() + theirControl.Stop() +} + +func TestWrongResponderHandshakeStaticHostMap(t *testing.T) { + ca, _, caKey, _ := NewTestCaCert(time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{}) + + theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(ca, caKey, "them", "10.128.0.99/24", nil) + evilControl, evilVpnIp, evilUdpAddr, _ := newSimpleServer(ca, caKey, "evil", "10.128.0.2/24", nil) + o := m{ + "static_host_map": m{ + theirVpnIpNet.Addr().String(): []string{evilUdpAddr.String()}, + }, + } + myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(ca, caKey, "me", "10.128.0.100/24", o) + + // Put the evil udp addr in for their vpn addr, this is a case of a remote at a static entry changing its vpn addr. myControl.InjectLightHouseAddr(theirVpnIpNet.Addr(), evilUdpAddr) // Build a router so we don't have to reason who gets which packet @@ -118,10 +197,30 @@ func TestWrongResponderHandshake(t *testing.T) { theirControl.Start() evilControl.Start() - t.Log("Start the handshake process, we will route until we see our cached packet get sent to them") + t.Log("Start the handshake process, we will route until we see the evil tunnel closed") myControl.InjectTunUDPPacket(theirVpnIpNet.Addr(), 80, 80, []byte("Hi from me")) + + h := &header.H{} + r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType { + err := h.Parse(p.Data) + if err != nil { + panic(err) + } + + if h.Type == header.CloseTunnel && p.To == evilUdpAddr { + return router.RouteAndExit + } + + return router.KeepRouting + }) + + t.Log("Evil tunnel is closed, inject the correct udp addr for them") + myControl.InjectLightHouseAddr(theirVpnIpNet.Addr(), theirUdpAddr) + pendingHi := myControl.GetHostInfoByVpnIp(theirVpnIpNet.Addr(), true) + assert.NotContains(t, pendingHi.RemoteAddrs, evilUdpAddr) + + t.Log("Route until we see the cached packet") r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType { - h := &header.H{} err := h.Parse(p.Data) if err != nil { panic(err) @@ -150,7 +249,6 @@ func TestWrongResponderHandshake(t *testing.T) { t.Log("Ensure ensure I don't have any hostinfo artifacts from evil") assert.Nil(t, myControl.GetHostInfoByVpnIp(evilVpnIp.Addr(), true), "My pending hostmap should not contain evil") assert.Nil(t, myControl.GetHostInfoByVpnIp(evilVpnIp.Addr(), false), "My main hostmap should not contain evil") - //NOTE: if evil lost the handshake race it may still have a tunnel since me would reject the handshake since the tunnel is complete //TODO: assert hostmaps for everyone r.RenderHostmaps("Final hostmaps", myControl, theirControl, evilControl) @@ -538,9 +636,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 +656,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 +669,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 +740,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 +760,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 +773,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 +835,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 +854,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 +862,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 +893,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 +936,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 +956,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 +993,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) diff --git a/e2e/helpers.go b/e2e/helpers.go index 71df805f8..c0893aca2 100644 --- a/e2e/helpers.go +++ b/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) { diff --git a/e2e/helpers_test.go b/e2e/helpers_test.go index 527f55bc7..77996f3da 100644 --- a/e2e/helpers_test.go +++ b/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) } diff --git a/e2e/router/hostmap.go b/e2e/router/hostmap.go index c14ab2e77..29fa95991 100644 --- a/e2e/router/hostmap.go +++ b/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 } diff --git a/firewall.go b/firewall.go index 8a409d25d..80a828057 100644 --- a/firewall.go +++ b/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 } diff --git a/firewall_test.go b/firewall_test.go index 4d47e785f..57cd32ae5 100644 --- a/firewall_test.go +++ b/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) diff --git a/go.mod b/go.mod index 56871f1ef..f46499099 100644 --- a/go.mod +++ b/go.mod @@ -5,48 +5,52 @@ go 1.22.0 toolchain go1.22.2 require ( - dario.cat/mergo v1.0.0 + dario.cat/mergo v1.0.1 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/armon/go-radix v1.0.0 github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 github.com/flynn/noise v1.1.0 - github.com/gaissmai/bart v0.11.1 + github.com/gaissmai/bart v0.13.0 github.com/gogo/protobuf v1.3.2 github.com/google/gopacket v1.1.19 github.com/kardianos/service v1.2.2 - github.com/miekg/dns v1.1.61 + github.com/miekg/dns v1.1.62 + github.com/miekg/pkcs11 v1.1.2-0.20231115102856-9078ad6b9d4b github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f - github.com/prometheus/client_golang v1.19.1 + github.com/prometheus/client_golang v1.20.4 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/sirupsen/logrus v1.9.3 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 + github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 github.com/stretchr/testify v1.9.0 - github.com/vishvananda/netlink v1.2.1-beta.2 - golang.org/x/crypto v0.25.0 + github.com/vishvananda/netlink v1.3.0 + golang.org/x/crypto v0.28.0 golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 - golang.org/x/net v0.27.0 + golang.org/x/net v0.30.0 golang.org/x/sync v0.8.0 - golang.org/x/sys v0.23.0 - golang.org/x/term v0.22.0 + golang.org/x/sys v0.26.0 + golang.org/x/term v0.25.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b golang.zx2c4.com/wireguard/windows v0.5.3 - google.golang.org/protobuf v1.34.2 + google.golang.org/protobuf v1.35.1 gopkg.in/yaml.v2 v2.4.0 gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.13.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/bits-and-blooms/bitset v1.14.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/btree v1.1.2 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/vishvananda/netns v0.0.4 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/time v0.5.0 // indirect diff --git a/go.sum b/go.sum index 2688b7ef8..dacc3d37e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -14,11 +14,11 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= -github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA= +github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -26,8 +26,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= -github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= +github.com/gaissmai/bart v0.13.0 h1:pItEhXDVVebUa+i978FfQ7ye8xZc1FrMgs8nJPPWAgA= +github.com/gaissmai/bart v0.13.0/go.mod h1:qSes2fnJ8hB410BW0ymHUN/eQkuGpTYyJcN8sKMYpJU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -70,6 +70,8 @@ github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -80,13 +82,19 @@ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3x github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= -github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/miekg/pkcs11 v1.1.2-0.20231115102856-9078ad6b9d4b h1:J/AzCvg5z0Hn1rqZUJjpbzALUmkKX0Zwbc/i4fw7Sfk= +github.com/miekg/pkcs11 v1.1.2-0.20231115102856-9078ad6b9d4b/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f h1:8dM0ilqKL0Uzl42GABzzC4Oqlc3kGRILz0vgoff7nwg= @@ -100,24 +108,24 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -131,6 +139,8 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -139,9 +149,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= -github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -151,8 +160,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -171,8 +180,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -188,22 +197,22 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -234,8 +243,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/handshake_ix.go b/handshake_ix.go index 8cf534112..3add83d88 100644 --- a/handshake_ix.go +++ b/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,25 @@ 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() + + hostinfo.remoteIndexId = hs.Details.ResponderIndex + hostinfo.lastHandshakeTime = hs.Details.Time + + // Store their cert and our symmetric keys + ci.peerCert = remoteCert + ci.dKey = NewNebulaCipherState(dKey) + ci.eKey = NewNebulaCipherState(eKey) + + // Make sure the current udpAddr being used is set for responding + if addr.IsValid() { + hostinfo.SetRemote(addr) + } else { + hostinfo.relayState.InsertRelayTo(via.relayHI.vpnIp) + } // Ensure the right host responded if vpnIp != hostinfo.vpnIp { @@ -437,10 +450,8 @@ func ixHandshakeStage2(f *Interface, addr netip.AddrPort, via *ViaSender, hh *Ha newHH.hostinfo.remotes = hostinfo.remotes newHH.hostinfo.remotes.BlockRemote(addr) - // Get the correct remote list for the host we did handshake with - hostinfo.remotes = f.lightHouse.QueryCache(vpnIp) - - f.l.WithField("blockedUdpAddrs", newHH.hostinfo.remotes.CopyBlockedRemotes()).WithField("vpnIp", vpnIp). + f.l.WithField("blockedUdpAddrs", newHH.hostinfo.remotes.CopyBlockedRemotes()). + WithField("vpnIp", newHH.hostinfo.vpnIp). WithField("remotes", newHH.hostinfo.remotes.CopyAddrs(f.hostMap.GetPreferredRanges())). Info("Blocked addresses for handshakes") @@ -448,6 +459,9 @@ func ixHandshakeStage2(f *Interface, addr netip.AddrPort, via *ViaSender, hh *Ha newHH.packetStore = hh.packetStore hh.packetStore = []*cachedPacket{} + // Get the correct remote list for the host we did handshake with + hostinfo.SetRemote(addr) + hostinfo.remotes = f.lightHouse.QueryCache(vpnIp) // Finally, put the correct vpn ip in the host info, tell them to close the tunnel, and return true to tear down hostinfo.vpnIp = vpnIp f.sendCloseTunnel(hostinfo) @@ -470,23 +484,8 @@ func ixHandshakeStage2(f *Interface, addr netip.AddrPort, via *ViaSender, hh *Ha WithField("sentCachedPackets", len(hh.packetStore)). Info("Handshake message received") - hostinfo.remoteIndexId = hs.Details.ResponderIndex - hostinfo.lastHandshakeTime = hs.Details.Time - - // Store their cert and our symmetric keys - ci.peerCert = remoteCert - ci.dKey = NewNebulaCipherState(dKey) - ci.eKey = NewNebulaCipherState(eKey) - - // Make sure the current udpAddr being used is set for responding - if addr.IsValid() { - hostinfo.SetRemote(addr) - } else { - hostinfo.relayState.InsertRelayTo(via.relayHI.vpnIp) - } - // 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) diff --git a/handshake_manager.go b/handshake_manager.go index 1df37bdbc..48348939b 100644 --- a/handshake_manager.go +++ b/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 ( diff --git a/handshake_manager_test.go b/handshake_manager_test.go index a78b45f54..daa867564 100644 --- a/handshake_manager_test.go +++ b/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{}, } diff --git a/hostmap.go b/hostmap.go index fb97b76d7..d83151eb3 100644 --- a/hostmap.go +++ b/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()) } } diff --git a/interface.go b/interface.go index 26c25340d..a83f76552 100644 --- a/interface.go +++ b/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)) } } } diff --git a/main.go b/main.go index c6edc9133..8f4535951 100644 --- a/main.go +++ b/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 { diff --git a/noiseutil/pkcs11.go b/noiseutil/pkcs11.go new file mode 100644 index 000000000..d1c7ba918 --- /dev/null +++ b/noiseutil/pkcs11.go @@ -0,0 +1,50 @@ +package noiseutil + +import ( + "crypto/ecdh" + "fmt" + "strings" + + "github.com/slackhq/nebula/pkclient" + + "github.com/flynn/noise" +) + +// DHP256PKCS11 is the NIST P-256 ECDH function +var DHP256PKCS11 noise.DHFunc = newNISTP11Curve("P256", ecdh.P256(), 32) + +type nistP11Curve struct { + nistCurve +} + +func newNISTP11Curve(name string, curve ecdh.Curve, byteLen int) nistP11Curve { + return nistP11Curve{ + newNISTCurve(name, curve, byteLen), + } +} + +func (c nistP11Curve) DH(privkey, pubkey []byte) ([]byte, error) { + //for this function "privkey" is actually a pkcs11 URI + pkStr := string(privkey) + + //to set up a handshake, we need to also do non-pkcs11-DH. Handle that here. + if !strings.HasPrefix(pkStr, "pkcs11:") { + return DHP256.DH(privkey, pubkey) + } + ecdhPubKey, err := c.curve.NewPublicKey(pubkey) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal pubkey: %w", err) + } + + //this is not the most performant way to do this (a long-lived client would be better) + //but, it works, and helps avoid problems with stale sessions and HSMs used by multiple users. + client, err := pkclient.FromUrl(pkStr) + if err != nil { + return nil, err + } + defer func(client *pkclient.PKClient) { + _ = client.Close() + }(client) + + return client.DeriveNoise(ecdhPubKey.Bytes()) +} diff --git a/outside.go b/outside.go index be60294da..6a71fe77f 100644 --- a/outside.go +++ b/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 } diff --git a/pkclient/pkclient.go b/pkclient/pkclient.go new file mode 100644 index 000000000..7061de6ac --- /dev/null +++ b/pkclient/pkclient.go @@ -0,0 +1,87 @@ +package pkclient + +import ( + "crypto/ecdsa" + "crypto/x509" + "fmt" + "io" + "strconv" + + "github.com/stefanberger/go-pkcs11uri" +) + +type Client interface { + io.Closer + GetPubKey() ([]byte, error) + DeriveNoise(peerPubKey []byte) ([]byte, error) + Test() error +} + +const NoiseKeySize = 32 + +func FromUrl(pkurl string) (*PKClient, error) { + uri := pkcs11uri.New() + uri.SetAllowAnyModule(true) //todo + err := uri.Parse(pkurl) + if err != nil { + return nil, err + } + + module, err := uri.GetModule() + if err != nil { + return nil, err + } + + slotid := 0 + slot, ok := uri.GetPathAttribute("slot-id", false) + if !ok { + slotid = 0 + } else { + slotid, err = strconv.Atoi(slot) + if err != nil { + return nil, err + } + } + + pin, _ := uri.GetPIN() + id, _ := uri.GetPathAttribute("id", false) + label, _ := uri.GetPathAttribute("object", false) + + return New(module, uint(slotid), pin, id, label) +} + +func ecKeyToArray(key *ecdsa.PublicKey) []byte { + x := make([]byte, 32) + y := make([]byte, 32) + key.X.FillBytes(x) + key.Y.FillBytes(y) + return append([]byte{0x04}, append(x, y...)...) +} + +func formatPubkeyFromPublicKeyInfoAttr(d []byte) ([]byte, error) { + e, err := x509.ParsePKIXPublicKey(d) + if err != nil { + return nil, err + } + switch t := e.(type) { + case *ecdsa.PublicKey: + return ecKeyToArray(e.(*ecdsa.PublicKey)), nil + default: + return nil, fmt.Errorf("unknown public key type: %T", t) + } +} + +func (c *PKClient) Test() error { + pub, err := c.GetPubKey() + if err != nil { + return fmt.Errorf("failed to get public key: %w", err) + } + out, err := c.DeriveNoise(pub) //do an ECDH with ourselves as a quick test + if err != nil { + return err + } + if len(out) != NoiseKeySize { + return fmt.Errorf("got a key of %d bytes, expected %d", len(out), NoiseKeySize) + } + return nil +} diff --git a/pkclient/pkclient_cgo.go b/pkclient/pkclient_cgo.go new file mode 100644 index 000000000..a2ead5518 --- /dev/null +++ b/pkclient/pkclient_cgo.go @@ -0,0 +1,229 @@ +//go:build cgo && pkcs11 + +package pkclient + +import ( + "encoding/asn1" + "errors" + "fmt" + "log" + "math/big" + + "github.com/miekg/pkcs11" + "github.com/miekg/pkcs11/p11" +) + +type PKClient struct { + module p11.Module + session p11.Session + id []byte + label []byte + privKeyObj p11.Object + pubKeyObj p11.Object +} + +type ecdsaSignature struct { + R, S *big.Int +} + +// New tries to open a session with the HSM, select the slot and login to it +func New(hsmPath string, slotId uint, pin string, id string, label string) (*PKClient, error) { + module, err := p11.OpenModule(hsmPath) + if err != nil { + return nil, fmt.Errorf("failed to load module library: %s", hsmPath) + } + + slots, err := module.Slots() + if err != nil { + module.Destroy() + return nil, err + } + + // Try to open a session on the slot + slotIdx := 0 + for i, slot := range slots { + if slot.ID() == slotId { + slotIdx = i + break + } + } + + client := &PKClient{ + module: module, + id: []byte(id), + label: []byte(label), + } + + client.session, err = slots[slotIdx].OpenWriteSession() + if err != nil { + module.Destroy() + return nil, fmt.Errorf("failed to open session on slot %d", slotId) + } + + if len(pin) != 0 { + err = client.session.Login(pin) + if err != nil { + // ignore "already logged in" + if !errors.Is(err, pkcs11.Error(256)) { + _ = client.session.Close() + return nil, fmt.Errorf("unable to login. error: %w", err) + } + } + } + + // Make sure the hsm has a private key for deriving + client.privKeyObj, err = client.findDeriveKey(client.id, client.label, true) + if err != nil { + _ = client.Close() //log out, close session, destroy module + return nil, fmt.Errorf("failed to find private key for deriving: %w", err) + } + + return client, nil +} + +// Close cleans up properly and logs out +func (c *PKClient) Close() error { + var err error = nil + if c.session != nil { + _ = c.session.Logout() //if logout fails, we still want to close + err = c.session.Close() + } + + c.module.Destroy() + return err +} + +// Try to find a suitable key on the hsm for key derivation +// parameter GET_PUB_KEY sets the search pattern for a public or private key +func (c *PKClient) findDeriveKey(id []byte, label []byte, private bool) (key p11.Object, err error) { + keyClass := pkcs11.CKO_PRIVATE_KEY + if !private { + keyClass = pkcs11.CKO_PUBLIC_KEY + } + keyAttrs := []*pkcs11.Attribute{ + //todo, not all HSMs seem to report this, even if its true: pkcs11.NewAttribute(pkcs11.CKA_DERIVE, true), + pkcs11.NewAttribute(pkcs11.CKA_CLASS, keyClass), + } + + if id != nil && len(id) != 0 { + keyAttrs = append(keyAttrs, pkcs11.NewAttribute(pkcs11.CKA_ID, id)) + } + if label != nil && len(label) != 0 { + keyAttrs = append(keyAttrs, pkcs11.NewAttribute(pkcs11.CKA_LABEL, label)) + } + + return c.session.FindObject(keyAttrs) +} + +func (c *PKClient) listDeriveKeys(id []byte, label []byte, private bool) { + keyClass := pkcs11.CKO_PRIVATE_KEY + if !private { + keyClass = pkcs11.CKO_PUBLIC_KEY + } + keyAttrs := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_CLASS, keyClass), + } + + if id != nil && len(id) != 0 { + keyAttrs = append(keyAttrs, pkcs11.NewAttribute(pkcs11.CKA_ID, id)) + } + if label != nil && len(label) != 0 { + keyAttrs = append(keyAttrs, pkcs11.NewAttribute(pkcs11.CKA_LABEL, label)) + } + + objects, err := c.session.FindObjects(keyAttrs) + if err != nil { + return + } + + for _, obj := range objects { + l, err := obj.Label() + log.Printf("%s, %v", l, err) + a, err := obj.Attribute(pkcs11.CKA_DERIVE) + log.Printf("DERIVE: %s %v, %v", l, a, err) + } +} + +// SignASN1 signs some data. Returns the ASN.1 encoded signature. +func (c *PKClient) SignASN1(data []byte) ([]byte, error) { + mech := pkcs11.NewMechanism(pkcs11.CKM_ECDSA_SHA256, nil) + sk := p11.PrivateKey(c.privKeyObj) + rawSig, err := sk.Sign(*mech, data) + if err != nil { + return nil, err + } + + // PKCS #11 Mechanisms v2.30: + // "The signature octets correspond to the concatenation of the ECDSA values r and s, + // both represented as an octet string of equal length of at most nLen with the most + // significant byte first. If r and s have different octet length, the shorter of both + // must be padded with leading zero octets such that both have the same octet length. + // Loosely spoken, the first half of the signature is r and the second half is s." + r := new(big.Int).SetBytes(rawSig[:len(rawSig)/2]) + s := new(big.Int).SetBytes(rawSig[len(rawSig)/2:]) + return asn1.Marshal(ecdsaSignature{r, s}) +} + +// DeriveNoise derives a shared secret using the input public key against the private key that was found during setup. +// Returns a fixed 32 byte array. +func (c *PKClient) DeriveNoise(peerPubKey []byte) ([]byte, error) { + // Before we call derive, we need to have an array of attributes which specify the type of + // key to be returned, in our case, it's the shared secret key, produced via deriving + // This template pulled from OpenSC pkclient-tool.c line 4038 + attrTemplate := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, false), + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_SECRET_KEY), + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_GENERIC_SECRET), + pkcs11.NewAttribute(pkcs11.CKA_SENSITIVE, false), + pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, true), + pkcs11.NewAttribute(pkcs11.CKA_ENCRYPT, true), + pkcs11.NewAttribute(pkcs11.CKA_DECRYPT, true), + pkcs11.NewAttribute(pkcs11.CKA_WRAP, true), + pkcs11.NewAttribute(pkcs11.CKA_UNWRAP, true), + } + + // Set up the parameters which include the peer's public key + ecdhParams := pkcs11.NewECDH1DeriveParams(pkcs11.CKD_NULL, nil, peerPubKey) + mech := pkcs11.NewMechanism(pkcs11.CKM_ECDH1_DERIVE, ecdhParams) + sk := p11.PrivateKey(c.privKeyObj) + + tmpKey, err := sk.Derive(*mech, attrTemplate) + if err != nil { + return nil, err + } + if tmpKey == nil || len(tmpKey) == 0 { + return nil, fmt.Errorf("got an empty secret key") + } + secret := make([]byte, NoiseKeySize) + copy(secret[:], tmpKey[:NoiseKeySize]) + return secret, nil +} + +func (c *PKClient) GetPubKey() ([]byte, error) { + d, err := c.privKeyObj.Attribute(pkcs11.CKA_PUBLIC_KEY_INFO) + if err != nil { + return nil, err + } + if d != nil && len(d) > 0 { + return formatPubkeyFromPublicKeyInfoAttr(d) + } + c.pubKeyObj, err = c.findDeriveKey(c.id, c.label, false) + if err != nil { + return nil, fmt.Errorf("pkcs11 module gave us a nil CKA_PUBLIC_KEY_INFO, and looking up the public key also failed: %w", err) + } + d, err = c.pubKeyObj.Attribute(pkcs11.CKA_EC_POINT) + if err != nil { + return nil, fmt.Errorf("pkcs11 module gave us a nil CKA_PUBLIC_KEY_INFO, and reading CKA_EC_POINT also failed: %w", err) + } + if d == nil || len(d) < 1 { + return nil, fmt.Errorf("pkcs11 module gave us a nil or empty CKA_EC_POINT") + } + switch len(d) { + case 65: //length of 0x04 + len(X) + len(Y) + return d, nil + case 67: //as above, DER-encoded IIRC? + return d[2:], nil + default: + return nil, fmt.Errorf("unknown public key length: %d", len(d)) + } +} diff --git a/pkclient/pkclient_stub.go b/pkclient/pkclient_stub.go new file mode 100644 index 000000000..36b0fc94c --- /dev/null +++ b/pkclient/pkclient_stub.go @@ -0,0 +1,30 @@ +//go:build !cgo || !pkcs11 + +package pkclient + +import "errors" + +type PKClient struct { +} + +var notImplemented = errors.New("not implemented") + +func New(hsmPath string, slotId uint, pin string, id string, label string) (*PKClient, error) { + return nil, notImplemented +} + +func (c *PKClient) Close() error { + return nil +} + +func (c *PKClient) SignASN1(data []byte) ([]byte, error) { + return nil, notImplemented +} + +func (c *PKClient) DeriveNoise(_ []byte) ([]byte, error) { + return nil, notImplemented +} + +func (c *PKClient) GetPubKey() ([]byte, error) { + return nil, notImplemented +} diff --git a/pki.go b/pki.go index ab95a0477..fe64ea5ee 100644 --- a/pki.go +++ b/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,55 +116,68 @@ 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 } -func newCertStateFromConfig(c *config.C) (*CertState, error) { +func loadPrivateKey(privPathOrPEM string) (rawKey []byte, curve cert.Curve, isPkcs11 bool, err error) { var pemPrivateKey []byte - var err error - - privPathOrPEM := c.GetString("pki.key", "") - if privPathOrPEM == "" { - return nil, errors.New("no pki.key path or PEM data provided") - } - if strings.Contains(privPathOrPEM, "-----BEGIN") { pemPrivateKey = []byte(privPathOrPEM) privPathOrPEM = "" - + rawKey, _, curve, err = cert.UnmarshalPrivateKeyFromPEM(pemPrivateKey) + if err != nil { + return nil, curve, false, fmt.Errorf("error while unmarshaling pki.key %s: %s", privPathOrPEM, err) + } + } else if strings.HasPrefix(privPathOrPEM, "pkcs11:") { + rawKey = []byte(privPathOrPEM) + return rawKey, cert.Curve_P256, true, nil } else { pemPrivateKey, err = os.ReadFile(privPathOrPEM) if err != nil { - return nil, fmt.Errorf("unable to read pki.key file %s: %s", privPathOrPEM, err) + return nil, curve, false, fmt.Errorf("unable to read pki.key file %s: %s", privPathOrPEM, err) } + rawKey, _, curve, err = cert.UnmarshalPrivateKeyFromPEM(pemPrivateKey) + if err != nil { + return nil, curve, false, fmt.Errorf("error while unmarshaling pki.key %s: %s", privPathOrPEM, err) + } + } + + return +} + +func newCertStateFromConfig(c *config.C) (*CertState, error) { + var err error + + privPathOrPEM := c.GetString("pki.key", "") + if privPathOrPEM == "" { + return nil, errors.New("no pki.key path or PEM data provided") } - rawKey, _, curve, err := cert.UnmarshalPrivateKey(pemPrivateKey) + rawKey, curve, isPkcs11, err := loadPrivateKey(privPathOrPEM) if err != nil { - return nil, fmt.Errorf("error while unmarshaling pki.key %s: %s", privPathOrPEM, err) + return nil, err } var rawCert []byte @@ -184,7 +198,7 @@ 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) } @@ -193,18 +207,18 @@ func newCertStateFromConfig(c *config.C) (*CertState, error) { 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 @@ -223,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") } diff --git a/remote_list.go b/remote_list.go index fa14f4295..94db8f2f8 100644 --- a/remote_list.go +++ b/remote_list.go @@ -576,7 +576,9 @@ func (r *RemoteList) unlockedCollect() { dnsAddrs := r.hr.GetIPs() for _, addr := range dnsAddrs { if r.shouldAdd == nil || r.shouldAdd(addr.Addr()) { - addrs = append(addrs, addr) + if !r.unlockedIsBad(addr) { + addrs = append(addrs, addr) + } } } diff --git a/service/service_testhelpers.go b/service/service_testhelpers.go index c77c1c574..e93a1b746 100644 --- a/service/service_testhelpers.go +++ b/service/service_testhelpers.go @@ -29,12 +29,12 @@ func (o LogOutputWithPrefix) Write(p []byte) (n int, err error) { return o.out.Write(p) } -func newSimpleService(caCrt *cert.NebulaCertificate, caKey []byte, name string, udpIp netip.Addr, overrides m) (*Service, *logrus.Logger) { +func newSimpleService(caCrt cert.Certificate, caKey []byte, name string, udpIp netip.Addr, overrides m) (*Service, *logrus.Logger) { _, _, myPrivKey, myPEM := e2e.NewTestCert(caCrt, caKey, name, time.Now().Add(-3*time.Minute), time.Now().Add(30*time.Minute), - netip.PrefixFrom(udpIp, 24), nil, []string{}) - caB, err := caCrt.MarshalToPEM() + []netip.Prefix{netip.PrefixFrom(udpIp, 24)}, nil, []string{}) + caB, err := caCrt.MarshalPEM() if err != nil { panic(err) } diff --git a/ssh.go b/ssh.go index 2ff0954d6..881ee4696 100644 --- a/ssh.go +++ b/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 diff --git a/test/assert.go b/test/assert.go index 6c6c79515..d34252e74 100644 --- a/test/assert.go +++ b/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++ {