diff --git a/authority/authorize.go b/authority/authorize.go index 7c1c2ff6..0c1c6f22 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -130,22 +130,19 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc // According to "rfc7519 JSON Web Token" acceptable skew should be no // more than a few minutes. if err := claims.ValidateWithLeeway(jose.Expected{ - Issuer: prov.GetName(), + Issuer: "step-admin-client/1.0", Time: time.Now().UTC(), }, time.Minute); err != nil { return nil, admin.WrapError(admin.ErrorUnauthorizedType, err, "x5c.authorizeToken; invalid x5c claims") } // validate audience: path matches the current path - if r.URL.Path != claims.Audience[0] { - return nil, admin.NewError(admin.ErrorUnauthorizedType, - "x5c.authorizeToken; x5c token has invalid audience "+ - "claim (aud); expected %s, but got %s", r.URL.Path, claims.Audience) + if !matchesAudience(claims.Audience, a.config.Audience(r.URL.Path)) { + return nil, admin.NewError(admin.ErrorUnauthorizedType, "x5c.authorizeToken; x5c token has invalid audience claim (aud)") } if claims.Subject == "" { - return nil, admin.NewError(admin.ErrorUnauthorizedType, - "x5c.authorizeToken; x5c token subject cannot be empty") + return nil, admin.NewError(admin.ErrorUnauthorizedType, "x5c.authorizeToken; x5c token subject cannot be empty") } var ( @@ -156,7 +153,7 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc adminSANs := append([]string{leaf.Subject.CommonName}, leaf.DNSNames...) adminSANs = append(adminSANs, leaf.EmailAddresses...) for _, san := range adminSANs { - if adm, ok = a.LoadAdminBySubProv(san, claims.Issuer); ok { + if adm, ok = a.LoadAdminBySubProv(san, prov.GetName()); ok { adminFound = true break } diff --git a/authority/config/config.go b/authority/config/config.go index 2c437725..4ed7a1cb 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -304,6 +304,18 @@ func (c *Config) GetAudiences() provisioner.Audiences { return audiences } +// Audience returns the list of audiences for a given path. +func (c *Config) Audience(path string) []string { + audiences := make([]string, len(c.DNSNames)+1) + for i, name := range c.DNSNames { + hostname := toHostname(name) + audiences[i] = "https://" + hostname + path + } + // For backward compatibility + audiences[len(c.DNSNames)] = path + return audiences +} + func toHostname(name string) string { // ensure an IPv6 address is represented with square brackets when used as hostname if ip := net.ParseIP(name); ip != nil && ip.To4() == nil { diff --git a/authority/config/config_test.go b/authority/config/config_test.go index b921be13..5a05b3f6 100644 --- a/authority/config/config_test.go +++ b/authority/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "reflect" "testing" "github.com/pkg/errors" @@ -317,3 +318,38 @@ func Test_toHostname(t *testing.T) { }) } } + +func TestConfig_Audience(t *testing.T) { + type fields struct { + DNSNames []string + } + type args struct { + path string + } + tests := []struct { + name string + fields fields + args args + want []string + }{ + {"ok", fields{[]string{ + "ca", "ca.example.com", "127.0.0.1", "::1", + }}, args{"/path"}, []string{ + "https://ca/path", + "https://ca.example.com/path", + "https://127.0.0.1/path", + "https://[::1]/path", + "/path", + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Config{ + DNSNames: tt.fields.DNSNames, + } + if got := c.Audience(tt.args.path); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Config.Audience() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/linkedca.go b/authority/linkedca.go index 6a0800c2..1b920ce8 100644 --- a/authority/linkedca.go +++ b/authority/linkedca.go @@ -235,6 +235,28 @@ func (c *linkedCaClient) DeleteAdmin(ctx context.Context, id string) error { return errors.Wrap(err, "error deleting admin") } +func (c *linkedCaClient) GetCertificateData(serial string) (*db.CertificateData, error) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + resp, err := c.client.GetCertificate(ctx, &linkedca.GetCertificateRequest{ + Serial: serial, + }) + if err != nil { + return nil, err + } + + var provisioner *db.ProvisionerData + if p := resp.Provisioner; p != nil { + provisioner = &db.ProvisionerData{ + ID: p.Id, Name: p.Name, Type: p.Type.String(), + } + } + return &db.CertificateData{ + Provisioner: provisioner, + }, nil +} + func (c *linkedCaClient) StoreCertificateChain(prov provisioner.Interface, fullchain ...*x509.Certificate) error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() diff --git a/authority/provisioners.go b/authority/provisioners.go index c0eec520..3713705f 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -54,14 +54,19 @@ func (a *Authority) LoadProvisionerByCertificate(crt *x509.Certificate) (provisi return p, nil } - // Attempt to load the provisioner using the linked db - // TODO:(mariano) - - // Attempt to load the provisioner from the db - if db, ok := a.db.(interface { + // certificateDataGetter is an interface that can be use to retrieve the + // provisioner from a db or a linked ca. + type certificateDataGetter interface { GetCertificateData(string) (*db.CertificateData, error) - }); ok { - if data, err := db.GetCertificateData(crt.SerialNumber.String()); err == nil && data.Provisioner != nil { + } + var cdg certificateDataGetter + if getter, ok := a.adminDB.(certificateDataGetter); ok { + cdg = getter + } else if getter, ok := a.db.(certificateDataGetter); ok { + cdg = getter + } + if cdg != nil { + if data, err := cdg.GetCertificateData(crt.SerialNumber.String()); err == nil && data.Provisioner != nil { loadProvisioner = func() (provisioner.Interface, error) { p, ok := a.provisioners.Load(data.Provisioner.ID) if !ok { diff --git a/ca/adminClient.go b/ca/adminClient.go index 5f3993b1..c3ba666f 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -23,7 +23,10 @@ import ( "google.golang.org/protobuf/encoding/protojson" ) -var adminURLPrefix = "admin" +const ( + adminURLPrefix = "admin" + adminIssuer = "step-admin-client/1.0" +) // AdminClient implements an HTTP client for the CA server. type AdminClient struct { @@ -35,7 +38,6 @@ type AdminClient struct { x5cCertFile string x5cCertStrs []string x5cCert *x509.Certificate - x5cIssuer string x5cSubject string } @@ -77,12 +79,11 @@ func NewAdminClient(endpoint string, opts ...ClientOption) (*AdminClient, error) x5cCertFile: o.x5cCertFile, x5cCertStrs: o.x5cCertStrs, x5cCert: o.x5cCert, - x5cIssuer: o.x5cIssuer, x5cSubject: o.x5cSubject, }, nil } -func (c *AdminClient) generateAdminToken(urlPath string) (string, error) { +func (c *AdminClient) generateAdminToken(aud *url.URL) (string, error) { // A random jwt id will be used to identify duplicated tokens jwtID, err := randutil.Hex(64) // 256 bits if err != nil { @@ -93,8 +94,8 @@ func (c *AdminClient) generateAdminToken(urlPath string) (string, error) { tokOptions := []token.Options{ token.WithJWTID(jwtID), token.WithKid(c.x5cJWK.KeyID), - token.WithIssuer(c.x5cIssuer), - token.WithAudience(urlPath), + token.WithIssuer(adminIssuer), + token.WithAudience(aud.String()), token.WithValidity(now, now.Add(token.DefaultValidity)), token.WithX5CCerts(c.x5cCertStrs), } @@ -205,7 +206,7 @@ func (c *AdminClient) GetAdminsPaginate(opts ...AdminOption) (*adminAPI.GetAdmin Path: "/admin/admins", RawQuery: o.rawQuery(), }) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -260,7 +261,7 @@ func (c *AdminClient) CreateAdmin(createAdminRequest *adminAPI.CreateAdminReques return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: "/admin/admins"}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -292,7 +293,7 @@ retry: func (c *AdminClient) RemoveAdmin(id string) error { var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "admins", id)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return errors.Wrapf(err, "error generating admin token") } @@ -324,7 +325,7 @@ func (c *AdminClient) UpdateAdmin(id string, uar *adminAPI.UpdateAdminRequest) ( return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "admins", id)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -371,7 +372,7 @@ func (c *AdminClient) GetProvisioner(opts ...ProvisionerOption) (*linkedca.Provi default: return nil, errors.New("must set either name or id in method options") } - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -410,7 +411,7 @@ func (c *AdminClient) GetProvisionersPaginate(opts ...ProvisionerOption) (*admin Path: "/admin/provisioners", RawQuery: o.rawQuery(), }) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -480,7 +481,7 @@ func (c *AdminClient) RemoveProvisioner(opts ...ProvisionerOption) error { default: return errors.New("must set either name or id in method options") } - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return errors.Wrapf(err, "error generating admin token") } @@ -512,7 +513,7 @@ func (c *AdminClient) CreateProvisioner(prov *linkedca.Provisioner) (*linkedca.P return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners")}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -548,7 +549,7 @@ func (c *AdminClient) UpdateProvisioner(name string, prov *linkedca.Provisioner) return errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", name)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return errors.Wrapf(err, "error generating admin token") } @@ -587,7 +588,7 @@ func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName, reference Path: p, RawQuery: o.rawQuery(), }) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -623,7 +624,7 @@ func (c *AdminClient) CreateExternalAccountKey(provisionerName string, eakReques return nil, errs.Wrap(http.StatusInternalServerError, err, "error marshaling request") } u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab/", provisionerName)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return nil, errors.Wrapf(err, "error generating admin token") } @@ -655,7 +656,7 @@ retry: func (c *AdminClient) RemoveExternalAccountKey(provisionerName, keyID string) error { var retried bool u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "acme/eab", provisionerName, "/", keyID)}) - tok, err := c.generateAdminToken(u.Path) + tok, err := c.generateAdminToken(u) if err != nil { return errors.Wrapf(err, "error generating admin token") } diff --git a/ca/client.go b/ca/client.go index 3a36fcd6..4aa66aac 100644 --- a/ca/client.go +++ b/ca/client.go @@ -116,7 +116,6 @@ type clientOptions struct { x5cCertFile string x5cCertStrs []string x5cCert *x509.Certificate - x5cIssuer string x5cSubject string } @@ -332,19 +331,13 @@ func WithAdminX5C(certs []*x509.Certificate, key interface{}, passwordFile strin } o.x5cCert = certs[0] - o.x5cSubject = o.x5cCert.Subject.CommonName - - for _, e := range o.x5cCert.Extensions { - if e.Id.Equal(stepOIDProvisioner) { - var prov stepProvisionerASN1 - if _, err := asn1.Unmarshal(e.Value, &prov); err != nil { - return errors.Wrap(err, "error unmarshaling provisioner OID from certificate") - } - o.x5cIssuer = string(prov.Name) - } - } - if o.x5cIssuer == "" { - return errors.New("provisioner extension not found in certificate") + switch leaf := certs[0]; { + case leaf.Subject.CommonName != "": + o.x5cSubject = leaf.Subject.CommonName + case len(leaf.DNSNames) > 0: + o.x5cSubject = leaf.DNSNames[0] + case len(leaf.EmailAddresses) > 0: + o.x5cSubject = leaf.EmailAddresses[0] } return nil diff --git a/go.mod b/go.mod index 16007f5b..85d21ceb 100644 --- a/go.mod +++ b/go.mod @@ -50,4 +50,4 @@ require ( // replace github.com/smallstep/nosql => ../nosql // replace go.step.sm/crypto => ../crypto // replace go.step.sm/cli-utils => ../cli-utils -// replace go.step.sm/linkedca => ../linkedca +replace go.step.sm/linkedca => ../linkedca diff --git a/go.sum b/go.sum index 108cfec9..47f84801 100644 --- a/go.sum +++ b/go.sum @@ -709,8 +709,6 @@ go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/ go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.15.3 h1:f3GMl+aCydt294BZRjTYwpaXRqwwndvoTY2NLN4wu10= go.step.sm/crypto v0.15.3/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= -go.step.sm/linkedca v0.12.0 h1:FA18uJO5P6W2pklcezMs+w+N3dVbpKEE1LP9HLsJgg4= -go.step.sm/linkedca v0.12.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=