smallstep-certificates/authority/authorize.go
max furman ee7db4006a change sign + authorize authority api | add provisioners
* authorize returns []interface{}
 - operators in this list can conform to any interface the user decides
 - our implementation has a combination of certificate claim validators
 and certificate template modifiers.
* provisioners can set and enforce tls cert options
2018-10-18 22:26:39 -07:00

118 lines
3.3 KiB
Go

package authority
import (
"net/http"
"time"
"github.com/pkg/errors"
"gopkg.in/square/go-jose.v2/jwt"
)
type idUsed struct {
UsedAt int64 `json:"ua,omitempty"`
Subject string `json:"sub,omitempty"`
}
// containsAtLeastOneAudience returns true if 'as' contains at least one element
// of 'bs', otherwise returns false.
func containsAtLeastOneAudience(as []string, bs []string) bool {
if len(bs) == 0 {
return true
}
if len(as) == 0 {
return false
}
for _, b := range bs {
for _, a := range as {
if b == a {
return true
}
}
}
return false
}
// Authorize authorizes a signature request by validating and authenticating
// a OTT that must be sent w/ the request.
func (a *Authority) Authorize(ott string) ([]interface{}, error) {
var (
errContext = map[string]interface{}{"ott": ott}
claims = jwt.Claims{}
)
// Validate payload
token, err := jwt.ParseSigned(ott)
if err != nil {
return nil, &apiError{errors.Wrapf(err, "authorize: error parsing token"),
http.StatusUnauthorized, errContext}
}
kid := token.Headers[0].KeyID // JWT will only have 1 header.
if len(kid) == 0 {
return nil, &apiError{errors.New("authorize: token KeyID cannot be empty"),
http.StatusUnauthorized, errContext}
}
val, ok := a.provisionerIDIndex.Load(kid)
if !ok {
return nil, &apiError{errors.Errorf("authorize: provisioner with KeyID %s not found", kid),
http.StatusUnauthorized, errContext}
}
p, ok := val.(*Provisioner)
if !ok {
return nil, &apiError{errors.Errorf("authorize: invalid provisioner type"),
http.StatusInternalServerError, errContext}
}
if err = token.Claims(p.Key, &claims); err != nil {
return nil, &apiError{err, http.StatusUnauthorized, errContext}
}
// According to "rfc7519 JSON Web Token" acceptable skew should be no
// more than a few minutes.
if err = claims.ValidateWithLeeway(jwt.Expected{
Issuer: p.Issuer,
}, time.Minute); err != nil {
return nil, &apiError{errors.Wrapf(err, "authorize: invalid token"),
http.StatusUnauthorized, errContext}
}
// 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 > 0 && claims.IssuedAt.Time().Before(a.startTime) {
return nil, &apiError{errors.New("token issued before the bootstrap of certificate authority"),
http.StatusUnauthorized, errContext}
}
}
if !containsAtLeastOneAudience(claims.Audience, a.audiences) {
return nil, &apiError{errors.New("authorize: token audience invalid"), http.StatusUnauthorized,
errContext}
}
if claims.Subject == "" {
return nil, &apiError{errors.New("authorize: token subject cannot be empty"),
http.StatusUnauthorized, errContext}
}
signOps := []interface{}{
&commonNameClaim{claims.Subject},
&dnsNamesClaim{claims.Subject},
&ipAddressesClaim{claims.Subject},
withIssuerAlternativeNameExtension(p.Issuer + ":" + p.Key.KeyID),
p,
}
// Store the token to protect against reuse.
if _, ok := a.ottMap.LoadOrStore(claims.ID, &idUsed{
UsedAt: time.Now().Unix(),
Subject: claims.Subject,
}); ok {
return nil, &apiError{errors.Errorf("token already used"), http.StatusUnauthorized,
errContext}
}
return signOps, nil
}