forked from TrueCloudLab/certificates
sshpop provisioner + ssh renew | revoke | rekey first pass
This commit is contained in:
parent
c04f1e1bd4
commit
29853ae016
26 changed files with 1185 additions and 338 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
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
|
// 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext}
|
||||||
return nil, errors.Wrap(err, "authorizeRevoke")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
// 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,
|
||||||
|
|
|
@ -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{}
|
||||||
}
|
}
|
||||||
|
@ -194,8 +177,11 @@ func (c *Config) Validate() error {
|
||||||
// front so we cannot rely on the port.
|
// front so we cannot rely on the port.
|
||||||
func (c *Config) getAudiences() provisioner.Audiences {
|
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
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,27 +22,45 @@ 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
|
||||||
// given fragment.
|
// given fragment.
|
||||||
func (a Audiences) WithFragment(fragment string) Audiences {
|
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{}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
275
authority/ssh.go
275
authority/ssh.go
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,15 +149,15 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error
|
||||||
ExtKeyUsage: oldCert.ExtKeyUsage,
|
ExtKeyUsage: oldCert.ExtKeyUsage,
|
||||||
UnknownExtKeyUsage: oldCert.UnknownExtKeyUsage,
|
UnknownExtKeyUsage: oldCert.UnknownExtKeyUsage,
|
||||||
BasicConstraintsValid: oldCert.BasicConstraintsValid,
|
BasicConstraintsValid: oldCert.BasicConstraintsValid,
|
||||||
IsCA: oldCert.IsCA,
|
IsCA: oldCert.IsCA,
|
||||||
MaxPathLen: oldCert.MaxPathLen,
|
MaxPathLen: oldCert.MaxPathLen,
|
||||||
MaxPathLenZero: oldCert.MaxPathLenZero,
|
MaxPathLenZero: oldCert.MaxPathLenZero,
|
||||||
OCSPServer: oldCert.OCSPServer,
|
OCSPServer: oldCert.OCSPServer,
|
||||||
IssuingCertificateURL: oldCert.IssuingCertificateURL,
|
IssuingCertificateURL: oldCert.IssuingCertificateURL,
|
||||||
DNSNames: oldCert.DNSNames,
|
DNSNames: oldCert.DNSNames,
|
||||||
EmailAddresses: oldCert.EmailAddresses,
|
EmailAddresses: oldCert.EmailAddresses,
|
||||||
IPAddresses: oldCert.IPAddresses,
|
IPAddresses: oldCert.IPAddresses,
|
||||||
URIs: oldCert.URIs,
|
URIs: oldCert.URIs,
|
||||||
PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical,
|
PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical,
|
||||||
PermittedDNSDomains: oldCert.PermittedDNSDomains,
|
PermittedDNSDomains: oldCert.PermittedDNSDomains,
|
||||||
ExcludedDNSDomains: oldCert.ExcludedDNSDomains,
|
ExcludedDNSDomains: oldCert.ExcludedDNSDomains,
|
||||||
|
@ -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
|
||||||
if err != nil {
|
err error
|
||||||
return &apiError{errors.Wrap(err, "revoke"),
|
)
|
||||||
http.StatusUnauthorized, errContext}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not mTLS then get the TokenID of the token.
|
// If not mTLS then get the TokenID of the token.
|
||||||
if !opts.MTLS {
|
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)
|
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
|
||||||
|
|
||||||
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 {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
return 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
|
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) {
|
||||||
|
|
45
db/db.go
45
db/db.go
|
@ -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 {
|
||||||
|
|
10
db/simple.go
10
db/simple.go
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue