You've already forked step-ca-cli
mirror of
https://github.com/smallstep/cli.git
synced 2026-10-24 11:19:19 +03:00
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.
272 lines
6.7 KiB
Go
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")
|
|
}
|