2018-10-05 21:48:36 +00:00
|
|
|
package authority
|
|
|
|
|
|
|
|
import (
|
2018-11-01 22:43:24 +00:00
|
|
|
"crypto/x509"
|
2018-10-05 21:48:36 +00:00
|
|
|
"net/http"
|
2018-12-21 23:27:22 +00:00
|
|
|
"net/url"
|
2019-03-07 01:00:45 +00:00
|
|
|
"time"
|
2018-10-05 21:48:36 +00:00
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2019-03-06 23:04:28 +00:00
|
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
|
|
|
"github.com/smallstep/cli/jose"
|
2018-10-05 21:48:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type idUsed struct {
|
|
|
|
UsedAt int64 `json:"ua,omitempty"`
|
|
|
|
Subject string `json:"sub,omitempty"`
|
|
|
|
}
|
|
|
|
|
2019-03-06 23:04:28 +00:00
|
|
|
// Claims extends jose.Claims with step attributes.
|
2019-01-30 23:36:42 +00:00
|
|
|
type Claims struct {
|
2019-03-06 23:04:28 +00:00
|
|
|
jose.Claims
|
2019-03-07 01:00:45 +00:00
|
|
|
SANs []string `json:"sans,omitempty"`
|
|
|
|
Email string `json:"email,omitempty"`
|
|
|
|
Nonce string `json:"nonce,omitempty"`
|
2019-01-30 23:36:42 +00:00
|
|
|
}
|
|
|
|
|
2018-12-21 23:27:22 +00:00
|
|
|
// matchesAudience returns true if A and B share at least one element.
|
|
|
|
func matchesAudience(as, bs []string) bool {
|
2018-10-25 22:17:22 +00:00
|
|
|
if len(bs) == 0 || len(as) == 0 {
|
2018-10-05 21:48:36 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-10-19 05:26:39 +00:00
|
|
|
for _, b := range bs {
|
|
|
|
for _, a := range as {
|
2018-12-21 23:27:22 +00:00
|
|
|
if b == a || stripPort(a) == stripPort(b) {
|
2018-10-05 21:48:36 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-12-21 23:27:22 +00:00
|
|
|
// stripPort attempts to strip the port from the given url. If parsing the url
|
|
|
|
// produces errors it will just return the passed argument.
|
|
|
|
func stripPort(rawurl string) string {
|
|
|
|
u, err := url.Parse(rawurl)
|
|
|
|
if err != nil {
|
|
|
|
return rawurl
|
|
|
|
}
|
|
|
|
u.Host = u.Hostname()
|
|
|
|
return u.String()
|
|
|
|
}
|
|
|
|
|
2018-10-05 21:48:36 +00:00
|
|
|
// Authorize authorizes a signature request by validating and authenticating
|
|
|
|
// a OTT that must be sent w/ the request.
|
2019-03-07 01:00:45 +00:00
|
|
|
// TODO(mariano): protection against reuse for oidc
|
2019-03-06 23:04:28 +00:00
|
|
|
func (a *Authority) Authorize(ott string) ([]provisioner.SignOption, error) {
|
|
|
|
var errContext = map[string]interface{}{"ott": ott}
|
2018-10-05 21:48:36 +00:00
|
|
|
|
|
|
|
// Validate payload
|
2019-03-06 23:04:28 +00:00
|
|
|
token, err := jose.ParseSigned(ott)
|
2018-10-05 21:48:36 +00:00
|
|
|
if err != nil {
|
2018-10-19 05:26:39 +00:00
|
|
|
return nil, &apiError{errors.Wrapf(err, "authorize: error parsing token"),
|
2018-10-05 21:48:36 +00:00
|
|
|
http.StatusUnauthorized, errContext}
|
|
|
|
}
|
|
|
|
|
2018-10-30 01:00:30 +00:00
|
|
|
// 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.
|
2019-03-07 01:00:45 +00:00
|
|
|
var claims Claims
|
2018-10-30 01:00:30 +00:00
|
|
|
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
|
|
|
return nil, &apiError{err, http.StatusUnauthorized, errContext}
|
|
|
|
}
|
2018-10-05 21:48:36 +00:00
|
|
|
|
2018-10-25 01:59:48 +00:00
|
|
|
// 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}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-07 01:37:49 +00:00
|
|
|
// This method will also validate the audiences for JWK provisioners.
|
2019-03-07 01:00:45 +00:00
|
|
|
p, ok := a.provisioners.LoadByToken(token, &claims.Claims)
|
2019-03-06 23:04:28 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, &apiError{errors.Errorf("authorize: provisioner not found"),
|
2018-10-05 21:48:36 +00:00
|
|
|
http.StatusUnauthorized, errContext}
|
|
|
|
}
|
2018-10-19 05:26:39 +00:00
|
|
|
|
2018-10-05 21:48:36 +00:00
|
|
|
// Store the token to protect against reuse.
|
2019-03-07 01:00:45 +00:00
|
|
|
if p.GetType() == provisioner.TypeJWK && claims.ID != "" {
|
|
|
|
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}
|
|
|
|
}
|
|
|
|
}
|
2019-03-06 23:04:28 +00:00
|
|
|
|
|
|
|
return p.Authorize(ott)
|
2018-10-05 21:48:36 +00:00
|
|
|
}
|
2018-11-01 22:43:24 +00:00
|
|
|
|
|
|
|
// authorizeRenewal tries to locate the step provisioner extension, and checks
|
|
|
|
// if for the configured provisioner, the renewal is enabled or not. If the
|
|
|
|
// extra extension cannot be found, authorize the renewal by default.
|
|
|
|
//
|
|
|
|
// TODO(mariano): should we authorize by default?
|
|
|
|
func (a *Authority) authorizeRenewal(crt *x509.Certificate) error {
|
|
|
|
errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()}
|
2019-03-06 23:04:28 +00:00
|
|
|
p, ok := a.provisioners.LoadByCertificate(crt)
|
|
|
|
if !ok {
|
|
|
|
return &apiError{
|
|
|
|
err: errors.New("provisioner not found"),
|
|
|
|
code: http.StatusUnauthorized,
|
|
|
|
context: errContext,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := p.AuthorizeRenewal(crt); err != nil {
|
|
|
|
return &apiError{
|
|
|
|
err: err,
|
|
|
|
code: http.StatusUnauthorized,
|
|
|
|
context: errContext,
|
2018-11-01 22:43:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|