Proposal Details
This is the initial proposal for a new API series, marked with the V2 suffix, intended to replace the existing API once the x/crypto/ssh
package is moved to the standard library. This approach allows users to begin testing and familiarizing themselves with the new API ahead of the migration.
The Signer
API has evolved significantly, and there are currently three distinct interfaces for signers:
- Signer
- AlgorithmSigner
- MultiAlgorithmSigner
AlgorithmSigner
and MultiAlgorithmSigner
were introduced to accommodate different signature algorithms supported by RSA keys.
In V2, we propose consolidating these into a single Signer
interface that allows the user to select the signature algorithm.
We also have:
- NewSignerFromKey(key interface{}) (Signer, error)
- NewSignerFromSigner(signer crypto.Signer) (Signer, error)
- NewCertSigner(cert *Certificate, signer Signer) (Signer, error)
- NewSignerWithAlgorithms(signer AlgorithmSigner, algorithms []string) (MultiAlgorithmSigner, error)
NewSignerFromSigner
accepts a crypto.Signer
, while NewSignerFromKey
accepts a generic interface to support both crypto.Signer
and *dsa.PrivateKey
. However DSA keys have beed deprecated and removed from OpenSSH since version 10.0 so we can remove DSA support and this duplication.
All our internal signer implementations already support the MultiAlgorithmSigner interface (or can easily support it).
Additionally we have:
- ParsePrivateKey(pemBytes []byte) (Signer, error)
- ParsePrivateKeyWithPassphrase(pemBytes, passphrase []byte) (Signer, error)
- ParseRawPrivateKey(pemBytes []byte) (interface{}, error)
- ParseRawPrivateKeyWithPassphrase(pemBytes, passphrase []byte) (interface{}, error)
where ParsePrivateKey
calls ParseRawPrivateKey
followed from NewSignerFromKey
.
We also have:
- MarshalPrivateKey(key crypto.PrivateKey, comment string) (*pem.Block, error)
crypto.PrivateKey
is an empty interface.
I propose introducing a SignerV2
interface that allows restricting the allowed algorithms and selecting specific signing algorithms:
type SignerV2 interface {
// PublicKey returns the associated PublicKey.
PublicKey() PublicKey
// Sign returns a signature for the given data. This method will hash the
// data appropriately first. The signature algorithm is expected to match
// the key format returned by the PublicKey.Type method (and not to be any
// alternative algorithm supported by the key format).
Sign(rand io.Reader, data []byte) (*Signature, error)
// SignWithAlgorithm is like Signer.Sign, but allows specifying a desired
// signing algorithm. Callers may pass an empty string for the algorithm in
// which case the AlgorithmSigner will use a default algorithm. This default
// doesn't currently control any behavior in this package.
SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*Signature, error)
// Algorithms returns the available algorithms in preference order. The list
// must not be empty, and it must not include certificate types.
Algorithms() []string
}
SignerV2 will be created from a crypto.Signer
, we'll drop DSA support:
// NewSignerV2 wraps any crypto.Signer implementation and returns a
// corresponding Signer interface. This is useful, for example, when working
// with keys stored in hardware security modules (HSMs). For RSA signers, SHA-1
// algorithms are excluded by default. If you need to specify which algorithms
// to use, consider using [NewSignerV2WithAlgorithms] instead.
func NewSignerV2(signer crypto.Signer) (SignerV2, error)
// NewSignerV2WithAlgorithms takes any crypto.Signer implementation and returns
// a corresponding Signer interface restricted to the specified algorithms.
func NewSignerV2WithAlgorithms(signer crypto.Signer, algorithms []string) (SignerV2, error)
// NewCertificateSignerV2 returns a SignerV2 that signs with the given
// Certificate, whose private key is held by signer. It returns an error if the
// public key in cert doesn't match the key used by signer.
func NewCertificateSignerV2(cert *Certificate, signer SignerV2) (SignerV2, error)
The following API will be added to parse a signer and marshal a signer in PEM format:
// MarshalPrivateKeyOptionsV2 defines the available options to Marshal a private
// key in OpenSSH format.
type MarshalPrivateKeyOptionsV2 struct {
Comment string
// If set the key will be encrypted.
Passphrase string
// Defines the number of rounds for key derivation. The default value is 24.
// Increasing the number of rounds enhances security but also slows down key
// derivation.
SaltRounds int
}
// MarshalPrivateKeyV2 returns a PEM block with the private key serialized in the
// OpenSSH format.
func MarshalPrivateKeyV2(key crypto.Signer, options *MarshalPrivateKeyOptionsV2) (*pem.Block, error)
// ParsePrivateKeyOptionsV2 defines the available options to Parse a PEM encoded
// private key .
type ParsePrivateKeyOptionsV2 struct {
// If set the key will be encrypted.
Passphrase string
}
// ParsePrivateKeyV2 returns a crypto.Signer from a PEM encoded private key.
func ParsePrivateKeyV2(pemBytes []byte, options *ParsePrivateKeyOptionsV2) (crypto.Signer, error)
When parsing an encrypted private key, we will no longer support the legacy PEM encryption specified in RFC 1423, and as a result, we will no longer use the deprecated x509.DecryptPEMBlock
method.
This approach provides one API for parsing and another for marshaling keys, with crypto.Signer
used consistently.
Unlike before, we no longer provide a utility API that returns a SignerV2 directly from PEM bytes. Should we add one?
Comment From: gabyhelp
Related Issues
- proposal: x/crypto/ssh: add SSHSIG support #68197
- x/crypto/ssh: publicKeyCallback cannot handshake using ssh-rsa keys signed using the ssh-rsa-sha2-256 algorithm #39885 (closed)
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)