New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
crypto: add ContextSigner and use in crypto/tls #56508
Comments
cc @golang/security |
Can you explain the problem with currying in more detail? Thanks. |
@ianlancetaylor In crypto/tls, the crypto.Signer is inside the tls.Certificate, which in turn is inside the tls.Config. So only way for the currying approach to work is to make a separate tls.Config for each connection. In addition, without this change tls.Conn.HandshakeContext cannot uphold its contract properly, since it has no way of canceling the call to |
I see that Would you anticipate that all type exampleSigner struct {}
func (s *exampleSigner) SignContext(ctx context.Context, rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error) {
// ...
}
func (s *exampleSigner) Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error) {
return s.SignContext(context.TODO(), rand, digest, opts)
} This is just a question, not an objection. I'm curious as to whether you or someone else has an idea about how to avoid this boilerplate, but if not then at a least doesn't seem too arduous. Perhaps the boilerplate example could be included in the godoc for the new interface. |
Yes, although it is ultimately up to each implementation what exactly that context is. In most cases I would expect
You kind of can, but I don't think it is worth it in this particular case, as you would be trading one line of trivial boilerplate for another. For example: type ContextSigner interface {
Public() PublicKey
SignContext(ctx context.Context, rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error)
}
type ContextSignerAdapter struct {
ContextSigner
}
func (s ContextSignerAdapter) Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error) {
return s.SignContext(context.Background(), rand, digest, opts)
} Also, if you did this then other methods implemented by your |
The obvious place to add a |
@apparentlymart, in theory that boilerplate could be implemented with a generic wrapper. However, a wrapper that preserves additional extension methods is not currently possible due to #43621. (If it were allowed, it might look like this: https://go.dev/play/p/Hp-kP_UM9c5) However, if we are ok with dropping extension methods, it can be implemented as a generic wrapper today: https://go.dev/play/p/IJpyNKgbgE2 (Note that the wrapper that drops extension methods doesn't even need to be generic — but making it generic allows for a future change to preserve extension methods.) |
It does seem unfortunate that the package crypto
func SignerOptsContext(opts SignerOpts) context.Context {
if withCtx, ok := opts.(SignerOptsContext); ok {
return withCtx.SignerOptsContext()
}
return context.Background()
} A signer that would benefit from a context can pass the given opts to this function and hopefully get a useful context, but if not then get a harmless useless one. My assumption in proposing this is that there would be relatively few |
@apparentlymart Unfortunately, the situation with Since Had |
So, another option might be to define the // ContextSignerOpts extends SignerOpts with a Context method.
//
// Signer implementations are encouraged to check whether the SignerOpts
// implement ContextSignerOpts and make use of its Context method.
type ContextSignerOpts {
Context() context.Context
SignerOpts
}
// SignerContext returns the Context from opts, if present, or else context.TODO().
func SignerContext(opts SignerOpts) context.Context {
ctxOpts, ok := opts.(ContextSignerOpts)
if !ok {
return context.TODO()
}
return ctxOpts.Context()
} ...and then also update |
As I mentioned, adding a wrapper for |
By the way, if we were designing the crypto package from scratch, I think double dispatch would have made much more sense. package crypto
type Signer interface {
Public() PublicKey
Sign(ctx context.Context, rand io.Reader, digest []byte, hash crypto.Hash) (signature []byte, err error)
}
type SignerOpts interface {
Sign(ctx context.Context, rand io.Reader, digest []byte, s Signer) (signature []byte, err error)
}
type Hash int
func (h Hash) Sign(ctx context.Context, rand io.Reader, digest []byte, s Signer) (signature []byte, err error) {
return s.Sign(ctx, rand, digest, h)
} package rsa
type PSSSigner interface {
crypto.Signer
SignPSS(ctx context.Context, rand io.Reader, digest []byte, opts PSSOptions) (signature []byte, err error)
}
type PSSOptions struct {
SaltLength int
Hash crypto.Hash
}
func (opts PSSOptions) Sign(ctx context.Context, rand io.Reader, digest []byte, s Signer) (signature []byte, err error) {
ps, ok := s.(PSSSigner)
if !ok {
return nil, errors.New(...)
}
return ps.SignPSS(ctx, rand, digest, opts)
} |
Maybe it would be worth combing both fixes into one? package crypto
type ContextSigner interface {
Signer
SignContext(ctx context.Context, rand io.Reader, digest []byte, hash crypto.Hash) (signature []byte, err error)
}
// I don't have a better name for this right now.
type SignerOptsV2 interface {
Sign(ctx context.Context, s Signer, rand io.Reader, digest []byte) (signature []byte, err error)
}
func (h Hash) Sign(ctx context.Context, s Signer, rand io.Reader, digest []byte) (signature []byte, err error) {
cs, ok := s.(ContextSigner)
if !ok {
return s.Sign(rand, digest, h)
}
return cs.SignContext(ctx, rand, digest, h)
}
func SignContext(ctx context.Context, s Signer, rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error) {
ov2, ok := opts.(SignerOptsV2)
if !ok {
return s.Sign(rand, digest, opts)
}
return ov2.Sign(ctx, s, rand, digest)
} package rsa
type PSSSigner interface {
crypto.ContextSigner
SignPSS(ctx context.Context, rand io.Reader, digest []byte, opts *PSSOptions) (signature []byte, err error)
}
func (opts *PSSOptions) Sign(ctx context.Context, s crypto.Signer, rand io.Reader, digest []byte) (signature []byte, err error) {
ps, ok := s.(PSSSigner)
if !ok {
return s.Sign(rand, digest, opts)
}
return ps.SignPSS(ctx, rand, digest, opts)
}
func (priv *PrivateKey) SignContext(ctx context.Context, rand io.Reader, digest []byte, hash crypto.Hash) (signature []byte, err error) {
...
}
func (priv *PrivateKey) SignPSS(ctx context.Context, rand io.Reader, digest []byte, opts *PSSOptions) (signature []byte, err error) {
...
}
Another option would be this: package crypto
type SignerOptsV2 interface {
Sign(ctx context.Context, s Signer, rand io.Reader, digest []byte) (signature []byte, err error)
}
type HashSigner interface {
crypto.Signer
SignHash(ctx context.Context, rand io.Reader, digest []byte, h Hash) (signature []byte, err error)
}
type SignerWrapper interface {
SignWithOpts(ctx context.Context, rand io.Reader, digest []byte, opts SignerOptsV2) (signature []byte, err error)
}
func (h Hash) Sign(ctx context.Context, s Signer, rand io.Reader, digest []byte) (signature []byte, err error) {
hs, ok := s.(HashSigner)
if !ok {
return s.Sign(rand, digest, h)
}
return hs.SignHash(ctx, rand, digest, h)
}
func Sign(ctx context.Context, s Signer, rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error) {
ov2, ok := opts.(SignerOptsV2)
if !ok {
return s.Sign(rand, digest, opts)
}
w, ok := s.(SignerWrapper)
if !ok {
return ov2.Sign(ctx, s, rand, digest)
}
return w.SignWithOpts(ctx, rand, digest, ov2)
} package rsa
type PSSSigner interface {
crypto.Signer
SignPSS(ctx context.Context, rand io.Reader, digest []byte, opts *PSSOptions) (signature []byte, err error)
}
func (opts *PSSOptions) Sign(ctx context.Context, s crypto.Signer, rand io.Reader, digest []byte) (signature []byte, err error) {
ps, ok := s.(PSSSigner)
if !ok {
return s.Sign(rand, digest, opts)
}
return ps.SignPSS(ctx, rand, digest, opts)
}
func (priv *PrivateKey) SignHash(ctx context.Context, rand io.Reader, digest []byte, h crypto.Hash) (signature []byte, err error) {
...
}
func (priv *PrivateKey) SignPSS(ctx context.Context, rand io.Reader, digest []byte, opts *PSSOptions) (signature []byte, err error) {
...
} In this case, |
Merely defining a new wrapper type (without changing (Moreover, since the |
Sure but any attempt by crypto/tls (or others) to use to new type to support
Because of the way that |
This proposal has been added to the active column of the proposals project |
It sounds like there are really two parts to this proposal:
Are there other pieces of code that would also change, besides crypto/tls? We should enumerate the full set to understand the concrete details of the proposal. Thanks. |
The only places that we use crypto.Signer are:
As crypto/x509 is not context-aware it is not affected by this proposal. (Probably using the currying approach here is still good enough.) I see x/crypto/ssh also appears not to be context-aware. I am not familiar with any other APIs using crypto.Signer. |
OK, so it sounds like we understand the proposal. Does anyone object to this proposal? |
crypto/tls is the only place I can think of off in the standard library where we'd want to upgrade I think. I've seen the currying pattern a handful of times outside of the standard library (in particular when using HSMs, and other cloud key management services, for signing), so there is clearly a desire for something like this, although it is clearly doable when you're using an API you can route the context/signer through cleanly yourself. I have no real objection, seems like it'd allow some cleaner API usage, and has relatively low complexity/risk. |
Currying is technically possible, and creating a new Config for each connection is a pretty established pattern through GetConfigForClient, but I can see that being cumbersome when operating through net/http, which is the main user of HandshakeContext. Adding a context-aware interface and upgrading to it in crypto/tls sounds good. The (I'll note that technically also Decrypter would need a context-aware version, but that's only used for the legacy RSA key exchange, and I am totally fine with leaving that behind.) |
I don't think we need the helper either. I hadn't noticed it. I believe the proposal is simply to add
and then use that interface in crypto/tls. No other API additions. |
@FiloSottile @rsc As I mentioned in the original proposal, the |
Based on the discussion above, this proposal seems like a likely accept. |
No change in consensus, so accepted. 🎉 |
This was previously proposed in #28427 but rejected. However, I don't think it was given proper consideration.
As mentioned in #28427, it is possible for a crypto.Signer to require remote calls to some external service such as AWS KMS. Currently the
Sign
method does not take acontext.Context
, meaning that the timeout/cancellation and any logging/monitoring metadata cannot make it across the method call. @bcmills mentioned using currying to simulate this but that does not work well with the crypto/tls API (and possibly others). Instead the desire there would be to forward the context fromHandshakeContext
across.I propose the following changes. First add a new interface to crypto.
Then add a helper function to crypto. (Not strictly required but will make people's lives easier.)
Then the internals of net/tls can be amended to call the new
crypto.SignContext
function fromHandshakeContext
.The text was updated successfully, but these errors were encountered: