From dc6ffb7670cec32cfb5b1a75c1810ee80c9b0a7b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 24 Sep 2019 19:12:13 -0700 Subject: [PATCH] Add initial implementation of ssh config. --- api/ssh.go | 47 ++++++++++++++++++++++++++++++++++++++++++ authority/authority.go | 17 +++++++++++---- authority/ssh.go | 47 ++++++++++++++++++++++++------------------ 3 files changed, 87 insertions(+), 24 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index 7bcae7cf..456f239a 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "golang.org/x/crypto/ssh" ) @@ -15,6 +16,7 @@ import ( 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) + SSHConfig() (*authority.SSHConfiguration, error) } // SignSSHRequest is the request body of an SSH certificate request. @@ -34,6 +36,13 @@ type SignSSHResponse struct { AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"` } +// 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"` +} + // SSHCertificate represents the response SSH certificate. type SSHCertificate struct { *ssh.Certificate `json:"omitempty"` @@ -49,6 +58,21 @@ func (c SSHCertificate) MarshalJSON() ([]byte, error) { return []byte(`"` + s + `"`), nil } +// 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 +} + // UnmarshalJSON implements the json.Unmarshaler interface. The certificate is // expected to be a quoted, base64 encoded, openssh wire formatted block of bytes. func (c *SSHCertificate) UnmarshalJSON(data []byte) error { @@ -157,3 +181,26 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) { AddUserCertificate: addUserCertificate, }) } + +// 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, + }) +} diff --git a/authority/authority.go b/authority/authority.go index 03fd1a99..1ddcf13c 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -13,6 +13,7 @@ import ( "github.com/smallstep/certificates/db" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/x509util" + "golang.org/x/crypto/ssh" ) const ( @@ -24,8 +25,8 @@ type Authority struct { config *Config rootX509Certs []*x509.Certificate intermediateIdentity *x509util.Identity - sshCAUserCertSignKey crypto.Signer - sshCAHostCertSignKey crypto.Signer + sshCAUserCertSignKey ssh.Signer + sshCAHostCertSignKey ssh.Signer certificates *sync.Map startTime time.Time provisioners *provisioner.Collection @@ -125,16 +126,24 @@ func (a *Authority) init() error { // Decrypt and load SSH keys if a.config.SSH != nil { if a.config.SSH.HostKey != "" { - a.sshCAHostCertSignKey, err = parseCryptoSigner(a.config.SSH.HostKey, a.config.Password) + signer, err := parseCryptoSigner(a.config.SSH.HostKey, a.config.Password) if err != nil { return err } + a.sshCAHostCertSignKey, err = ssh.NewSignerFromSigner(signer) + if err != nil { + return errors.Wrap(err, "error creating ssh signer") + } } if a.config.SSH.UserKey != "" { - a.sshCAUserCertSignKey, err = parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) + signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) if err != nil { return err } + a.sshCAUserCertSignKey, err = ssh.NewSignerFromSigner(signer) + if err != nil { + return errors.Wrap(err, "error creating ssh signer") + } } } diff --git a/authority/ssh.go b/authority/ssh.go index 2f69b3ca..dc7ebe0c 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -24,6 +24,30 @@ const ( SSHAddUserCommand = "sudo useradd -m ; nc -q0 localhost 22" ) +// SSHConfiguration is the return type for SSHConfig. +type SSHConfiguration struct { + UserKey ssh.PublicKey + HostKey ssh.PublicKey +} + +// SSHConfig returns the SSH User and Host public keys. +func (a *Authority) SSHConfig() (*SSHConfiguration, error) { + var config SSHConfiguration + if a.sshCAUserCertSignKey != nil { + config.UserKey = a.sshCAUserCertSignKey.PublicKey() + } + if a.sshCAHostCertSignKey != nil { + config.HostKey = a.sshCAHostCertSignKey.PublicKey() + } + if config.UserKey == nil && config.HostKey == nil { + return nil, &apiError{ + err: errors.New("sshConfig: ssh is not configured"), + code: http.StatusNotFound, + } + } + return &config, nil +} + // SignSSH creates a signed SSH certificate with the given public key and options. func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { var mods []provisioner.SSHCertificateModifier @@ -95,12 +119,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign code: http.StatusNotImplemented, } } - if signer, err = ssh.NewSignerFromSigner(a.sshCAUserCertSignKey); err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "signSSH: error creating signer"), - code: http.StatusInternalServerError, - } - } + signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { return nil, &apiError{ @@ -108,12 +127,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign code: http.StatusNotImplemented, } } - if signer, err = ssh.NewSignerFromSigner(a.sshCAHostCertSignKey); err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "signSSH: error creating signer"), - code: http.StatusInternalServerError, - } - } + signer = a.sshCAHostCertSignKey default: return nil, &apiError{ err: errors.Errorf("signSSH: unexpected ssh certificate type: %d", cert.CertType), @@ -180,14 +194,7 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) } } - signer, err := ssh.NewSignerFromSigner(a.sshCAUserCertSignKey) - if err != nil { - return nil, &apiError{ - err: errors.Wrap(err, "signSSHProxy: error creating signer"), - code: http.StatusInternalServerError, - } - } - + signer := a.sshCAUserCertSignKey principal := subject.ValidPrincipals[0] addUserPrincipal := a.getAddUserPrincipal()