Closed FiloSottile closed 2 years ago
Change https://go.dev/cl/398914 mentions this issue: crypto/ecdh: new package
This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — rsc for the proposal review group
There are broadly three publishers of relevant specifications that matter to us: NIST, ANSI, and SECG. NIST makes open standards for the US government, ANSI makes paywalled standards for the banking industry, and SECG made a couple open standards. NIST standards cited ANSI standards until recently, while SECG effectively made open versions of them.
NIST Draft FIPS 186-5 specifies ECDSA. FIPS 186-4 (2013) used to reference ANSI X9.62 (2005). NIST SP 800-56A Rev. 3 (2018) specifies ECDH. Rev. 2 (2013) used to reference ANSI X9.63 (2011). SEC 1, Version 2.0 (2009) profiles all of the above. See Appendix B.6 of SEC 1 for an extensive discussion of its interoperability.
NIST P curves are defined in Appendix D of FIPS 186-4, in Draft NIST SP 800-186, and in SEC 2, Version 2.0 (2010).
We reference SEC 1, Version 2.0 and FIPS 186-4, but we target a subset that is compatible with all of them 🎉
In #34105, we added support for MarshalCompressed and UnmarshalCompressed to crypto/elliptic. It would seem logical to support compressed points in crypto/ecdh, too.
If we want to support them, I would propose adding
func (k *PublicKey) BytesCompressed() []byte
type Curve interface {
NewPublicKeyFromCompressed(key []byte) (*PublicKey, error)
}
Technically, we could just make Curve.NewPublicKey
support both compressed and uncompressed encodings, as they have different type prefixes. However, it’s unlikely that any application wishes to support both at the same time, and this would force every user (including our own crypto/tls) to check the prefix before calling NewPublicKey
.
A wrinkle is that technically speaking all X25519 public keys are compressed. So, Bytes
/BytesCompressed
and NewPublicKey
/NewPublicKeyFromCompressed
would do the same thing for X25519.
An option I like is to not add these methods now, and wait some time to see if the requirement materializes, and in what shape.
A few people mentioned needing interoperability with Apple’s CryptoKit.
I played with it in the Swift Playground to figure out what its encodings are, because the docs are very intent on being vague about it.
The summary is that for public keys, their x963Representation
is what our Bytes()
method generates, their rawRepresentation
is just x963Representation
without the 0x04
prefix, and their compactRepresentation
is what our BytesCompressed()
would return without the 0x02
/0x03
prefix.
“But wait”, you’ll say, “that prefix conveys an important bit of information!” Uh, I agree? Looking at the implementation reveals that this follows an expired 2014 IETF draft, draft-jivsov-ecc-compact-05, which basically says… to make sure the key always has a lexicographically lower Y coordinate. Indeed, CryptoKit will loop until it finds such a key, unless compactRepresentable: false
is used in init
, in which case publicKey.compactRepresentation
might fail. Now, that makes me sad because it doesn’t match the disambiguation that all other specs use (which switch on the least significant bit, not on lexicographical order that corresponds to the most significant bit instead), so you can’t just say “always add or remove a 0x20
prefix”. However, since the ECDH operation only returns the x coordinate, the y coordinate doesn’t really matter: if you always use a 0x20
prefix you have a 50% chance of being wrong, but the ECDH output will be correct either way.
For private keys, their rawRepresentation
is what our Bytes()
method generates, and their x963Representation
is the concatenation of PublicKey.Bytes()
and Bytes()
. The shared secret is what our ECDH()
method returns. The PEM/DER encodings are the PKCS#8 and PKIX formats we support in crypto/x509.
In summary, it takes some tweaking but the proposed APIs are compatible with CryptoKit. If we implement the compressed encoding, it will be possible to support the Apple compactRepresentation
with some tweaking and approximation. Otherwise, we’ll support only the x963Representation
and rawRepresentation
.
I made a small change to the proposed API: the PublicKey
embedded in PrivateKey
is gone, so now NewPrivateKey
doesn't have to generate the public key every time, which is an expensive operation. This doesn't matter in ephemeral ECDH, because the public key needs to be generated and set to the peer, but for static ECDH it would have been unnecessary overhead.
Instead, we now have these two methods on PrivateKey
, where the PublicKey
will compute the public key with a sync.Once
. The Curve
method is just because it's not visible through the embedding anymore.
func (k *PrivateKey) Curve() Curve
func (k *PrivateKey) PublicKey() *PublicKey
PublicKey()
gets away with not returning an error because in X25519 the operation can always succeed, and with NIST curves it only fails (well, return an irregular encoding) for the identity element, which can only happen for the zero key, which we reject in NewPrivateKey
.
Why both Public()
and PublicKey()
? The latter returns a *PublicKey
, while the former returns a crypto.PublicKey
to implement the informal crypto.PrivateKey
interface.
interface{
Public() crypto.PublicKey
Equal(x crypto.PrivateKey) bool
}
PublicKey() gets away with not returning an error because in X25519 the operation can always succeed, and with NIST curves it only fails (well, return an irregular encoding) for the identity element, which can only happen for the zero key, which we reject in NewPrivateKey.
In other words PublicKey
can panic? (Thinking about BoringCrypto and other similar implementations.)
PublicKey() gets away with not returning an error because in X25519 the operation can always succeed, and with NIST curves it only fails (well, return an irregular encoding) for the identity element, which can only happen for the zero key, which we reject in NewPrivateKey.
In other words
PublicKey
can panic? (Thinking about BoringCrypto and other similar implementations.)
No, as long as the semantics of NewPrivateKey
are correctly implemented (that is, the zero scalar is rejected, as it's documented to do), PublicKey()
can't hit the panic conditions. BoringCrypto is expected to follow those semantics.
@FiloSottile I don't know what Go's BoringCrypto ECDH will look like since it doesn't exist yet, but I'm looking at mine right now and there are 5 spots where it can "fail":
EC_POINT_new
returns NULLEC_POINT_set_affine_coordinates_GFp
returns false (zero)EC_KEY_new_by_curve_name
returns NULLEC_KEY_set_private_key
returns false (zero)ECDH_compute_key
returns an invalid lengthMaybe I'm missing something or being too pessimistic, but those cases seem unavoidable.
Change https://go.dev/cl/404276 mentions this issue: crypto/ecdh: implement compressed points
@elagergren-spideroak hmm, EC_POINT_new
should only fail on a malloc failure, EC_POINT_set_affine_coordinates_GFp
should not fail for a point that is known to be valid, EC_KEY_new_by_curve_name
should not fail for a curve that is known to be supported, EC_KEY_set_private_key
should not fail for a private key that is known to be good, and ECDH_compute_key
should not return an invalid length unless the private key is zero which must be rejected by NewPrivateKey
.
In general, I don't think we should make the Go API significantly harder to use to accommodate cgo reimplementations, but in this specific case I think panic'ing would be fine because those cases would be unreachable.
Otherwise, cgo reimplementations are free to do all the work in NewPrivateKey (which returns an error) and just take the performance hit.
@FiloSottile yeah, so I also agree that PublicKey
shouldn't return an error. But it does mean that PublicKey
isn't exactly panic free (in the way that the Go implementations are panic free).
Like I mentioned, it's possible I'm being overly pessimistic here. BoringSSL is great, but I've also seen too many "that can't happen" failures from other C cryptography libraries I've written cgo for. 🤷♀️
At the end of the day, cgo backends are a tradeoff in simplicity, performance, and reliability against compliance, so yeah :)
The CLs for crypto/ecdh are ready and reviewed: https://go.dev/cl/398914 and https://go.dev/cl/402555.
https://go.dev/cl/404276 implements compressed points per https://github.com/golang/go/issues/52221#issuecomment-1111153164, but I've decided to wait to land it. They add complexity and we can always add them in Go 1.20.
Change https://go.dev/cl/402555 mentions this issue: crypto/ecdh,crypto/internal/nistec: enable pruning of unused curves
ECDH public and private keys are encoded exactly like ECDSA keys (as PKIX and PKCS#8, respectively), so we already have parsers and encoders for them in crypto/x509, but they return *ecdsa.PublicKey
and *ecdsa.PrivateKey
. There are two options: add methods to the ECDSA key types to convert them to the ECDH key types, or duplicate the parsers and encoders in crypto/ecdh.
It will be hard to preserve the property that "if you only use curve X, the implementation of curve Y is not reachable" property when using generic encoders/decoders, because they are expected to be capable of returning any curve based on the OID in the encoding. Unless we make them methods on Curve
and make them support only one curve at a time, which would be a nice nudge towards avoiding needless agility.
We could also do both. Like compressed points, I'm suggesting leaving it for Go 1.20, as we can always add more things, and so we have time to collect feedback and look at early adopters, too.
@FiloSottile
Why both Public() and PublicKey()? The latter returns a *PrivateKey, while the former returns a crypto.PublicKey to implement the informal crypto.PrivateKey interface.
Do you mean PublicKey()
returns a *PublicKey
?
This part of the proposed API seems confusing to me. It's not clear when to use which of the two methods, or why there are PrivateKeys returned in the PublicKey methods. Could the method names be made more clear?
ECDH public and private keys are encoded exactly like ECDSA keys (as PKIX and PKCS#8, respectively), so we already have parsers and encoders for them in crypto/x509, but they return ecdsa.PublicKey and ecdsa.PrivateKey.
When using crypto/elliptic
to implement ECDH, it's currently necessary to import the *ecdsa.{PublicKey, PrivateKey}
types which is already a little strange. Ideally there'd be a generic *ec.{PublicKey, PrivateKey}
type shared by the ecdh, ecdsa, and x509 packages (but this is obviously not possible now due to breaking changes).
There are two options: add methods to the ECDSA key types to convert them to the ECDH key types, or duplicate the parsers and encoders in crypto/ecdh.
If we go with option A it'd be useful to be able to convert in either direction. With option B it's somewhat strange to have different ways of encoding/decoding ecdh and ecdsa keys (through methods vs. calling crypto/x509)
Why both Public() and PublicKey()? The latter returns a *PrivateKey, while the former returns a crypto.PublicKey to implement the informal crypto.PrivateKey interface.
Do you mean
PublicKey()
returns a*PublicKey
?This part of the proposed API seems confusing to me. It's not clear when to use which of the two methods, or why there are PrivateKeys returned in the PublicKey methods. Could the method names be made more clear?
Typo! Yeah I meant it returns a *PublicKey
like in the API listing in the top comment.
func (k *PrivateKey) PublicKey() *PublicKey
ECDH public and private keys are encoded exactly like ECDSA keys (as PKIX and PKCS#8, respectively), so we already have parsers and encoders for them in crypto/x509, but they return ecdsa.PublicKey and ecdsa.PrivateKey.
When using
crypto/elliptic
to implement ECDH, it's currently necessary to import the*ecdsa.{PublicKey, PrivateKey}
types which is already a little strange. Ideally there'd be a generic*ec.{PublicKey, PrivateKey}
type shared by the ecdh, ecdsa, and x509 packages (but this is obviously not possible now due to breaking changes).There are two options: add methods to the ECDSA key types to convert them to the ECDH key types, or duplicate the parsers and encoders in crypto/ecdh.
If we go with option A it'd be useful to be able to convert in either direction. With option B it's somewhat strange to have different ways of encoding/decoding ecdh and ecdsa keys (through methods vs. calling crypto/x509)
I agree it would be a bit weird, but the crypto/ecdsa types force a round-trip through big.Int
which is unfortunate, and it would be nice to have a way to avoid it. We also want to avoid making crypto/ecdh depend on math/big at all.
Does anyone object to the API as proposed?
Based on the discussion above, this proposal seems like a likely accept. — rsc for the proposal review group
No change in consensus, so accepted. 🎉 This issue now tracks the work of implementing the proposal. — rsc for the proposal review group
@FiloSottile is Curve
safe for comparison?
Yeah, they are comparable singletons like elliptic.Curve
values. We should document that.
@FiloSottile thanks. Another thing that could be useful is some sort of String
method to print out the name of the curve. I've been playing with the API and found myself wanting to include it in logs, etc.
Yeah, they are comparable singletons like
elliptic.Curve
values. We should document that.
From the docs of the P256()
, P384()
, and P521()
functions:
Multiple invocations of this function will return the same value, so it can be used for equality checks and switch statements.
Added String functions to the Curve implementations (but not to the interface). Not an exposed API change, so I guess we don't need to go through the proposal process again.
@FiloSottile thanks!
Change https://go.dev/cl/450816 mentions this issue: crypto/ecdsa,crypto/x509: add encoding paths for NIST crypto/ecdh keys
Change https://go.dev/cl/451115 mentions this issue: curve25519: use crypto/ecdh on Go 1.20
Change https://go.dev/cl/453256 mentions this issue: crypto/elliptic: remove deprecation markers
Change https://go.dev/cl/459977 mentions this issue: crypto/elliptic: re-apply some deprecation markers
cannot find package "crypto/ecdh" in any of:
how to fix it
Change https://go.dev/cl/404276 mentions this issue:
crypto/ecdh: implement compressed points
@FiloSottile What ever happened to this? Currently we have to pass compressed bytes through elliptic.UnmarshalCompressed
and the deprecated elliptic.Marshal
just to use crypto/ecdh.
According to the Debian and the Google internal code search, crypto/elliptic is used almost exclusively as part of ECDSA (via
crypto/ecdsa
) and ECDH. It is however a very low-level and unsafe API for ECDH.As part of an effort to move
math/big
outside the security perimeter, I have been moving the NIST curve implementations to a safe API in thenistec
package (#52182). Thenistec
API is safe but still lower-level than necessary.ECDH is used in TLS, SSH, JOSE, OpenPGP, PIV, and HPKE, as well as a component of other various ECIES schemes. There are a myriad of standards (ISO, NIST, ANSI, SECG, IETF) but thankfully they all work the same for NIST P curves at the ECDH level.
I'm proposing adding a new
crypto/ecdh
package that exposes a safe,[]byte
-based API for ECDH.Between this package and
crypto/ecdsa
, there should be no need for direct uses ofcrypto/elliptic
, and thebig.Int
-based methods ofelliptic.Curve
(ScalarMult
,ScalarBaseMult
,Add
,Double
,IsOnCurve
) can be deprecated.Below is the proposed API. Here are the motivating design goals of the API:
Public() crypto.PublicKey
method./cc @golang/security @golang/proposal-review