Skip to content

proposal: x/crypto/ssh: refactor signers API #74424

@drakkan

Description

@drakkan

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.

Current implementation

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.

Proposal Details

I propose introducing a SignerV2 interface that allows restricting the allowed algorithms and selecting specific signing algorithms:

// A SignerV2 can create signatures that verify against a public key.
type SignerV2 interface {
	// PublicKey returns the associated PublicKey.
	PublicKey() PublicKey

	// Sign generates a signature for the given data. It behaves like
	// SignContext but uses context.Background and an empty algorithm. This
	// method is provided for backward compatibility with the Signer interface.
	Sign(rand io.Reader, data []byte) (*Signature, error)

	// SignContext generates a signature for the given data, allowing to set a
	// context and a desired signing algorithm. If an empty string is provided
	// for the algorithm, the Signer will use a default algorithm. Note that the
	// default algorithm does not currently influence behavior in this package.
	SignContext(ctx context.Context, 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

	// Signer returns the underlying [crypto.Signer] used for signing
	// operations, if available. Signer may return an error wrapping
	// [errors.ErrUnsupported]. Otherwise, Signer must always return a nil
	// error.
	Signer() (crypto.Signer, error)
}

SignerV2 will be created from a crypto.Signer or from a pem.Block, we'll drop DSA support and legacy PEM encryption.

// 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 wraps an existing SignerV2 implementation and
// returns a new SignerV2 that is restricted to the specified signature
// algorithms.
func NewSignerV2WithAlgorithms(signer SignerV2, 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 APIs will be added to parse a SignerV2 from PEM format and to marshal a SignerV2 into PEM format

// MarshalPrivateKeyV2Options defines the available options to Marshal a private
// key in OpenSSH format.
type MarshalPrivateKeyV2Options 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 SignerV2, options *MarshalPrivateKeyV2Options) (*pem.Block, error) 

// ParsePrivateKeyV2Options defines the available options to Parse a PEM encoded
// private key.
type ParsePrivateKeyV2Options struct {
	// If set the key will be encrypted.
	Passphrase string

       // SignatureAlgorithms is a list of supported signature algorithms of which
	// the return of SignerV2 algorithms will be a subset. If nil a list of safe
	// defaults will be used. This list may change over time.
	SignatureAlgorithms []string
}

// ParsePrivateKeyV2 parses a PEM-encoded private key and returns a SignerV2.
// The key must implement the [crypto.Signer] interface and be one of the
// following types: *rsa.PrivateKey, ed25519.PrivateKey, or *ecdsa.PrivateKey.
// For encrypted private keys, the OpenSSH format is currently supported.
func ParsePrivateKeyV2(pemBytes []byte, options *ParsePrivateKeyV2Options) (SignerV2, 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.

SignerV2 is compatible with the existing Signer interface. Internally, the package uses a wrapper that prioritizes SignContext, falling back to SignWithAlgorithm, and, if necessary, Sign.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    Status

    Active

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions