certificates/kms/sshagentkms/sshagentkms.go
Anton Lundin 3e6137110b Add support for using ssh-agent as a KMS
This adds a new KMS, SSHAgentKMS, which is a KMS to provide signing keys
for issuing ssh certificates signed by a key managed by a ssh-agent. It
uses the golang.org/x/crypto package to get a native Go implementation
to talk to a ssh-agent.

This was primarly written to be able to use gpg-agent to provide the
keys stored in a YubiKeys openpgp interface, but can be used for other
setups like proxying a ssh-agent over network.

That way the signing key for ssh certificates can be kept in a
"sign-only" hsm.

This code was written for my employer Intinor AB, but for simplicity
sake gifted to me to contribute upstream.

Signed-off-by: Anton Lundin <glance@acc.umu.se>
2020-11-04 09:06:23 +01:00

201 lines
4.8 KiB
Go

package sshagentkms
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"io"
"log"
"net"
"os"
"strings"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"github.com/pkg/errors"
"github.com/smallstep/certificates/kms/apiv1"
"go.step.sm/crypto/pemutil"
)
// SSHAgentKMS is a key manager that uses keys provided by ssh-agent
type SSHAgentKMS struct {
agentClient agent.Agent
}
// New returns a new SSHAgentKMS.
func New(ctx context.Context, opts apiv1.Options) (*SSHAgentKMS, error) {
socket := os.Getenv("SSH_AUTH_SOCK")
conn, err := net.Dial("unix", socket)
if err != nil {
log.Fatalf("Failed to open SSH_AUTH_SOCK: %v", err)
}
agentClient := agent.NewClient(conn)
return &SSHAgentKMS{
agentClient: agentClient,
}, nil
}
// For testing
func NewFromAgent(ctx context.Context, opts apiv1.Options, agentClient agent.Agent) (*SSHAgentKMS, error) {
return &SSHAgentKMS{
agentClient: agentClient,
}, nil
}
func init() {
apiv1.Register(apiv1.SSHAgentKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) {
return New(ctx, opts)
})
}
func (k *SSHAgentKMS) Close() error {
// TODO: Is there any cleanup in Agent we can do?
return nil
}
// Utility class to wrap a ssh.Signer as a crypto.Signer
type WrappedSSHSigner struct {
Sshsigner ssh.Signer
}
func (s *WrappedSSHSigner) Public() crypto.PublicKey {
return s.Sshsigner.PublicKey()
}
func (s *WrappedSSHSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
sig, err := s.Sshsigner.Sign(rand, digest)
if err != nil {
return nil, err
}
return sig.Blob, nil
}
func NewWrappedSignerFromSSHSigner(signer ssh.Signer) crypto.Signer {
return &WrappedSSHSigner{signer}
}
func (k *SSHAgentKMS) findKey(signingKey string) (target int, err error) {
if strings.HasPrefix(signingKey, "sshagentkms:") {
var key = strings.TrimPrefix(signingKey, "sshagentkms:")
l, err := k.agentClient.List()
if err != nil {
return -1, err
}
for i, s := range l {
if s.Comment == key {
return i, nil
}
}
}
return -1, errors.Errorf("SSHAgentKMS couldn't find %s", signingKey)
}
// CreateSigner returns a new signer configured with the given signing key.
func (k *SSHAgentKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) {
if req.Signer != nil {
return req.Signer, nil
}
if strings.HasPrefix(req.SigningKey, "sshagentkms:") {
target, err := k.findKey(req.SigningKey)
if err != nil {
return nil, err
}
s, err := k.agentClient.Signers()
if err != nil {
return nil, err
}
return NewWrappedSignerFromSSHSigner(s[target]), nil
}
// OK: We don't actually care about non-ssh certificates,
// but we can't disable it in step-ca so this code is copy-pasted from
// softkms just to keep step-ca happy.
var opts []pemutil.Options
if req.Password != nil {
opts = append(opts, pemutil.WithPassword(req.Password))
}
switch {
case len(req.SigningKeyPEM) != 0:
v, err := pemutil.ParseKey(req.SigningKeyPEM, opts...)
if err != nil {
return nil, err
}
sig, ok := v.(crypto.Signer)
if !ok {
return nil, errors.New("signingKeyPEM is not a crypto.Signer")
}
return sig, nil
case req.SigningKey != "":
v, err := pemutil.Read(req.SigningKey, opts...)
if err != nil {
return nil, err
}
sig, ok := v.(crypto.Signer)
if !ok {
return nil, errors.New("signingKey is not a crypto.Signer")
}
return sig, nil
default:
return nil, errors.New("failed to load softKMS: please define signingKeyPEM or signingKey")
}
}
// CreateKey generates a new key and returns both public and private key.
func (k *SSHAgentKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) {
return nil, errors.Errorf("SSHAgentKMS doesn't support generating keys")
}
// GetPublicKey returns the public key from the file passed in the request name.
func (k *SSHAgentKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) {
var v crypto.PublicKey
if strings.HasPrefix(req.Name, "sshagentkms:") {
target, err := k.findKey(req.Name)
if err != nil {
return nil, err
}
s, err := k.agentClient.Signers()
if err != nil {
return nil, err
}
sshPub := s[target].PublicKey()
sshPubBytes := sshPub.Marshal()
parsed, err := ssh.ParsePublicKey(sshPubBytes)
if err != nil {
return nil, err
}
parsedCryptoKey := parsed.(ssh.CryptoPublicKey)
// Then, we can call CryptoPublicKey() to get the actual crypto.PublicKey
v = parsedCryptoKey.CryptoPublicKey()
} else {
var err error
v, err = pemutil.Read(req.Name)
if err != nil {
return nil, err
}
}
switch vv := v.(type) {
case *x509.Certificate:
return vv.PublicKey, nil
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:
return vv, nil
default:
return nil, errors.Errorf("unsupported public key type %T", v)
}
}