diff --git a/authority/provisioner/nebula.go b/authority/provisioner/nebula.go index 66c523dc4..fa9c283f8 100644 --- a/authority/provisioner/nebula.go +++ b/authority/provisioner/nebula.go @@ -2,9 +2,13 @@ package provisioner import ( "context" + "crypto/ecdh" + "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" "crypto/x509" "encoding/base64" + "math/big" "net" "time" @@ -337,10 +341,23 @@ func (p *Nebula) authorizeToken(token string, audiences []string) (*nebula.Nebul return nil, nil, errs.Unauthorized("token is not valid: failed to verify certificate against configured CA") } - var pub interface{} - if c.Details.IsCA { + var pub any + switch { + case c.Details.Curve == nebula.Curve_P256: + // When Nebula is used with ECDSA P-256 keys, both CAs and clients use the same type. + ecdhPub, err := ecdh.P256().NewPublicKey(c.Details.PublicKey) + if err != nil { + return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("failed to parse nebula public key")) + } + publicKeyBytes := ecdhPub.Bytes() + pub = &ecdsa.PublicKey{ // convert back to *ecdsa.PublicKey, because our jose package nor go-jose supports *ecdh.PublicKey + Curve: elliptic.P256(), + X: big.NewInt(0).SetBytes(publicKeyBytes[1:33]), + Y: big.NewInt(0).SetBytes(publicKeyBytes[33:]), + } + case c.Details.IsCA: pub = ed25519.PublicKey(c.Details.PublicKey) - } else { + default: pub = x25519.PublicKey(c.Details.PublicKey) } @@ -358,6 +375,7 @@ func (p *Nebula) authorizeToken(token string, audiences []string) (*nebula.Nebul }, time.Minute); err != nil { return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("token is not valid: invalid claims")) } + // Validate token and subject too. if !matchesAudience(claims.Audience, audiences) { return nil, nil, errs.Unauthorized("token is not valid: invalid claims") diff --git a/authority/provisioner/nebula_test.go b/authority/provisioner/nebula_test.go index b190d6071..5ab7c2a05 100644 --- a/authority/provisioner/nebula_test.go +++ b/authority/provisioner/nebula_test.go @@ -3,7 +3,9 @@ package provisioner import ( "context" "crypto" + "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" "crypto/rand" "crypto/x509" "net" @@ -13,21 +15,21 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" "github.com/slackhq/nebula/cert" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" + "go.step.sm/crypto/jose" "go.step.sm/crypto/randutil" "go.step.sm/crypto/x25519" "go.step.sm/crypto/x509util" - "golang.org/x/crypto/ssh" ) func mustNebulaIPNet(t *testing.T, s string) *net.IPNet { t.Helper() ip, ipNet, err := net.ParseCIDR(s) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) if ip = ip.To4(); ip == nil { t.Fatalf("nebula only supports ipv4, have %s", s) } @@ -38,9 +40,7 @@ func mustNebulaIPNet(t *testing.T, s string) *net.IPNet { func mustNebulaCA(t *testing.T) (*cert.NebulaCertificate, ed25519.PrivateKey) { t.Helper() pub, priv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) nc := &cert.NebulaCertificate{ Details: cert.NebulaCertificateDetails{ Name: "TestCA", @@ -53,26 +53,51 @@ func mustNebulaCA(t *testing.T) (*cert.NebulaCertificate, ed25519.PrivateKey) { NotAfter: time.Now().Add(10 * time.Minute), PublicKey: pub, IsCA: true, + Curve: cert.Curve_CURVE25519, }, } - if err := nc.Sign(priv); err != nil { - t.Fatal(err) - } + err = nc.Sign(cert.Curve_CURVE25519, priv) + require.NoError(t, err) + return nc, priv } +func mustNebulaP256CA(t *testing.T) (*cert.NebulaCertificate, *ecdsa.PrivateKey) { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + priv, err := key.ECDH() + require.NoError(t, err) + + nc := &cert.NebulaCertificate{ + Details: cert.NebulaCertificateDetails{ + Name: "TestCA", + Groups: []string{"test"}, + Ips: []*net.IPNet{ + mustNebulaIPNet(t, "10.1.0.0/16"), + }, + Subnets: []*net.IPNet{}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(10 * time.Minute), + PublicKey: priv.PublicKey().Bytes(), + IsCA: true, + Curve: cert.Curve_P256, + }, + } + err = nc.Sign(cert.Curve_P256, priv.Bytes()) + require.NoError(t, err) + return nc, key +} + func mustNebulaCert(t *testing.T, name string, ipNet *net.IPNet, groups []string, ca *cert.NebulaCertificate, signer ed25519.PrivateKey) (*cert.NebulaCertificate, crypto.Signer) { t.Helper() pub, priv, err := x25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) issuer, err := ca.Sha256Sum() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) invertedGroups := make(map[string]struct{}, len(groups)) for _, name := range groups { @@ -92,24 +117,64 @@ func mustNebulaCert(t *testing.T, name string, ipNet *net.IPNet, groups []string IsCA: false, Issuer: issuer, InvertedGroups: invertedGroups, + Curve: cert.Curve_CURVE25519, }, } - if err := nc.Sign(signer); err != nil { - t.Fatal(err) - } + err = nc.Sign(cert.Curve_CURVE25519, signer) + require.NoError(t, err) return nc, priv } +func mustNebulaP256Cert(t *testing.T, name string, ipNet *net.IPNet, groups []string, ca *cert.NebulaCertificate, signer *ecdsa.PrivateKey) (*cert.NebulaCertificate, crypto.Signer) { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + priv, err := key.ECDH() + require.NoError(t, err) + + issuer, err := ca.Sha256Sum() + require.NoError(t, err) + + invertedGroups := make(map[string]struct{}, len(groups)) + for _, name := range groups { + invertedGroups[name] = struct{}{} + } + + t1 := time.Now().Truncate(time.Second) + nc := &cert.NebulaCertificate{ + Details: cert.NebulaCertificateDetails{ + Name: name, + Ips: []*net.IPNet{ipNet}, + Subnets: []*net.IPNet{}, + Groups: groups, + NotBefore: t1, + NotAfter: t1.Add(5 * time.Minute), + PublicKey: priv.PublicKey().Bytes(), + IsCA: false, + Issuer: issuer, + InvertedGroups: invertedGroups, + Curve: cert.Curve_P256, + }, + } + + ecdhKey, err := signer.ECDH() + require.NoError(t, err) + + err = nc.Sign(cert.Curve_P256, ecdhKey.Bytes()) + require.NoError(t, err) + + return nc, key +} + func mustNebulaProvisioner(t *testing.T) (*Nebula, *cert.NebulaCertificate, ed25519.PrivateKey) { t.Helper() nc, signer := mustNebulaCA(t) ncPem, err := nc.MarshalToPEM() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) bTrue := true p := &Nebula{ Type: TypeNebula.String(), @@ -119,36 +184,53 @@ func mustNebulaProvisioner(t *testing.T) (*Nebula, *cert.NebulaCertificate, ed25 EnableSSHCA: &bTrue, }, } - if err := p.Init(Config{ + err = p.Init(Config{ Claims: globalProvisionerClaims, Audiences: testAudiences, - }); err != nil { - t.Fatal(err) + }) + require.NoError(t, err) + + return p, nc, signer +} + +func mustNebulaP256Provisioner(t *testing.T) (*Nebula, *cert.NebulaCertificate, *ecdsa.PrivateKey) { + t.Helper() + + nc, signer := mustNebulaP256CA(t) + ncPem, err := nc.MarshalToPEM() + require.NoError(t, err) + bTrue := true + p := &Nebula{ + Type: TypeNebula.String(), + Name: "nebulous", + Roots: ncPem, + Claims: &Claims{ + EnableSSHCA: &bTrue, + }, } + err = p.Init(Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + }) + require.NoError(t, err) return p, nc, signer } -func mustNebulaToken(t *testing.T, sub, iss, aud string, iat time.Time, sans []string, nc *cert.NebulaCertificate, key crypto.Signer) string { +func mustNebulaToken(t *testing.T, sub, iss, aud string, iat time.Time, sans []string, nc *cert.NebulaCertificate, key crypto.Signer, algorithm jose.SignatureAlgorithm) string { t.Helper() ncDer, err := nc.Marshal() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) so := new(jose.SignerOptions) so.WithType("JWT") so.WithHeader(NebulaCertHeader, ncDer) - sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.XEdDSA, Key: key}, so) - if err != nil { - t.Fatal(err) - } + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: algorithm, Key: key}, so) + require.NoError(t, err) id, err := randutil.ASCII(64) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) claims := struct { jose.Claims @@ -166,32 +248,25 @@ func mustNebulaToken(t *testing.T, sub, iss, aud string, iat time.Time, sans []s SANS: sans, } tok, err := jose.Signed(sig).Claims(claims).CompactSerialize() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + return tok } -func mustNebulaSSHToken(t *testing.T, sub, iss, aud string, iat time.Time, opts *SignSSHOptions, nc *cert.NebulaCertificate, key crypto.Signer) string { +func mustNebulaSSHToken(t *testing.T, sub, iss, aud string, iat time.Time, opts *SignSSHOptions, nc *cert.NebulaCertificate, key crypto.Signer, algorithm jose.SignatureAlgorithm) string { t.Helper() ncDer, err := nc.Marshal() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) so := new(jose.SignerOptions) so.WithType("JWT") so.WithHeader(NebulaCertHeader, ncDer) - sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.XEdDSA, Key: key}, so) - if err != nil { - t.Fatal(err) - } + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: algorithm, Key: key}, so) + require.NoError(t, err) id, err := randutil.ASCII(64) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) claims := struct { jose.Claims @@ -214,18 +289,15 @@ func mustNebulaSSHToken(t *testing.T, sub, iss, aud string, iat time.Time, opts } tok, err := jose.Signed(sig).Claims(claims).CompactSerialize() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + return tok } func TestNebula_Init(t *testing.T) { nc, _ := mustNebulaCA(t) ncPem, err := nc.MarshalToPEM() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) cfg := Config{ Claims: globalProvisionerClaims, @@ -327,11 +399,9 @@ func TestNebula_GetIDForToken(t *testing.T) { func TestNebula_GetTokenID(t *testing.T) { p, ca, signer := mustNebulaProvisioner(t) c1, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"group"}, ca, signer) - t1 := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, c1, priv) + t1 := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, c1, priv, jose.XEdDSA) _, claims, err := parseToken(t1) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) type args struct { token string @@ -441,8 +511,8 @@ func TestNebula_AuthorizeSign(t *testing.T) { ctx := context.TODO() p, ca, signer := mustNebulaProvisioner(t) crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer) - ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, crt, priv) - okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), nil, crt, priv) + ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, crt, priv, jose.XEdDSA) + okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), nil, crt, priv, jose.XEdDSA) pBadOptions, _, _ := mustNebulaProvisioner(t) pBadOptions.caPool = p.caPool @@ -487,20 +557,20 @@ func TestNebula_AuthorizeSSHSign(t *testing.T) { CertType: "host", KeyID: "test.lan", Principals: []string{"test.lan", "10.1.0.1"}, - }, crt, priv) - okNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), nil, crt, priv) + }, crt, priv, jose.XEdDSA) + okNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), nil, crt, priv, jose.XEdDSA) okWithValidity := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{ ValidAfter: NewTimeDuration(now().Add(1 * time.Hour)), ValidBefore: NewTimeDuration(now().Add(10 * time.Hour)), - }, crt, priv) + }, crt, priv, jose.XEdDSA) failUserCert := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{ CertType: "user", - }, crt, priv) + }, crt, priv, jose.XEdDSA) failPrincipals := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{ CertType: "host", KeyID: "test.lan", Principals: []string{"test.lan", "10.1.0.1", "foo.bar"}, - }, crt, priv) + }, crt, priv, jose.XEdDSA) // Provisioner with SSH disabled var bFalse bool @@ -592,12 +662,12 @@ func TestNebula_AuthorizeRevoke(t *testing.T) { // Ok provisioner p, ca, signer := mustNebulaProvisioner(t) crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer) - ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv) + ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv, jose.XEdDSA) // Fail different CA nc, signer := mustNebulaCA(t) crt, priv = mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, nc, signer) - failToken := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv) + failToken := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv, jose.XEdDSA) type args struct { ctx context.Context @@ -626,12 +696,12 @@ func TestNebula_AuthorizeSSHRevoke(t *testing.T) { // Ok provisioner p, ca, signer := mustNebulaProvisioner(t) crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer) - ok := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv) + ok := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv, jose.XEdDSA) // Fail different CA nc, signer := mustNebulaCA(t) crt, priv = mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, nc, signer) - failToken := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv) + failToken := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv, jose.XEdDSA) // Provisioner with SSH disabled var bFalse bool @@ -665,7 +735,7 @@ func TestNebula_AuthorizeSSHRevoke(t *testing.T) { func TestNebula_AuthorizeSSHRenew(t *testing.T) { p, ca, signer := mustNebulaProvisioner(t) crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer) - t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRenew[0], now(), nil, crt, priv) + t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRenew[0], now(), nil, crt, priv, jose.XEdDSA) type args struct { ctx context.Context @@ -697,7 +767,7 @@ func TestNebula_AuthorizeSSHRenew(t *testing.T) { func TestNebula_AuthorizeSSHRekey(t *testing.T) { p, ca, signer := mustNebulaProvisioner(t) crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer) - t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRekey[0], now(), nil, crt, priv) + t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRekey[0], now(), nil, crt, priv, jose.XEdDSA) type args struct { ctx context.Context @@ -734,30 +804,26 @@ func TestNebula_authorizeToken(t *testing.T) { t1 := now() p, ca, signer := mustNebulaProvisioner(t) crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer) - ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv) - okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, nil, crt, priv) + ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.XEdDSA) + okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, nil, crt, priv, jose.XEdDSA) okSSH := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, &SignSSHOptions{ CertType: "host", KeyID: "test.lan", Principals: []string{"test.lan"}, - }, crt, priv) - okSSHNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, nil, crt, priv) + }, crt, priv, jose.XEdDSA) + okSSHNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, nil, crt, priv, jose.XEdDSA) // Token with errors - failNotBefore := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1.Add(1*time.Hour), []string{"10.1.0.1"}, crt, priv) - failIssuer := mustNebulaToken(t, "test.lan", "foo", p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv) - failAudience := mustNebulaToken(t, "test.lan", p.Name, "foo", t1, []string{"10.1.0.1"}, crt, priv) - failSubject := mustNebulaToken(t, "", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv) + failNotBefore := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1.Add(1*time.Hour), []string{"10.1.0.1"}, crt, priv, jose.XEdDSA) + failIssuer := mustNebulaToken(t, "test.lan", "foo", p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.XEdDSA) + failAudience := mustNebulaToken(t, "test.lan", p.Name, "foo", t1, []string{"10.1.0.1"}, crt, priv, jose.XEdDSA) + failSubject := mustNebulaToken(t, "", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.XEdDSA) // Not a nebula token jwk, err := generateJSONWebKey() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) simpleToken, err := generateSimpleToken("iss", "aud", jwk) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // Provisioner with a different CA p2, _, _ := mustNebulaProvisioner(t) @@ -824,22 +890,128 @@ func TestNebula_authorizeToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, got1, err := tt.p.authorizeToken(tt.args.token, tt.args.audiences) - if (err != nil) != tt.wantErr { - t.Errorf("Nebula.authorizeToken() error = %v, wantErr %v", err, tt.wantErr) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + assert.Nil(t, got1) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Nebula.authorizeToken() got = %#v, want %#v", got, tt.want) - t.Error(cmp.Equal(got, tt.want)) - } if got1 != nil && tt.want1 != nil { tt.want1.ID = got1.ID } - if !reflect.DeepEqual(got1, tt.want1) { - t.Errorf("Nebula.authorizeToken() got1 = %v, want %v", got1, tt.want1) + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.want1, got1) + }) + } +} + +func TestNebula_authorizeToken_P256(t *testing.T) { + t1 := now() + p, ca, signer := mustNebulaP256Provisioner(t) + crt, priv := mustNebulaP256Cert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer) + ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.ES256) + okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, nil, crt, priv, jose.ES256) + okSSH := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, &SignSSHOptions{ + CertType: "host", + KeyID: "test.lan", + Principals: []string{"test.lan"}, + }, crt, priv, jose.ES256) + okSSHNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, nil, crt, priv, jose.ES256) + + // Token with errors + failNotBefore := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1.Add(1*time.Hour), []string{"10.1.0.1"}, crt, priv, jose.ES256) + failIssuer := mustNebulaToken(t, "test.lan", "foo", p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.ES256) + failAudience := mustNebulaToken(t, "test.lan", p.Name, "foo", t1, []string{"10.1.0.1"}, crt, priv, jose.ES256) + failSubject := mustNebulaToken(t, "", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.ES256) + + // Not a nebula token + jwk, err := generateJSONWebKey() + require.NoError(t, err) + simpleToken, err := generateSimpleToken("iss", "aud", jwk) + require.NoError(t, err) + + // Provisioner with a different CA + p2, _, _ := mustNebulaP256Provisioner(t) + + x509Claims := jose.Claims{ + ID: "[REPLACEME]", + Subject: "test.lan", + Issuer: p.Name, + IssuedAt: jose.NewNumericDate(t1), + NotBefore: jose.NewNumericDate(t1), + Expiry: jose.NewNumericDate(t1.Add(5 * time.Minute)), + Audience: []string{p.ctl.Audiences.Sign[0]}, + } + sshClaims := jose.Claims{ + ID: "[REPLACEME]", + Subject: "test.lan", + Issuer: p.Name, + IssuedAt: jose.NewNumericDate(t1), + NotBefore: jose.NewNumericDate(t1), + Expiry: jose.NewNumericDate(t1.Add(5 * time.Minute)), + Audience: []string{p.ctl.Audiences.SSHSign[0]}, + } + + type args struct { + token string + audiences []string + } + tests := []struct { + name string + p *Nebula + args args + want *cert.NebulaCertificate + want1 *jwtPayload + wantErr bool + }{ + {"ok x509", p, args{ok, p.ctl.Audiences.Sign}, crt, &jwtPayload{ + Claims: x509Claims, + SANs: []string{"10.1.0.1"}, + }, false}, + {"ok x509 no sans", p, args{okNoSANs, p.ctl.Audiences.Sign}, crt, &jwtPayload{ + Claims: x509Claims, + }, false}, + {"ok ssh", p, args{okSSH, p.ctl.Audiences.SSHSign}, crt, &jwtPayload{ + Claims: sshClaims, + Step: &stepPayload{ + SSH: &SignSSHOptions{ + CertType: "host", + KeyID: "test.lan", + Principals: []string{"test.lan"}, + }, + }, + }, false}, + {"ok ssh no principals", p, args{okSSHNoOptions, p.ctl.Audiences.SSHSign}, crt, &jwtPayload{ + Claims: sshClaims, + }, false}, + {"fail parse", p, args{"bad.token", p.ctl.Audiences.Sign}, nil, nil, true}, + {"fail header", p, args{simpleToken, p.ctl.Audiences.Sign}, nil, nil, true}, + {"fail verify", p2, args{ok, p.ctl.Audiences.Sign}, nil, nil, true}, + {"fail claims nbf", p, args{failNotBefore, p.ctl.Audiences.Sign}, nil, nil, true}, + {"fail claims iss", p, args{failIssuer, p.ctl.Audiences.Sign}, nil, nil, true}, + {"fail claims aud", p, args{failAudience, p.ctl.Audiences.Sign}, nil, nil, true}, + {"fail claims sub", p, args{failSubject, p.ctl.Audiences.Sign}, nil, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := tt.p.authorizeToken(tt.args.token, tt.args.audiences) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + assert.Nil(t, got1) + return } + + if got1 != nil && tt.want1 != nil { + tt.want1.ID = got1.ID + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.want1, got1) }) } } diff --git a/go.mod b/go.mod index a9d7f7981..04d1e5a7a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/smallstep/certificates -go 1.22 +go 1.22.0 require ( cloud.google.com/go/longrunning v0.5.11 @@ -25,7 +25,7 @@ require ( github.com/prometheus/client_golang v1.20.1 github.com/rs/xid v1.5.0 github.com/sirupsen/logrus v1.9.3 - github.com/slackhq/nebula v1.6.1 + github.com/slackhq/nebula v1.9.3 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 github.com/smallstep/nosql v0.7.0 @@ -38,7 +38,7 @@ require ( go.step.sm/linkedca v0.22.1 golang.org/x/crypto v0.26.0 golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 - golang.org/x/net v0.27.0 + golang.org/x/net v0.28.0 google.golang.org/api v0.191.0 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 @@ -156,7 +156,7 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.6.0 // indirect google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf // indirect diff --git a/go.sum b/go.sum index 6d9d883b4..7128cafa2 100644 --- a/go.sum +++ b/go.sum @@ -352,8 +352,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM= -github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI= +github.com/slackhq/nebula v1.9.3 h1:WK5Oipy4NsVfNm41pywGmdy048F8RRkfSRG+lPHxcJQ= +github.com/slackhq/nebula v1.9.3/go.mod h1:PMJer5rZe0H/O+kUiKOL9AJ/pL9+ryzNXtSN7ABfjfM= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA= @@ -456,8 +456,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -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.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -492,8 +492,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=