Repository: go
Issue: proposal: crypto: single-shot signing interface
Labels: Proposal,Proposal-Crypto

# Abstract

This proposal introduces a new interface for creating digital signatures in a single-shot manner, by combining message hashing and signing as an inseparable operation. The new interface can coexist alongside the existing `crypto.Signer` interface, while providing a clear indication that an entire message is expected as the input.

# Background

The `crypto.Signer` interface is defined as follows:

```go
type Signer interface {
	Public() PublicKey
	Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error)
}
```

This interface is implemented for private keys in the RSA (`crypto/rsa`) and ECDSA (`crypto/ecdsa`) modules, as well as Ed25519 (`crypto/ed25519`), which expects the second parameter `digest` to be either an entire message (Ed25519) or the hash over it (Ed25519ph), depending on whether a special hash function (`crypto.Hash(0)`) is specified in `opts`. This design has the following drawbacks:
* Code readability: it is not obvious whether the `Sign` function operates on a digest or a message from the calling sites, as the interpretation of `opts` is up to the implementation of `crypto.Signer`
* Memory consumption: when the second parameter is a message, the entire content needs to be loaded before calling the `Sign` function, which can consume unreasonable amount of memory
* FIPS compliance: FIPS 140-3 requires both hashing and signing operations to be performed within the same cryptographic module boundary. With the `crypto.Signer` interface, it is not straightforward to enforce the requirement

# Proposal

This proposal describes a new interface with a single `SignMessage` function which takes `io.Reader` as the input:

```go
type MessageSigner interface {
	SignMessage(rand, message io.Reader, opts SignerOpts) (signature []byte, err error)
}
```

This interface can be used in a backward compatible manner, by checking the implementation at run-time, as suggested in https://go.dev/blog/module-compatibility#working-with-interfaces:

```go
// in crypto/ed25519/ed25519.go:
func (priv PrivateKey) SignMessage(rand, message io.Reader, opts crypto.SignerOpts) (signature []byte, err error) {
	...
}

// in crypto/tls/handshake_client_tls13.go
func (hs *clientHandshakeStateTLS13) sendClientCertificate() error {
	…
	if ms, ok := cert.PrivateKey.(crypto.MessageSigner); ok {
    		// Use single-shot ms.SignMessage without hashing.
	} else if hs, ok := cert.PrivateKey.(crypto.Signer); ok {
    		// Use hs.Sign after hashing the message.
	} else {
		// Return an error.
	}
}
```

# Rationale

This approach is non-invasive as the use of the new interface is on an opt-in basis. One shortcoming is the amount of code to be rewritten using the single-shot interface for FIPS compliance.

# Compatibility

This change would maintain backward compatibility.

# Implementation

There is a preliminary implementation of this proposal at [1], though it currently simply calls the Sign function in crypto.Signer underneath, which could be either optimized by reading a message in a piecemeal fashion or deferred to the single-shot signing API in BoringSSL.

1. https://github.com/golang/go/compare/master...ueno:go:wip/single-shot-signing