sshpop provisioner + ssh renew | revoke | rekey first pass

This commit is contained in:
max furman 2019-10-28 11:50:43 -07:00
parent c04f1e1bd4
commit 29853ae016
26 changed files with 1185 additions and 338 deletions

View file

@ -38,7 +38,7 @@ type Authority interface {
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error) LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
LoadProvisionerByID(string) (provisioner.Interface, error) LoadProvisionerByID(string) (provisioner.Interface, error)
GetProvisioners(cursor string, limit int) (provisioner.List, string, error) GetProvisioners(cursor string, limit int) (provisioner.List, string, error)
Revoke(*authority.RevokeOptions) error Revoke(context.Context, *authority.RevokeOptions) error
GetEncryptedKey(kid string) (string, error) GetEncryptedKey(kid string) (string, error)
GetRoots() (federation []*x509.Certificate, err error) GetRoots() (federation []*x509.Certificate, err error)
GetFederation() ([]*x509.Certificate, error) GetFederation() ([]*x509.Certificate, error)
@ -252,6 +252,9 @@ func (h *caHandler) Route(r Router) {
r.MethodFunc("GET", "/federation", h.Federation) r.MethodFunc("GET", "/federation", h.Federation)
// SSH CA // SSH CA
r.MethodFunc("POST", "/ssh/sign", h.SSHSign) r.MethodFunc("POST", "/ssh/sign", h.SSHSign)
r.MethodFunc("POST", "/ssh/renew", h.SSHRenew)
r.MethodFunc("POST", "/ssh/revoke", h.SSHRevoke)
r.MethodFunc("POST", "/ssh/rekey", h.SSHRekey)
r.MethodFunc("GET", "/ssh/roots", h.SSHRoots) r.MethodFunc("GET", "/ssh/roots", h.SSHRoots)
r.MethodFunc("GET", "/ssh/federation", h.SSHFederation) r.MethodFunc("GET", "/ssh/federation", h.SSHFederation)
r.MethodFunc("POST", "/ssh/config", h.SSHConfig) r.MethodFunc("POST", "/ssh/config", h.SSHConfig)

View file

@ -1,10 +1,12 @@
package api package api
import ( import (
"context"
"net/http" "net/http"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/logging" "github.com/smallstep/certificates/logging"
"golang.org/x/crypto/ocsp" "golang.org/x/crypto/ocsp"
) )
@ -63,10 +65,15 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) {
PassiveOnly: body.Passive, PassiveOnly: body.Passive,
} }
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)
// A token indicates that we are using the api via a provisioner token, // A token indicates that we are using the api via a provisioner token,
// otherwise it is assumed that the certificate is revoking itself over mTLS. // otherwise it is assumed that the certificate is revoking itself over mTLS.
if len(body.OTT) > 0 { if len(body.OTT) > 0 {
logOtt(w, body.OTT) logOtt(w, body.OTT)
if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil {
WriteError(w, Unauthorized(err))
return
}
opts.OTT = body.OTT opts.OTT = body.OTT
} else { } else {
// If no token is present, then the request must be made over mTLS and // If no token is present, then the request must be made over mTLS and
@ -77,11 +84,18 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) {
return return
} }
opts.Crt = r.TLS.PeerCertificates[0] opts.Crt = r.TLS.PeerCertificates[0]
if opts.Crt.SerialNumber.String() != opts.Serial {
WriteError(w, BadRequest(errors.New("revoke: serial number in mtls certificate different than body")))
return
}
// TODO: should probably be checking if the certificate was revoked here.
// Will need to thread that request down to the authority, so will need
// to add API for that.
logCertificate(w, opts.Crt) logCertificate(w, opts.Crt)
opts.MTLS = true opts.MTLS = true
} }
if err := h.Authority.Revoke(opts); err != nil { if err := h.Authority.Revoke(ctx, opts); err != nil {
WriteError(w, Forbidden(err)) WriteError(w, Forbidden(err))
return return
} }

View file

@ -16,6 +16,8 @@ import (
// SSHAuthority is the interface implemented by a SSH CA authority. // SSHAuthority is the interface implemented by a SSH CA authority.
type SSHAuthority interface { type SSHAuthority interface {
SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
RenewSSH(cert *ssh.Certificate) (*ssh.Certificate, error)
RekeySSH(cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
GetSSHRoots() (*authority.SSHKeys, error) GetSSHRoots() (*authority.SSHKeys, error)
GetSSHFederation() (*authority.SSHKeys, error) GetSSHFederation() (*authority.SSHKeys, error)
@ -67,7 +69,8 @@ type SSHCertificate struct {
*ssh.Certificate `json:"omitempty"` *ssh.Certificate `json:"omitempty"`
} }
// SSHGetHostsResponse // SSHGetHostsResponse is the response object that returns the list of valid
// hosts for SSH.
type SSHGetHostsResponse struct { type SSHGetHostsResponse struct {
Hosts []string `json:"hosts"` Hosts []string `json:"hosts"`
} }

78
api/sshRekey.go Normal file
View file

@ -0,0 +1,78 @@
package api
import (
"context"
"net/http"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"golang.org/x/crypto/ssh"
)
// SSHRekeyRequest is the request body of an SSH certificate request.
type SSHRekeyRequest struct {
OTT string `json:"ott"`
PublicKey []byte `json:"publicKey"` //base64 encoded
}
// Validate validates the SSHSignRekey.
func (s *SSHRekeyRequest) Validate() error {
switch {
case len(s.OTT) == 0:
return errors.New("missing or empty ott")
case len(s.PublicKey) == 0:
return errors.New("missing or empty public key")
default:
return nil
}
}
// SSHRekeyResponse is the response object that returns the SSH certificate.
type SSHRekeyResponse struct {
Certificate SSHCertificate `json:"crt"`
}
// SSHRekey is an HTTP handler that reads an RekeySSHRequest with a one-time-token
// (ott) from the body and creates a new SSH certificate with the information in
// the request.
func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) {
var body SSHRekeyRequest
if err := ReadJSON(r.Body, &body); err != nil {
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
return
}
logOtt(w, body.OTT)
if err := body.Validate(); err != nil {
WriteError(w, BadRequest(err))
return
}
publicKey, err := ssh.ParsePublicKey(body.PublicKey)
if err != nil {
WriteError(w, BadRequest(errors.Wrap(err, "error parsing publicKey")))
return
}
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RekeySSHMethod)
signOpts, err := h.Authority.Authorize(ctx, body.OTT)
if err != nil {
WriteError(w, Unauthorized(err))
return
}
oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT)
if err != nil {
WriteError(w, InternalServerError(err))
}
newCert, err := h.Authority.RekeySSH(oldCert, publicKey, signOpts...)
if err != nil {
WriteError(w, Forbidden(err))
return
}
w.WriteHeader(http.StatusCreated)
JSON(w, &SSHSignResponse{
Certificate: SSHCertificate{newCert},
})
}

68
api/sshRenew.go Normal file
View file

@ -0,0 +1,68 @@
package api
import (
"context"
"net/http"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
)
// SSHRenewRequest is the request body of an SSH certificate request.
type SSHRenewRequest struct {
OTT string `json:"ott"`
}
// Validate validates the SSHSignRequest.
func (s *SSHRenewRequest) Validate() error {
switch {
case len(s.OTT) == 0:
return errors.New("missing or empty ott")
default:
return nil
}
}
// SSHRenewResponse is the response object that returns the SSH certificate.
type SSHRenewResponse struct {
Certificate SSHCertificate `json:"crt"`
}
// SSHRenew is an HTTP handler that reads an RenewSSHRequest with a one-time-token
// (ott) from the body and creates a new SSH certificate with the information in
// the request.
func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) {
var body SSHRenewRequest
if err := ReadJSON(r.Body, &body); err != nil {
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
return
}
logOtt(w, body.OTT)
if err := body.Validate(); err != nil {
WriteError(w, BadRequest(err))
return
}
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RenewSSHMethod)
_, err := h.Authority.Authorize(ctx, body.OTT)
if err != nil {
WriteError(w, Unauthorized(err))
return
}
oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT)
if err != nil {
WriteError(w, InternalServerError(err))
}
newCert, err := h.Authority.RenewSSH(oldCert)
if err != nil {
WriteError(w, Forbidden(err))
return
}
w.WriteHeader(http.StatusCreated)
JSON(w, &SSHSignResponse{
Certificate: SSHCertificate{newCert},
})
}

98
api/sshRevoke.go Normal file
View file

@ -0,0 +1,98 @@
package api
import (
"context"
"net/http"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/logging"
"golang.org/x/crypto/ocsp"
)
// SSHRevokeResponse is the response object that returns the health of the server.
type SSHRevokeResponse struct {
Status string `json:"status"`
}
// SSHRevokeRequest is the request body for a revocation request.
type SSHRevokeRequest struct {
Serial string `json:"serial"`
OTT string `json:"ott"`
ReasonCode int `json:"reasonCode"`
Reason string `json:"reason"`
Passive bool `json:"passive"`
}
// Validate checks the fields of the RevokeRequest and returns nil if they are ok
// or an error if something is wrong.
func (r *SSHRevokeRequest) Validate() (err error) {
if r.Serial == "" {
return BadRequest(errors.New("missing serial"))
}
if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise {
return BadRequest(errors.New("reasonCode out of bounds"))
}
if !r.Passive {
return NotImplemented(errors.New("non-passive revocation not implemented"))
}
if len(r.OTT) == 0 {
return BadRequest(errors.New("missing ott"))
}
return
}
// Revoke supports handful of different methods that revoke a Certificate.
//
// NOTE: currently only Passive revocation is supported.
func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) {
var body SSHRevokeRequest
if err := ReadJSON(r.Body, &body); err != nil {
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
return
}
if err := body.Validate(); err != nil {
WriteError(w, err)
return
}
opts := &authority.RevokeOptions{
Serial: body.Serial,
Reason: body.Reason,
ReasonCode: body.ReasonCode,
PassiveOnly: body.Passive,
}
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeSSHMethod)
// A token indicates that we are using the api via a provisioner token,
// otherwise it is assumed that the certificate is revoking itself over mTLS.
logOtt(w, body.OTT)
if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil {
WriteError(w, Unauthorized(err))
return
}
opts.OTT = body.OTT
if err := h.Authority.Revoke(ctx, opts); err != nil {
WriteError(w, Forbidden(err))
return
}
logSSHRevoke(w, opts)
JSON(w, &SSHRevokeResponse{Status: "ok"})
}
func logSSHRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {
if rl, ok := w.(logging.ResponseLogger); ok {
rl.WithFields(map[string]interface{}{
"serial": ri.Serial,
"reasonCode": ri.ReasonCode,
"reason": ri.Reason,
"passiveOnly": ri.PassiveOnly,
"mTLS": ri.MTLS,
"ssh": true,
})
}
}

View file

@ -179,8 +179,33 @@ func (a *Authority) init() error {
} }
} }
// Merge global and configuration claims
claimer, err := provisioner.NewClaimer(a.config.AuthorityConfig.Claims, globalProvisionerClaims)
if err != nil {
return err
}
// TODO: should we also be combining the ssh federated roots here?
// If we rotate ssh roots keys, sshpop provisioner will lose ability to
// validate old SSH certificates, unless they are added as federated certs.
sshKeys, err := a.GetSSHRoots()
if err != nil {
return err
}
// Initialize provisioners
config := provisioner.Config{
Claims: claimer.Claims(),
Audiences: a.config.getAudiences(),
DB: a.db,
SSHKeys: &provisioner.SSHKeys{
UserKeys: sshKeys.UserKeys,
HostKeys: sshKeys.HostKeys,
},
}
// Store all the provisioners // Store all the provisioners
for _, p := range a.config.AuthorityConfig.Provisioners { for _, p := range a.config.AuthorityConfig.Provisioners {
if err := p.Init(config); err != nil {
return err
}
if err := a.provisioners.Store(p); err != nil { if err := a.provisioners.Store(p); err != nil {
return err return err
} }

View file

@ -80,13 +80,32 @@ func (a *Authority) Authorize(ctx context.Context, ott string) ([]provisioner.Si
switch m := provisioner.MethodFromContext(ctx); m { switch m := provisioner.MethodFromContext(ctx); m {
case provisioner.SignMethod: case provisioner.SignMethod:
return a.authorizeSign(ctx, ott) return a.authorizeSign(ctx, ott)
case provisioner.RevokeMethod:
return nil, a.authorizeRevoke(ctx, ott)
case provisioner.SignSSHMethod: case provisioner.SignSSHMethod:
if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil { if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext} return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
} }
return a.authorizeSign(ctx, ott) return a.authorizeSSHSign(ctx, ott)
case provisioner.RevokeMethod: case provisioner.RenewSSHMethod:
return nil, &apiError{errors.New("authorize: revoke method is not supported"), http.StatusInternalServerError, errContext} if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
}
if _, err := a.authorizeSSHRenew(ctx, ott); err != nil {
return nil, err
}
return nil, nil
case provisioner.RevokeSSHMethod:
return nil, a.authorizeSSHRevoke(ctx, ott)
case provisioner.RekeySSHMethod:
if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
}
_, opts, err := a.authorizeSSHRekey(ctx, ott)
if err != nil {
return nil, err
}
return opts, nil
default: default:
return nil, &apiError{errors.Errorf("authorize: method %d is not supported", m), http.StatusInternalServerError, errContext} return nil, &apiError{errors.Errorf("authorize: method %d is not supported", m), http.StatusInternalServerError, errContext}
} }
@ -121,38 +140,25 @@ func (a *Authority) AuthorizeSign(ott string) ([]provisioner.SignOption, error)
// authorizeRevoke authorizes a revocation request by validating and authenticating // authorizeRevoke authorizes a revocation request by validating and authenticating
// the RevokeOptions POSTed with the request. // the RevokeOptions POSTed with the request.
// Returns a tuple of the provisioner ID and error, if one occurred. // Returns a tuple of the provisioner ID and error, if one occurred.
func (a *Authority) authorizeRevoke(opts *RevokeOptions) (p provisioner.Interface, err error) { func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
if opts.MTLS { errContext := map[string]interface{}{"ott": token}
if opts.Crt.SerialNumber.String() != opts.Serial {
return nil, errors.New("authorizeRevoke: serial number in certificate different than body")
}
// Load the Certificate provisioner if one exists.
p, err = a.LoadProvisionerByCertificate(opts.Crt)
if err != nil {
return nil, errors.Wrap(err, "authorizeRevoke")
}
} else {
// Gets the token provisioner and validates common token fields.
p, err = a.authorizeToken(opts.OTT)
if err != nil {
return nil, errors.Wrap(err, "authorizeRevoke")
}
// Call the provisioner AuthorizeRevoke to apply provisioner specific auth claims. p, err := a.authorizeToken(token)
err = p.AuthorizeRevoke(opts.OTT)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "authorizeRevoke") return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext}
} }
if err = p.AuthorizeSSHRevoke(ctx, token); err != nil {
return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext}
} }
return return nil
} }
// authorizeRenewal tries to locate the step provisioner extension, and checks // authorizeRenewl tries to locate the step provisioner extension, and checks
// if for the configured provisioner, the renewal is enabled or not. If the // if for the configured provisioner, the renewal is enabled or not. If the
// extra extension cannot be found, authorize the renewal by default. // extra extension cannot be found, authorize the renewal by default.
// //
// TODO(mariano): should we authorize by default? // TODO(mariano): should we authorize by default?
func (a *Authority) authorizeRenewal(crt *x509.Certificate) error { func (a *Authority) authorizeRenew(crt *x509.Certificate) error {
errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()} errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()}
// Check the passive revocation table. // Check the passive revocation table.
@ -180,7 +186,7 @@ func (a *Authority) authorizeRenewal(crt *x509.Certificate) error {
context: errContext, context: errContext,
} }
} }
if err := p.AuthorizeRenewal(crt); err != nil { if err := p.AuthorizeRenew(context.Background(), crt); err != nil {
return &apiError{ return &apiError{
err: errors.Wrap(err, "renew"), err: errors.Wrap(err, "renew"),
code: http.StatusUnauthorized, code: http.StatusUnauthorized,

View file

@ -81,23 +81,6 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
return errors.New("authority.provisioners cannot be empty") return errors.New("authority.provisioners cannot be empty")
} }
// Merge global and configuration claims
claimer, err := provisioner.NewClaimer(c.Claims, globalProvisionerClaims)
if err != nil {
return err
}
// Initialize provisioners
config := provisioner.Config{
Claims: claimer.Claims(),
Audiences: audiences,
}
for _, p := range c.Provisioners {
if err := p.Init(config); err != nil {
return err
}
}
if c.Template == nil { if c.Template == nil {
c.Template = &x509util.ASN1DN{} c.Template = &x509util.ASN1DN{}
} }
@ -196,6 +179,9 @@ func (c *Config) getAudiences() provisioner.Audiences {
audiences := provisioner.Audiences{ audiences := provisioner.Audiences{
Sign: []string{legacyAuthority}, Sign: []string{legacyAuthority},
Revoke: []string{legacyAuthority}, Revoke: []string{legacyAuthority},
SSHSign: []string{},
SSHRevoke: []string{},
SSHRenew: []string{},
} }
for _, name := range c.DNSNames { for _, name := range c.DNSNames {
@ -203,6 +189,14 @@ func (c *Config) getAudiences() provisioner.Audiences {
fmt.Sprintf("https://%s/sign", name), fmt.Sprintf("https://%s/1.0/sign", name)) fmt.Sprintf("https://%s/sign", name), fmt.Sprintf("https://%s/1.0/sign", name))
audiences.Revoke = append(audiences.Revoke, audiences.Revoke = append(audiences.Revoke,
fmt.Sprintf("https://%s/revoke", name), fmt.Sprintf("https://%s/1.0/revoke", name)) fmt.Sprintf("https://%s/revoke", name), fmt.Sprintf("https://%s/1.0/revoke", name))
audiences.SSHSign = append(audiences.SSHSign,
fmt.Sprintf("https://%s/ssh/sign", name), fmt.Sprintf("https://%s/1.0/ssh/sign", name))
audiences.SSHRevoke = append(audiences.SSHRevoke,
fmt.Sprintf("https://%s/ssh/revoke", name), fmt.Sprintf("https://%s/1.0/ssh/revoke", name))
audiences.SSHRenew = append(audiences.SSHRenew,
fmt.Sprintf("https://%s/ssh/renew", name), fmt.Sprintf("https://%s/1.0/ssh/renew", name))
audiences.SSHRekey = append(audiences.SSHRekey,
fmt.Sprintf("https://%s/ssh/rekey", name), fmt.Sprintf("https://%s/1.0/ssh/rekey", name))
} }
return audiences return audiences

View file

@ -10,6 +10,7 @@ import (
// ACME is the acme provisioner type, an entity that can authorize the ACME // ACME is the acme provisioner type, an entity that can authorize the ACME
// provisioning flow. // provisioning flow.
type ACME struct { type ACME struct {
*base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Claims *Claims `json:"claims,omitempty"` Claims *Claims `json:"claims,omitempty"`
@ -58,16 +59,10 @@ func (p *ACME) Init(config Config) (err error) {
return err return err
} }
// AuthorizeRevoke is not implemented yet for the ACME provisioner. // AuthorizeSign does not do any validation, because all validation is handled
func (p *ACME) AuthorizeRevoke(token string) error { // in the ACME protocol. This method returns a list of modifiers / constraints
return nil // on the resulting certificate.
} func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
// AuthorizeSign validates the given token.
func (p *ACME) AuthorizeSign(ctx context.Context, _ string) ([]SignOption, error) {
if m := MethodFromContext(ctx); m != SignMethod {
return nil, errors.Errorf("unexpected method type %d in context", m)
}
return []SignOption{ return []SignOption{
// modifiers / withOptions // modifiers / withOptions
newProvisionerExtensionOption(TypeACME, p.Name, ""), newProvisionerExtensionOption(TypeACME, p.Name, ""),
@ -78,8 +73,11 @@ func (p *ACME) AuthorizeSign(ctx context.Context, _ string) ([]SignOption, error
}, nil }, nil
} }
// AuthorizeRenewal is not implemented for the ACME provisioner. // AuthorizeRenew returns an error if the renewal is disabled.
func (p *ACME) AuthorizeRenewal(cert *x509.Certificate) error { // NOTE: This method does not actually validate the certificate or check it's
// revocation status. Just confirms that the provisioner that created the
// certificate was configured to allow renewals.
func (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() { if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
} }

View file

@ -123,6 +123,7 @@ type awsInstanceIdentityDocument struct {
// Amazon Identity docs are available at // Amazon Identity docs are available at
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
type AWS struct { type AWS struct {
*base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Accounts []string `json:"accounts"` Accounts []string `json:"accounts"`
@ -273,14 +274,6 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
return nil, err return nil, err
} }
// Check for the sign ssh method, default to sign X.509
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(payload)
}
doc := payload.document doc := payload.document
// Enforce known CN and default DNS and IP if configured. // Enforce known CN and default DNS and IP if configured.
// By default we'll accept the CN and SANs in the CSR. // By default we'll accept the CN and SANs in the CSR.
@ -306,20 +299,17 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
), nil ), nil
} }
// AuthorizeRenewal returns an error if the renewal is disabled. // AuthorizeRenew returns an error if the renewal is disabled.
func (p *AWS) AuthorizeRenewal(cert *x509.Certificate) error { // NOTE: This method does not actually validate the certificate or check it's
// revocation status. Just confirms that the provisioner that created the
// certificate was configured to allow renewals.
func (p *AWS) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() { if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
} }
return nil return nil
} }
// AuthorizeRevoke returns an error because revoke is not supported on AWS
// provisioners.
func (p *AWS) AuthorizeRevoke(token string) error {
return errors.New("revoke is not supported on a AWS provisioner")
}
// assertConfig initializes the config if it has not been initialized // assertConfig initializes the config if it has not been initialized
func (p *AWS) assertConfig() (err error) { func (p *AWS) assertConfig() (err error) {
if p.config != nil { if p.config != nil {
@ -445,8 +435,16 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
return &payload, nil return &payload, nil
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request. // AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) { func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
claims, err := p.authorizeToken(token)
if err != nil {
return nil, err
}
doc := claims.document doc := claims.document
signOptions := []SignOption{ signOptions := []SignOption{

View file

@ -80,6 +80,7 @@ type azurePayload struct {
// https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token // https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token
// and https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service // and https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service
type Azure struct { type Azure struct {
*base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
TenantID string `json:"tenantId"` TenantID string `json:"tenantId"`
@ -208,15 +209,14 @@ func (p *Azure) Init(config Config) (err error) {
return nil return nil
} }
// AuthorizeSign validates the given token and returns the sign options that // parseToken returuns the claims, name, group, error.
// will be used on certificate creation. func (p *Azure) parseToken(token string) (*azurePayload, string, string, error) {
func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
jwt, err := jose.ParseSigned(token) jwt, err := jose.ParseSigned(token)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "error parsing token") return nil, "", "", errors.Wrapf(err, "error parsing token")
} }
if len(jwt.Headers) == 0 { if len(jwt.Headers) == 0 {
return nil, errors.New("error parsing token: header is missing") return nil, "", "", errors.New("error parsing token: header is missing")
} }
var found bool var found bool
@ -229,7 +229,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
} }
} }
if !found { if !found {
return nil, errors.New("cannot validate token") return nil, "", "", errors.New("cannot validate token")
} }
if err := claims.ValidateWithLeeway(jose.Expected{ if err := claims.ValidateWithLeeway(jose.Expected{
@ -237,19 +237,29 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
Issuer: p.oidcConfig.Issuer, Issuer: p.oidcConfig.Issuer,
Time: time.Now(), Time: time.Now(),
}, 1*time.Minute); err != nil { }, 1*time.Minute); err != nil {
return nil, errors.Wrap(err, "failed to validate payload") return nil, "", "", errors.Wrap(err, "failed to validate payload")
} }
// Validate TenantID // Validate TenantID
if claims.TenantID != p.TenantID { if claims.TenantID != p.TenantID {
return nil, errors.New("validation failed: invalid tenant id claim (tid)") return nil, "", "", errors.New("validation failed: invalid tenant id claim (tid)")
} }
re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID) re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID)
if len(re) != 4 { if len(re) != 4 {
return nil, errors.Errorf("error parsing xms_mirid claim: %s", claims.XMSMirID) return nil, "", "", errors.Errorf("error parsing xms_mirid claim: %s", claims.XMSMirID)
} }
group, name := re[2], re[3] group, name := re[2], re[3]
return &claims, name, group, nil
}
// AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation.
func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
_, name, group, err := p.parseToken(token)
if err != nil {
return nil, err
}
// Filter by resource group // Filter by resource group
if len(p.ResourceGroups) > 0 { if len(p.ResourceGroups) > 0 {
@ -265,14 +275,6 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
} }
} }
// Check for the sign ssh method, default to sign X.509
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(claims, name)
}
// Enforce known common name and default DNS if configured. // Enforce known common name and default DNS if configured.
// By default we'll accept the CN and SANs in the CSR. // By default we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU. // There's no way to trust them other than TOFU.
@ -293,22 +295,27 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
), nil ), nil
} }
// AuthorizeRenewal returns an error if the renewal is disabled. // AuthorizeRenew returns an error if the renewal is disabled.
func (p *Azure) AuthorizeRenewal(cert *x509.Certificate) error { // NOTE: This method does not actually validate the certificate or check it's
// revocation status. Just confirms that the provisioner that created the
// certificate was configured to allow renewals.
func (p *Azure) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() { if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
} }
return nil return nil
} }
// AuthorizeRevoke returns an error because revoke is not supported on Azure // AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
// provisioners. func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
func (p *Azure) AuthorizeRevoke(token string) error { if !p.claimer.IsSSHCAEnabled() {
return errors.New("revoke is not supported on a Azure provisioner") return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request. _, name, _, err := p.parseToken(token)
func (p *Azure) authorizeSSHSign(claims azurePayload, name string) ([]SignOption, error) { if err != nil {
return nil, err
}
signOptions := []SignOption{ signOptions := []SignOption{
// set the key id to the token subject // set the key id to the token subject
sshCertificateKeyIDModifier(name), sshCertificateKeyIDModifier(name),

View file

@ -74,6 +74,7 @@ func newGCPConfig() *gcpConfig {
// Google Identity docs are available at // Google Identity docs are available at
// https://cloud.google.com/compute/docs/instances/verifying-instance-identity // https://cloud.google.com/compute/docs/instances/verifying-instance-identity
type GCP struct { type GCP struct {
*base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
ServiceAccounts []string `json:"serviceAccounts"` ServiceAccounts []string `json:"serviceAccounts"`
@ -212,14 +213,6 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
return nil, err return nil, err
} }
// Check for the sign ssh method, default to sign X.509
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(claims)
}
ce := claims.Google.ComputeEngine ce := claims.Google.ComputeEngine
// Enforce known common name and default DNS if configured. // Enforce known common name and default DNS if configured.
// By default we we'll accept the CN and SANs in the CSR. // By default we we'll accept the CN and SANs in the CSR.
@ -247,19 +240,13 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
} }
// AuthorizeRenewal returns an error if the renewal is disabled. // AuthorizeRenewal returns an error if the renewal is disabled.
func (p *GCP) AuthorizeRenewal(cert *x509.Certificate) error { func (p *GCP) AuthorizeRenewal(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() { if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
} }
return nil return nil
} }
// AuthorizeRevoke returns an error because revoke is not supported on GCP
// provisioners.
func (p *GCP) AuthorizeRevoke(token string) error {
return errors.New("revoke is not supported on a GCP provisioner")
}
// assertConfig initializes the config if it has not been initialized. // assertConfig initializes the config if it has not been initialized.
func (p *GCP) assertConfig() { func (p *GCP) assertConfig() {
if p.config == nil { if p.config == nil {
@ -357,8 +344,16 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
return &claims, nil return &claims, nil
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request. // AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) { func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
claims, err := p.authorizeToken(token)
if err != nil {
return nil, err
}
ce := claims.Google.ComputeEngine ce := claims.Google.ComputeEngine
signOptions := []SignOption{ signOptions := []SignOption{

View file

@ -24,6 +24,7 @@ type stepPayload struct {
// JWK is the default provisioner, an entity that can sign tokens necessary for // JWK is the default provisioner, an entity that can sign tokens necessary for
// signature requests. // signature requests.
type JWK struct { type JWK struct {
*base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Key *jose.JSONWebKey `json:"key"` Key *jose.JSONWebKey `json:"key"`
@ -129,7 +130,7 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err
// AuthorizeRevoke returns an error if the provisioner does not have rights to // AuthorizeRevoke returns an error if the provisioner does not have rights to
// revoke the certificate with serial number in the `sub` property. // revoke the certificate with serial number in the `sub` property.
func (p *JWK) AuthorizeRevoke(token string) error { func (p *JWK) AuthorizeRevoke(ctx context.Context, token string) error {
_, err := p.authorizeToken(token, p.audiences.Revoke) _, err := p.authorizeToken(token, p.audiences.Revoke)
return err return err
} }
@ -141,14 +142,6 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
return nil, err return nil, err
} }
// Check for SSH sign-ing request.
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(claims)
}
// NOTE: This is for backwards compatibility with older versions of cli // NOTE: This is for backwards compatibility with older versions of cli
// and certificates. Older versions added the token subject as the only SAN // and certificates. Older versions added the token subject as the only SAN
// in a CSR by default. // in a CSR by default.
@ -171,17 +164,27 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
}, nil }, nil
} }
// AuthorizeRenewal returns an error if the renewal is disabled. // AuthorizeRenew returns an error if the renewal is disabled.
func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error { // NOTE: This method does not actually validate the certificate or check it's
// revocation status. Just confirms that the provisioner that created the
// certificate was configured to allow renewals.
func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() { if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
} }
return nil return nil
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request. // AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) { func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
t := now() if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
// TODO: fix audiences
claims, err := p.authorizeToken(token, p.audiences.Sign)
if err != nil {
return nil, err
}
if claims.Step == nil || claims.Step.SSH == nil { if claims.Step == nil || claims.Step.SSH == nil {
return nil, errors.New("authorization token must be an SSH provisioning token") return nil, errors.New("authorization token must be an SSH provisioning token")
} }
@ -193,6 +196,7 @@ func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
sshCertificateKeyIDModifier(claims.Subject), sshCertificateKeyIDModifier(claims.Subject),
} }
t := now()
// Add modifiers from custom claims // Add modifiers from custom claims
if opts.CertType != "" { if opts.CertType != "" {
signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType))
@ -223,3 +227,10 @@ func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
&sshCertificateDefaultValidator{}, &sshCertificateDefaultValidator{},
), nil ), nil
} }
// AuthorizeSSHRevoke returns nil if the token is valid, false otherwise.
func (p *JWK) AuthorizeSSHRevoke(ctx context.Context, token string) error {
// TODO fix audience.
_, err := p.authorizeToken(token, p.audiences.SSHRevoke)
return err
}

View file

@ -40,6 +40,7 @@ type k8sSAPayload struct {
// K8sSA represents a Kubernetes ServiceAccount provisioner; an // K8sSA represents a Kubernetes ServiceAccount provisioner; an
// entity trusted to make signature requests. // entity trusted to make signature requests.
type K8sSA struct { type K8sSA struct {
*base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Claims *Claims `json:"claims,omitempty"` Claims *Claims `json:"claims,omitempty"`
@ -199,7 +200,7 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload,
// AuthorizeRevoke returns an error if the provisioner does not have rights to // AuthorizeRevoke returns an error if the provisioner does not have rights to
// revoke the certificate with serial number in the `sub` property. // revoke the certificate with serial number in the `sub` property.
func (p *K8sSA) AuthorizeRevoke(token string) error { func (p *K8sSA) AuthorizeRevoke(ctx context.Context, token string) error {
_, err := p.authorizeToken(token, p.audiences.Revoke) _, err := p.authorizeToken(token, p.audiences.Revoke)
return err return err
} }

View file

@ -14,12 +14,38 @@ type methodKey struct{}
const ( const (
// SignMethod is the method used to sign X.509 certificates. // SignMethod is the method used to sign X.509 certificates.
SignMethod Method = iota SignMethod Method = iota
// SignSSHMethod is the method used to sign SSH certificate.
SignSSHMethod
// RevokeMethod is the method used to revoke X.509 certificates. // RevokeMethod is the method used to revoke X.509 certificates.
RevokeMethod RevokeMethod
// SignSSHMethod is the method used to sign SSH certificates.
SignSSHMethod
// RenewSSHMethod is the method used to renew SSH certificates.
RenewSSHMethod
// RevokeSSHMethod is the method used to revoke SSH certificates.
RevokeSSHMethod
// RekeySSHMethod is the method used to rekey SSH certificates.
RekeySSHMethod
) )
// String returns a string representation of the context method.
func (m Method) String() string {
switch m {
case SignMethod:
return "sign-method"
case RevokeMethod:
return "revoke-method"
case SignSSHMethod:
return "sign-ssh-method"
case RenewSSHMethod:
return "renew-ssh-method"
case RevokeSSHMethod:
return "revoke-ssh-method"
case RekeySSHMethod:
return "rekey-ssh-method"
default:
return "unknown"
}
}
// NewContextWithMethod creates a new context from ctx and attaches method to // NewContextWithMethod creates a new context from ctx and attaches method to
// it. // it.
func NewContextWithMethod(ctx context.Context, method Method) context.Context { func NewContextWithMethod(ctx context.Context, method Method) context.Context {

View file

@ -3,6 +3,8 @@ package provisioner
import ( import (
"context" "context"
"crypto/x509" "crypto/x509"
"golang.org/x/crypto/ssh"
) )
// noop provisioners is a provisioner that accepts anything. // noop provisioners is a provisioner that accepts anything.
@ -35,10 +37,26 @@ func (p *noop) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
return []SignOption{}, nil return []SignOption{}, nil
} }
func (p *noop) AuthorizeRenewal(cert *x509.Certificate) error { func (p *noop) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
return nil return nil
} }
func (p *noop) AuthorizeRevoke(token string) error { func (p *noop) AuthorizeRevoke(ctx context.Context, token string) error {
return nil return nil
} }
func (p *noop) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
return []SignOption{}, nil
}
func (p *noop) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
return nil, nil
}
func (p *noop) AuthorizeSSHRevoke(ctx context.Context, token string) error {
return nil
}
func (p *noop) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) {
return nil, []SignOption{}, nil
}

View file

@ -50,6 +50,7 @@ type openIDPayload struct {
// //
// ClientSecret is mandatory, but it can be an empty string. // ClientSecret is mandatory, but it can be an empty string.
type OIDC struct { type OIDC struct {
*base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
ClientID string `json:"clientID"` ClientID string `json:"clientID"`
@ -264,7 +265,7 @@ func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) {
// AuthorizeRevoke returns an error if the provisioner does not have rights to // AuthorizeRevoke returns an error if the provisioner does not have rights to
// revoke the certificate with serial number in the `sub` property. // revoke the certificate with serial number in the `sub` property.
// Only tokens generated by an admin have the right to revoke a certificate. // Only tokens generated by an admin have the right to revoke a certificate.
func (o *OIDC) AuthorizeRevoke(token string) error { func (o *OIDC) AuthorizeRevoke(ctx context.Context, token string) error {
claims, err := o.authorizeToken(token) claims, err := o.authorizeToken(token)
if err != nil { if err != nil {
return err return err
@ -284,14 +285,6 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
return nil, err return nil, err
} }
// Check for the sign ssh method, default to sign X.509
if MethodFromContext(ctx) == SignSSHMethod {
if !o.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID())
}
return o.authorizeSSHSign(claims)
}
so := []SignOption{ so := []SignOption{
// modifiers / withOptions // modifiers / withOptions
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID), newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID),
@ -308,16 +301,26 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
return append(so, emailOnlyIdentity(claims.Email)), nil return append(so, emailOnlyIdentity(claims.Email)), nil
} }
// AuthorizeRenewal returns an error if the renewal is disabled. // AuthorizeRenew returns an error if the renewal is disabled.
func (o *OIDC) AuthorizeRenewal(cert *x509.Certificate) error { // NOTE: This method does not actually validate the certificate or check it's
// revocation status. Just confirms that the provisioner that created the
// certificate was configured to allow renewals.
func (o *OIDC) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if o.claimer.IsDisableRenewal() { if o.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", o.GetID()) return errors.Errorf("renew is disabled for provisioner %s", o.GetID())
} }
return nil return nil
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request. // AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) { func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !o.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID())
}
claims, err := o.authorizeToken(token)
if err != nil {
return nil, err
}
signOptions := []SignOption{ signOptions := []SignOption{
// set the key id to the token subject // set the key id to the token subject
sshCertificateKeyIDModifier(claims.Email), sshCertificateKeyIDModifier(claims.Email),
@ -356,6 +359,20 @@ func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) {
), nil ), nil
} }
// AuthorizeSSHRevoke returns nil if the token is valid, false otherwise.
func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error {
claims, err := o.authorizeToken(token)
if err != nil {
return err
}
// Only admins can revoke certificates.
if o.IsAdmin(claims.Email) {
return nil
}
return errors.New("cannot revoke with non-admin token")
}
func getAndDecode(uri string, v interface{}) error { func getAndDecode(uri string, v interface{}) error {
resp, err := http.Get(uri) resp, err := http.Get(uri)
if err != nil { if err != nil {

View file

@ -9,6 +9,8 @@ import (
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/db"
"golang.org/x/crypto/ssh"
) )
// Interface is the interface that all provisioner types must implement. // Interface is the interface that all provisioner types must implement.
@ -20,19 +22,33 @@ type Interface interface {
GetEncryptedKey() (kid string, key string, ok bool) GetEncryptedKey() (kid string, key string, ok bool)
Init(config Config) error Init(config Config) error
AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error)
AuthorizeRenewal(cert *x509.Certificate) error AuthorizeRevoke(ctx context.Context, token string) error
AuthorizeRevoke(token string) error AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error
AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error)
AuthorizeSSHRevoke(ctx context.Context, token string) error
AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error)
AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error)
} }
// Audiences stores all supported audiences by request type. // Audiences stores all supported audiences by request type.
type Audiences struct { type Audiences struct {
Sign []string Sign []string
Revoke []string Revoke []string
SSHSign []string
SSHRevoke []string
SSHRenew []string
SSHRekey []string
} }
// All returns all supported audiences across all request types in one list. // All returns all supported audiences across all request types in one list.
func (a Audiences) All() []string { func (a Audiences) All() (auds []string) {
return append(a.Sign, a.Revoke...) auds = a.Sign
auds = append(auds, a.Revoke...)
auds = append(auds, a.SSHSign...)
auds = append(auds, a.SSHRevoke...)
auds = append(auds, a.SSHRenew...)
auds = append(auds, a.SSHRekey...)
return
} }
// WithFragment returns a copy of audiences where the url audiences contains the // WithFragment returns a copy of audiences where the url audiences contains the
@ -41,6 +57,10 @@ func (a Audiences) WithFragment(fragment string) Audiences {
ret := Audiences{ ret := Audiences{
Sign: make([]string, len(a.Sign)), Sign: make([]string, len(a.Sign)),
Revoke: make([]string, len(a.Revoke)), Revoke: make([]string, len(a.Revoke)),
SSHSign: make([]string, len(a.SSHSign)),
SSHRevoke: make([]string, len(a.SSHRevoke)),
SSHRenew: make([]string, len(a.SSHRenew)),
SSHRekey: make([]string, len(a.SSHRekey)),
} }
for i, s := range a.Sign { for i, s := range a.Sign {
if u, err := url.Parse(s); err == nil { if u, err := url.Parse(s); err == nil {
@ -56,6 +76,34 @@ func (a Audiences) WithFragment(fragment string) Audiences {
ret.Revoke[i] = s ret.Revoke[i] = s
} }
} }
for i, s := range a.SSHSign {
if u, err := url.Parse(s); err == nil {
ret.SSHSign[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()
} else {
ret.SSHSign[i] = s
}
}
for i, s := range a.SSHRevoke {
if u, err := url.Parse(s); err == nil {
ret.SSHRevoke[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()
} else {
ret.SSHRevoke[i] = s
}
}
for i, s := range a.SSHRenew {
if u, err := url.Parse(s); err == nil {
ret.SSHRenew[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()
} else {
ret.SSHRenew[i] = s
}
}
for i, s := range a.SSHRekey {
if u, err := url.Parse(s); err == nil {
ret.SSHRekey[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()
} else {
ret.SSHRekey[i] = s
}
}
return ret return ret
} }
@ -92,11 +140,6 @@ const (
TypeK8sSA Type = 8 TypeK8sSA Type = 8
// TypeSSHPOP is used to indicate the SSHPOP provisioners. // TypeSSHPOP is used to indicate the SSHPOP provisioners.
TypeSSHPOP Type = 9 TypeSSHPOP Type = 9
// RevokeAudienceKey is the key for the 'revoke' audiences in the audiences map.
RevokeAudienceKey = "revoke"
// SignAudienceKey is the key for the 'sign' audiences in the audiences map.
SignAudienceKey = "sign"
) )
// String returns the string representation of the type. // String returns the string representation of the type.
@ -125,6 +168,12 @@ func (t Type) String() string {
} }
} }
// SSHKeys represents the SSH User and Host public keys.
type SSHKeys struct {
UserKeys []ssh.PublicKey
HostKeys []ssh.PublicKey
}
// Config defines the default parameters used in the initialization of // Config defines the default parameters used in the initialization of
// provisioners. // provisioners.
type Config struct { type Config struct {
@ -132,6 +181,10 @@ type Config struct {
Claims Claims Claims Claims
// Audiences are the audiences used in the default provisioner, (JWK). // Audiences are the audiences used in the default provisioner, (JWK).
Audiences Audiences Audiences Audiences
// DB is the interface to the authority DB client.
DB db.AuthDB
// SSHKeys are the root SSH public keys
SSHKeys *SSHKeys
} }
type provisioner struct { type provisioner struct {
@ -222,6 +275,50 @@ func SanitizeSSHUserPrincipal(email string) string {
}, strings.ToLower(email)) }, strings.ToLower(email))
} }
type base struct{}
// AuthorizeSign returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for signing x509 Certificates.
func (b *base) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
return nil, errors.New("not implemented; provisioner does not implement AuthorizeSign")
}
// AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for revoking x509 Certificates.
func (b *base) AuthorizeRevoke(ctx context.Context, token string) error {
return errors.New("not implemented; provisioner does not implement AuthorizeRevoke")
}
// AuthorizeRenew returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for renewing x509 Certificates.
func (b *base) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
return errors.New("not implemented; provisioner does not implement AuthorizeRenew")
}
// AuthorizeSSHSign returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for signing SSH Certificates.
func (b *base) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
return nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHSign")
}
// AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for revoking SSH Certificates.
func (b *base) AuthorizeSSHRevoke(ctx context.Context, token string) error {
return errors.New("not implemented; provisioner does not implement AuthorizeSSHRevoke")
}
// AuthorizeSSHRenew returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for renewing SSH Certificates.
func (b *base) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
return nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRenew")
}
// AuthorizeSSHRekey returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for renewing SSH Certificates.
func (b *base) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) {
return nil, nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRekey")
}
// MockProvisioner for testing // MockProvisioner for testing
type MockProvisioner struct { type MockProvisioner struct {
Mret1, Mret2, Mret3 interface{} Mret1, Mret2, Mret3 interface{}

View file

@ -2,18 +2,14 @@ package provisioner
import ( import (
"context" "context"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/pem" "fmt"
"strconv"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/certificates/db"
"github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/jose" "github.com/smallstep/cli/jose"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@ -28,13 +24,14 @@ type sshPOPPayload struct {
// SSHPOP is the default provisioner, an entity that can sign tokens necessary for // SSHPOP is the default provisioner, an entity that can sign tokens necessary for
// signature requests. // signature requests.
type SSHPOP struct { type SSHPOP struct {
*base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
PubKeys []byte `json:"pubKeys"`
Claims *Claims `json:"claims,omitempty"` Claims *Claims `json:"claims,omitempty"`
db db.AuthDB
claimer *Claimer claimer *Claimer
audiences Audiences audiences Audiences
sshPubKeys []ssh.PublicKey sshPubKeys *SSHKeys
} }
// GetID returns the provisioner unique identifier. The name and credential id // GetID returns the provisioner unique identifier. The name and credential id
@ -83,38 +80,8 @@ func (p *SSHPOP) Init(config Config) error {
return errors.New("provisioner type cannot be empty") return errors.New("provisioner type cannot be empty")
case p.Name == "": case p.Name == "":
return errors.New("provisioner name cannot be empty") return errors.New("provisioner name cannot be empty")
case len(p.PubKeys) == 0: case config.SSHKeys == nil:
return errors.New("provisioner root(s) cannot be empty") return errors.New("provisioner public SSH validation keys cannot be empty")
}
var (
block *pem.Block
rest = p.PubKeys
)
for rest != nil {
block, rest = pem.Decode(rest)
if block == nil {
break
}
key, err := pemutil.ParseKey(pem.EncodeToMemory(block))
if err != nil {
return errors.Wrapf(err, "error parsing public key in provisioner %s", p.GetID())
}
switch q := key.(type) {
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:
sshKey, err := ssh.NewPublicKey(key)
if err != nil {
return errors.Wrap(err, "error converting pub key to SSH pub key")
}
p.sshPubKeys = append(p.sshPubKeys, sshKey)
default:
return errors.Errorf("Unexpected public key type %T in provisioner %s", q, p.GetID())
}
}
// Verify that at least one root was found.
if len(p.sshPubKeys) == 0 {
return errors.Errorf("no root public keys found in pub keys attribute for provisioner %s", p.GetName())
} }
// Update claims with global ones // Update claims with global ones
@ -124,6 +91,8 @@ func (p *SSHPOP) Init(config Config) error {
} }
p.audiences = config.Audiences.WithFragment(p.GetID()) p.audiences = config.Audiences.WithFragment(p.GetID())
p.db = config.DB
p.sshPubKeys = config.SSHKeys
return nil return nil
} }
@ -131,50 +100,63 @@ func (p *SSHPOP) Init(config Config) error {
// claims for case specific downstream parsing. // claims for case specific downstream parsing.
// e.g. a Sign request will auth/validate different fields than a Revoke request. // e.g. a Sign request will auth/validate different fields than a Revoke request.
func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayload, error) { func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayload, error) {
sshCert, err := ExtractSSHPOPCert(token)
if err != nil {
return nil, errors.Wrap(err, "authorizeToken ssh-pop")
}
// Check for revocation.
if isRevoked, err := p.db.IsSSHRevoked(strconv.FormatUint(sshCert.Serial, 10)); err != nil {
return nil, errors.Wrap(err, "authorizeToken ssh-pop")
} else if isRevoked {
return nil, errors.New("authorizeToken ssh-pop: ssh certificate has been revoked")
}
jwt, err := jose.ParseSigned(token) jwt, err := jose.ParseSigned(token)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "error parsing token") return nil, errors.Wrapf(err, "error parsing token")
} }
// Check validity period of the certificate.
n := time.Now()
if sshCert.ValidAfter != 0 && time.Unix(int64(sshCert.ValidAfter), 0).After(n) {
return nil, errors.New("sshpop certificate validAfter is in the future")
}
if sshCert.ValidBefore != 0 && time.Unix(int64(sshCert.ValidBefore), 0).Before(n) {
return nil, errors.New("sshpop certificate validBefore is in the past")
}
sshCryptoPubKey, ok := sshCert.Key.(ssh.CryptoPublicKey)
if !ok {
return nil, errors.New("ssh public key could not be cast to ssh CryptoPublicKey")
}
pubKey := sshCryptoPubKey.CryptoPublicKey()
encodedSSHCert, ok := jwt.Headers[0].ExtraHeaders["sshpop"] var (
if !ok { found bool
return nil, errors.New("token missing sshpop header") data = bytesForSigning(sshCert)
keys []ssh.PublicKey
)
if sshCert.CertType == ssh.UserCert {
keys = p.sshPubKeys.UserKeys
} else {
keys = p.sshPubKeys.HostKeys
} }
encodedSSHCertStr, ok := encodedSSHCert.(string) for _, k := range keys {
if !ok {
return nil, errors.New("error unexpected type for sshpop header")
}
sshCertBytes, err := base64.RawURLEncoding.DecodeString(encodedSSHCertStr)
if err != nil {
return nil, errors.Wrap(err, "error decoding sshpop header")
}
sshPub, err := ssh.ParsePublicKey(sshCertBytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing ssh public key")
}
sshCert, ok := sshPub.(*ssh.Certificate)
if !ok {
return nil, errors.New("error converting ssh public key to ssh certificate")
}
data := bytesForSigning(sshCert)
var found bool
for _, k := range p.sshPubKeys {
if err = (&ssh.Certificate{Key: k}).Verify(data, sshCert.Signature); err == nil { if err = (&ssh.Certificate{Key: k}).Verify(data, sshCert.Signature); err == nil {
found = true found = true
break
} }
} }
if !found { if !found {
return nil, errors.New("error: provisioner could could not verify the sshpop header certificate") return nil, errors.New("error: provisioner could could not verify the sshpop header certificate")
} }
// Using the leaf certificates key to validate the claims accomplishes two // Using the ssh certificates key to validate the claims accomplishes two
// things: // things:
// 1. Asserts that the private key used to sign the token corresponds // 1. Asserts that the private key used to sign the token corresponds
// to the public certificate in the `sshpop` header of the token. // to the public certificate in the `sshpop` header of the token.
// 2. Asserts that the claims are valid - have not been tampered with. // 2. Asserts that the claims are valid - have not been tampered with.
var claims sshPOPPayload var claims sshPOPPayload
if err = jwt.Claims(sshCert.Key, &claims); err != nil { if err = jwt.Claims(pubKey, &claims); err != nil {
return nil, errors.Wrap(err, "error parsing claims") return nil, errors.Wrap(err, "error parsing claims")
} }
@ -189,6 +171,8 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa
// validate audiences with the defaults // validate audiences with the defaults
if !matchesAudience(claims.Audience, audiences) { if !matchesAudience(claims.Audience, audiences) {
fmt.Printf("claims.Audience = %+v\n", claims.Audience)
fmt.Printf("audiences = %+v\n", audiences)
return nil, errors.New("invalid token: invalid audience claim (aud)") return nil, errors.New("invalid token: invalid audience claim (aud)")
} }
@ -200,102 +184,77 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa
return &claims, nil return &claims, nil
} }
// AuthorizeRevoke returns an error if the provisioner does not have rights to // AuthorizeSSHRevoke validates the authorization token and extracts/validates
// revoke the certificate with serial number in the `sub` property. // the SSH certificate from the ssh-pop header.
func (p *SSHPOP) AuthorizeRevoke(token string) error { func (p *SSHPOP) AuthorizeSSHRevoke(ctx context.Context, token string) error {
_, err := p.authorizeToken(token, p.audiences.Revoke) claims, err := p.authorizeToken(token, p.audiences.SSHRevoke)
if err != nil {
return err
}
if claims.Subject != strconv.FormatUint(claims.sshCert.Serial, 10) {
return errors.New("token subject must be equivalent to certificate serial number")
}
return err return err
} }
// AuthorizeSign validates the given token. // AuthorizeSSHRenew validates the authorization token and extracts/validates
func (p *SSHPOP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { // the SSH certificate from the ssh-pop header.
claims, err := p.authorizeToken(token, p.audiences.Sign) func (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
claims, err := p.authorizeToken(token, p.audiences.SSHRenew)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return claims.sshCert, nil
// Check for SSH sign-ing request.
if MethodFromContext(ctx) == SignSSHMethod {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
}
return p.authorizeSSHSign(claims)
}
// NOTE: This is for backwards compatibility with older versions of cli
// and certificates. Older versions added the token subject as the only SAN
// in a CSR by default.
if len(claims.SANs) == 0 {
claims.SANs = []string{claims.Subject}
}
dnsNames, ips, emails := x509util.SplitSANs(claims.SANs)
return []SignOption{
// modifiers / withOptions
newProvisionerExtensionOption(TypeSSHPOP, p.Name, ""),
profileLimitDuration{p.claimer.DefaultTLSCertDuration(), time.Unix(int64(claims.sshCert.ValidBefore), 0)},
// validators
commonNameValidator(claims.Subject),
defaultPublicKeyValidator{},
dnsNamesValidator(dnsNames),
emailAddressesValidator(emails),
ipAddressesValidator(ips),
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
}, nil
} }
// AuthorizeRenewal returns an error if the renewal is disabled. // AuthorizeSSHRekey validates the authorization token and extracts/validates
func (p *SSHPOP) AuthorizeRenewal(cert *x509.Certificate) error { // the SSH certificate from the ssh-pop header.
if p.claimer.IsDisableRenewal() { func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) claims, err := p.authorizeToken(token, p.audiences.SSHRekey)
if err != nil {
return nil, nil, err
} }
return nil return claims.sshCert, []SignOption{
} // Validate public key
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *SSHPOP) authorizeSSHSign(claims *sshPOPPayload) ([]SignOption, error) {
if claims.Step == nil || claims.Step.SSH == nil {
return nil, errors.New("authorization token must be an SSH provisioning token")
}
opts := claims.Step.SSH
signOptions := []SignOption{
// validates user's SSHOptions with the ones in the token
sshCertificateOptionsValidator(*opts),
// set the key id to the token subject
sshCertificateKeyIDModifier(claims.Subject),
}
// Add modifiers from custom claims
if opts.CertType != "" {
signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType))
}
if len(opts.Principals) > 0 {
signOptions = append(signOptions, sshCertificatePrincipalsModifier(opts.Principals))
}
t := now()
if !opts.ValidAfter.IsZero() {
signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix()))
}
if !opts.ValidBefore.IsZero() {
signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix()))
}
// Default to a user certificate with no principals if not set
signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert})
return append(signOptions,
// Set the default extensions.
&sshDefaultExtensionModifier{},
// Checks the validity bounds, and set the validity if has not been set.
sshLimitValidityModifier(p.claimer, time.Unix(int64(claims.sshCert.ValidBefore), 0)),
// Validate public key.
&sshDefaultPublicKeyValidator{}, &sshDefaultPublicKeyValidator{},
// Validate the validity period. // Validate the validity period.
&sshCertificateValidityValidator{p.claimer}, &sshCertificateValidityValidator{p.claimer},
// Require all the fields in the SSH certificate // Require and validate all the default fields in the SSH certificate.
&sshCertificateDefaultValidator{}, &sshCertificateDefaultValidator{},
), nil }, nil
}
// ExtractSSHPOPCert parses a JWT and extracts and loads the SSH Certificate
// in the sshpop header. If the header is missing, an error is returned.
func ExtractSSHPOPCert(token string) (*ssh.Certificate, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
}
encodedSSHCert, ok := jwt.Headers[0].ExtraHeaders["sshpop"]
if !ok {
return nil, errors.New("token missing sshpop header")
}
encodedSSHCertStr, ok := encodedSSHCert.(string)
if !ok {
return nil, errors.New("error unexpected type for sshpop header")
}
sshCertBytes, err := base64.StdEncoding.DecodeString(encodedSSHCertStr)
if err != nil {
return nil, errors.Wrap(err, "error decoding sshpop header")
}
sshPub, err := ssh.ParsePublicKey(sshCertBytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing ssh public key")
}
sshCert, ok := sshPub.(*ssh.Certificate)
if !ok {
return nil, errors.New("error converting ssh public key to ssh certificate")
}
return sshCert, nil
} }
func bytesForSigning(cert *ssh.Certificate) []byte { func bytesForSigning(cert *ssh.Certificate) []byte {

View file

@ -22,6 +22,7 @@ type x5cPayload struct {
// X5C is the default provisioner, an entity that can sign tokens necessary for // X5C is the default provisioner, an entity that can sign tokens necessary for
// signature requests. // signature requests.
type X5C struct { type X5C struct {
*base
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Roots []byte `json:"roots"` Roots []byte `json:"roots"`
@ -170,7 +171,7 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err
// AuthorizeRevoke returns an error if the provisioner does not have rights to // AuthorizeRevoke returns an error if the provisioner does not have rights to
// revoke the certificate with serial number in the `sub` property. // revoke the certificate with serial number in the `sub` property.
func (p *X5C) AuthorizeRevoke(token string) error { func (p *X5C) AuthorizeRevoke(ctx context.Context, token string) error {
_, err := p.authorizeToken(token, p.audiences.Revoke) _, err := p.authorizeToken(token, p.audiences.Revoke)
return err return err
} }
@ -213,8 +214,8 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
}, nil }, nil
} }
// AuthorizeRenewal returns an error if the renewal is disabled. // AuthorizeRenew returns an error if the renewal is disabled.
func (p *X5C) AuthorizeRenewal(cert *x509.Certificate) error { func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() { if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
} }

View file

@ -1,10 +1,12 @@
package authority package authority
import ( import (
"context"
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
@ -155,6 +157,22 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template
return output, nil return output, nil
} }
// authorizeSSHSign loads the provisioner from the token, checks that it has not
// been used again and calls the provisioner AuthorizeSSHSign method. Returns a
// list of methods to apply to the signing flow.
func (a *Authority) authorizeSSHSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
var errContext = apiCtx{"ott": ott}
p, err := a.authorizeToken(ott)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext}
}
opts, err := p.AuthorizeSSHSign(ctx, ott)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext}
}
return opts, nil
}
// SignSSH creates a signed SSH certificate with the given public key and options. // SignSSH creates a signed SSH certificate with the given public key and options.
func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
var mods []provisioner.SSHCertificateModifier var mods []provisioner.SSHCertificateModifier
@ -274,6 +292,263 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign
return cert, nil return cert, nil
} }
// authorizeSSHRenew authorizes an SSH certificate renewal request, by
// validating the contents of an SSHPOP token.
func (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
errContext := map[string]interface{}{"ott": token}
p, err := a.authorizeToken(token)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "authorizeSSHRenew"),
code: http.StatusUnauthorized,
context: errContext,
}
}
cert, err := p.AuthorizeSSHRenew(ctx, token)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "authorizeSSHRenew"),
code: http.StatusUnauthorized,
context: errContext,
}
}
return cert, nil
}
// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template.
func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) {
nonce, err := randutil.ASCII(32)
if err != nil {
return nil, &apiError{err: err, code: http.StatusInternalServerError}
}
var serial uint64
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
return nil, &apiError{
err: errors.Wrap(err, "renewSSH: error reading random number"),
code: http.StatusInternalServerError,
}
}
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
return nil, errors.New("rewnewSSh: cannot renew certificate without validity period")
}
dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second
va := time.Now()
vb := va.Add(dur)
// Build base certificate with the key and some random values
cert := &ssh.Certificate{
Nonce: []byte(nonce),
Key: oldCert.Key,
Serial: serial,
CertType: oldCert.CertType,
KeyId: oldCert.KeyId,
ValidPrincipals: oldCert.ValidPrincipals,
Permissions: oldCert.Permissions,
ValidAfter: uint64(va.Unix()),
ValidBefore: uint64(vb.Unix()),
}
// Get signer from authority keys
var signer ssh.Signer
switch cert.CertType {
case ssh.UserCert:
if a.sshCAUserCertSignKey == nil {
return nil, &apiError{
err: errors.New("renewSSH: user certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
}
signer = a.sshCAUserCertSignKey
case ssh.HostCert:
if a.sshCAHostCertSignKey == nil {
return nil, &apiError{
err: errors.New("renewSSH: host certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
}
signer = a.sshCAHostCertSignKey
default:
return nil, &apiError{
err: errors.Errorf("renewSSH: unexpected ssh certificate type: %d", cert.CertType),
code: http.StatusInternalServerError,
}
}
cert.SignatureKey = signer.PublicKey()
// Get bytes for signing trailing the signature length.
data := cert.Marshal()
data = data[:len(data)-4]
// Sign the certificate
sig, err := signer.Sign(rand.Reader, data)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "renewSSH: error signing certificate"),
code: http.StatusInternalServerError,
}
}
cert.Signature = sig
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
return nil, &apiError{
err: errors.Wrap(err, "renewSSH: error storing certificate in db"),
code: http.StatusInternalServerError,
}
}
return cert, nil
}
// authorizeSSHRekey authorizes an SSH certificate rekey request, by
// validating the contents of an SSHPOP token.
func (a *Authority) authorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) {
errContext := map[string]interface{}{"ott": token}
p, err := a.authorizeToken(token)
if err != nil {
return nil, nil, &apiError{
err: errors.Wrap(err, "authorizeSSHRenew"),
code: http.StatusUnauthorized,
context: errContext,
}
}
cert, opts, err := p.AuthorizeSSHRekey(ctx, token)
if err != nil {
return nil, nil, &apiError{
err: errors.Wrap(err, "authorizeSSHRekey"),
code: http.StatusUnauthorized,
context: errContext,
}
}
return cert, opts, nil
}
// RekeySSH creates a signed SSH certificate using the old SSH certificate as a template.
func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
var validators []provisioner.SSHCertificateValidator
for _, op := range signOpts {
switch o := op.(type) {
// validate the ssh.Certificate
case provisioner.SSHCertificateValidator:
validators = append(validators, o)
default:
return nil, &apiError{
err: errors.Errorf("rekeySSH: invalid extra option type %T", o),
code: http.StatusInternalServerError,
}
}
}
nonce, err := randutil.ASCII(32)
if err != nil {
return nil, &apiError{err: err, code: http.StatusInternalServerError}
}
var serial uint64
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
return nil, &apiError{
err: errors.Wrap(err, "rekeySSH: error reading random number"),
code: http.StatusInternalServerError,
}
}
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
return nil, errors.New("rekeySSh: cannot rekey certificate without validity period")
}
dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second
va := time.Now()
vb := va.Add(dur)
// Build base certificate with the key and some random values
cert := &ssh.Certificate{
Nonce: []byte(nonce),
Key: pub,
Serial: serial,
CertType: oldCert.CertType,
KeyId: oldCert.KeyId,
ValidPrincipals: oldCert.ValidPrincipals,
Permissions: oldCert.Permissions,
ValidAfter: uint64(va.Unix()),
ValidBefore: uint64(vb.Unix()),
}
// Get signer from authority keys
var signer ssh.Signer
switch cert.CertType {
case ssh.UserCert:
if a.sshCAUserCertSignKey == nil {
return nil, &apiError{
err: errors.New("rekeySSH: user certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
}
signer = a.sshCAUserCertSignKey
case ssh.HostCert:
if a.sshCAHostCertSignKey == nil {
return nil, &apiError{
err: errors.New("rekeySSH: host certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
}
signer = a.sshCAHostCertSignKey
default:
return nil, &apiError{
err: errors.Errorf("rekeySSH: unexpected ssh certificate type: %d", cert.CertType),
code: http.StatusInternalServerError,
}
}
cert.SignatureKey = signer.PublicKey()
// Get bytes for signing trailing the signature length.
data := cert.Marshal()
data = data[:len(data)-4]
// Sign the certificate
sig, err := signer.Sign(rand.Reader, data)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "rekeySSH: error signing certificate"),
code: http.StatusInternalServerError,
}
}
cert.Signature = sig
// User provisioners validators
for _, v := range validators {
if err := v.Valid(cert); err != nil {
return nil, &apiError{err: err, code: http.StatusForbidden}
}
}
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
return nil, &apiError{
err: errors.Wrap(err, "rekeySSH: error storing certificate in db"),
code: http.StatusInternalServerError,
}
}
return cert, nil
}
// authorizeSSHRevoke authorizes an SSH certificate revoke request, by
// validating the contents of an SSHPOP token.
func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error {
errContext := map[string]interface{}{"ott": token}
p, err := a.authorizeToken(token)
if err != nil {
return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext}
}
if err = p.AuthorizeSSHRevoke(ctx, token); err != nil {
return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext}
}
return nil
}
// SignSSHAddUser signs a certificate that provisions a new user in a server. // SignSSHAddUser signs a certificate that provisions a new user in a server.
func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) { func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) {
if a.sshCAUserCertSignKey == nil { if a.sshCAUserCertSignKey == nil {

View file

@ -1,6 +1,7 @@
package authority package authority
import ( import (
"context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/asn1" "encoding/asn1"
@ -16,6 +17,7 @@ import (
"github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util" "github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/jose"
) )
// GetTLSOptions returns the tls options configured. // GetTLSOptions returns the tls options configured.
@ -127,7 +129,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti
// with a validity window that begins 'now'. // with a validity window that begins 'now'.
func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {
// Check step provisioner extensions // Check step provisioner extensions
if err := a.authorizeRenewal(oldCert); err != nil { if err := a.authorizeRenew(oldCert); err != nil {
return nil, err return nil, err
} }
@ -220,13 +222,14 @@ type RevokeOptions struct {
// being renewed. // being renewed.
// //
// TODO: Add OCSP and CRL support. // TODO: Add OCSP and CRL support.
func (a *Authority) Revoke(opts *RevokeOptions) error { func (a *Authority) Revoke(ctx context.Context, opts *RevokeOptions) error {
errContext := apiCtx{ errContext := apiCtx{
"serialNumber": opts.Serial, "serialNumber": opts.Serial,
"reasonCode": opts.ReasonCode, "reasonCode": opts.ReasonCode,
"reason": opts.Reason, "reason": opts.Reason,
"passiveOnly": opts.PassiveOnly, "passiveOnly": opts.PassiveOnly,
"mTLS": opts.MTLS, "mTLS": opts.MTLS,
"context": string(provisioner.MethodFromContext(ctx)),
} }
if opts.MTLS { if opts.MTLS {
errContext["certificate"] = base64.StdEncoding.EncodeToString(opts.Crt.Raw) errContext["certificate"] = base64.StdEncoding.EncodeToString(opts.Crt.Raw)
@ -242,26 +245,57 @@ func (a *Authority) Revoke(opts *RevokeOptions) error {
RevokedAt: time.Now().UTC(), RevokedAt: time.Now().UTC(),
} }
// Authorize mTLS or token request and get back a provisioner interface. var (
p, err := a.authorizeRevoke(opts) p provisioner.Interface
err error
)
// If not mTLS then get the TokenID of the token.
if !opts.MTLS {
// Validate payload
token, err := jose.ParseSigned(opts.OTT)
if err != nil { if err != nil {
return &apiError{errors.Wrap(err, "revoke"), return &apiError{errors.Wrapf(err, "revoke: error parsing token"),
http.StatusUnauthorized, errContext} http.StatusUnauthorized, errContext}
} }
// If not mTLS then get the TokenID of the token. // Get claims w/out verification. We should have already verified this token
if !opts.MTLS { // earlier with a call to authorizeSSHRevoke.
var claims Claims
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
return &apiError{errors.Wrap(err, "revoke"), http.StatusUnauthorized, errContext}
}
// This method will also validate the audiences for JWK provisioners.
var ok bool
p, ok = a.provisioners.LoadByToken(token, &claims.Claims)
if !ok {
return &apiError{
errors.Errorf("revoke: provisioner not found"),
http.StatusInternalServerError, errContext}
}
rci.TokenID, err = p.GetTokenID(opts.OTT) rci.TokenID, err = p.GetTokenID(opts.OTT)
if err != nil { if err != nil {
return &apiError{errors.Wrap(err, "revoke: could not get ID for token"), return &apiError{errors.Wrap(err, "revoke: could not get ID for token"),
http.StatusInternalServerError, errContext} http.StatusInternalServerError, errContext}
} }
errContext["tokenID"] = rci.TokenID errContext["tokenID"] = rci.TokenID
} else {
// Load the Certificate provisioner if one exists.
p, err = a.LoadProvisionerByCertificate(opts.Crt)
if err != nil {
return &apiError{
errors.Wrap(err, "revoke: unable to load certificate provisioner"),
http.StatusUnauthorized, errContext}
}
} }
rci.ProvisionerID = p.GetID() rci.ProvisionerID = p.GetID()
errContext["provisionerID"] = rci.ProvisionerID errContext["provisionerID"] = rci.ProvisionerID
if provisioner.MethodFromContext(ctx) == provisioner.RevokeSSHMethod {
err = a.db.RevokeSSH(rci)
} else { // default to revoke x509
err = a.db.Revoke(rci) err = a.db.Revoke(rci)
}
switch err { switch err {
case nil: case nil:
return nil return nil

View file

@ -528,6 +528,72 @@ func (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error)
return &sign, nil return &sign, nil
} }
// SSHRenew performs the POST /ssh/renew request to the CA and returns the
// api.SSHRenewResponse struct.
func (c *Client) SSHRenew(req *api.SSHRenewRequest) (*api.SSHRenewResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, errors.Wrap(err, "error marshaling request")
}
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/renew"})
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
if err != nil {
return nil, errors.Wrapf(err, "client POST %s failed", u)
}
if resp.StatusCode >= 400 {
return nil, readError(resp.Body)
}
var renew api.SSHRenewResponse
if err := readJSON(resp.Body, &renew); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
}
return &renew, nil
}
// SSHRekey performs the POST /ssh/rekey request to the CA and returns the
// api.SSHRekeyResponse struct.
func (c *Client) SSHRekey(req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, errors.Wrap(err, "error marshaling request")
}
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/rekey"})
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
if err != nil {
return nil, errors.Wrapf(err, "client POST %s failed", u)
}
if resp.StatusCode >= 400 {
return nil, readError(resp.Body)
}
var rekey api.SSHRekeyResponse
if err := readJSON(resp.Body, &rekey); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
}
return &rekey, nil
}
// SSHRevoke performs the POST /ssh/revoke request to the CA and returns the
// api.SSHRevokeResponse struct.
func (c *Client) SSHRevoke(req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, errors.Wrap(err, "error marshaling request")
}
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/revoke"})
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
if err != nil {
return nil, errors.Wrapf(err, "client POST %s failed", u)
}
if resp.StatusCode >= 400 {
return nil, readError(resp.Body)
}
var revoke api.SSHRevokeResponse
if err := readJSON(resp.Body, &revoke); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
}
return &revoke, nil
}
// SSHRoots performs the GET /ssh/roots request to the CA and returns the // SSHRoots performs the GET /ssh/roots request to the CA and returns the
// api.SSHRootsResponse struct. // api.SSHRootsResponse struct.
func (c *Client) SSHRoots() (*api.SSHRootsResponse, error) { func (c *Client) SSHRoots() (*api.SSHRootsResponse, error) {

View file

@ -16,6 +16,7 @@ import (
var ( var (
certsTable = []byte("x509_certs") certsTable = []byte("x509_certs")
revokedCertsTable = []byte("revoked_x509_certs") revokedCertsTable = []byte("revoked_x509_certs")
revokedSSHCertsTable = []byte("revoked_ssh_certs")
usedOTTTable = []byte("used_ott") usedOTTTable = []byte("used_ott")
sshCertsTable = []byte("ssh_certs") sshCertsTable = []byte("ssh_certs")
sshHostsTable = []byte("ssh_hosts") sshHostsTable = []byte("ssh_hosts")
@ -38,7 +39,9 @@ type Config struct {
// AuthDB is an interface over an Authority DB client that implements a nosql.DB interface. // AuthDB is an interface over an Authority DB client that implements a nosql.DB interface.
type AuthDB interface { type AuthDB interface {
IsRevoked(sn string) (bool, error) IsRevoked(sn string) (bool, error)
IsSSHRevoked(sn string) (bool, error)
Revoke(rci *RevokedCertificateInfo) error Revoke(rci *RevokedCertificateInfo) error
RevokeSSH(rci *RevokedCertificateInfo) error
StoreCertificate(crt *x509.Certificate) error StoreCertificate(crt *x509.Certificate) error
UseToken(id, tok string) (bool, error) UseToken(id, tok string) (bool, error)
IsSSHHost(name string) (bool, error) IsSSHHost(name string) (bool, error)
@ -68,6 +71,7 @@ func New(c *Config) (AuthDB, error) {
tables := [][]byte{ tables := [][]byte{
revokedCertsTable, certsTable, usedOTTTable, revokedCertsTable, certsTable, usedOTTTable,
sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable, sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable,
revokedSSHCertsTable,
} }
for _, b := range tables { for _, b := range tables {
if err := db.CreateTable(b); err != nil { if err := db.CreateTable(b); err != nil {
@ -114,6 +118,29 @@ func (db *DB) IsRevoked(sn string) (bool, error) {
return true, nil return true, nil
} }
// IsSSHRevoked returns whether or not a certificate with the given identifier
// has been revoked.
// In the case of an X509 Certificate the `id` should be the Serial Number of
// the Certificate.
func (db *DB) IsSSHRevoked(sn string) (bool, error) {
// If the DB is nil then act as pass through.
if db == nil {
return false, nil
}
// If the error is `Not Found` then the certificate has not been revoked.
// Any other error should be propagated to the caller.
if _, err := db.Get(revokedSSHCertsTable, []byte(sn)); err != nil {
if nosql.IsErrNotFound(err) {
return false, nil
}
return false, errors.Wrap(err, "error checking revocation bucket")
}
// This certificate has been revoked.
return true, nil
}
// Revoke adds a certificate to the revocation table. // Revoke adds a certificate to the revocation table.
func (db *DB) Revoke(rci *RevokedCertificateInfo) error { func (db *DB) Revoke(rci *RevokedCertificateInfo) error {
rcib, err := json.Marshal(rci) rcib, err := json.Marshal(rci)
@ -132,6 +159,24 @@ func (db *DB) Revoke(rci *RevokedCertificateInfo) error {
} }
} }
// RevokeSSH adds a SSH certificate to the revocation table.
func (db *DB) RevokeSSH(rci *RevokedCertificateInfo) error {
rcib, err := json.Marshal(rci)
if err != nil {
return errors.Wrap(err, "error marshaling revoked certificate info")
}
_, swapped, err := db.CmpAndSwap(revokedSSHCertsTable, []byte(rci.Serial), nil, rcib)
switch {
case err != nil:
return errors.Wrap(err, "error AuthDB CmpAndSwap")
case !swapped:
return ErrAlreadyExists
default:
return nil
}
}
// StoreCertificate stores a certificate PEM. // StoreCertificate stores a certificate PEM.
func (db *DB) StoreCertificate(crt *x509.Certificate) error { func (db *DB) StoreCertificate(crt *x509.Certificate) error {
if err := db.Set(certsTable, []byte(crt.SerialNumber.String()), crt.Raw); err != nil { if err := db.Set(certsTable, []byte(crt.SerialNumber.String()), crt.Raw); err != nil {

View file

@ -31,11 +31,21 @@ func (s *SimpleDB) IsRevoked(sn string) (bool, error) {
return false, nil return false, nil
} }
// IsSSHRevoked noop
func (s *SimpleDB) IsSSHRevoked(sn string) (bool, error) {
return false, nil
}
// Revoke returns a "NotImplemented" error. // Revoke returns a "NotImplemented" error.
func (s *SimpleDB) Revoke(rci *RevokedCertificateInfo) error { func (s *SimpleDB) Revoke(rci *RevokedCertificateInfo) error {
return ErrNotImplemented return ErrNotImplemented
} }
// RevokeSSH returns a "NotImplemented" error.
func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error {
return ErrNotImplemented
}
// StoreCertificate returns a "NotImplemented" error. // StoreCertificate returns a "NotImplemented" error.
func (s *SimpleDB) StoreCertificate(crt *x509.Certificate) error { func (s *SimpleDB) StoreCertificate(crt *x509.Certificate) error {
return ErrNotImplemented return ErrNotImplemented