Skip to content

Commit 29853ae

Browse files
committed
sshpop provisioner + ssh renew | revoke | rekey first pass
1 parent c04f1e1 commit 29853ae

26 files changed

+1176
-329
lines changed

api/api.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type Authority interface {
3838
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
3939
LoadProvisionerByID(string) (provisioner.Interface, error)
4040
GetProvisioners(cursor string, limit int) (provisioner.List, string, error)
41-
Revoke(*authority.RevokeOptions) error
41+
Revoke(context.Context, *authority.RevokeOptions) error
4242
GetEncryptedKey(kid string) (string, error)
4343
GetRoots() (federation []*x509.Certificate, err error)
4444
GetFederation() ([]*x509.Certificate, error)
@@ -252,6 +252,9 @@ func (h *caHandler) Route(r Router) {
252252
r.MethodFunc("GET", "/federation", h.Federation)
253253
// SSH CA
254254
r.MethodFunc("POST", "/ssh/sign", h.SSHSign)
255+
r.MethodFunc("POST", "/ssh/renew", h.SSHRenew)
256+
r.MethodFunc("POST", "/ssh/revoke", h.SSHRevoke)
257+
r.MethodFunc("POST", "/ssh/rekey", h.SSHRekey)
255258
r.MethodFunc("GET", "/ssh/roots", h.SSHRoots)
256259
r.MethodFunc("GET", "/ssh/federation", h.SSHFederation)
257260
r.MethodFunc("POST", "/ssh/config", h.SSHConfig)

api/revoke.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package api
22

33
import (
4+
"context"
45
"net/http"
56

67
"github.com/pkg/errors"
78
"github.com/smallstep/certificates/authority"
9+
"github.com/smallstep/certificates/authority/provisioner"
810
"github.com/smallstep/certificates/logging"
911
"golang.org/x/crypto/ocsp"
1012
)
@@ -63,10 +65,15 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) {
6365
PassiveOnly: body.Passive,
6466
}
6567

68+
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)
6669
// A token indicates that we are using the api via a provisioner token,
6770
// otherwise it is assumed that the certificate is revoking itself over mTLS.
6871
if len(body.OTT) > 0 {
6972
logOtt(w, body.OTT)
73+
if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil {
74+
WriteError(w, Unauthorized(err))
75+
return
76+
}
7077
opts.OTT = body.OTT
7178
} else {
7279
// 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) {
7784
return
7885
}
7986
opts.Crt = r.TLS.PeerCertificates[0]
87+
if opts.Crt.SerialNumber.String() != opts.Serial {
88+
WriteError(w, BadRequest(errors.New("revoke: serial number in mtls certificate different than body")))
89+
return
90+
}
91+
// TODO: should probably be checking if the certificate was revoked here.
92+
// Will need to thread that request down to the authority, so will need
93+
// to add API for that.
8094
logCertificate(w, opts.Crt)
8195
opts.MTLS = true
8296
}
8397

84-
if err := h.Authority.Revoke(opts); err != nil {
98+
if err := h.Authority.Revoke(ctx, opts); err != nil {
8599
WriteError(w, Forbidden(err))
86100
return
87101
}

api/ssh.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
// SSHAuthority is the interface implemented by a SSH CA authority.
1717
type SSHAuthority interface {
1818
SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
19+
RenewSSH(cert *ssh.Certificate) (*ssh.Certificate, error)
20+
RekeySSH(cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
1921
SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
2022
GetSSHRoots() (*authority.SSHKeys, error)
2123
GetSSHFederation() (*authority.SSHKeys, error)
@@ -67,7 +69,8 @@ type SSHCertificate struct {
6769
*ssh.Certificate `json:"omitempty"`
6870
}
6971

70-
// SSHGetHostsResponse
72+
// SSHGetHostsResponse is the response object that returns the list of valid
73+
// hosts for SSH.
7174
type SSHGetHostsResponse struct {
7275
Hosts []string `json:"hosts"`
7376
}

api/sshRekey.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/pkg/errors"
8+
"github.com/smallstep/certificates/authority/provisioner"
9+
"golang.org/x/crypto/ssh"
10+
)
11+
12+
// SSHRekeyRequest is the request body of an SSH certificate request.
13+
type SSHRekeyRequest struct {
14+
OTT string `json:"ott"`
15+
PublicKey []byte `json:"publicKey"` //base64 encoded
16+
}
17+
18+
// Validate validates the SSHSignRekey.
19+
func (s *SSHRekeyRequest) Validate() error {
20+
switch {
21+
case len(s.OTT) == 0:
22+
return errors.New("missing or empty ott")
23+
case len(s.PublicKey) == 0:
24+
return errors.New("missing or empty public key")
25+
default:
26+
return nil
27+
}
28+
}
29+
30+
// SSHRekeyResponse is the response object that returns the SSH certificate.
31+
type SSHRekeyResponse struct {
32+
Certificate SSHCertificate `json:"crt"`
33+
}
34+
35+
// SSHRekey is an HTTP handler that reads an RekeySSHRequest with a one-time-token
36+
// (ott) from the body and creates a new SSH certificate with the information in
37+
// the request.
38+
func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) {
39+
var body SSHRekeyRequest
40+
if err := ReadJSON(r.Body, &body); err != nil {
41+
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
42+
return
43+
}
44+
45+
logOtt(w, body.OTT)
46+
if err := body.Validate(); err != nil {
47+
WriteError(w, BadRequest(err))
48+
return
49+
}
50+
51+
publicKey, err := ssh.ParsePublicKey(body.PublicKey)
52+
if err != nil {
53+
WriteError(w, BadRequest(errors.Wrap(err, "error parsing publicKey")))
54+
return
55+
}
56+
57+
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RekeySSHMethod)
58+
signOpts, err := h.Authority.Authorize(ctx, body.OTT)
59+
if err != nil {
60+
WriteError(w, Unauthorized(err))
61+
return
62+
}
63+
oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT)
64+
if err != nil {
65+
WriteError(w, InternalServerError(err))
66+
}
67+
68+
newCert, err := h.Authority.RekeySSH(oldCert, publicKey, signOpts...)
69+
if err != nil {
70+
WriteError(w, Forbidden(err))
71+
return
72+
}
73+
74+
w.WriteHeader(http.StatusCreated)
75+
JSON(w, &SSHSignResponse{
76+
Certificate: SSHCertificate{newCert},
77+
})
78+
}

api/sshRenew.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/pkg/errors"
8+
"github.com/smallstep/certificates/authority/provisioner"
9+
)
10+
11+
// SSHRenewRequest is the request body of an SSH certificate request.
12+
type SSHRenewRequest struct {
13+
OTT string `json:"ott"`
14+
}
15+
16+
// Validate validates the SSHSignRequest.
17+
func (s *SSHRenewRequest) Validate() error {
18+
switch {
19+
case len(s.OTT) == 0:
20+
return errors.New("missing or empty ott")
21+
default:
22+
return nil
23+
}
24+
}
25+
26+
// SSHRenewResponse is the response object that returns the SSH certificate.
27+
type SSHRenewResponse struct {
28+
Certificate SSHCertificate `json:"crt"`
29+
}
30+
31+
// SSHRenew is an HTTP handler that reads an RenewSSHRequest with a one-time-token
32+
// (ott) from the body and creates a new SSH certificate with the information in
33+
// the request.
34+
func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) {
35+
var body SSHRenewRequest
36+
if err := ReadJSON(r.Body, &body); err != nil {
37+
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
38+
return
39+
}
40+
41+
logOtt(w, body.OTT)
42+
if err := body.Validate(); err != nil {
43+
WriteError(w, BadRequest(err))
44+
return
45+
}
46+
47+
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RenewSSHMethod)
48+
_, err := h.Authority.Authorize(ctx, body.OTT)
49+
if err != nil {
50+
WriteError(w, Unauthorized(err))
51+
return
52+
}
53+
oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT)
54+
if err != nil {
55+
WriteError(w, InternalServerError(err))
56+
}
57+
58+
newCert, err := h.Authority.RenewSSH(oldCert)
59+
if err != nil {
60+
WriteError(w, Forbidden(err))
61+
return
62+
}
63+
64+
w.WriteHeader(http.StatusCreated)
65+
JSON(w, &SSHSignResponse{
66+
Certificate: SSHCertificate{newCert},
67+
})
68+
}

api/sshRevoke.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/pkg/errors"
8+
"github.com/smallstep/certificates/authority"
9+
"github.com/smallstep/certificates/authority/provisioner"
10+
"github.com/smallstep/certificates/logging"
11+
"golang.org/x/crypto/ocsp"
12+
)
13+
14+
// SSHRevokeResponse is the response object that returns the health of the server.
15+
type SSHRevokeResponse struct {
16+
Status string `json:"status"`
17+
}
18+
19+
// SSHRevokeRequest is the request body for a revocation request.
20+
type SSHRevokeRequest struct {
21+
Serial string `json:"serial"`
22+
OTT string `json:"ott"`
23+
ReasonCode int `json:"reasonCode"`
24+
Reason string `json:"reason"`
25+
Passive bool `json:"passive"`
26+
}
27+
28+
// Validate checks the fields of the RevokeRequest and returns nil if they are ok
29+
// or an error if something is wrong.
30+
func (r *SSHRevokeRequest) Validate() (err error) {
31+
if r.Serial == "" {
32+
return BadRequest(errors.New("missing serial"))
33+
}
34+
if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise {
35+
return BadRequest(errors.New("reasonCode out of bounds"))
36+
}
37+
if !r.Passive {
38+
return NotImplemented(errors.New("non-passive revocation not implemented"))
39+
}
40+
if len(r.OTT) == 0 {
41+
return BadRequest(errors.New("missing ott"))
42+
}
43+
return
44+
}
45+
46+
// Revoke supports handful of different methods that revoke a Certificate.
47+
//
48+
// NOTE: currently only Passive revocation is supported.
49+
func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) {
50+
var body SSHRevokeRequest
51+
if err := ReadJSON(r.Body, &body); err != nil {
52+
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
53+
return
54+
}
55+
56+
if err := body.Validate(); err != nil {
57+
WriteError(w, err)
58+
return
59+
}
60+
61+
opts := &authority.RevokeOptions{
62+
Serial: body.Serial,
63+
Reason: body.Reason,
64+
ReasonCode: body.ReasonCode,
65+
PassiveOnly: body.Passive,
66+
}
67+
68+
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeSSHMethod)
69+
// A token indicates that we are using the api via a provisioner token,
70+
// otherwise it is assumed that the certificate is revoking itself over mTLS.
71+
logOtt(w, body.OTT)
72+
if _, err := h.Authority.Authorize(ctx, body.OTT); err != nil {
73+
WriteError(w, Unauthorized(err))
74+
return
75+
}
76+
opts.OTT = body.OTT
77+
78+
if err := h.Authority.Revoke(ctx, opts); err != nil {
79+
WriteError(w, Forbidden(err))
80+
return
81+
}
82+
83+
logSSHRevoke(w, opts)
84+
JSON(w, &SSHRevokeResponse{Status: "ok"})
85+
}
86+
87+
func logSSHRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {
88+
if rl, ok := w.(logging.ResponseLogger); ok {
89+
rl.WithFields(map[string]interface{}{
90+
"serial": ri.Serial,
91+
"reasonCode": ri.ReasonCode,
92+
"reason": ri.Reason,
93+
"passiveOnly": ri.PassiveOnly,
94+
"mTLS": ri.MTLS,
95+
"ssh": true,
96+
})
97+
}
98+
}

authority/authority.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,33 @@ func (a *Authority) init() error {
179179
}
180180
}
181181

182+
// Merge global and configuration claims
183+
claimer, err := provisioner.NewClaimer(a.config.AuthorityConfig.Claims, globalProvisionerClaims)
184+
if err != nil {
185+
return err
186+
}
187+
// TODO: should we also be combining the ssh federated roots here?
188+
// If we rotate ssh roots keys, sshpop provisioner will lose ability to
189+
// validate old SSH certificates, unless they are added as federated certs.
190+
sshKeys, err := a.GetSSHRoots()
191+
if err != nil {
192+
return err
193+
}
194+
// Initialize provisioners
195+
config := provisioner.Config{
196+
Claims: claimer.Claims(),
197+
Audiences: a.config.getAudiences(),
198+
DB: a.db,
199+
SSHKeys: &provisioner.SSHKeys{
200+
UserKeys: sshKeys.UserKeys,
201+
HostKeys: sshKeys.HostKeys,
202+
},
203+
}
182204
// Store all the provisioners
183205
for _, p := range a.config.AuthorityConfig.Provisioners {
206+
if err := p.Init(config); err != nil {
207+
return err
208+
}
184209
if err := a.provisioners.Store(p); err != nil {
185210
return err
186211
}

0 commit comments

Comments
 (0)