smallstep-certificates/authority/authorize.go
2019-11-20 11:52:20 -08:00

214 lines
7.6 KiB
Go

package authority
import (
"context"
"crypto/x509"
"net/http"
"strings"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/cli/jose"
)
// Claims extends jose.Claims with step attributes.
type Claims struct {
jose.Claims
SANs []string `json:"sans,omitempty"`
Email string `json:"email,omitempty"`
Nonce string `json:"nonce,omitempty"`
}
type skipTokenReuseKey struct{}
// NewContextWithSkipTokenReuse creates a new context from ctx and attaches a
// value to skip the token reuse.
func NewContextWithSkipTokenReuse(ctx context.Context) context.Context {
return context.WithValue(ctx, skipTokenReuseKey{}, true)
}
// SkipTokenReuseFromContext returns if the token reuse needs to be ignored.
func SkipTokenReuseFromContext(ctx context.Context) bool {
m, _ := ctx.Value(skipTokenReuseKey{}).(bool)
return m
}
// authorizeToken parses the token and returns the provisioner used to generate
// the token. This method enforces the One-Time use policy (tokens can only be
// used once).
func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner.Interface, error) {
var errContext = map[string]interface{}{"ott": ott}
// Validate payload
token, err := jose.ParseSigned(ott)
if err != nil {
return nil, &apiError{errors.Wrapf(err, "authorizeToken: error parsing token"),
http.StatusUnauthorized, errContext}
}
// Get claims w/out verification. We need to look up the provisioner
// key in order to verify the claims and we need the issuer from the claims
// before we can look up the provisioner.
var claims Claims
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeToken"), http.StatusUnauthorized, errContext}
}
// TODO: use new persistence layer abstraction.
// Do not accept tokens issued before the start of the ca.
// This check is meant as a stopgap solution to the current lack of a persistence layer.
if a.config.AuthorityConfig != nil && !a.config.AuthorityConfig.DisableIssuedAtCheck {
if claims.IssuedAt != nil && claims.IssuedAt.Time().Before(a.startTime) {
return nil, &apiError{errors.New("authorizeToken: token issued before the bootstrap of certificate authority"),
http.StatusUnauthorized, errContext}
}
}
// This method will also validate the audiences for JWK provisioners.
p, ok := a.provisioners.LoadByToken(token, &claims.Claims)
if !ok {
return nil, &apiError{
errors.Errorf("authorizeToken: provisioner not found or invalid audience (%s)", strings.Join(claims.Audience, ", ")),
http.StatusUnauthorized, errContext}
}
// Store the token to protect against reuse unless it's skipped.
if !SkipTokenReuseFromContext(ctx) {
if reuseKey, err := p.GetTokenID(ott); err == nil {
ok, err := a.db.UseToken(reuseKey, ott)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeToken: failed when checking if token already used"),
http.StatusInternalServerError, errContext}
}
if !ok {
return nil, &apiError{errors.Errorf("authorizeToken: token already used"), http.StatusUnauthorized, errContext}
}
}
}
return p, nil
}
// Authorize grabs the method from the context and authorizes a signature
// request by validating the one-time-token.
func (a *Authority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
var errContext = apiCtx{"ott": ott}
switch m := provisioner.MethodFromContext(ctx); m {
case provisioner.SignMethod:
return a.authorizeSign(ctx, ott)
case provisioner.RevokeMethod:
return nil, a.authorizeRevoke(ctx, ott)
case provisioner.SignSSHMethod:
if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
}
return a.authorizeSSHSign(ctx, ott)
case provisioner.RenewSSHMethod:
if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
}
if _, err := a.authorizeSSHRenew(ctx, ott); err != nil {
return nil, err
}
return nil, nil
case provisioner.RevokeSSHMethod:
return nil, a.authorizeSSHRevoke(ctx, ott)
case provisioner.RekeySSHMethod:
if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
}
_, opts, err := a.authorizeSSHRekey(ctx, ott)
if err != nil {
return nil, err
}
return opts, nil
default:
return nil, &apiError{errors.Errorf("authorize: method %d is not supported", m), http.StatusInternalServerError, errContext}
}
}
// authorizeSign loads the provisioner from the token, checks that it has not
// been used again and calls the provisioner AuthorizeSign method. Returns a
// list of methods to apply to the signing flow.
func (a *Authority) authorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
var errContext = apiCtx{"ott": ott}
p, err := a.authorizeToken(ctx, ott)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext}
}
opts, err := p.AuthorizeSign(ctx, ott)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext}
}
return opts, nil
}
// AuthorizeSign authorizes a signature request by validating and authenticating
// a OTT that must be sent w/ the request.
//
// NOTE: This method is deprecated and should not be used. We make it available
// in the short term os as not to break existing clients.
func (a *Authority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) {
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod)
return a.Authorize(ctx, ott)
}
// authorizeRevoke authorizes a revocation request by validating and authenticating
// the RevokeOptions POSTed with the request.
// Returns a tuple of the provisioner ID and error, if one occurred.
func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
errContext := map[string]interface{}{"ott": token}
p, err := a.authorizeToken(ctx, token)
if err != nil {
return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext}
}
if err = p.AuthorizeSSHRevoke(ctx, token); err != nil {
return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext}
}
return nil
}
// authorizeRenewl tries to locate the step provisioner extension, and checks
// if for the configured provisioner, the renewal is enabled or not. If the
// extra extension cannot be found, authorize the renewal by default.
//
// TODO(mariano): should we authorize by default?
func (a *Authority) authorizeRenew(crt *x509.Certificate) error {
errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()}
// Check the passive revocation table.
isRevoked, err := a.db.IsRevoked(crt.SerialNumber.String())
if err != nil {
return &apiError{
err: errors.Wrap(err, "renew"),
code: http.StatusInternalServerError,
context: errContext,
}
}
if isRevoked {
return &apiError{
err: errors.New("renew: certificate has been revoked"),
code: http.StatusUnauthorized,
context: errContext,
}
}
p, ok := a.provisioners.LoadByCertificate(crt)
if !ok {
return &apiError{
err: errors.New("renew: provisioner not found"),
code: http.StatusUnauthorized,
context: errContext,
}
}
if err := p.AuthorizeRenew(context.Background(), crt); err != nil {
return &apiError{
err: errors.Wrap(err, "renew"),
code: http.StatusUnauthorized,
context: errContext,
}
}
return nil
}