Proposal Details

I'd like to propose we support encoding and decoding SSHSIG signature format.

I already have a working implementation (armoring a *ssh.Signature and then parsing it back into the signed data), but I'm not sure what the api should look like.

We have a couple of steps to create a signature:

  • create a blob
  • sign the blob (this signing step is already implemented here)
  • create the signed data
  • encode it into a PEM format

To verify a signature, we need to:

  • create a blob
  • decode the previously created PEM formatted signature
  • call publickey.Verify(blob, decodedBlod)

Given all this, I'd suggest the following functions:

func CreateBlob(r io.Reader) ([]byte, error) // or (io.Reader, error)
func Encode(pk ssh.PublicKey, sig *ssh.Signature) ([]byte, error) // or (io.Reader, error)
func Decode(r io.Reader) (*ssh.Signature, ssh.PublicKey, error)

We would also need these two structs:

// Blob according to the SSHSIG protocol.
type Blob struct {
    Namespace     string
    Reserved      string
    HashAlgorithm string
    Hash          string
}

// SignedData according to the SSHSIG protocol.
type SignedData struct {
    MagicPreamble [6]byte
    Version       uint32
    PublicKey     string
    Namespace     string
    Reserved      string
    HashAlgorithm string
    Signature     string
}

and some constants:

const (
    magicPreamble = "SSHSIG"
    version       = 1
    namespace     = "file"
    hashAlgorithm = "sha512"
    armorType     = "SSH SIGNATURE"
)

There's also the discussion of which hash algorithms to support... only rsa-sha2-512 or rsa-sha2-256, which I think it's easy enough to support both.

Finally, the namespace, not sure if we allow to customize that or not.


Anyway, I would love to work on this, just need some direction on how the API should look like.

Comment From: ianlancetaylor

CC @golang/security @drakkan

Comment From: FiloSottile

I suggest we implement a higher-level API, without exposing the formatting of the signed blob to the application.

// Sign returns a detached SSH Signature for the provided message.
//
// The namespace is a domain-specific identifier for the context in which the
// signature will be used. It must match between the Sign and [Verify] calls. A
// fully-qualified suffix is recommended, e.g. "receiptV2@example.com".
//
// These signatures are compatible with those generated by "ssh-keygen -Y sign",
// and can be verified with [Verify] or "ssh-keygen -Y verify". The returned
// bytes are usually PEM encoded with [encoding/pem] and type "SSH SIGNATURE".
//
// If the Signer has an RSA PublicKey, it must also implement [AlgorithmSigner].
// If it also implements [MultiAlgorithmSigner], the first algorithm returned by
// Algorithms will be used, otherwise "rsa-sha2-512" is used.
func Sign(s Signer, rand io.Reader, message []byte, namespace string) ([]byte, error)

// Verify verifies a detached SSH Signature for the provided message.
//
// The namespace is a domain-specific identifier for the context in which the
// signature will be used. It must match between the [Sign] and Verify calls. A
// fully-qualified suffix is recommended, e.g. "receiptV2@example.com".
//
// The provided signature is usually decoded from a PEM block of type "SSH
// SIGNATURE" using [encoding/pem].
func Verify(pub PublicKey, message, signature []byte, namespace string) error

We can accept both sha256 and sha512 in Verify, and always use sha512 in Sign, like ssh-keygen.

As required by the specification, we will reject ssh-rsa signatures in Verify.

Comment From: caarlos0

Thanks @FiloSottile!

Implemented it here: https://github.com/golang/crypto/pull/316

Still unsure about how to best unit test this, but otherwise the API feels good I think.

Comment From: gopherbot

Change https://go.dev/cl/659715 mentions this issue: ssh: sign and verify

Comment From: FiloSottile

Thank you for the PR! I see that you implemented it to take/return PEM. In the docs I meant "is usually decoded / encoded" as a suggestion for the user to do it themselves, if they need it armored. We should make the docs clearer if we return raw bytes, or maybe we should return just PEM?

As for unit testing, round-trip tests and a test against a ssh-keygen produced signature (for at least each signature algorithm) would be enough.

Comment From: rolandshoemaker

@FiloSottile given that we are planning on removing rand io.Readers in a lot of places, would it make sense to do the same here, as it's not explicitly defined how we are going to use those bytes (I guess it's a property of the Signer, presumably)?