forked from TrueCloudLab/certificates
sshpop provisioner + ssh renew | revoke | rekey first pass
This commit is contained in:
parent
b5f15531d8
commit
a9ea292bd4
26 changed files with 1185 additions and 338 deletions
|
@ -38,7 +38,7 @@ type Authority interface {
|
|||
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
|
||||
LoadProvisionerByID(string) (provisioner.Interface, 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)
|
||||
GetRoots() (federation []*x509.Certificate, err error)
|
||||
GetFederation() ([]*x509.Certificate, error)
|
||||
|
@ -252,6 +252,9 @@ func (h *caHandler) Route(r Router) {
|
|||
r.MethodFunc("GET", "/federation", h.Federation)
|
||||
// SSH CA
|
||||
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/federation", h.SSHFederation)
|
||||
r.MethodFunc("POST", "/ssh/config", h.SSHConfig)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
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"
|
||||
)
|
||||
|
@ -63,10 +65,15 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) {
|
|||
PassiveOnly: body.Passive,
|
||||
}
|
||||
|
||||
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)
|
||||
// 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.
|
||||
if len(body.OTT) > 0 {
|
||||
logOtt(w, body.OTT)
|
||||
if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil {
|
||||
WriteError(w, Unauthorized(err))
|
||||
return
|
||||
}
|
||||
opts.OTT = body.OTT
|
||||
} else {
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
opts.MTLS = true
|
||||
}
|
||||
|
||||
if err := h.Authority.Revoke(opts); err != nil {
|
||||
if err := h.Authority.Revoke(ctx, opts); err != nil {
|
||||
WriteError(w, Forbidden(err))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ import (
|
|||
// SSHAuthority is the interface implemented by a SSH CA authority.
|
||||
type SSHAuthority interface {
|
||||
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)
|
||||
GetSSHRoots() (*authority.SSHKeys, error)
|
||||
GetSSHFederation() (*authority.SSHKeys, error)
|
||||
|
@ -67,7 +69,8 @@ type SSHCertificate struct {
|
|||
*ssh.Certificate `json:"omitempty"`
|
||||
}
|
||||
|
||||
// SSHGetHostsResponse
|
||||
// SSHGetHostsResponse is the response object that returns the list of valid
|
||||
// hosts for SSH.
|
||||
type SSHGetHostsResponse struct {
|
||||
Hosts []string `json:"hosts"`
|
||||
}
|
||||
|
|
78
api/sshRekey.go
Normal file
78
api/sshRekey.go
Normal 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
68
api/sshRenew.go
Normal 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
98
api/sshRevoke.go
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
for _, p := range a.config.AuthorityConfig.Provisioners {
|
||||
if err := p.Init(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.provisioners.Store(p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -80,13 +80,32 @@ func (a *Authority) Authorize(ctx context.Context, ott string) ([]provisioner.Si
|
|||
switch m := provisioner.MethodFromContext(ctx); m {
|
||||
case provisioner.SignMethod:
|
||||
return a.authorizeSign(ctx, ott)
|
||||
case provisioner.RevokeMethod:
|
||||
return nil, a.authorizeRevoke(ctx, ott)
|
||||
case provisioner.SignSSHMethod:
|
||||
if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
|
||||
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
|
||||
}
|
||||
return a.authorizeSign(ctx, ott)
|
||||
case provisioner.RevokeMethod:
|
||||
return nil, &apiError{errors.New("authorize: revoke method is not supported"), http.StatusInternalServerError, errContext}
|
||||
return a.authorizeSSHSign(ctx, ott)
|
||||
case provisioner.RenewSSHMethod:
|
||||
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:
|
||||
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
|
||||
// the RevokeOptions POSTed with the request.
|
||||
// Returns a tuple of the provisioner ID and error, if one occurred.
|
||||
func (a *Authority) authorizeRevoke(opts *RevokeOptions) (p provisioner.Interface, err error) {
|
||||
if opts.MTLS {
|
||||
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")
|
||||
}
|
||||
func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
|
||||
errContext := map[string]interface{}{"ott": token}
|
||||
|
||||
// Call the provisioner AuthorizeRevoke to apply provisioner specific auth claims.
|
||||
err = p.AuthorizeRevoke(opts.OTT)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "authorizeRevoke")
|
||||
}
|
||||
p, err := a.authorizeToken(token)
|
||||
if err != nil {
|
||||
return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext}
|
||||
}
|
||||
return
|
||||
if err = p.AuthorizeSSHRevoke(ctx, token); err != nil {
|
||||
return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext}
|
||||
}
|
||||
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
|
||||
// extra extension cannot be found, authorize the renewal 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()}
|
||||
|
||||
// Check the passive revocation table.
|
||||
|
@ -180,7 +186,7 @@ func (a *Authority) authorizeRenewal(crt *x509.Certificate) error {
|
|||
context: errContext,
|
||||
}
|
||||
}
|
||||
if err := p.AuthorizeRenewal(crt); err != nil {
|
||||
if err := p.AuthorizeRenew(context.Background(), crt); err != nil {
|
||||
return &apiError{
|
||||
err: errors.Wrap(err, "renew"),
|
||||
code: http.StatusUnauthorized,
|
||||
|
|
|
@ -81,23 +81,6 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
|
|||
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 {
|
||||
c.Template = &x509util.ASN1DN{}
|
||||
}
|
||||
|
@ -194,8 +177,11 @@ func (c *Config) Validate() error {
|
|||
// front so we cannot rely on the port.
|
||||
func (c *Config) getAudiences() provisioner.Audiences {
|
||||
audiences := provisioner.Audiences{
|
||||
Sign: []string{legacyAuthority},
|
||||
Revoke: []string{legacyAuthority},
|
||||
Sign: []string{legacyAuthority},
|
||||
Revoke: []string{legacyAuthority},
|
||||
SSHSign: []string{},
|
||||
SSHRevoke: []string{},
|
||||
SSHRenew: []string{},
|
||||
}
|
||||
|
||||
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))
|
||||
audiences.Revoke = append(audiences.Revoke,
|
||||
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
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
// ACME is the acme provisioner type, an entity that can authorize the ACME
|
||||
// provisioning flow.
|
||||
type ACME struct {
|
||||
*base
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
|
@ -58,16 +59,10 @@ func (p *ACME) Init(config Config) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// AuthorizeRevoke is not implemented yet for the ACME provisioner.
|
||||
func (p *ACME) AuthorizeRevoke(token string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// AuthorizeSign does not do any validation, because all validation is handled
|
||||
// in the ACME protocol. This method returns a list of modifiers / constraints
|
||||
// on the resulting certificate.
|
||||
func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
return []SignOption{
|
||||
// modifiers / withOptions
|
||||
newProvisionerExtensionOption(TypeACME, p.Name, ""),
|
||||
|
@ -78,8 +73,11 @@ func (p *ACME) AuthorizeSign(ctx context.Context, _ string) ([]SignOption, error
|
|||
}, nil
|
||||
}
|
||||
|
||||
// AuthorizeRenewal is not implemented for the ACME provisioner.
|
||||
func (p *ACME) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
// AuthorizeRenew returns an error if the renewal is disabled.
|
||||
// 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() {
|
||||
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
|
||||
}
|
||||
|
|
|
@ -123,6 +123,7 @@ type awsInstanceIdentityDocument struct {
|
|||
// Amazon Identity docs are available at
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
|
||||
type AWS struct {
|
||||
*base
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Accounts []string `json:"accounts"`
|
||||
|
@ -273,14 +274,6 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
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
|
||||
// Enforce known CN and default DNS and IP if configured.
|
||||
// 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
|
||||
}
|
||||
|
||||
// AuthorizeRenewal returns an error if the renewal is disabled.
|
||||
func (p *AWS) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
// AuthorizeRenew returns an error if the renewal is disabled.
|
||||
// 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() {
|
||||
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
|
||||
}
|
||||
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
|
||||
func (p *AWS) assertConfig() (err error) {
|
||||
if p.config != nil {
|
||||
|
@ -445,8 +435,16 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
|
|||
return &payload, nil
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) {
|
||||
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
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
|
||||
|
||||
signOptions := []SignOption{
|
||||
|
|
|
@ -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
|
||||
// and https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service
|
||||
type Azure struct {
|
||||
*base
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
TenantID string `json:"tenantId"`
|
||||
|
@ -208,15 +209,14 @@ func (p *Azure) Init(config Config) (err error) {
|
|||
return 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) {
|
||||
// parseToken returuns the claims, name, group, error.
|
||||
func (p *Azure) parseToken(token string) (*azurePayload, string, string, error) {
|
||||
jwt, err := jose.ParseSigned(token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing token")
|
||||
return nil, "", "", errors.Wrapf(err, "error parsing token")
|
||||
}
|
||||
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
|
||||
|
@ -229,7 +229,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
|||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, errors.New("cannot validate token")
|
||||
return nil, "", "", errors.New("cannot validate token")
|
||||
}
|
||||
|
||||
if err := claims.ValidateWithLeeway(jose.Expected{
|
||||
|
@ -237,19 +237,29 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
|||
Issuer: p.oidcConfig.Issuer,
|
||||
Time: time.Now(),
|
||||
}, 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
|
||||
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)
|
||||
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]
|
||||
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
|
||||
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.
|
||||
// By default we'll accept the CN and SANs in the CSR.
|
||||
// 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
|
||||
}
|
||||
|
||||
// AuthorizeRenewal returns an error if the renewal is disabled.
|
||||
func (p *Azure) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
// AuthorizeRenew returns an error if the renewal is disabled.
|
||||
// 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() {
|
||||
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthorizeRevoke returns an error because revoke is not supported on Azure
|
||||
// provisioners.
|
||||
func (p *Azure) AuthorizeRevoke(token string) error {
|
||||
return errors.New("revoke is not supported on a Azure provisioner")
|
||||
}
|
||||
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *Azure) 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())
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *Azure) authorizeSSHSign(claims azurePayload, name string) ([]SignOption, error) {
|
||||
_, name, _, err := p.parseToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signOptions := []SignOption{
|
||||
// set the key id to the token subject
|
||||
sshCertificateKeyIDModifier(name),
|
||||
|
|
|
@ -74,6 +74,7 @@ func newGCPConfig() *gcpConfig {
|
|||
// Google Identity docs are available at
|
||||
// https://cloud.google.com/compute/docs/instances/verifying-instance-identity
|
||||
type GCP struct {
|
||||
*base
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ServiceAccounts []string `json:"serviceAccounts"`
|
||||
|
@ -212,14 +213,6 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
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
|
||||
// Enforce known common name and default DNS if configured.
|
||||
// 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.
|
||||
func (p *GCP) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
func (p *GCP) AuthorizeRenewal(ctx context.Context, cert *x509.Certificate) error {
|
||||
if p.claimer.IsDisableRenewal() {
|
||||
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
|
||||
}
|
||||
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.
|
||||
func (p *GCP) assertConfig() {
|
||||
if p.config == nil {
|
||||
|
@ -357,8 +344,16 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
|
|||
return &claims, nil
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) {
|
||||
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
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
|
||||
|
||||
signOptions := []SignOption{
|
||||
|
|
|
@ -24,6 +24,7 @@ type stepPayload struct {
|
|||
// JWK is the default provisioner, an entity that can sign tokens necessary for
|
||||
// signature requests.
|
||||
type JWK struct {
|
||||
*base
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
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
|
||||
// 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)
|
||||
return err
|
||||
}
|
||||
|
@ -141,14 +142,6 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
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
|
||||
// and certificates. Older versions added the token subject as the only SAN
|
||||
// in a CSR by default.
|
||||
|
@ -171,17 +164,27 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
}, nil
|
||||
}
|
||||
|
||||
// AuthorizeRenewal returns an error if the renewal is disabled.
|
||||
func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
// AuthorizeRenew returns an error if the renewal is disabled.
|
||||
// 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() {
|
||||
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
|
||||
t := now()
|
||||
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (p *JWK) 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())
|
||||
}
|
||||
// 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 {
|
||||
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),
|
||||
}
|
||||
|
||||
t := now()
|
||||
// Add modifiers from custom claims
|
||||
if opts.CertType != "" {
|
||||
signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType))
|
||||
|
@ -223,3 +227,10 @@ func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
|
|||
&sshCertificateDefaultValidator{},
|
||||
), 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
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ type k8sSAPayload struct {
|
|||
// K8sSA represents a Kubernetes ServiceAccount provisioner; an
|
||||
// entity trusted to make signature requests.
|
||||
type K8sSA struct {
|
||||
*base
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
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
|
||||
// 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)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -14,12 +14,38 @@ type methodKey struct{}
|
|||
const (
|
||||
// SignMethod is the method used to sign X.509 certificates.
|
||||
SignMethod Method = iota
|
||||
// SignSSHMethod is the method used to sign SSH certificate.
|
||||
SignSSHMethod
|
||||
// RevokeMethod is the method used to revoke X.509 certificates.
|
||||
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
|
||||
// it.
|
||||
func NewContextWithMethod(ctx context.Context, method Method) context.Context {
|
||||
|
|
|
@ -3,6 +3,8 @@ package provisioner
|
|||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (p *noop) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
func (p *noop) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *noop) AuthorizeRevoke(token string) error {
|
||||
func (p *noop) AuthorizeRevoke(ctx context.Context, token string) error {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ type openIDPayload struct {
|
|||
//
|
||||
// ClientSecret is mandatory, but it can be an empty string.
|
||||
type OIDC struct {
|
||||
*base
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
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
|
||||
// revoke the certificate with serial number in the `sub` property.
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -284,14 +285,6 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
|||
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{
|
||||
// modifiers / withOptions
|
||||
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
|
||||
}
|
||||
|
||||
// AuthorizeRenewal returns an error if the renewal is disabled.
|
||||
func (o *OIDC) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
// AuthorizeRenew returns an error if the renewal is disabled.
|
||||
// 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() {
|
||||
return errors.Errorf("renew is disabled for provisioner %s", o.GetID())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) {
|
||||
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||
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{
|
||||
// set the key id to the token subject
|
||||
sshCertificateKeyIDModifier(claims.Email),
|
||||
|
@ -356,6 +359,20 @@ func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) {
|
|||
), 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 {
|
||||
resp, err := http.Get(uri)
|
||||
if err != nil {
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// Interface is the interface that all provisioner types must implement.
|
||||
|
@ -20,27 +22,45 @@ type Interface interface {
|
|||
GetEncryptedKey() (kid string, key string, ok bool)
|
||||
Init(config Config) error
|
||||
AuthorizeSign(ctx context.Context, token string) ([]SignOption, error)
|
||||
AuthorizeRenewal(cert *x509.Certificate) error
|
||||
AuthorizeRevoke(token string) error
|
||||
AuthorizeRevoke(ctx context.Context, 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.
|
||||
type Audiences struct {
|
||||
Sign []string
|
||||
Revoke []string
|
||||
Sign []string
|
||||
Revoke []string
|
||||
SSHSign []string
|
||||
SSHRevoke []string
|
||||
SSHRenew []string
|
||||
SSHRekey []string
|
||||
}
|
||||
|
||||
// All returns all supported audiences across all request types in one list.
|
||||
func (a Audiences) All() []string {
|
||||
return append(a.Sign, a.Revoke...)
|
||||
func (a Audiences) All() (auds []string) {
|
||||
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
|
||||
// given fragment.
|
||||
func (a Audiences) WithFragment(fragment string) Audiences {
|
||||
ret := Audiences{
|
||||
Sign: make([]string, len(a.Sign)),
|
||||
Revoke: make([]string, len(a.Revoke)),
|
||||
Sign: make([]string, len(a.Sign)),
|
||||
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 {
|
||||
if u, err := url.Parse(s); err == nil {
|
||||
|
@ -56,6 +76,34 @@ func (a Audiences) WithFragment(fragment string) Audiences {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -92,11 +140,6 @@ const (
|
|||
TypeK8sSA Type = 8
|
||||
// TypeSSHPOP is used to indicate the SSHPOP provisioners.
|
||||
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.
|
||||
|
@ -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
|
||||
// provisioners.
|
||||
type Config struct {
|
||||
|
@ -132,6 +181,10 @@ type Config struct {
|
|||
Claims Claims
|
||||
// Audiences are the audiences used in the default provisioner, (JWK).
|
||||
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 {
|
||||
|
@ -222,6 +275,50 @@ func SanitizeSSHUserPrincipal(email string) string {
|
|||
}, 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
|
||||
type MockProvisioner struct {
|
||||
Mret1, Mret2, Mret3 interface{}
|
||||
|
|
|
@ -2,18 +2,14 @@ package provisioner
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"github.com/smallstep/cli/crypto/x509util"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/cli/jose"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"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
|
||||
// signature requests.
|
||||
type SSHPOP struct {
|
||||
*base
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
PubKeys []byte `json:"pubKeys"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
db db.AuthDB
|
||||
claimer *Claimer
|
||||
audiences Audiences
|
||||
sshPubKeys []ssh.PublicKey
|
||||
sshPubKeys *SSHKeys
|
||||
}
|
||||
|
||||
// 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")
|
||||
case p.Name == "":
|
||||
return errors.New("provisioner name cannot be empty")
|
||||
case len(p.PubKeys) == 0:
|
||||
return errors.New("provisioner root(s) 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())
|
||||
case config.SSHKeys == nil:
|
||||
return errors.New("provisioner public SSH validation keys cannot be empty")
|
||||
}
|
||||
|
||||
// Update claims with global ones
|
||||
|
@ -124,6 +91,8 @@ func (p *SSHPOP) Init(config Config) error {
|
|||
}
|
||||
|
||||
p.audiences = config.Audiences.WithFragment(p.GetID())
|
||||
p.db = config.DB
|
||||
p.sshPubKeys = config.SSHKeys
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -131,50 +100,63 @@ func (p *SSHPOP) Init(config Config) error {
|
|||
// claims for case specific downstream parsing.
|
||||
// e.g. a Sign request will auth/validate different fields than a Revoke request.
|
||||
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)
|
||||
if err != nil {
|
||||
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"]
|
||||
if !ok {
|
||||
return nil, errors.New("token missing sshpop header")
|
||||
var (
|
||||
found bool
|
||||
data = bytesForSigning(sshCert)
|
||||
keys []ssh.PublicKey
|
||||
)
|
||||
if sshCert.CertType == ssh.UserCert {
|
||||
keys = p.sshPubKeys.UserKeys
|
||||
} else {
|
||||
keys = p.sshPubKeys.HostKeys
|
||||
}
|
||||
encodedSSHCertStr, ok := encodedSSHCert.(string)
|
||||
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 {
|
||||
for _, k := range keys {
|
||||
if err = (&ssh.Certificate{Key: k}).Verify(data, sshCert.Signature); err == nil {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
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:
|
||||
// 1. Asserts that the private key used to sign the token corresponds
|
||||
// to the public certificate in the `sshpop` header of the token.
|
||||
// 2. Asserts that the claims are valid - have not been tampered with.
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -189,6 +171,8 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa
|
|||
|
||||
// validate audiences with the defaults
|
||||
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)")
|
||||
}
|
||||
|
||||
|
@ -200,102 +184,77 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa
|
|||
return &claims, nil
|
||||
}
|
||||
|
||||
// AuthorizeRevoke returns an error if the provisioner does not have rights to
|
||||
// revoke the certificate with serial number in the `sub` property.
|
||||
func (p *SSHPOP) AuthorizeRevoke(token string) error {
|
||||
_, err := p.authorizeToken(token, p.audiences.Revoke)
|
||||
// AuthorizeSSHRevoke validates the authorization token and extracts/validates
|
||||
// the SSH certificate from the ssh-pop header.
|
||||
func (p *SSHPOP) AuthorizeSSHRevoke(ctx context.Context, token string) error {
|
||||
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
|
||||
}
|
||||
|
||||
// AuthorizeSign validates the given token.
|
||||
func (p *SSHPOP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
claims, err := p.authorizeToken(token, p.audiences.Sign)
|
||||
// AuthorizeSSHRenew validates the authorization token and extracts/validates
|
||||
// the SSH certificate from the ssh-pop header.
|
||||
func (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
|
||||
claims, err := p.authorizeToken(token, p.audiences.SSHRenew)
|
||||
if err != nil {
|
||||
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.
|
||||
func (p *SSHPOP) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
if p.claimer.IsDisableRenewal() {
|
||||
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
|
||||
// AuthorizeSSHRekey validates the authorization token and extracts/validates
|
||||
// the SSH certificate from the ssh-pop header.
|
||||
func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) {
|
||||
claims, err := p.authorizeToken(token, p.audiences.SSHRekey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
return claims.sshCert, []SignOption{
|
||||
// Validate public key
|
||||
&sshDefaultPublicKeyValidator{},
|
||||
// Validate the validity period.
|
||||
&sshCertificateValidityValidator{p.claimer},
|
||||
// Require all the fields in the SSH certificate
|
||||
// Require and validate all the default fields in the SSH certificate.
|
||||
&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 {
|
||||
|
|
|
@ -22,6 +22,7 @@ type x5cPayload struct {
|
|||
// X5C is the default provisioner, an entity that can sign tokens necessary for
|
||||
// signature requests.
|
||||
type X5C struct {
|
||||
*base
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
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
|
||||
// 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)
|
||||
return err
|
||||
}
|
||||
|
@ -213,8 +214,8 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
}, nil
|
||||
}
|
||||
|
||||
// AuthorizeRenewal returns an error if the renewal is disabled.
|
||||
func (p *X5C) AuthorizeRenewal(cert *x509.Certificate) error {
|
||||
// AuthorizeRenew returns an error if the renewal is disabled.
|
||||
func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
||||
if p.claimer.IsDisableRenewal() {
|
||||
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
|
||||
}
|
||||
|
|
275
authority/ssh.go
275
authority/ssh.go
|
@ -1,10 +1,12 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
|
@ -155,6 +157,22 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template
|
|||
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.
|
||||
func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
||||
var mods []provisioner.SSHCertificateModifier
|
||||
|
@ -274,6 +292,263 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign
|
|||
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.
|
||||
func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) {
|
||||
if a.sshCAUserCertSignKey == nil {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"github.com/smallstep/cli/crypto/tlsutil"
|
||||
"github.com/smallstep/cli/crypto/x509util"
|
||||
"github.com/smallstep/cli/jose"
|
||||
)
|
||||
|
||||
// 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'.
|
||||
func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {
|
||||
// Check step provisioner extensions
|
||||
if err := a.authorizeRenewal(oldCert); err != nil {
|
||||
if err := a.authorizeRenew(oldCert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -147,15 +149,15 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error
|
|||
ExtKeyUsage: oldCert.ExtKeyUsage,
|
||||
UnknownExtKeyUsage: oldCert.UnknownExtKeyUsage,
|
||||
BasicConstraintsValid: oldCert.BasicConstraintsValid,
|
||||
IsCA: oldCert.IsCA,
|
||||
MaxPathLen: oldCert.MaxPathLen,
|
||||
MaxPathLenZero: oldCert.MaxPathLenZero,
|
||||
OCSPServer: oldCert.OCSPServer,
|
||||
IssuingCertificateURL: oldCert.IssuingCertificateURL,
|
||||
DNSNames: oldCert.DNSNames,
|
||||
EmailAddresses: oldCert.EmailAddresses,
|
||||
IPAddresses: oldCert.IPAddresses,
|
||||
URIs: oldCert.URIs,
|
||||
IsCA: oldCert.IsCA,
|
||||
MaxPathLen: oldCert.MaxPathLen,
|
||||
MaxPathLenZero: oldCert.MaxPathLenZero,
|
||||
OCSPServer: oldCert.OCSPServer,
|
||||
IssuingCertificateURL: oldCert.IssuingCertificateURL,
|
||||
DNSNames: oldCert.DNSNames,
|
||||
EmailAddresses: oldCert.EmailAddresses,
|
||||
IPAddresses: oldCert.IPAddresses,
|
||||
URIs: oldCert.URIs,
|
||||
PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical,
|
||||
PermittedDNSDomains: oldCert.PermittedDNSDomains,
|
||||
ExcludedDNSDomains: oldCert.ExcludedDNSDomains,
|
||||
|
@ -220,13 +222,14 @@ type RevokeOptions struct {
|
|||
// being renewed.
|
||||
//
|
||||
// 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{
|
||||
"serialNumber": opts.Serial,
|
||||
"reasonCode": opts.ReasonCode,
|
||||
"reason": opts.Reason,
|
||||
"passiveOnly": opts.PassiveOnly,
|
||||
"mTLS": opts.MTLS,
|
||||
"context": string(provisioner.MethodFromContext(ctx)),
|
||||
}
|
||||
if opts.MTLS {
|
||||
errContext["certificate"] = base64.StdEncoding.EncodeToString(opts.Crt.Raw)
|
||||
|
@ -242,26 +245,57 @@ func (a *Authority) Revoke(opts *RevokeOptions) error {
|
|||
RevokedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
// Authorize mTLS or token request and get back a provisioner interface.
|
||||
p, err := a.authorizeRevoke(opts)
|
||||
if err != nil {
|
||||
return &apiError{errors.Wrap(err, "revoke"),
|
||||
http.StatusUnauthorized, errContext}
|
||||
}
|
||||
|
||||
var (
|
||||
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 {
|
||||
return &apiError{errors.Wrapf(err, "revoke: error parsing token"),
|
||||
http.StatusUnauthorized, errContext}
|
||||
}
|
||||
|
||||
// Get claims w/out verification. We should have already verified this token
|
||||
// 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)
|
||||
if err != nil {
|
||||
return &apiError{errors.Wrap(err, "revoke: could not get ID for token"),
|
||||
http.StatusInternalServerError, errContext}
|
||||
}
|
||||
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()
|
||||
errContext["provisionerID"] = rci.ProvisionerID
|
||||
|
||||
err = a.db.Revoke(rci)
|
||||
if provisioner.MethodFromContext(ctx) == provisioner.RevokeSSHMethod {
|
||||
err = a.db.RevokeSSH(rci)
|
||||
} else { // default to revoke x509
|
||||
err = a.db.Revoke(rci)
|
||||
}
|
||||
switch err {
|
||||
case nil:
|
||||
return nil
|
||||
|
|
66
ca/client.go
66
ca/client.go
|
@ -528,6 +528,72 @@ func (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error)
|
|||
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
|
||||
// api.SSHRootsResponse struct.
|
||||
func (c *Client) SSHRoots() (*api.SSHRootsResponse, error) {
|
||||
|
|
45
db/db.go
45
db/db.go
|
@ -16,6 +16,7 @@ import (
|
|||
var (
|
||||
certsTable = []byte("x509_certs")
|
||||
revokedCertsTable = []byte("revoked_x509_certs")
|
||||
revokedSSHCertsTable = []byte("revoked_ssh_certs")
|
||||
usedOTTTable = []byte("used_ott")
|
||||
sshCertsTable = []byte("ssh_certs")
|
||||
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.
|
||||
type AuthDB interface {
|
||||
IsRevoked(sn string) (bool, error)
|
||||
IsSSHRevoked(sn string) (bool, error)
|
||||
Revoke(rci *RevokedCertificateInfo) error
|
||||
RevokeSSH(rci *RevokedCertificateInfo) error
|
||||
StoreCertificate(crt *x509.Certificate) error
|
||||
UseToken(id, tok string) (bool, error)
|
||||
IsSSHHost(name string) (bool, error)
|
||||
|
@ -68,6 +71,7 @@ func New(c *Config) (AuthDB, error) {
|
|||
tables := [][]byte{
|
||||
revokedCertsTable, certsTable, usedOTTTable,
|
||||
sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable,
|
||||
revokedSSHCertsTable,
|
||||
}
|
||||
for _, b := range tables {
|
||||
if err := db.CreateTable(b); err != nil {
|
||||
|
@ -114,6 +118,29 @@ func (db *DB) IsRevoked(sn string) (bool, error) {
|
|||
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.
|
||||
func (db *DB) Revoke(rci *RevokedCertificateInfo) error {
|
||||
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.
|
||||
func (db *DB) StoreCertificate(crt *x509.Certificate) error {
|
||||
if err := db.Set(certsTable, []byte(crt.SerialNumber.String()), crt.Raw); err != nil {
|
||||
|
|
10
db/simple.go
10
db/simple.go
|
@ -31,11 +31,21 @@ func (s *SimpleDB) IsRevoked(sn string) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// IsSSHRevoked noop
|
||||
func (s *SimpleDB) IsSSHRevoked(sn string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Revoke returns a "NotImplemented" error.
|
||||
func (s *SimpleDB) Revoke(rci *RevokedCertificateInfo) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// RevokeSSH returns a "NotImplemented" error.
|
||||
func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// StoreCertificate returns a "NotImplemented" error.
|
||||
func (s *SimpleDB) StoreCertificate(crt *x509.Certificate) error {
|
||||
return ErrNotImplemented
|
||||
|
|
Loading…
Reference in a new issue