diff --git a/api/ssh.go b/api/ssh.go index 15c3c4b2..43b24d52 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -29,14 +29,15 @@ type SSHAuthority interface { // SSHSignRequest is the request body of an SSH certificate request. type SSHSignRequest struct { - PublicKey []byte `json:"publicKey"` //base64 encoded - OTT string `json:"ott"` - CertType string `json:"certType,omitempty"` - Principals []string `json:"principals,omitempty"` - ValidAfter TimeDuration `json:"validAfter,omitempty"` - ValidBefore TimeDuration `json:"validBefore,omitempty"` - AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"` - KeyID string `json:"keyID"` + PublicKey []byte `json:"publicKey"` // base64 encoded + OTT string `json:"ott"` + CertType string `json:"certType,omitempty"` + Principals []string `json:"principals,omitempty"` + ValidAfter TimeDuration `json:"validAfter,omitempty"` + ValidBefore TimeDuration `json:"validBefore,omitempty"` + AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"` + KeyID string `json:"keyID"` + IdentityCSR CertificateRequest `json:"identityCSR,omitempty"` } // Validate validates the SSHSignRequest. @@ -49,14 +50,21 @@ func (s *SSHSignRequest) Validate() error { case len(s.OTT) == 0: return errors.New("missing or empty ott") default: + // Validate identity signature if provided + if s.IdentityCSR.CertificateRequest != nil { + if err := s.IdentityCSR.CertificateRequest.CheckSignature(); err != nil { + return errors.Wrap(err, "invalid csr") + } + } return nil } } // SSHSignResponse is the response object that returns the SSH certificate. type SSHSignResponse struct { - Certificate SSHCertificate `json:"crt"` - AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"` + Certificate SSHCertificate `json:"crt"` + AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"` + IdentityCertificate []Certificate `json:"identityCrt,omitempty"` } // SSHRootsResponse represents the response object that returns the SSH user and @@ -292,11 +300,33 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) { addUserCertificate = &SSHCertificate{addUserCert} } - w.WriteHeader(http.StatusCreated) - JSON(w, &SSHSignResponse{ - Certificate: SSHCertificate{cert}, - AddUserCertificate: addUserCertificate, - }) + // Sign identity certificate if available. + var identityCertificate []Certificate + if cr := body.IdentityCSR.CertificateRequest; cr != nil { + opts := provisioner.Options{ + NotBefore: body.ValidAfter, + NotAfter: body.ValidBefore, + } + ctx := authority.NewContextWithSkipTokenReuse(context.Background()) + ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) + signOpts, err := h.Authority.Authorize(ctx, body.OTT) + if err != nil { + WriteError(w, Unauthorized(err)) + return + } + certChain, err := h.Authority.Sign(cr, opts, signOpts...) + if err != nil { + WriteError(w, Forbidden(err)) + return + } + identityCertificate = certChainToPEM(certChain) + } + + JSONStatus(w, &SSHSignResponse{ + Certificate: SSHCertificate{cert}, + AddUserCertificate: addUserCertificate, + IdentityCertificate: identityCertificate, + }, http.StatusCreated) } // SSHRoots is an HTTP handler that returns the SSH public keys for user and host diff --git a/authority/authorize.go b/authority/authorize.go index 18eba6b9..db2b2414 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -19,10 +19,24 @@ type Claims struct { 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(ott string) (provisioner.Interface, error) { +func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner.Interface, error) { var errContext = map[string]interface{}{"ott": ott} // Validate payload @@ -58,15 +72,17 @@ func (a *Authority) authorizeToken(ott string) (provisioner.Interface, error) { http.StatusUnauthorized, errContext} } - // Store the token to protect against reuse. - 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} + // 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} + } } } @@ -116,7 +132,7 @@ func (a *Authority) Authorize(ctx context.Context, ott string) ([]provisioner.Si // 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(ott) + p, err := a.authorizeToken(ctx, ott) if err != nil { return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext} } @@ -143,7 +159,7 @@ func (a *Authority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) func (a *Authority) authorizeRevoke(ctx context.Context, token string) error { errContext := map[string]interface{}{"ott": token} - p, err := a.authorizeToken(token) + p, err := a.authorizeToken(ctx, token) if err != nil { return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext} } diff --git a/authority/config.go b/authority/config.go index 0fd1e5c9..462a764b 100644 --- a/authority/config.go +++ b/authority/config.go @@ -198,7 +198,9 @@ func (c *Config) getAudiences() provisioner.Audiences { for _, name := range c.DNSNames { audiences.Sign = append(audiences.Sign, fmt.Sprintf("https://%s/1.0/sign", name), - fmt.Sprintf("https://%s/sign", name)) + fmt.Sprintf("https://%s/sign", name), + fmt.Sprintf("https://%s/1.0/ssh/sign", name), + fmt.Sprintf("https://%s/ssh/sign", name)) audiences.Revoke = append(audiences.Revoke, fmt.Sprintf("https://%s/1.0/revoke", name), fmt.Sprintf("https://%s/revoke", name)) diff --git a/authority/ssh.go b/authority/ssh.go index 36806ae6..4f34d81a 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -190,7 +190,7 @@ func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error // 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) + p, err := a.authorizeToken(ctx, ott) if err != nil { return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext} } @@ -325,7 +325,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign func (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) { errContext := map[string]interface{}{"ott": token} - p, err := a.authorizeToken(token) + p, err := a.authorizeToken(ctx, token) if err != nil { return nil, &apiError{ err: errors.Wrap(err, "authorizeSSHRenew"), @@ -435,7 +435,7 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) 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) + p, err := a.authorizeToken(ctx, token) if err != nil { return nil, nil, &apiError{ err: errors.Wrap(err, "authorizeSSHRenew"), @@ -567,7 +567,7 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error { errContext := map[string]interface{}{"ott": token} - p, err := a.authorizeToken(token) + p, err := a.authorizeToken(ctx, token) if err != nil { return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext} }