Skip to content

Commit 11536b4

Browse files
authored
Merge branch 'cert-v2' into cert-v2-sign-rules
2 parents acf1257 + 3f31517 commit 11536b4

25 files changed

+704
-377
lines changed

cert/ca_pool.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func (ncp *CAPool) GetCAForCert(c Certificate) (*CachedCertificate, error) {
213213
return signer, nil
214214
}
215215

216-
return nil, fmt.Errorf("could not find ca for the certificate")
216+
return nil, ErrCaNotFound
217217
}
218218

219219
// GetFingerprints returns an array of trusted CA fingerprints

cert/cert_v2.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@ func (c *certificateV2) Fingerprint() (string, error) {
137137
}
138138

139139
func (c *certificateV2) CheckSignature(key []byte) bool {
140+
if len(c.rawDetails) == 0 {
141+
return false
142+
}
140143
b := make([]byte, len(c.rawDetails)+1+len(c.publicKey))
141-
//TODO: double check this, panic on empty raw details
142144
copy(b, c.rawDetails)
143145
b[len(c.rawDetails)] = byte(c.curve)
144146
copy(b[len(c.rawDetails)+1:], c.publicKey)
@@ -147,7 +149,6 @@ func (c *certificateV2) CheckSignature(key []byte) bool {
147149
case Curve_CURVE25519:
148150
return ed25519.Verify(key, b, c.signature)
149151
case Curve_P256:
150-
//TODO: NewPublicKey
151152
x, y := elliptic.Unmarshal(elliptic.P256(), key)
152153
pubKey := &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}
153154
hashed := sha256.Sum256(b)
@@ -229,12 +230,14 @@ func (c *certificateV2) String() string {
229230
}
230231

231232
func (c *certificateV2) MarshalForHandshakes() ([]byte, error) {
233+
if c.rawDetails == nil {
234+
return nil, ErrEmptyRawDetails
235+
}
232236
var b cryptobyte.Builder
233237
// Outermost certificate
234238
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
235239

236240
// Add the cert details which is already marshalled
237-
//TODO: panic on nil rawDetails
238241
b.AddBytes(c.rawDetails)
239242

240243
// Skipping the curve and public key since those come across in a different part of the handshake
@@ -249,6 +252,9 @@ func (c *certificateV2) MarshalForHandshakes() ([]byte, error) {
249252
}
250253

251254
func (c *certificateV2) Marshal() ([]byte, error) {
255+
if c.rawDetails == nil {
256+
return nil, ErrEmptyRawDetails
257+
}
252258
var b cryptobyte.Builder
253259
// Outermost certificate
254260
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
@@ -447,13 +453,11 @@ func (c *certificateV2) validate() error {
447453
func (c *certificateV2) marshalForSigning() ([]byte, error) {
448454
d, err := c.details.Marshal()
449455
if err != nil {
450-
//TODO: annotate?
451-
return nil, err
456+
return nil, fmt.Errorf("marshalling certificate details failed: %w", err)
452457
}
453458
c.rawDetails = d
454459

455460
b := make([]byte, len(c.rawDetails)+1+len(c.publicKey))
456-
//TODO: double check this
457461
copy(b, c.rawDetails)
458462
b[len(c.rawDetails)] = byte(c.curve)
459463
copy(b[len(c.rawDetails)+1:], c.publicKey)
@@ -587,6 +591,10 @@ func unmarshalCertificateV2(b []byte, publicKey []byte, curve Curve) (*certifica
587591
return nil, ErrBadFormat
588592
}
589593

594+
if len(rawPublicKey) == 0 {
595+
return nil, ErrBadFormat
596+
}
597+
590598
// Grab the signature
591599
var rawSignature cryptobyte.String
592600
if !input.ReadASN1(&rawSignature, TagCertSignature) || rawSignature.Empty() {

cert/cert_v2_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cert
33
import (
44
"crypto/ed25519"
55
"crypto/rand"
6+
"encoding/hex"
67
"net/netip"
78
"slices"
89
"testing"
@@ -224,3 +225,43 @@ func TestUnmarshalCertificateV2(t *testing.T) {
224225
_, err := unmarshalCertificateV2(data, nil, Curve_CURVE25519)
225226
assert.EqualError(t, err, "bad wire format")
226227
}
228+
229+
func TestCertificateV2_marshalForSigningStability(t *testing.T) {
230+
before := time.Date(1996, time.May, 5, 0, 0, 0, 0, time.UTC)
231+
after := before.Add(time.Second * 60).Round(time.Second)
232+
pubKey := []byte("1234567890abcedfghij1234567890ab")
233+
234+
nc := certificateV2{
235+
details: detailsV2{
236+
name: "testing",
237+
networks: []netip.Prefix{
238+
mustParsePrefixUnmapped("10.1.1.2/16"),
239+
mustParsePrefixUnmapped("10.1.1.1/24"),
240+
},
241+
unsafeNetworks: []netip.Prefix{
242+
mustParsePrefixUnmapped("9.1.1.3/16"),
243+
mustParsePrefixUnmapped("9.1.1.2/24"),
244+
},
245+
groups: []string{"test-group1", "test-group2", "test-group3"},
246+
notBefore: before,
247+
notAfter: after,
248+
isCA: false,
249+
issuer: "1234567890abcdef1234567890abcdef",
250+
},
251+
signature: []byte("1234567890abcdef1234567890abcdef"),
252+
publicKey: pubKey,
253+
}
254+
255+
const expectedRawDetailsStr = "a070800774657374696e67a10e04050a0101021004050a01010118a20e0405090101031004050901010218a3270c0b746573742d67726f7570310c0b746573742d67726f7570320c0b746573742d67726f7570338504318bef808604318befbc87101234567890abcdef1234567890abcdef"
256+
expectedRawDetails, err := hex.DecodeString(expectedRawDetailsStr)
257+
require.NoError(t, err)
258+
259+
db, err := nc.details.Marshal()
260+
require.NoError(t, err)
261+
assert.Equal(t, expectedRawDetails, db)
262+
263+
expectedForSigning, err := hex.DecodeString(expectedRawDetailsStr + "00313233343536373839306162636564666768696a313233343536373839306162")
264+
b, err := nc.marshalForSigning()
265+
require.NoError(t, err)
266+
assert.Equal(t, expectedForSigning, b)
267+
}

cert/errors.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var (
1919
ErrPublicPrivateCurveMismatch = errors.New("public key does not match private key curve")
2020
ErrPublicPrivateKeyMismatch = errors.New("public key and private key are not a pair")
2121
ErrPrivateKeyEncrypted = errors.New("private key must be decrypted")
22+
ErrCaNotFound = errors.New("could not find ca for the certi
2223

2324
ErrInvalidPEMBlock = errors.New("input did not contain a valid PEM encoded block")
2425
ErrInvalidPEMCertificateBanner = errors.New("bytes did not contain a proper certificate banner")
@@ -30,8 +31,9 @@ var (
3031
ErrNoPeerStaticKey = errors.New("no peer static key was present")
3132
ErrNoPayload = errors.New("provided payload was empty")
3233

33-
ErrMissingDetails = errors.New("certificate did not contain details")
34-
ErrEmptySignature = errors.New("empty signature")
34+
ErrMissingDetails = errors.New("certificate did not contain details")
35+
ErrEmptySignature = errors.New("empty signature")
36+
ErrEmptyRawDetails = errors.New("empty rawDetails not allowed")
3537
)
3638

3739
type ErrInvalidCertificateProperties struct {

cert/sign_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func TestCertificateV1_SignP256(t *testing.T) {
7373
}
7474

7575
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
76+
assert.NoError(t, err)
7677
pub := elliptic.Marshal(elliptic.P256(), priv.PublicKey.X, priv.PublicKey.Y)
7778
rawPriv := priv.D.FillBytes(make([]byte, 32))
7879

cmd/nebula-cert/verify.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"errors"
45
"flag"
56
"fmt"
67
"io"
@@ -41,14 +42,14 @@ func verify(args []string, out io.Writer, errOut io.Writer) error {
4142

4243
rawCACert, err := os.ReadFile(*vf.caPath)
4344
if err != nil {
44-
return fmt.Errorf("error while reading ca: %s", err)
45+
return fmt.Errorf("error while reading ca: %w", err)
4546
}
4647

4748
caPool := cert.NewCAPool()
4849
for {
4950
rawCACert, err = caPool.AddCAFromPEM(rawCACert)
5051
if err != nil {
51-
return fmt.Errorf("error while adding ca cert to pool: %s", err)
52+
return fmt.Errorf("error while adding ca cert to pool: %w", err)
5253
}
5354

5455
if rawCACert == nil || len(rawCACert) == 0 || strings.TrimSpace(string(rawCACert)) == "" {
@@ -58,20 +59,30 @@ func verify(args []string, out io.Writer, errOut io.Writer) error {
5859

5960
rawCert, err := os.ReadFile(*vf.certPath)
6061
if err != nil {
61-
return fmt.Errorf("unable to read crt; %s", err)
62+
return fmt.Errorf("unable to read crt: %w", err)
6263
}
63-
64-
c, _, err := cert.UnmarshalCertificateFromPEM(rawCert)
65-
if err != nil {
66-
return fmt.Errorf("error while parsing crt: %s", err)
67-
}
68-
69-
_, err = caPool.VerifyCertificate(time.Now(), c)
70-
if err != nil {
71-
return err
64+
var errs []error
65+
for {
66+
if len(rawCert) == 0 {
67+
break
68+
}
69+
c, extra, err := cert.UnmarshalCertificateFromPEM(rawCert)
70+
if err != nil {
71+
return fmt.Errorf("error while parsing crt: %w", err)
72+
}
73+
rawCert = extra
74+
_, err = caPool.VerifyCertificate(time.Now(), c)
75+
if err != nil {
76+
switch {
77+
case errors.Is(err, cert.ErrCaNotFound):
78+
errs = append(errs, fmt.Errorf("error while verifying certificate v%d %s with issuer %s: %w", c.Version(), c.Name(), c.Issuer(), err))
79+
default:
80+
errs = append(errs, fmt.Errorf("error while verifying certificate %+v: %w", c, err))
81+
}
82+
}
7283
}
7384

74-
return nil
85+
return errors.Join(errs...)
7586
}
7687

7788
func verifySummary() string {
@@ -80,7 +91,7 @@ func verifySummary() string {
8091

8192
func verifyHelp(out io.Writer) {
8293
vf := newVerifyFlags()
83-
out.Write([]byte("Usage of " + os.Args[0] + " " + verifySummary() + "\n"))
94+
_, _ = out.Write([]byte("Usage of " + os.Args[0] + " " + verifySummary() + "\n"))
8495
vf.set.SetOutput(out)
8596
vf.set.PrintDefaults()
8697
}

cmd/nebula-cert/verify_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package main
33
import (
44
"bytes"
55
"crypto/rand"
6+
"errors"
67
"os"
78
"testing"
89
"time"
910

11+
"github.com/slackhq/nebula/cert"
1012
"github.com/stretchr/testify/assert"
1113
"golang.org/x/crypto/ed25519"
1214
)
@@ -76,7 +78,7 @@ func Test_verify(t *testing.T) {
7678
err = verify([]string{"-ca", caFile.Name(), "-crt", "does_not_exist"}, ob, eb)
7779
assert.Equal(t, "", ob.String())
7880
assert.Equal(t, "", eb.String())
79-
assert.EqualError(t, err, "unable to read crt; open does_not_exist: "+NoSuchFileError)
81+
assert.EqualError(t, err, "unable to read crt: open does_not_exist: "+NoSuchFileError)
8082

8183
// invalid crt at path
8284
ob.Reset()
@@ -106,7 +108,7 @@ func Test_verify(t *testing.T) {
106108
err = verify([]string{"-ca", caFile.Name(), "-crt", certFile.Name()}, ob, eb)
107109
assert.Equal(t, "", ob.String())
108110
assert.Equal(t, "", eb.String())
109-
assert.EqualError(t, err, "certificate signature did not match")
111+
assert.True(t, errors.Is(err, cert.ErrSignatureMismatch))
110112

111113
// verified cert at path
112114
crt, _ = NewTestCert(ca, caPriv, "test-cert", time.Now().Add(time.Hour*-1), time.Now().Add(time.Hour), nil, nil, nil)

connection_manager.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -426,17 +426,17 @@ func (n *connectionManager) shouldSwapPrimary(current, primary *HostInfo) bool {
426426
// If we are here then we have multiple tunnels for a host pair and neither side believes the same tunnel is primary.
427427
// Let's sort this out.
428428

429-
//TODO: current.vpnIp should become an array of vpnIps
429+
// Only one side should swap because if both swap then we may never resolve to a single tunnel.
430+
// vpn addr is static across all tunnels for this host pair so lets
431+
// use that to determine if we should consider swapping.
430432
if current.vpnAddrs[0].Compare(n.intf.myVpnAddrs[0]) < 0 {
431-
// Only one side should flip primary because if both flip then we may never resolve to a single tunnel.
432-
// vpn ip is static across all tunnels for this host pair so lets use that to determine who is flipping.
433-
// The remotes vpn ip is lower than mine. I will not flip.
433+
// Their primary vpn addr is less than mine. Do not swap.
434434
return false
435435
}
436436

437-
//TODO: we should favor v2 over v1 certificates if configured to send them
438-
439-
crt := n.intf.pki.getCertificate(current.ConnectionState.myCert.Version())
437+
crt := n.intf.pki.getCertState().getCertificate(current.ConnectionState.myCert.Version())
438+
// If this tunnel is using the latest certificate then we should swap it to primary for a bit and see if things
439+
// settle down.
440440
return bytes.Equal(current.ConnectionState.myCert.Signature(), crt.Signature())
441441
}
442442

@@ -495,13 +495,14 @@ func (n *connectionManager) sendPunch(hostinfo *HostInfo) {
495495
}
496496

497497
func (n *connectionManager) tryRehandshake(hostinfo *HostInfo) {
498-
crt := n.intf.pki.getCertificate(hostinfo.ConnectionState.myCert.Version())
499-
if bytes.Equal(hostinfo.ConnectionState.myCert.Signature(), crt.Signature()) {
498+
cs := n.intf.pki.getCertState()
499+
curCrt := hostinfo.ConnectionState.myCert
500+
myCrt := cs.getCertificate(curCrt.Version())
501+
if curCrt.Version() >= cs.defaultVersion && bytes.Equal(curCrt.Signature(), myCrt.Signature()) == true {
502+
// The current tunnel is using the latest certificate and version, no need to rehandshake.
500503
return
501504
}
502505

503-
//TODO: we should favor v2 over v1 certificates if configured to send them
504-
505506
n.l.WithField("vpnAddrs", hostinfo.vpnAddrs).
506507
WithField("reason", "local certificate is not current").
507508
Info("Re-handshaking with remote")

control.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ func (c *Control) ListHostmapIndexes(pendingMap bool) []ControlHostInfo {
133133
func (c *Control) GetCertByVpnIp(vpnIp netip.Addr) cert.Certificate {
134134
_, found := c.f.myVpnAddrsTable.Lookup(vpnIp)
135135
if found {
136-
//TODO: we might have 2 certs....
137-
//TODO: this should return our latest version cert
138-
return c.f.pki.getDefaultCertificate().Copy()
136+
// Only returning the default certificate since its impossible
137+
// for any other host but ourselves to have more than 1
138+
return c.f.pki.getCertState().GetDefaultCertificate().Copy()
139139
}
140140
hi := c.f.hostMap.QueryVpnAddr(vpnIp)
141141
if hi == nil {
@@ -228,13 +228,9 @@ func (c *Control) CloseTunnel(vpnIp netip.Addr, localOnly bool) bool {
228228
// the int returned is a count of tunnels closed
229229
func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
230230
//TODO: this is probably better as a function in ConnectionManager or HostMap directly
231-
lighthouses := c.f.lightHouse.GetLighthouses()
232-
233231
shutdown := func(h *HostInfo) {
234-
if excludeLighthouses {
235-
if _, ok := lighthouses[h.vpnAddrs[0]]; ok {
236-
return
237-
}
232+
if excludeLighthouses && c.f.lightHouse.IsAnyLighthouseAddr(h.vpnAddrs) {
233+
return
238234
}
239235
c.f.send(header.CloseTunnel, 0, h.ConnectionState, h, []byte{}, make([]byte, 12, 12), make([]byte, mtu))
240236
c.f.closeTunnel(h)

control_tester.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (c *Control) InjectRelays(vpnIp netip.Addr, relayVpnIps []netip.Addr) {
7070
defer remoteList.Unlock()
7171
c.f.lightHouse.Unlock()
7272

73-
remoteList.unlockedSetRelay(vpnIp, vpnIp, relayVpnIps)
73+
remoteList.unlockedSetRelay(vpnIp, relayVpnIps)
7474
}
7575

7676
// GetFromTun will pull a packet off the tun side of nebula

0 commit comments

Comments
 (0)