2019-07-24 01:46:43 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
2019-07-29 19:52:13 +00:00
|
|
|
"context"
|
2019-07-24 01:46:43 +00:00
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2019-09-25 02:12:13 +00:00
|
|
|
"github.com/smallstep/certificates/authority"
|
2019-07-24 01:46:43 +00:00
|
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
)
|
|
|
|
|
|
|
|
// SSHAuthority is the interface implemented by a SSH CA authority.
|
|
|
|
type SSHAuthority interface {
|
|
|
|
SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
2019-08-03 00:48:34 +00:00
|
|
|
SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
2019-09-25 02:12:13 +00:00
|
|
|
SSHConfig() (*authority.SSHConfiguration, error)
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SignSSHRequest is the request body of an SSH certificate request.
|
|
|
|
type SignSSHRequest struct {
|
2019-08-03 00:48:34 +00:00
|
|
|
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"`
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SignSSHResponse is the response object that returns the SSH certificate.
|
|
|
|
type SignSSHResponse struct {
|
2019-08-05 23:06:05 +00:00
|
|
|
Certificate SSHCertificate `json:"crt"`
|
|
|
|
AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"`
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
2019-09-25 02:12:13 +00:00
|
|
|
// SSHConfigResponse represents the response object that returns the SSH user
|
|
|
|
// and host keys.
|
|
|
|
type SSHConfigResponse struct {
|
|
|
|
UserKey *SSHPublicKey `json:"userKey,omitempty"`
|
|
|
|
HostKey *SSHPublicKey `json:"hostKey,omitempty"`
|
|
|
|
}
|
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
// SSHCertificate represents the response SSH certificate.
|
|
|
|
type SSHCertificate struct {
|
2019-08-05 23:06:05 +00:00
|
|
|
*ssh.Certificate `json:"omitempty"`
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
2019-09-09 04:05:36 +00:00
|
|
|
// MarshalJSON implements the json.Marshaler interface. Returns a quoted,
|
|
|
|
// base64 encoded, openssh wire format version of the certificate.
|
2019-07-24 01:46:43 +00:00
|
|
|
func (c SSHCertificate) MarshalJSON() ([]byte, error) {
|
|
|
|
if c.Certificate == nil {
|
|
|
|
return []byte("null"), nil
|
|
|
|
}
|
|
|
|
s := base64.StdEncoding.EncodeToString(c.Certificate.Marshal())
|
|
|
|
return []byte(`"` + s + `"`), nil
|
|
|
|
}
|
|
|
|
|
2019-09-25 02:12:13 +00:00
|
|
|
// SSHPublicKey represents a public key in a response object.
|
|
|
|
type SSHPublicKey struct {
|
|
|
|
ssh.PublicKey
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalJSON implements the json.Marshaler interface. Returns a quoted,
|
|
|
|
// base64 encoded, openssh wire format version of the public key.
|
|
|
|
func (p *SSHPublicKey) MarshalJSON() ([]byte, error) {
|
|
|
|
if p == nil || p.PublicKey == nil {
|
|
|
|
return []byte("null"), nil
|
|
|
|
}
|
|
|
|
s := base64.StdEncoding.EncodeToString(p.PublicKey.Marshal())
|
|
|
|
return []byte(`"` + s + `"`), nil
|
|
|
|
}
|
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
// UnmarshalJSON implements the json.Unmarshaler interface. The certificate is
|
2019-09-09 04:05:36 +00:00
|
|
|
// expected to be a quoted, base64 encoded, openssh wire formatted block of bytes.
|
2019-07-24 01:46:43 +00:00
|
|
|
func (c *SSHCertificate) UnmarshalJSON(data []byte) error {
|
|
|
|
var s string
|
|
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
|
|
return errors.Wrap(err, "error decoding certificate")
|
|
|
|
}
|
2019-08-03 00:48:34 +00:00
|
|
|
if s == "" {
|
|
|
|
c.Certificate = nil
|
|
|
|
return nil
|
|
|
|
}
|
2019-07-24 01:46:43 +00:00
|
|
|
certData, err := base64.StdEncoding.DecodeString(s)
|
|
|
|
if err != nil {
|
2019-09-09 04:05:36 +00:00
|
|
|
return errors.Wrap(err, "error decoding ssh certificate")
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
pub, err := ssh.ParsePublicKey(certData)
|
|
|
|
if err != nil {
|
2019-09-09 04:05:36 +00:00
|
|
|
return errors.Wrap(err, "error parsing ssh certificate")
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
cert, ok := pub.(*ssh.Certificate)
|
|
|
|
if !ok {
|
2019-09-09 04:05:36 +00:00
|
|
|
return errors.Errorf("error decoding ssh certificate: %T is not an *ssh.Certificate", pub)
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
c.Certificate = cert
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate validates the SignSSHRequest.
|
|
|
|
func (s *SignSSHRequest) Validate() error {
|
|
|
|
switch {
|
|
|
|
case s.CertType != "" && s.CertType != provisioner.SSHUserCert && s.CertType != provisioner.SSHHostCert:
|
|
|
|
return errors.Errorf("unknown certType %s", s.CertType)
|
|
|
|
case len(s.PublicKey) == 0:
|
|
|
|
return errors.New("missing or empty publicKey")
|
|
|
|
case len(s.OTT) == 0:
|
|
|
|
return errors.New("missing or empty ott")
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SignSSH is an HTTP handler that reads an SignSSHRequest with a one-time-token
|
|
|
|
// (ott) from the body and creates a new SSH certificate with the information in
|
|
|
|
// the request.
|
|
|
|
func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var body SignSSHRequest
|
|
|
|
if err := ReadJSON(r.Body, &body); err != nil {
|
|
|
|
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
logOtt(w, body.OTT)
|
|
|
|
if err := body.Validate(); err != nil {
|
2019-08-05 23:06:05 +00:00
|
|
|
WriteError(w, BadRequest(err))
|
2019-07-24 01:46:43 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-07-26 01:42:32 +00:00
|
|
|
publicKey, err := ssh.ParsePublicKey(body.PublicKey)
|
2019-07-24 01:46:43 +00:00
|
|
|
if err != nil {
|
2019-07-26 01:42:32 +00:00
|
|
|
WriteError(w, BadRequest(errors.Wrap(err, "error parsing publicKey")))
|
2019-07-24 01:46:43 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-08-03 00:48:34 +00:00
|
|
|
var addUserPublicKey ssh.PublicKey
|
|
|
|
if body.AddUserPublicKey != nil {
|
|
|
|
addUserPublicKey, err = ssh.ParsePublicKey(body.AddUserPublicKey)
|
|
|
|
if err != nil {
|
|
|
|
WriteError(w, BadRequest(errors.Wrap(err, "error parsing addUserPublicKey")))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
opts := provisioner.SSHOptions{
|
|
|
|
CertType: body.CertType,
|
|
|
|
Principals: body.Principals,
|
|
|
|
ValidBefore: body.ValidBefore,
|
|
|
|
ValidAfter: body.ValidAfter,
|
|
|
|
}
|
|
|
|
|
2019-07-29 19:52:13 +00:00
|
|
|
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignSSHMethod)
|
|
|
|
signOpts, err := h.Authority.Authorize(ctx, body.OTT)
|
2019-07-24 01:46:43 +00:00
|
|
|
if err != nil {
|
|
|
|
WriteError(w, Unauthorized(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
cert, err := h.Authority.SignSSH(publicKey, opts, signOpts...)
|
|
|
|
if err != nil {
|
|
|
|
WriteError(w, Forbidden(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-08-05 23:06:05 +00:00
|
|
|
var addUserCertificate *SSHCertificate
|
2019-08-03 00:48:34 +00:00
|
|
|
if addUserPublicKey != nil && cert.CertType == ssh.UserCert && len(cert.ValidPrincipals) == 1 {
|
2019-08-05 23:06:05 +00:00
|
|
|
addUserCert, err := h.Authority.SignSSHAddUser(addUserPublicKey, cert)
|
2019-08-03 00:48:34 +00:00
|
|
|
if err != nil {
|
|
|
|
WriteError(w, Forbidden(err))
|
|
|
|
return
|
|
|
|
}
|
2019-08-05 23:06:05 +00:00
|
|
|
addUserCertificate = &SSHCertificate{addUserCert}
|
2019-08-03 00:48:34 +00:00
|
|
|
}
|
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
JSON(w, &SignSSHResponse{
|
2019-08-03 00:48:34 +00:00
|
|
|
Certificate: SSHCertificate{cert},
|
2019-08-05 23:06:05 +00:00
|
|
|
AddUserCertificate: addUserCertificate,
|
2019-07-24 01:46:43 +00:00
|
|
|
})
|
|
|
|
}
|
2019-09-25 02:12:13 +00:00
|
|
|
|
|
|
|
// SSHConfig is an HTTP handler that returns the SSH public keys for user and
|
|
|
|
// host certificates.
|
|
|
|
func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) {
|
|
|
|
config, err := h.Authority.SSHConfig()
|
|
|
|
if err != nil {
|
|
|
|
WriteError(w, NotFound(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var host, user *SSHPublicKey
|
|
|
|
if config.HostKey != nil {
|
|
|
|
host = &SSHPublicKey{config.HostKey}
|
|
|
|
}
|
|
|
|
if config.UserKey != nil {
|
|
|
|
user = &SSHPublicKey{config.UserKey}
|
|
|
|
}
|
|
|
|
|
|
|
|
JSON(w, &SSHConfigResponse{
|
|
|
|
HostKey: host,
|
|
|
|
UserKey: user,
|
|
|
|
})
|
|
|
|
}
|