From e84489775ba1cc9058ff01c2c3b5407faafb968a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 8 Oct 2019 18:09:41 -0700 Subject: [PATCH] Add support for multiple ssh roots. Fixes #125 --- api/api.go | 1 + api/ssh.go | 50 ++++++++++++++++++------ authority/authority.go | 48 ++++++++++++++++++----- authority/config.go | 13 +++---- authority/ssh.go | 86 ++++++++++++++++++++++++++++++++++-------- ca/client.go | 20 +++++++++- 6 files changed, 173 insertions(+), 45 deletions(-) diff --git a/api/api.go b/api/api.go index 6029557c..47cc118f 100644 --- a/api/api.go +++ b/api/api.go @@ -253,6 +253,7 @@ func (h *caHandler) Route(r Router) { // SSH CA r.MethodFunc("POST", "/ssh/sign", h.SignSSH) r.MethodFunc("GET", "/ssh/keys", h.SSHKeys) + r.MethodFunc("GET", "/ssh/federation", h.SSHFederatedKeys) r.MethodFunc("POST", "/ssh/config", h.SSHConfig) r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) diff --git a/api/ssh.go b/api/ssh.go index f8142356..8d0b421d 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -18,6 +18,7 @@ type SSHAuthority interface { SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) GetSSHKeys() (*authority.SSHKeys, error) + GetSSHFederatedKeys() (*authority.SSHKeys, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) } @@ -55,8 +56,8 @@ type SignSSHResponse struct { // SSHKeysResponse represents the response object that returns the SSH user and // host keys. type SSHKeysResponse struct { - UserKey *SSHPublicKey `json:"userKey,omitempty"` - HostKey *SSHPublicKey `json:"hostKey,omitempty"` + UserKeys []SSHPublicKey `json:"userKey,omitempty"` + HostKeys []SSHPublicKey `json:"hostKey,omitempty"` } // SSHCertificate represents the response SSH certificate. @@ -241,23 +242,50 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) { // certificates. func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) { keys, err := h.Authority.GetSSHKeys() + if err != nil { + WriteError(w, InternalServerError(err)) + return + } + + if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 { + WriteError(w, NotFound(errors.New("no keys found"))) + return + } + + resp := new(SSHKeysResponse) + for _, k := range keys.HostKeys { + resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k}) + } + for _, k := range keys.UserKeys { + resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k}) + } + + JSON(w, resp) +} + +// SSHFederatedKeys is an HTTP handler that returns the federated SSH public +// keys for user and host certificates. +func (h *caHandler) SSHFederatedKeys(w http.ResponseWriter, r *http.Request) { + keys, err := h.Authority.GetSSHFederatedKeys() if err != nil { WriteError(w, NotFound(err)) return } - var host, user *SSHPublicKey - if keys.HostKey != nil { - host = &SSHPublicKey{PublicKey: keys.HostKey} + if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 { + WriteError(w, NotFound(errors.New("no keys found"))) + return + } + + resp := new(SSHKeysResponse) + for _, k := range keys.HostKeys { + resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k}) } - if keys.UserKey != nil { - user = &SSHPublicKey{PublicKey: keys.UserKey} + for _, k := range keys.UserKeys { + resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k}) } - JSON(w, &SSHKeysResponse{ - HostKey: host, - UserKey: user, - }) + JSON(w, resp) } // SSHConfig is an HTTP handler that returns rendered templates for ssh clients diff --git a/authority/authority.go b/authority/authority.go index b3f5fb94..88c829c6 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -24,15 +24,19 @@ const ( // Authority implements the Certificate Authority internal interface. type Authority struct { - config *Config - rootX509Certs []*x509.Certificate - intermediateIdentity *x509util.Identity - sshCAUserCertSignKey ssh.Signer - sshCAHostCertSignKey ssh.Signer - certificates *sync.Map - startTime time.Time - provisioners *provisioner.Collection - db db.AuthDB + config *Config + rootX509Certs []*x509.Certificate + intermediateIdentity *x509util.Identity + sshCAUserCertSignKey ssh.Signer + sshCAHostCertSignKey ssh.Signer + sshCAUserCerts []ssh.PublicKey + sshCAHostCerts []ssh.PublicKey + sshCAUserFederatedCerts []ssh.PublicKey + sshCAHostFederatedCerts []ssh.PublicKey + certificates *sync.Map + startTime time.Time + provisioners *provisioner.Collection + db db.AuthDB // Do not re-initialize initOnce bool } @@ -136,6 +140,9 @@ func (a *Authority) init() error { if err != nil { return errors.Wrap(err, "error creating ssh signer") } + // Append public key to list of host certs + a.sshCAHostCerts = append(a.sshCAHostCerts, a.sshCAHostCertSignKey.PublicKey()) + a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, a.sshCAHostCertSignKey.PublicKey()) } if a.config.SSH.UserKey != "" { signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) @@ -146,6 +153,29 @@ func (a *Authority) init() error { if err != nil { return errors.Wrap(err, "error creating ssh signer") } + // Append public key to list of user certs + a.sshCAUserCerts = append(a.sshCAUserCerts, a.sshCAHostCertSignKey.PublicKey()) + a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, a.sshCAUserCertSignKey.PublicKey()) + } + + // Append other public keys + for _, key := range a.config.SSH.Keys { + switch key.Type { + case provisioner.SSHHostCert: + if key.Federated { + a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, key.PublicKey()) + } else { + a.sshCAHostCerts = append(a.sshCAHostCerts, key.PublicKey()) + } + case provisioner.SSHUserCert: + if key.Federated { + a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, key.PublicKey()) + } else { + a.sshCAUserCerts = append(a.sshCAUserCerts, key.PublicKey()) + } + default: + return errors.Errorf("unsupported type %s", key.Type) + } } } diff --git a/authority/config.go b/authority/config.go index a53507dd..30343b5f 100644 --- a/authority/config.go +++ b/authority/config.go @@ -104,14 +104,6 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error { return nil } -// SSHConfig contains the user and host keys. -type SSHConfig struct { - HostKey string `json:"hostKey"` - UserKey string `json:"userKey"` - AddUserPrincipal string `json:"addUserPrincipal"` - AddUserCommand string `json:"addUserCommand"` -} - // LoadConfiguration parses the given filename in JSON format and returns the // configuration struct. func LoadConfiguration(filename string) (*Config, error) { @@ -184,6 +176,11 @@ func (c *Config) Validate() error { c.TLS.Renegotiation = c.TLS.Renegotiation || DefaultTLSOptions.Renegotiation } + // Validate ssh: nil is ok + if err := c.SSH.Validate(); err != nil { + return err + } + // Validate templates: nil is ok if err := c.Templates.Validate(); err != nil { return err diff --git a/authority/ssh.go b/authority/ssh.go index 22cdd0f4..33f00cec 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -10,6 +10,7 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/templates" "github.com/smallstep/cli/crypto/randutil" + "github.com/smallstep/cli/jose" "golang.org/x/crypto/ssh" ) @@ -25,28 +26,81 @@ const ( SSHAddUserCommand = "sudo useradd -m ; nc -q0 localhost 22" ) +// SSHConfig contains the user and host keys. +type SSHConfig struct { + HostKey string `json:"hostKey"` + UserKey string `json:"userKey"` + Keys []*SSHPublicKey `json:"keys,omitempty"` + AddUserPrincipal string `json:"addUserPrincipal"` + AddUserCommand string `json:"addUserCommand"` +} + +// Validate checks the fields in SSHConfig. +func (c *SSHConfig) Validate() error { + if c == nil { + return nil + } + for _, k := range c.Keys { + if err := k.Validate(); err != nil { + return err + } + } + return nil +} + +// SSHPublicKey contains a public key used by federated CAs to keep old signing +// keys for this ca. +type SSHPublicKey struct { + Type string `json:"type"` + Federated bool `json:"federated"` + Key jose.JSONWebKey `json:"key"` + publicKey ssh.PublicKey +} + +// Validate checks the fields in SSHPublicKey. +func (k *SSHPublicKey) Validate() error { + switch { + case k.Type == "": + return errors.New("type cannot be empty") + case k.Type != provisioner.SSHHostCert && k.Type != provisioner.SSHUserCert: + return errors.Errorf("invalid type %s, it must be user or host", k.Type) + case !k.Key.IsPublic(): + return errors.New("invalid key type, it must be a public key") + } + + key, err := ssh.NewPublicKey(k.Key.Key) + if err != nil { + return errors.Wrap(err, "error creating ssh key") + } + k.publicKey = key + return nil +} + +// PublicKey returns the ssh public key. +func (k *SSHPublicKey) PublicKey() ssh.PublicKey { + return k.publicKey +} + // SSHKeys represents the SSH User and Host public keys. type SSHKeys struct { - UserKey ssh.PublicKey - HostKey ssh.PublicKey + UserKeys []ssh.PublicKey + HostKeys []ssh.PublicKey } // GetSSHKeys returns the SSH User and Host public keys. func (a *Authority) GetSSHKeys() (*SSHKeys, error) { - var keys SSHKeys - if a.sshCAUserCertSignKey != nil { - keys.UserKey = a.sshCAUserCertSignKey.PublicKey() - } - if a.sshCAHostCertSignKey != nil { - keys.HostKey = a.sshCAHostCertSignKey.PublicKey() - } - if keys.UserKey == nil && keys.HostKey == nil { - return nil, &apiError{ - err: errors.New("getSSHKeys: ssh is not configured"), - code: http.StatusNotFound, - } - } - return &keys, nil + return &SSHKeys{ + HostKeys: a.sshCAHostCerts, + UserKeys: a.sshCAUserCerts, + }, nil +} + +// GetSSHFederatedKeys returns the public keys for federated SSH signers. +func (a *Authority) GetSSHFederatedKeys() (*SSHKeys, error) { + return &SSHKeys{ + HostKeys: a.sshCAHostFederatedCerts, + UserKeys: a.sshCAUserFederatedCerts, + }, nil } // GetSSHConfig returns rendered templates for clients (user) or servers (host). diff --git a/ca/client.go b/ca/client.go index bbce5ee8..45e1e7ce 100644 --- a/ca/client.go +++ b/ca/client.go @@ -527,7 +527,7 @@ func (c *Client) Federation() (*api.FederationResponse, error) { return &federation, nil } -// SSHKeys performs the get ssh keys request to the CA and returns the +// SSHKeys performs the get /ssh/keys request to the CA and returns the // api.SSHKeysResponse struct. func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/keys"}) @@ -545,6 +545,24 @@ func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { return &keys, nil } +// SSHFederation performs the get /ssh/federation request to the CA and returns +// the api.SSHKeysResponse struct. +func (c *Client) SSHFederation() (*api.SSHKeysResponse, error) { + u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/federation"}) + resp, err := c.client.Get(u.String()) + if err != nil { + return nil, errors.Wrapf(err, "client GET %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var keys api.SSHKeysResponse + if err := readJSON(resp.Body, &keys); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &keys, nil +} + // SSHConfig performs the POST request to the CA to get the ssh configuration // templates. func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {