-
Notifications
You must be signed in to change notification settings - Fork 18k
crypto: add ContextSigner and use in crypto/tls #56508
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
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. 🎉 |
Just noting that plumbing the context through would be helpful for other users of |
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: