1
0
mirror of https://github.com/smallstep/cli.git synced 2026-10-24 11:19:19 +03:00
Files
step-ca-cli/internal/sshutil/agent.go
Mariano Cano b27a6d3790 Support for ML-DSA keys
This commit adds support for ML-DSA keys on ca, certificate, and key
commands. SSH and JOSE commands doesn't yet support ML-DSA keys.
2026-09-04 15:48:01 -07:00

272 lines
6.7 KiB
Go

package sshutil
import (
"bytes"
"net"
"runtime"
"time"
"github.com/pkg/errors"
"github.com/smallstep/cli/internal/cast"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
type options struct {
filterBySignatureKey func(*agent.Key) bool
removeExpiredKey func(*Agent, *agent.Key) bool
}
func newOptions(opts []AgentOption) *options {
o := new(options)
for _, fn := range opts {
fn(o)
}
return o
}
// AgentOption is the type used for variadic options in Agent methods.
type AgentOption func(o *options)
// WithSignatureKey filters certificate not signed by the given signing keys.
func WithSignatureKey(keys []ssh.PublicKey) AgentOption {
signingKeys := make([][]byte, len(keys))
for i, k := range keys {
signingKeys[i] = k.Marshal()
}
return func(o *options) {
o.filterBySignatureKey = func(k *agent.Key) bool {
cert, err := ParseCertificate(k.Marshal())
if err != nil {
return false
}
b := cert.SignatureKey.Marshal()
for _, sb := range signingKeys {
if bytes.Equal(b, sb) {
return true
}
}
return false
}
}
}
// WithCertsOnly filters only those keys accompanied by a certificate.
func WithCertsOnly() AgentOption {
return func(o *options) {
o.filterBySignatureKey = func(k *agent.Key) bool {
_, err := ParseCertificate(k.Marshal())
return err == nil
}
}
}
// WithRemoveExpiredCerts will remove the expired certificates automatically.
func WithRemoveExpiredCerts(t time.Time) AgentOption {
unixNow := t.Unix()
return func(o *options) {
o.removeExpiredKey = func(a *Agent, k *agent.Key) bool {
if cert, err := ParseCertificate(k.Marshal()); err == nil {
if before := cast.Int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) {
if err := a.Remove(k); err == nil {
return true
}
}
}
return false
}
}
}
// ErrNotFound is the error returned if a something is not found.
var ErrNotFound = errors.New("not found")
// Agent represents a client to an ssh.Agent.
type Agent struct {
agent.ExtendedAgent
Conn net.Conn
}
// DialAgent returns an ssh.Agent client. It uses the SSH_AUTH_SOCK to connect
// to the agent.
func DialAgent() (*Agent, error) {
return dialAgent()
}
// Close closes the connection to the agent.
func (a *Agent) Close() error {
return a.Conn.Close()
}
// AuthMethod returns the ssh.Agent as an ssh.AuthMethod.
func (a *Agent) AuthMethod() ssh.AuthMethod {
return ssh.PublicKeysCallback(a.Signers)
}
// HasKeys returns if a key filtered with the given options exists.
func (a *Agent) HasKeys(opts ...AgentOption) (bool, error) {
o := newOptions(opts)
keys, err := a.List()
if err != nil {
return false, errors.Wrap(err, "error listing keys")
}
for _, key := range keys {
if o.removeExpiredKey != nil && o.removeExpiredKey(a, key) {
continue
}
if o.filterBySignatureKey == nil || o.filterBySignatureKey(key) {
return true, nil
}
}
return false, nil
}
// ListKeys returns the list of keys in the agent.
func (a *Agent) ListKeys(opts ...AgentOption) ([]*agent.Key, error) {
o := newOptions(opts)
keys, err := a.List()
if err != nil {
return nil, errors.Wrap(err, "error listing keys")
}
var list []*agent.Key
for _, key := range keys {
if o.removeExpiredKey != nil && o.removeExpiredKey(a, key) {
continue
}
if o.filterBySignatureKey == nil || o.filterBySignatureKey(key) {
list = append(list, key)
}
}
return list, nil
}
// ListCertificates returns the list of certificates in the agent.
func (a *Agent) ListCertificates(opts ...AgentOption) ([]*ssh.Certificate, error) {
keys, err := a.ListKeys(opts...)
if err != nil {
return nil, err
}
var list []*ssh.Certificate
for _, key := range keys {
if cert, err := ParseCertificate(key.Marshal()); err == nil {
list = append(list, cert)
}
}
return list, nil
}
// GetKey retrieves a key from the agent by the given comment.
func (a *Agent) GetKey(comment string, opts ...AgentOption) (*agent.Key, error) {
o := newOptions(opts)
keys, err := a.List()
if err != nil {
return nil, errors.Wrap(err, "error listing keys")
}
for _, key := range keys {
if key.Comment == comment {
if o.removeExpiredKey != nil && o.removeExpiredKey(a, key) {
continue
}
if o.filterBySignatureKey == nil || o.filterBySignatureKey(key) {
return key, nil
}
}
}
return nil, ErrNotFound
}
// GetSigner returns a signer that has a key with the given comment.
func (a *Agent) GetSigner(comment string, opts ...AgentOption) (ssh.Signer, error) {
key, err := a.GetKey(comment, opts...)
if err != nil {
return nil, err
}
signers, err := a.Signers()
if err != nil {
return nil, errors.Wrap(err, "error listing signers")
}
keyBytes := key.Marshal()
for _, sig := range signers {
if bytes.Equal(keyBytes, sig.PublicKey().Marshal()) {
return sig, nil
}
}
return nil, ErrNotFound
}
// RemoveKeys removes the keys with the given comment from the agent.
func (a *Agent) RemoveKeys(comment string, opts ...AgentOption) (bool, error) {
o := newOptions(opts)
keys, err := a.List()
if err != nil {
return false, errors.Wrap(err, "error listing keys")
}
var removed bool
for _, key := range keys {
if key.Comment == comment {
if o.filterBySignatureKey == nil || o.filterBySignatureKey(key) {
if err := a.Remove(key); err != nil {
return false, errors.Wrap(err, "error removing key")
}
removed = true
}
}
}
return removed, nil
}
// RemoveAllKeys removes from the agent all the keys matching the given options.
func (a *Agent) RemoveAllKeys(opts ...AgentOption) (bool, error) {
o := newOptions(opts)
keys, err := a.List()
if err != nil {
return false, errors.Wrap(err, "error listing keys")
}
var removed bool
for _, key := range keys {
if o.filterBySignatureKey == nil || o.filterBySignatureKey(key) {
if err := a.Remove(key); err != nil {
return false, errors.Wrap(err, "error removing key")
}
removed = true
}
}
return removed, nil
}
// AddCertificate adds the given certificate to the agent.
func (a *Agent) AddCertificate(subject string, cert *ssh.Certificate, priv any) error {
var (
lifetime uint64
now = cast.Uint64(time.Now().Unix())
)
switch {
case cert.ValidBefore == ssh.CertTimeInfinity:
// 0 indicates that the certificate should never expire from the agent.
lifetime = 0
case cert.ValidBefore <= now:
return errors.New("error adding certificate to ssh agent - certificate is already expired")
default:
lifetime = cert.ValidBefore - now
}
// Windows SSH agent fails with a lifetime
if runtime.GOOS == "windows" {
lifetime = 0
}
return errors.Wrap(a.Add(agent.AddedKey{
PrivateKey: priv,
Certificate: cert,
Comment: subject,
LifetimeSecs: cast.Uint32(lifetime),
}), "error adding key to agent")
}