Dynamic Post Quantum Cryptograph support

231 views
Skip to first unread message

Scott Wisniewski

unread,
Apr 8, 2024, 11:00:44 PMApr 8
to golan...@googlegroups.com, Kirill Golodets
I saw in this thread:

https://groups.google.com/g/golang-dev/c/-hmSqJm03V0/m/_vK305F-AAAJ

That you may have tentative plans to implement support for X25519Kyber768Draft00 in golang 1.23, likely behind a GOEXPERIMENT flag. Something similar was also mentioned here: https://github.com/golang/go/issues/64537.

Have you considered potentially also modeling KEMs via an interface in the public API, and allowing external KEM support to be configured via a function member on tls.Config? 

This could enable support for post quantum algorithms to be enabled / disabled dynamically, based on SNI information via tls.Config.GetConfigForClient. 

If you hadn't planned on implementing something like this, would you welcome contributions to enable it?

In addition to enabling dynamic behavior, this could also be used to prototype additional algorithm support quickly, without having to rely on semi-annual golang releases.

Thanks,

-Scott



Filippo Valsorda

unread,
Apr 9, 2024, 5:41:18 AMApr 9
to Scott Wisniewski, golang-dev, Kirill Golodets
Hi Scott,

We're unlikely to expose a hook for arbitrary KEMs. We like to be able to control the potential parameters and configurations of crypto/tls, we aggressively limit complexity, and we consider it our responsibility to select and implement low level details like key exchanges.

A couple examples off the top of my head of complexity we'd have to carry forever if we exposed such a mechanism: how do user-defined key exchanges sort relative to our own PQ experimental ones? What about the final PQ ones? How do we know if the user-defined ones are PQ? Which one(s) do we select for the KeyShare? How do we avoid duplication of work across a user-defined X25519 hybrid and our own X25519? Do we allow overriding the codepoint for ones we implement? What if we later implement one that the application was defining?

Post quantum key exchanges will be controllable from tls.Config.GetConfigForClient like any other key exchange, by using the (now irredeemably misnamed) tls.Config.CurveID.

Cheers,
Filippo
--
You received this message because you are subscribed to the Google Groups "golang-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-dev+...@googlegroups.com.

Scott Wisniewski

unread,
Apr 9, 2024, 7:53:25 AMApr 9
to Filippo Valsorda, Scott Wisniewski, golang-dev, Kirill Golodets
I’m basically talking about implementing named_group extensions via a callback. I’ not sure I see where the complexity is:

Right now you support

	// CurvePreferences contains the elliptic curves that will be used in
	// an ECDHE handshake, in preference order. If empty, the default will
	// be used. The client will use the first preference as the type for
	// its key share in TLS 1.3. This may change in the future.
	CurvePreferences []CurveID

The sort order would be based on the order things were placed into CurvePreferences or the default if none are supplied, exactly how it works today.

The only difference would be that if a `GetKemForCurveID` function was present on the tls Config that method would be called to produce a Kem interface. If it returned non-nil then the kem methods would be used to produce the server share and the shared key.

Otherwise the existing code would run.

I don’t think any of the complexity you mentioned would happen. There’s no need for new ordering, mechanisms, there’s no need to differentiate between PQ vs Non PQ.  

It would, however, make it really easy experiment with Kyber implementations with very low cost and very low complexity.

I’m not super worried about my efforts duplicating yours. My goal is to experiment with existing open source kyber implementations doing hybrid pqc until such time as stable support is available in the runtime.

A tiny amount of code would make that  very inexpensive with basically zero complexity.

Right now other people experimenting with PQC In golang tls have forked the entire tool chain which seems much more expensive and complex to me.


Sent from my iPhone

On Apr 8, 2024, at 11:41 PM, Filippo Valsorda <fil...@golang.org> wrote:



Filippo Valsorda

unread,
Apr 9, 2024, 9:20:03 AMApr 9
to Scott Wisniewski, golang-dev, Kirill Golodets
crypto/tls benefited greatly from keeping close control of low-level details. For example, we took over cipher suite ordering from the application to ensure we can deprioritize less secure suites. https://go.dev/blog/tls-cipher-suites Maybe we'll want to do the same thing with CurvePreferences (maybe we will specifically to address the needs of the PQ transition), which would bring back all the ordering complexity I mentioned if we supported external KEMs.

Also, I don't think your proposal directly addresses any of the following questions in my previous email. They're not insurmountable, but they are additional complexity.
    • Which one(s) do we select for the KeyShare?
    • How do we avoid duplication of work across a user-defined X25519 hybrid and our own X25519? [Note that I am not talking about programmer work, I am talking about generating the X25519 key share.]
    • Do we allow overriding the codepoint for ones we implement?
    • What if we later implement one that the application was defining?

      Ultimately, crypto/tls is a production library, designed to make it easy to build safe applications. That goal is in contrast with the goals of an experimentation toolkit (or of a testing tool, which is the other source of commonly refused proposals). I'm always hoping that the community will coalesce around one or two forks for testing and experimentation, and I encourage anyone who's thinking about it to give it a go.

      This is my personal position FWIW, you're free to file a proposal if you wish, but I assumed you were emailing golang-dev to get a pulse on our appetite for this.

      Cheers,
      Filippo

      Scott Wisniewski

      unread,
      Apr 9, 2024, 3:30:04 PMApr 9
      to Filippo Valsorda, golang-dev, Kirill Golodets
      On Tue, Apr 9, 2024 at 3:19 AM Filippo Valsorda <fil...@golang.org> wrote:
      crypto/tls benefited greatly from keeping close control of low-level details. For example, we took over cipher suite ordering from the application to ensure we can deprioritize less secure suites. https://go.dev/blog/tls-cipher-suites Maybe we'll want to do the same thing with CurvePreferences (maybe we will specifically to address the needs of the PQ transition), which would bring back all the ordering complexity I mentioned if we supported external KEMs.

      Also, I don't think your proposal directly addresses any of the following questions in my previous email. They're not insurmountable, but they are additional complexity.
      • Which one(s) do we select for the KeyShare?
      I'm saying that the logic for selecting a named group on the server doesn't change. That code is defined here for the server:

      https://github.com/golang/go/blob/master/src/crypto/tls/handshake_server_tls13.go#L184 

      and here for the client:

      https://github.com/golang/go/blob/master/src/crypto/tls/handshake_client.go#L148

      The behavior on the server is:

      * Iterate over the curve preferences specified in TLS config (or the default order if no curve preferences are set) and find a match in the client hello message.

      GroupSelection:
      for _, preferredGroup := range c.config.curvePreferences() {
      for _, ks := range hs.clientHello.keyShares {
      if ks.group == preferredGroup {
      selectedGroup = ks.group
      clientKeyShare = &ks
      break GroupSelection
      }
      }
      if selectedGroup != 0 {
      continue
      }
      for _, group := range hs.clientHello.supportedCurves {
      if group == preferredGroup {
      selectedGroup = group
      break
      }
      }
      }
      if selectedGroup == 0 {
      c.sendAlert(alertHandshakeFailure)
      return errors.New("tls: no ECDHE curve supported by both client and server")
      }
      if clientKeyShare == nil {
      if err := hs.doHelloRetryRequest(selectedGroup); err != nil {
      return err
      }
      clientKeyShare = &hs.clientHello.keyShares[0]
      }

      if _, ok := curveForCurveID(selectedGroup); !ok {
      c.sendAlert(alertInternalError)
      return errors.New("tls: CurvePreferences includes unsupported curve")
      }
      key, err := generateECDHEKey(c.config.rand(), selectedGroup)
      if err != nil {
      c.sendAlert(alertInternalError)
      return err
      }
      hs.hello.serverShare = keyShare{group: selectedGroup, data: key.PublicKey().Bytes()}
      peerKey, err := key.Curve().NewPublicKey(clientKeyShare.data)
      if err != nil {
      c.sendAlert(alertIllegalParameter)
      return errors.New("tls: invalid client key share")
      }
      hs.sharedKey, err = key.ECDH(peerKey)
      if err != nil {
      c.sendAlert(alertIllegalParameter)
      return errors.New("tls: invalid client key share")
      }

      The behavior on the client is:

      * Grab the first curve preference and send that in the TLS config.

      curveID := config.curvePreferences()[0]
      if _, ok := curveForCurveID(curveID); !ok {
      return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve")
      }
      key, err = generateECDHEKey(config.rand(), curveID)
      if err != nil {
      return nil, nil, err
      }
      hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}}

      None of this logic would change, and would stay as is. The server logic after group selection would become
      if kem := c.config.getKemForCurve(selectedGroup); kem != nil {
      //user wants to specify a kem
      serverData, sharedKey, err := kem.Encapsulate(clientKeyShare.data)
      if err != nil {
      return err
      }
      hs.hello.serverShare = keyShare{group: selectedGroup, data: serverData}
      hs.sharedKey = sharedKey
      } else {
      //existing code path...
      //you would later add the built-in kem selection here...
      if _, ok := curveForCurveID(selectedGroup); !ok {
      c.sendAlert(alertInternalError)
      return errors.New("tls: CurvePreferences includes unsupported curve")
      }
      key, err := generateECDHEKey(c.config.rand(), selectedGroup)
      if err != nil {
      c.sendAlert(alertInternalError)
      return err
      }
      hs.hello.serverShare = keyShare{group: selectedGroup, data: key.PublicKey().Bytes()}
      peerKey, err := key.Curve().NewPublicKey(clientKeyShare.data)
      if err != nil {
      c.sendAlert(alertIllegalParameter)
      return errors.New("tls: invalid client key share")
      }
      hs.sharedKey, err = key.ECDH(peerKey)
      if err != nil {
      c.sendAlert(alertIllegalParameter)
      return errors.New("tls: invalid client key share")
      }
      }

      A similar change would be made on the client side.

      the definition of getKemForCurve would be:

      func (c *tls.Config) getKemForCurve(curve CurveID) Kem {
      if c.GetKemForCurve != nil {
      return c.GetKemForCurve(curve)
      }
      return nil
      }

      • How do we avoid duplication of work across a user-defined X25519 hybrid and our own X25519? [Note that I am not talking about programmer work, I am talking about generating the X25519 key share. 
      There's no duplication of work. Either a user supplied kem is used or the existing code runs. 
      • What if we later implement one that the application was defining?
      The application selection would win, because the user supplied GetKemForCurve would return non nil. 

      Ultimately, crypto/tls is a production library, designed to make it easy to build safe applications. That goal is in contrast with the goals of an experimentation toolkit (or of a testing tool, which is the other source of commonly refused proposals). I'm always hoping that the community will coalesce around one or two forks for testing and experimentation, and I encourage anyone who's thinking about it to give it a go.


      I actually want this for production code. There's a lot of production code that enables "experimental" PQC features, including Chrome and Firefox.  There are re-usable Kyber implementations from Google (Boring SSL), Cloudflare (circl) and Amazon (s2n-tls). I know both the Cloudflare implementation and the Amazon implementation are used in production.  I want to be able to use these implementations now.

      Also, I assume that the algorithms and standards are going to change frequently... because these are new algorithms...  So even more importantly I want to be able to rapidly iterate (in production) on PQC extensions to TLS. 

      This is my personal position FWIW, you're free to file a proposal if you wish, but I assumed you were emailing golang-dev to get a pulse on our appetite for this.

      Yes, I wanted to get the general appetite for this. 

      I'm hearing a lot of push back. I think the code change is very very simple (much smaller than this email thread), so a proposal wouldn't be pretty easy.
      This would obviously be easier to get approved if there was agreement on the direction beforehand.

      What do I need to do to convince you on this?

      Scott Wisniewski

      unread,
      Apr 9, 2024, 3:41:20 PMApr 9
      to Scott Wisniewski, Filippo Valsorda, golang-dev, Kirill Golodets
      Too many typeos. Sorry The email below should say “a proposal wouldn’t be hard”.

      But my hope is try to get some consensus here if I can.

      Sent from my iPhone

      On Apr 9, 2024, at 9:29 AM, Scott Wisniewski <sc...@agile.security> wrote:

      

      Filippo Valsorda

      unread,
      Apr 9, 2024, 3:47:11 PMApr 9
      to Scott Wisniewski, golang-dev, Kirill Golodets
      2024-04-09 21:29 GMT+02:00 Scott Wisniewski <sc...@agile.security>:
      On Tue, Apr 9, 2024 at 3:19 AM Filippo Valsorda <fil...@golang.org> wrote:
      crypto/tls benefited greatly from keeping close control of low-level details. For example, we took over cipher suite ordering from the application to ensure we can deprioritize less secure suites. https://go.dev/blog/tls-cipher-suites Maybe we'll want to do the same thing with CurvePreferences (maybe we will specifically to address the needs of the PQ transition), which would bring back all the ordering complexity I mentioned if we supported external KEMs.

      Also, I don't think your proposal directly addresses any of the following questions in my previous email. They're not insurmountable, but they are additional complexity.
      • Which one(s) do we select for the KeyShare?
      I'm saying that the logic for selecting a named group on the server doesn't change.

      And what if we want to change it, because we want to switch to sending a PQ key share and a non-PQ key share, which is likely the path to rolling out PQ client-side without causing HRR all over the place? This might actually ironically slow down our PQ rollout.

      Note from the CurvePreferences docs: "The client will use the first preference as the type for its key share in TLS 1.3. This may change in the future."

      • How do we avoid duplication of work across a user-defined X25519 hybrid and our own X25519? [Note that I am not talking about programmer work, I am talking about generating the X25519 key share. 
      There's no duplication of work. Either a user supplied kem is used or the existing code runs.

      Again, not programmer work. CPU work. If you send a Kyber+X25519 and a X25519 key share, you want to reuse the same X25519 public key across them. How do you suggest that works if Kyber+X25519 is application-defined?

      • What if we later implement one that the application was defining?
      The application selection would win, because the user supplied GetKemForCurve would return non nil. 

      I am definitely not a fan of letting applications change how e.g. X25519 works, which we might make assumptions about elsewhere.

      Ultimately, crypto/tls is a production library, designed to make it easy to build safe applications. That goal is in contrast with the goals of an experimentation toolkit (or of a testing tool, which is the other source of commonly refused proposals). I'm always hoping that the community will coalesce around one or two forks for testing and experimentation, and I encourage anyone who's thinking about it to give it a go.


      I actually want this for production code. There's a lot of production code that enables "experimental" PQC features, including Chrome and Firefox.  There are re-usable Kyber implementations from Google (Boring SSL), Cloudflare (circl) and Amazon (s2n-tls). I know both the Cloudflare implementation and the Amazon implementation are used in production.  I want to be able to use these implementations now.

      Support for those is planned in Go 1.23.

      Also, I assume that the algorithms and standards are going to change frequently... because these are new algorithms...  So even more importantly I want to be able to rapidly iterate (in production) on PQC extensions to TLS. 

      Most Go applications don't care about iterating faster than every six months on TLS post-quantum hybrids. They care about a well-maintained TLS library that makes decisions for them. Keeping things simple is how we deliver that.

      This is my personal position FWIW, you're free to file a proposal if you wish, but I assumed you were emailing golang-dev to get a pulse on our appetite for this.

      Yes, I wanted to get the general appetite for this. 

      I'm hearing a lot of push back. I think the code change is very very simple (much smaller than this email thread), so a proposal wouldn't be pretty easy.
      This would obviously be easier to get approved if there was agreement on the direction beforehand.

      What do I need to do to convince you on this?

      To be upfront it's very unlikely we will reverse all precedent and provide low-level hooks like we never did before, but the path to get there involves understanding and acknowledging the maintenance complexity that I'm telling you exists based on many years of experience, addressing it rather than dismissing it, working within the framework of the goals of the Go cryptography standard library (https://golang.org/design/cryptography-principles), and making the case for why the complexity that can't be mitigated is worth it. "You're wrong, there is no complexity" is very unconvincing.
      Reply all
      Reply to author
      Forward
      0 new messages