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.

Comment From: gabyhelp

Related Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: gopherbot

Change https://go.dev/cl/685678 mentions this issue: crypto/ssh: refactor signers API

Comment From: meling

Why not use a single Sign method and NewSignerV2 function that accepts a variadic optional argument for the algorithm portion?

Comment From: drakkan

Why not use a single Sign method and NewSignerV2 function that accepts a variadic optional argument for the algorithm portion?

Thank you for the comment.

I made some changes to the proposal: NewSignerV2 now creates a SignerV2 from a crypto.Signer, while NewSignerV2WithAlgorithms allows restricting a SignerV2 to the specified algorithms.

I believe this results in a more consistent API design.

Comment From: imirkin

A couple of questions, potentially borne out of confusion:

  1. I have some legacy PEM-encrypted SSH private keys. If support for them is gone from the new helpers, how will I be able to use them?
  2. In some contexts, I will need to allow "ssh-rsa" signing algorithm with SHA-1. But I don't necessarily know what kind of key I have - it's just an opaque key in the OpenSSH (encrypted) format, and if it's RSA, then I need ssh-rsa, but if not then not. Will ParsePrivateKeyV2 return a SignerV2 which supports this? If not, how do I request support for it? What happens if I request support for it but pass in an Ed22519 key?
  3. Your comments in NewSignerV2 suggest to use NewSignerV2WithAlgorithms if you want SHA-1, but the former takes a crypto.Signer while the latter wants a SignerV2. How will that work?

FWIW currently I have a MultiAlgorithmSigner wrapper which removes ssh-rsa from the wrapped signer, so that allows it to stay generic -- if it wasn't supported already, no harm done.

Comment From: meling

marked with the V2 suffix, intended to replace the existing API once the x/crypto/ssh package is moved to the standard library.

Why does this need a V2 suffix if it is being moved to the standard library?

Comment From: meling

Why not use a single Sign method and NewSignerV2 function that accepts a variadic optional argument for the algorithm portion?

Thank you for the comment.

I made some changes to the proposal: NewSignerV2 now creates a SignerV2 from a crypto.Signer, while NewSignerV2WithAlgorithms allows restricting a SignerV2 to the specified algorithms.

I believe this results in a more consistent API design.

I was thinking more along the lines of something like this:

type SigningOptions struct {
    // Algorithm specifies the desired signing algorithm. If empty, the
    // Signer will use a default algorithm.
    Algorithm string
}

func WithAlgorithm(algorithm string) SigningOptions 

// A SignerV2 can create signatures that verify against a public key.
type SignerV2 interface {
    PublicKey() PublicKey
    Sign(rand io.Reader, data []byte, opts ...SigningOptions) (*Signature, error)
    Algorithms() []string
    PrivateKey() (crypto.Signer, error)
}

And:

func NewSignerV2(signer crypto.Signer, opts ...SigningOptions) (SignerV2, error)
func NewCertificateSignerV2(cert *Certificate, signer SignerV2) (SignerV2, error)

Maybe I haven't thought enough about the implications here, but the idea is to make it flexible for future options, if any. At the same time, you only need one Sign method and one NewSigner function.

This is sort of similar to the crypto.Signer interface:

type Signer interface {
    Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error)
}

Except here SignerOpts is an interface.

Comment From: drakkan

A couple of questions, potentially borne out of confusion:

  1. I have some legacy PEM-encrypted SSH private keys. If support for them is gone from the new helpers, how will I be able to use them?

PEM-encrypted SSH private keys use outdated encryption schemes, such as DES-CBC and AES-CBC, which are no longer considered secure. It's recommended to migrate to the OpenSSH private key format.

  1. In some contexts, I will need to allow "ssh-rsa" signing algorithm with SHA-1. But I don't necessarily know what kind of key I have - it's just an opaque key in the OpenSSH (encrypted) format, and if it's RSA, then I need ssh-rsa, but if not then not. Will ParsePrivateKeyV2 return a SignerV2 which supports this? If not, how do I request support for it? What happens if I request support for it but pass in an Ed22519 key?

ParsePrivateKeyV2 returns a SignerV2 that supports rsa-sha2-256 and rsa-sha2-512 for RSA keys. If you need support for the legacy ssh-rsa algorithm, you must use NewSignerV2WithAlgorithms and explicitly include ssh-rsa in the algorithm list.

Attempting to use ssh-rsa with a non-RSA key (e.g., an ed25519 key) will result in an error. The SignerV2 interface provides the Algorithms() method that returns the list of supported signature algorithms.

  1. Your comments in NewSignerV2 suggest to use NewSignerV2WithAlgorithms if you want SHA-1, but the former takes a crypto.Signer while the latter wants a SignerV2. How will that work?>

You can obtain a SignerV2 by using ParsePrivateKeyV2 with a PEM-formatted key or NewSignerV2 if you have a crypto.Signer. The underlying crypto.Signer can be accessed from a SignerV2 instance via the PrivateKey() method. Once you have a SignerV2, you can create a signer with specific algorithms using NewSignerV2WithAlgorithms.

Comment From: drakkan

marked with the V2 suffix, intended to replace the existing API once the x/crypto/ssh package is moved to the standard library.

Why does this need a V2 suffix if it is being moved to the standard library?

The V2 suffix is used to distinguish the new API while it’s still hosted in x/crypto/ssh. This allows us to introduce improvements without breaking existing code. Once the API is finalized and integrated into the standard library, the suffix will be removed to provide a seamless experience.

The SignerV2 implementation is backward compatible, it can be used anywhere a regular Signer is expected. For example, you can create a SignerV2 and use it with methods like ServerConfig.AddHostKey. This design ensures a smooth transition while maintaining compatibility.

Comment From: imirkin

A couple of questions, potentially borne out of confusion:

  1. I have some legacy PEM-encrypted SSH private keys. If support for them is gone from the new helpers, how will I be able to use them?

PEM-encrypted SSH private keys use outdated encryption schemes, such as DES-CBC and AES-CBC, which are no longer considered secure. It's recommended to migrate to the OpenSSH private key format.

OK, so basically "tough luck"? You can say it's insecure but that doesn't stop people from having to deal with this stuff. The reality is that I have these keys sitting around. I can see not having code for generating these, but parsing would be nice.

  1. In some contexts, I will need to allow "ssh-rsa" signing algorithm with SHA-1. But I don't necessarily know what kind of key I have - it's just an opaque key in the OpenSSH (encrypted) format, and if it's RSA, then I need ssh-rsa, but if not then not. Will ParsePrivateKeyV2 return a SignerV2 which supports this? If not, how do I request support for it? What happens if I request support for it but pass in an Ed22519 key?

ParsePrivateKeyV2 returns a SignerV2 that supports rsa-sha2-256 and rsa-sha2-512 for RSA keys. If you need support for the legacy ssh-rsa algorithm, you must use NewSignerV2WithAlgorithms and explicitly include ssh-rsa in the algorithm list.

Attempting to use ssh-rsa with a non-RSA key (e.g., an ed25519 key) will result in an error. The SignerV2 interface provides the Algorithms() method that returns the list of supported signature algorithms.

So I will now need some specialty code which detects RSA keys in the underlying SignerV2 and recreate it with the extended algos list. With the current setup, I can just filter out ssh-rsa when I don't want it generically. Basically there are "best practices" contexts, and "the other side is using ancient stuff" contexts. It's nice to be able to support both easily. Maybe a Parse option which specifies if this stuff should be exposed or not.

  1. Your comments in NewSignerV2 suggest to use NewSignerV2WithAlgorithms if you want SHA-1, but the former takes a crypto.Signer while the latter wants a SignerV2. How will that work?>

You can obtain a SignerV2 by using ParsePrivateKeyV2 with a PEM-formatted key or NewSignerV2 if you have a crypto.Signer. The underlying crypto.Signer can be accessed from a SignerV2 instance via the PrivateKey() method. Once you have a SignerV2, you can create a signer with specific algorithms using NewSignerV2WithAlgorithms.

Wouldn't it make more sense to make NewSignerV2WithAlgorithms just take the crypto.Signer directly then? Seems very odd for it to take an object with a list of supported algorithms, and then specify a list which isn't a subset.

Comment From: drakkan

A couple of questions, potentially borne out of confusion:

  1. I have some legacy PEM-encrypted SSH private keys. If support for them is gone from the new helpers, how will I be able to use them?

PEM-encrypted SSH private keys use outdated encryption schemes, such as DES-CBC and AES-CBC, which are no longer considered secure. It's recommended to migrate to the OpenSSH private key format.

OK, so basically "tough luck"? You can say it's insecure but that doesn't stop people from having to deal with this stuff. The reality is that I have these keys sitting around. I can see not having code for generating these, but parsing would be nice.

Thanks for bringing this up, this is a proposal, we're open to discussion. Although DecryptPEMBlock is deprecated in the standard library, it’s still available, so we’ll review the suggestion with the other maintainers and consider the most appropriate path forward.

  1. In some contexts, I will need to allow "ssh-rsa" signing algorithm with SHA-1. But I don't necessarily know what kind of key I have - it's just an opaque key in the OpenSSH (encrypted) format, and if it's RSA, then I need ssh-rsa, but if not then not. Will ParsePrivateKeyV2 return a SignerV2 which supports this? If not, how do I request support for it? What happens if I request support for it but pass in an Ed22519 key?

ParsePrivateKeyV2 returns a SignerV2 that supports rsa-sha2-256 and rsa-sha2-512 for RSA keys. If you need support for the legacy ssh-rsa algorithm, you must use NewSignerV2WithAlgorithms and explicitly include ssh-rsa in the algorithm list. Attempting to use ssh-rsa with a non-RSA key (e.g., an ed25519 key) will result in an error. The SignerV2 interface provides the Algorithms() method that returns the list of supported signature algorithms.

So I will now need some specialty code which detects RSA keys in the underlying SignerV2 and recreate it with the extended algos list. With the current setup, I can just filter out ssh-rsa when I don't want it generically. Basically there are "best practices" contexts, and "the other side is using ancient stuff" contexts. It's nice to be able to support both easily. Maybe a Parse option which specifies if this stuff should be exposed or not.

In the new API, we plan to disable ssh-rsa by default for security reasons, while still providing the option to enable it explicitly if needed. The parse options are intentionally designed to be easily extensible, allowing this to be evaluated as needed. However, once you have a signer, restricting the algorithm is straightforward.

In general, I believe that enabling less secure algorithms should be an explicit choice. With the proposed API, enabling ssh-rsa requires using an RSA key, making the intent quite explicit

  1. Your comments in NewSignerV2 suggest to use NewSignerV2WithAlgorithms if you want SHA-1, but the former takes a crypto.Signer while the latter wants a SignerV2. How will that work?>

You can obtain a SignerV2 by using ParsePrivateKeyV2 with a PEM-formatted key or NewSignerV2 if you have a crypto.Signer. The underlying crypto.Signer can be accessed from a SignerV2 instance via the PrivateKey() method. Once you have a SignerV2, you can create a signer with specific algorithms using NewSignerV2WithAlgorithms.

Wouldn't it make more sense to make NewSignerV2WithAlgorithms just take the crypto.Signer directly then? Seems very odd for it to take an object with a list of supported algorithms, and then specify a list which isn't a subset.

I think you're right here, thanks.

Comment From: imirkin

A couple of questions, potentially borne out of confusion:

  1. I have some legacy PEM-encrypted SSH private keys. If support for them is gone from the new helpers, how will I be able to use them?

PEM-encrypted SSH private keys use outdated encryption schemes, such as DES-CBC and AES-CBC, which are no longer considered secure. It's recommended to migrate to the OpenSSH private key format.

OK, so basically "tough luck"? You can say it's insecure but that doesn't stop people from having to deal with this stuff. The reality is that I have these keys sitting around. I can see not having code for generating these, but parsing would be nice.

Thanks for bringing this up, this is a proposal, we're open to discussion. Although DecryptPEMBlock is deprecated in the standard library, it’s still available, so we’ll review the suggestion with the other maintainers and consider the most appropriate path forward.

Thank you. The thing to consider is that people have these, and if you don't support them, you encourage creation of third party libraries to make the ssh library "actually" work. Perhaps that's expected and fine. But currently x/crypto/ssh satisfies all (reasonable?) needs.

  1. In some contexts, I will need to allow "ssh-rsa" signing algorithm with SHA-1. But I don't necessarily know what kind of key I have - it's just an opaque key in the OpenSSH (encrypted) format, and if it's RSA, then I need ssh-rsa, but if not then not. Will ParsePrivateKeyV2 return a SignerV2 which supports this? If not, how do I request support for it? What happens if I request support for it but pass in an Ed22519 key?

In the new API, we plan to disable ssh-rsa by default for security reasons, while still providing the option to enable it explicitly if needed. The parse options are intentionally designed to be easily extensible, allowing this to be evaluated as needed. However, once you have a signer, restricting the algorithm is straightforward.

In general, I believe that enabling less secure algorithms should be an explicit choice. With the proposed API, enabling ssh-rsa requires using an RSA key, making the intent quite explicit

I'm strongly in favor of disabling ssh-rsa by default (i.e. very much agree with your statement). What I'd like for it to remain easy though is the ability to enable it. And when I say "it", I mean "all kinds of insecure stuff, including insecure kex algos, hashes, etc". With some of your other changes, you make it easy to just supply the secure vs insecure algorithms. It would be nice if such a simple "on/off" setting remained with this API, e.g. something in ParsePrivateKeyV2Options.

The alternative is having to wrap the parse function everywhere, conditionally recreating the private key if it's RSA and the "enable old stuff" condition is desired, which again encourages third-party libraries that "just work". This, BTW, is indirectly a similar problem to the encrypted PEM one above -- do you want to make the new ssh API easy to use for everyone, or do you want someone else to come along and create a "just works" API that people will use to parse private keys?

Comment From: hslatman

@drakkan I'll have another look at this another time, but one immediate thought I have is that it would be great if there's a way to pass a context.Context to the signing operation. Depending on the underlying signer implementation, it may be required to call a method requiring one, necessitating calling it with a new context.Background(), breaking context propagation. There have also been proposals to do something similar for crypto.Signer, the most recent of which is https://github.com/golang/go/issues/56508, which was accepted.

Comment From: hslatman

More of a nit, but perhaps the proposed PrivateKey function is better called Signer, as it's not returning an actual PrivateKey type (which, in other places, provides e.g. Decrypt and such), but a crypto.Signer. I can see its origins in the existing functions, but I think it's not necessary to remain close to those.

I'm also curious if the error return is necessary for that function. In what cases is it foreseen that it can return an error when calling it?

Comment From: drakkan

@drakkan I'll have another look at this another time, but one immediate thought I have is that it would be great if there's a way to pass a context.Context to the signing operation. Depending on the underlying signer implementation, it may be required to call a method requiring one, necessitating calling it with a new context.Background(), breaking context propagation. There have also been proposals to do something similar for crypto.Signer, the most recent of which is #56508, which was accepted.

Added SignContext to the proposal, thank you.

    // SignContext is like Signer.Sign, but allows specifying a context and a
    // desired signing algorithm. Callers may pass an empty string for the
    // algorithm in which case the Signer will use a default algorithm. This
    // default doesn't currently control any behavior in this package. This
    // package prioritizes SignContext if available, falling back to
    // SignWithAlgorithm (for backward compatibility), and then to Sign.
    SignContext(ctx context.Context, rand io.Reader, data []byte, algorithm string) (*Signature, error)

Comment From: drakkan

More of a nit, but perhaps the proposed PrivateKey function is better called Signer, as it's not returning an actual PrivateKey type (which, in other places, provides e.g. Decrypt and such), but a crypto.Signer. I can see its origins in the existing functions, but I think it's not necessary to remain close to those.

It is part of the SignerV2 interface, which will be renamed to Signer when moved to the standard library. At that point, there will be a Signer interface with a Signer method, doesn’t sound great

I'm also curious if the error return is necessary for that function. In what cases is it foreseen that it can return an error when calling it?

DSA, that will be removed, does not implement crypto.Signer, I think there may be external implementation that also don't implement crypto.Signer. The signers returned from crypto/ssh will never return an error.

Comment From: drakkan

I'm strongly in favor of disabling ssh-rsa by default (i.e. very much agree with your statement). What I'd like for it to remain easy though is the ability to enable it. And when I say "it", I mean "all kinds of insecure stuff, including insecure kex algos, hashes, etc". With some of your other changes, you make it easy to just supply the secure vs insecure algorithms. It would be nice if such a simple "on/off" setting remained with this API, e.g. something in ParsePrivateKeyV2Options.

Added SignatureAlgorithms to ParsePrivateKeyV2Options

// 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
}

Thank you

Comment From: imirkin

Added SignatureAlgorithms to ParsePrivateKeyV2Options

Thanks for your consideration. I'm just thinking through how this would be used in practice. Right now our code is like

type RestrictedSigner struct {
        ssh.MultiAlgorithmSigner
        RestrictedAlgorithms []string
}
func (s RestrictedSigner) Algorithms() []string {
        // return s.MultiAlgorithmSigner.Algorithms() - RestrictedAlgorithms
}

...

signer, err := ssh.ParsePrivateKeyWithPassphrase(...)
if secure {
  signer = RestrictedSigner{signer, []string{ssh.KeyAlgoRSA}}
}

The nice thing here is that we don't really care what kind of key it is, we can just not have KeyAlgoRSA. However here we have to provide the list of SignatureAlgorithms before we parse the key. So what would you recommend this be? Just the overall list of algos across all key types that we want to use and let ParsePrivateKey sort it out?

Comment From: drakkan

Added SignatureAlgorithms to ParsePrivateKeyV2Options

Thanks for your consideration. I'm just thinking through how this would be used in practice. Right now our code is like

type RestrictedSigner struct { ssh.MultiAlgorithmSigner RestrictedAlgorithms []string } func (s RestrictedSigner) Algorithms() []string { // return s.MultiAlgorithmSigner.Algorithms() - RestrictedAlgorithms }

...

signer, err := ssh.ParsePrivateKeyWithPassphrase(...) if secure { signer = RestrictedSigner{signer, []string{ssh.KeyAlgoRSA}} } The nice thing here is that we don't really care what kind of key it is, we can just not have KeyAlgoRSA. However here we have to provide the list of SignatureAlgorithms before we parse the key. So what would you recommend this be? Just the overall list of algos across all key types that we want to use and let ParsePrivateKey sort it out?

yes, the idea is that you can pass the whole list of acceptable algorithms without caring about the key type. The library internally will filter the configured signing algorithms based on the supported ones, something like this

supportedAlgorithms := algorithmsForKeyFormat(underlyingAlgo(pubKey.Type())
algorithms := slices.DeleteFunc(options.SignatureAlgorithms, func(signAlgo string) bool {
    return !slices.Contains(supportedAlgorithms, signAlgo)
})
return NewSignerV2WithAlgorithms(signer, algorithms)

You can pass the list obtained from SupportedAlgorithms and/or InsecureAlgorithms

Comment From: hslatman

More of a nit, but perhaps the proposed PrivateKey function is better called Signer, as it's not returning an actual PrivateKey type (which, in other places, provides e.g. Decrypt and such), but a crypto.Signer. I can see its origins in the existing functions, but I think it's not necessary to remain close to those.

It is part of the SignerV2 interface, which will be renamed to Signer when moved to the standard library. At that point, there will be a Signer interface with a Signer method, doesn’t sound great

Agreed that it doesn't look/sound great, but generally you don't see the interface type itself, and just see the function. I.e. not Signer.Signer(), but sometype.Signer(). So in practice it may not be that bad, but as said, just a nit.

I'm also curious if the error return is necessary for that function. In what cases is it foreseen that it can return an error when calling it?

DSA, that will be removed, does not implement crypto.Signer, I think there may be external implementation that also don't implement crypto.Signer. The signers returned from crypto/ssh will never return an error.

You could choose to enforce returning a crypto.Signer on the interface level, intentionally limiting it for simplicity, but I suppose an error is fine in this case.

Comment From: drakkan

More of a nit, but perhaps the proposed PrivateKey function is better called Signer, as it's not returning an actual PrivateKey type (which, in other places, provides e.g. Decrypt and such), but a crypto.Signer. I can see its origins in the existing functions, but I think it's not necessary to remain close to those.

It is part of the SignerV2 interface, which will be renamed to Signer when moved to the standard library. At that point, there will be a Signer interface with a Signer method, doesn’t sound great

Agreed that it doesn't look/sound great, but generally you don't see the interface type itself, and just see the function. I.e. not Signer.Signer(), but sometype.Signer(). So in practice it may not be that bad, but as said, just a nit.

Proposal updated, thank you!