From d184bc0099a5dca9413e0a86af55baff4ee59b92 Mon Sep 17 00:00:00 2001 From: Kris Kwiatkowski Date: Fri, 5 Oct 2018 22:43:00 +0100 Subject: [PATCH] sidh: adds PQ secure KEX * SIDH/P503-X25519 * adds interop tests --- 13.go | 253 ++++++++++++++++++++++++------- _dev/Makefile | 6 +- _dev/interop_test_runner | 16 +- _dev/tris-localserver/Dockerfile | 1 + _dev/tris-localserver/runner.sh | 1 + _dev/tris-localserver/server.go | 16 ++ _dev/tris-testclient/client.go | 33 +++- common.go | 12 +- 8 files changed, 270 insertions(+), 68 deletions(-) diff --git a/13.go b/13.go index 89ff022..308f441 100644 --- a/13.go +++ b/13.go @@ -21,6 +21,7 @@ import ( "sync/atomic" "time" + sidh "github_com/cloudflare/sidh/sidh" "golang_org/x/crypto/curve25519" ) @@ -29,11 +30,16 @@ import ( const numSessionTickets = 2 type secretLabel int -type role uint8 const ( - kRole_Server = iota - kRole_Client + x25519SharedSecretSz = 32 + + P503PubKeySz = 378 + P503PrvKeySz = 32 + P503SharedSecretSz = 126 + SidhP503Curve25519PubKeySz = x25519SharedSecretSz + P503PubKeySz + SidhP503Curve25519PrvKeySz = x25519SharedSecretSz + P503PrvKeySz + SidhP503Curve25519SharedKeySz = x25519SharedSecretSz + P503SharedSecretSz ) const ( @@ -55,6 +61,38 @@ type keySchedule13 struct { config *Config // Used for KeyLogWriter callback, nil if keylogging is disabled. } +// Interface implemented by DH key exchange strategies +type dhKex interface { + // c - context of current TLS handshake, groupId - ID of an algorithm + // (curve/field) being chosen for key agreement. Methods implmenting an + // interface always assume that provided groupId is correct. + // + // In case of success, function returns secret key and ephemeral key. Otherwise + // error is set. + generate(c *Conn, groupId CurveID) ([]byte, keyShare, error) + // c - context of current TLS handshake, ks - public key received + // from the other side of the connection, secretKey - is a private key + // used for DH key agreement. Function returns shared secret in case + // of success or empty slice otherwise. + derive(c *Conn, ks keyShare, secretKey []byte) []byte +} + +// Key Exchange strategies per curve type +type kexNist struct{} // Used by NIST curves; P-256, P-384, P-512 +type kexX25519 struct{} // Used by X25519 +type kexSidhP503 struct{} // Used by SIDH/P503 +type kexHybridSidhP503X25519 struct{} // Used by SIDH-ECDH hybrid scheme + +// Routing map for key exchange strategies +var dhKexStrat = map[CurveID]dhKex{ + CurveP256: &kexNist{}, + CurveP384: &kexNist{}, + CurveP521: &kexNist{}, + X25519: &kexX25519{}, + sidhP503: &kexSidhP503{}, + HybridSidhP503Curve25519: &kexHybridSidhP503X25519{}, +} + func newKeySchedule13(suite *cipherSuite, config *Config, clientRandom []byte) *keySchedule13 { if config.KeyLogWriter == nil { clientRandom = nil @@ -80,6 +118,15 @@ func (ks *keySchedule13) setSecret(secret []byte) { ks.secret = hkdfExtract(hash, secret, salt) } +// Depending on role returns pair of key variant to be used by +// local and remote process. +func getSidhKeyVariant(isClient bool) (sidh.KeyVariant, sidh.KeyVariant) { + if isClient { + return sidh.KeyVariant_SIDH_A, sidh.KeyVariant_SIDH_B + } + return sidh.KeyVariant_SIDH_B, sidh.KeyVariant_SIDH_A +} + // write appends the data to the transcript hash context. func (ks *keySchedule13) write(data []byte) { ks.handshakeCtx = nil @@ -186,7 +233,7 @@ CurvePreferenceLoop: earlyClientCipher, _ := hs.keySchedule.prepareCipher(secretEarlyClient) - ecdheSecret := c.deriveECDHESecret(ks, privateKey) + ecdheSecret := c.deriveDHESecret(ks, privateKey) if ecdheSecret == nil { c.sendAlert(alertIllegalParameter) return errors.New("tls: bad ECDHE client share") @@ -551,61 +598,22 @@ func prepareDigitallySigned(hash crypto.Hash, context string, data []byte) []byt return h.Sum(nil) } +// generateKeyShare generates keypair. Private key is returned as first argument, public key +// is returned in keyShare.data. keyshare.curveID stores ID of the scheme used. func (c *Conn) generateKeyShare(curveID CurveID) ([]byte, keyShare, error) { - if curveID == X25519 { - var scalar, public [32]byte - if _, err := io.ReadFull(c.config.rand(), scalar[:]); err != nil { - return nil, keyShare{}, err - } - - curve25519.ScalarBaseMult(&public, &scalar) - return scalar[:], keyShare{group: curveID, data: public[:]}, nil + if val, ok := dhKexStrat[curveID]; ok { + return val.generate(c, curveID) } - - curve, ok := curveForCurveID(curveID) - if !ok { - return nil, keyShare{}, errors.New("tls: preferredCurves includes unsupported curve") - } - - privateKey, x, y, err := elliptic.GenerateKey(curve, c.config.rand()) - if err != nil { - return nil, keyShare{}, err - } - ecdhePublic := elliptic.Marshal(curve, x, y) - - return privateKey, keyShare{group: curveID, data: ecdhePublic}, nil + return nil, keyShare{}, errors.New("tls: preferredCurves includes unsupported curve") } -func (c *Conn) deriveECDHESecret(ks keyShare, secretKey []byte) []byte { - if ks.group == X25519 { - if len(ks.data) != 32 { - return nil - } - - var theirPublic, sharedKey, scalar [32]byte - copy(theirPublic[:], ks.data) - copy(scalar[:], secretKey) - curve25519.ScalarMult(&sharedKey, &scalar, &theirPublic) - return sharedKey[:] +// DH key agreement. ks stores public key, secretKey stores private key used for ephemeral +// key agreement. Function returns shared secret in case of success or empty slice otherwise. +func (c *Conn) deriveDHESecret(ks keyShare, secretKey []byte) []byte { + if val, ok := dhKexStrat[ks.group]; ok { + return val.derive(c, ks, secretKey) } - - curve, ok := curveForCurveID(ks.group) - if !ok { - return nil - } - x, y := elliptic.Unmarshal(curve, ks.data) - if x == nil { - return nil - } - x, _ = curve.ScalarMult(x, y, secretKey) - xBytes := x.Bytes() - curveSize := (curve.Params().BitSize + 8 - 1) >> 3 - if len(xBytes) == curveSize { - return xBytes - } - buf := make([]byte, curveSize) - copy(buf[len(buf)-len(xBytes):], xBytes) - return buf + return nil } func hkdfExpandLabel(hash crypto.Hash, secret, hashValue []byte, label string, L int) []byte { @@ -981,7 +989,7 @@ func (hs *clientHandshakeState) doTLS13Handshake() error { // 0-RTT is not supported yet, so use an empty PSK. hs.keySchedule.setSecret(nil) - ecdheSecret := c.deriveECDHESecret(serverHello.keyShare, hs.privateKey) + ecdheSecret := c.deriveDHESecret(serverHello.keyShare, hs.privateKey) if ecdheSecret == nil { c.sendAlert(alertIllegalParameter) return errors.New("tls: bad ECDHE server share") @@ -1161,3 +1169,138 @@ func supportedSigAlgorithmsCert(schemes []SignatureScheme) (ret []SignatureSchem } return } + +// Functions below implement dhKex interface for different DH shared secret agreements + +// KEX: P-256, P-384, P-512 KEX +func (kexNist) generate(c *Conn, groupId CurveID) (private []byte, ks keyShare, err error) { + // never fails + curve, _ := curveForCurveID(groupId) + private, x, y, err := elliptic.GenerateKey(curve, c.config.rand()) + if err != nil { + return nil, keyShare{}, err + } + ks.group = groupId + ks.data = elliptic.Marshal(curve, x, y) + return +} +func (kexNist) derive(c *Conn, ks keyShare, secretKey []byte) []byte { + // never fails + curve, _ := curveForCurveID(ks.group) + x, y := elliptic.Unmarshal(curve, ks.data) + if x == nil { + return nil + } + x, _ = curve.ScalarMult(x, y, secretKey) + xBytes := x.Bytes() + curveSize := (curve.Params().BitSize + 8 - 1) >> 3 + if len(xBytes) == curveSize { + return xBytes + } + buf := make([]byte, curveSize) + copy(buf[len(buf)-len(xBytes):], xBytes) + return buf +} + +// KEX: X25519 +func (kexX25519) generate(c *Conn, groupId CurveID) ([]byte, keyShare, error) { + var scalar, public [x25519SharedSecretSz]byte + if _, err := io.ReadFull(c.config.rand(), scalar[:]); err != nil { + return nil, keyShare{}, err + } + curve25519.ScalarBaseMult(&public, &scalar) + return scalar[:], keyShare{group: X25519, data: public[:]}, nil +} + +func (kexX25519) derive(c *Conn, ks keyShare, secretKey []byte) []byte { + var theirPublic, sharedKey, scalar [x25519SharedSecretSz]byte + if len(ks.data) != x25519SharedSecretSz { + return nil + } + copy(theirPublic[:], ks.data) + copy(scalar[:], secretKey) + curve25519.ScalarMult(&sharedKey, &scalar, &theirPublic) + return sharedKey[:] +} + +// KEX: SIDH/503 +func (kexSidhP503) generate(c *Conn, groupId CurveID) ([]byte, keyShare, error) { + var variant, _ = getSidhKeyVariant(c.isClient) + var prvKey = sidh.NewPrivateKey(sidh.FP_503, variant) + if prvKey.Generate(c.config.rand()) != nil { + return nil, keyShare{}, errors.New("tls: private SIDH key generation failed") + } + pubKey := prvKey.GeneratePublicKey() + return prvKey.Export(), keyShare{group: sidhP503, data: pubKey.Export()}, nil +} + +func (kexSidhP503) derive(c *Conn, ks keyShare, key []byte) []byte { + var prvVariant, pubVariant = getSidhKeyVariant(c.isClient) + var prvKeySize = P503PrvKeySz + + if len(ks.data) != P503PubKeySz || len(key) != prvKeySize { + return nil + } + + prvKey := sidh.NewPrivateKey(sidh.FP_503, prvVariant) + pubKey := sidh.NewPublicKey(sidh.FP_503, pubVariant) + + if err := prvKey.Import(key); err != nil { + return nil + } + if err := pubKey.Import(ks.data); err != nil { + return nil + } + + // Never fails + sharedKey, _ := sidh.DeriveSecret(prvKey, pubKey) + return sharedKey +} + +// KEX Hybrid SIDH/503-X25519 +func (kexHybridSidhP503X25519) generate(c *Conn, groupId CurveID) (private []byte, ks keyShare, err error) { + var pubHybrid [SidhP503Curve25519PubKeySz]byte + var prvHybrid [SidhP503Curve25519PrvKeySz]byte + + // Generate ephemeral key for classic x25519 + private, ks, err = dhKexStrat[X25519].generate(c, groupId) + if err != nil { + return + } + copy(prvHybrid[:], private) + copy(pubHybrid[:], ks.data) + + // Generate PQ ephemeral key for SIDH + private, ks, err = dhKexStrat[sidhP503].generate(c, groupId) + if err != nil { + return + } + copy(prvHybrid[x25519SharedSecretSz:], private) + copy(pubHybrid[x25519SharedSecretSz:], ks.data) + return prvHybrid[:], keyShare{group: HybridSidhP503Curve25519, data: pubHybrid[:]}, nil +} + +func (kexHybridSidhP503X25519) derive(c *Conn, ks keyShare, key []byte) []byte { + var sharedKey [SidhP503Curve25519SharedKeySz]byte + var ret []byte + var tmpKs keyShare + + // Key agreement for classic + tmpKs.group = X25519 + tmpKs.data = ks.data[:x25519SharedSecretSz] + ret = dhKexStrat[X25519].derive(c, tmpKs, key[:x25519SharedSecretSz]) + if ret == nil { + return nil + } + copy(sharedKey[:], ret) + + // Key agreement for PQ + tmpKs.group = sidhP503 + tmpKs.data = ks.data[x25519SharedSecretSz:] + ret = dhKexStrat[sidhP503].derive(c, tmpKs, key[x25519SharedSecretSz:]) + if ret == nil { + return nil + } + copy(sharedKey[x25519SharedSecretSz:], ret) + return sharedKey[:] +} diff --git a/_dev/Makefile b/_dev/Makefile index 8766080..408c560 100644 --- a/_dev/Makefile +++ b/_dev/Makefile @@ -15,7 +15,7 @@ OS ?= $(shell $(GO) env GOHOSTOS) ARCH ?= $(shell $(GO) env GOHOSTARCH) OS_ARCH := $(OS)_$(ARCH) VER_OS_ARCH := $(shell $(GO) version | cut -d' ' -f 3)_$(OS)_$(ARCH) -GOROOT_ENV := $(shell $(GO) env GOROOT) +GOROOT_ENV ?= $(shell $(GO) env GOROOT) GOROOT_LOCAL = $(BUILD_DIR)/$(OS_ARCH) # Flag indicates wheter invoke "go install -race std". Supported only on amd64 with CGO enabled INSTALL_RACE:= $(words $(filter $(ARCH)_$(shell go env CGO_ENABLED), amd64_1)) @@ -30,8 +30,10 @@ BOGO_DOCKER_TRIS_LOCATION=/go/src/github.com/cloudflare/tls-tris # SIDH repository (TODO: change path) SIDH_REPO ?= https://github.com/cloudflare/sidh.git +SIDH_REPO_TAG ?= 25c0d9f15d5e5bd5652b9741aeb50d9eb37154dc # NOBS repo (SIKE depends on SHA3) NOBS_REPO ?= https://github.com/henrydcase/nobscrypto.git +NOBS_REPO_TAG ?= 597f68906e981e61160b8c959b326596c0b240be ############### # @@ -57,11 +59,13 @@ $(BUILD_DIR)/$(OS_ARCH)/.ok_$(VER_OS_ARCH): clean # Vendor NOBS library $(GIT) clone $(NOBS_REPO) $(TMP_DIR)/nobs + cd $(TMP_DIR)/nobs; $(GIT) checkout $(NOBS_REPO_TAG) cd $(TMP_DIR)/nobs; make vendor-sidh-for-tls cp -rf $(TMP_DIR)/nobs/tls_vendor/* $(GOROOT_LOCAL)/src/vendor/ # Vendor SIDH library $(GIT) clone $(SIDH_REPO) $(TMP_DIR)/sidh + cd $(TMP_DIR)/sidh; $(GIT) checkout $(SIDH_REPO_TAG) cd $(TMP_DIR)/sidh; make vendor cp -rf $(TMP_DIR)/sidh/build/vendor/* $(GOROOT_LOCAL)/src/vendor/ diff --git a/_dev/interop_test_runner b/_dev/interop_test_runner index 7c65395..3b92e4e 100755 --- a/_dev/interop_test_runner +++ b/_dev/interop_test_runner @@ -216,12 +216,10 @@ class InteropServer_BoringSSL(InteropServer, ServerNominalMixin, ServerClientAut Checks wether ALPN is sent back by tris server in EncryptedExtensions in case of TLS 1.3. The ALPN protocol is set to 'npn_proto', which is hardcoded in TRIS test server. ''' - res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":1443 "+'-alpn-protos npn_proto -debug') - print(res[1]) + res = self.d.run_client(self.CLIENT_NAME, self.server_ip+":1443 "+'-alpn-protos npn_proto') self.assertEqual(res[0], 0) self.assertIsNotNone(re.search(RE_PATTERN_ALPN, res[1], re.MULTILINE)) - # PicoTLS doesn't seem to implement draft-23 correctly. It will # be enabled when draft-28 is implemented. # class InteropServer_PicoTLS( @@ -262,5 +260,17 @@ class InteropServer_TRIS(ClientNominalMixin, InteropServer, unittest.TestCase): res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=false -cliauth '+self.server_ip+":6443") self.assertEqual(res[0], 0) + def test_qr(self): + res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=true -qr SIDH-P503-X25519 '+self.server_ip+":7443") + self.assertEqual(res[0], 0) + + def test_qrServerDoesntSupportSIDH(self): + ''' + Client advertises HybridSIDH and ECDH. Server supports ECDH only. Checks weather + TLS session can still be established. + ''' + res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=true '+self.server_ip+":7443") + self.assertEqual(res[0], 0) + if __name__ == '__main__': unittest.main() diff --git a/_dev/tris-localserver/Dockerfile b/_dev/tris-localserver/Dockerfile index aa23784..668b44d 100644 --- a/_dev/tris-localserver/Dockerfile +++ b/_dev/tris-localserver/Dockerfile @@ -8,6 +8,7 @@ EXPOSE 3443 EXPOSE 4443 EXPOSE 5443 EXPOSE 6443 +EXPOSE 7443 ADD tris-localserver / ADD runner.sh / diff --git a/_dev/tris-localserver/runner.sh b/_dev/tris-localserver/runner.sh index 8871b51..32d1600 100755 --- a/_dev/tris-localserver/runner.sh +++ b/_dev/tris-localserver/runner.sh @@ -6,5 +6,6 @@ ./tris-localserver -b 0.0.0.0:4443 -cert=ecdsa -rtt0=oa 2>&1 & # fourth port: offer and accept 0-RTT ./tris-localserver -b 0.0.0.0:5443 -cert=ecdsa -rtt0=oa -rtt0ack 2>&1 & # fifth port: offer and accept 0-RTT but confirm ./tris-localserver -b 0.0.0.0:6443 -cert=rsa -cliauth 2>&1 & # sixth port: RSA with required client authentication +./tris-localserver -b 0.0.0.0:7443 -cert=ecdsa -qr=c & # Enables support for both - post-quantum and classical KEX algorithms wait diff --git a/_dev/tris-localserver/server.go b/_dev/tris-localserver/server.go index 15c4709..6298b5b 100644 --- a/_dev/tris-localserver/server.go +++ b/_dev/tris-localserver/server.go @@ -55,6 +55,15 @@ func NewServer() *server { return s } +func enableQR(s *server, enableDefault bool) { + var sidhCurves = []tls.CurveID{tls.HybridSidhP503Curve25519} + if enableDefault { + var defaultCurvePreferences = []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384, tls.CurveP521} + s.TLS.CurvePreferences = append(s.TLS.CurvePreferences, defaultCurvePreferences...) + } + s.TLS.CurvePreferences = append(s.TLS.CurvePreferences, sidhCurves...) +} + func (s *server) start() { var err error if (s.ZeroRTT & ZeroRTT_Offer) == ZeroRTT_Offer { @@ -144,6 +153,7 @@ func main() { arg_zerortt := flag.String("rtt0", "n", `0-RTT, accepts following values [n: None, a: Accept, o: Offer, oa: Offer and Accept]`) arg_confirm := flag.Bool("rtt0ack", false, "0-RTT confirm") arg_clientauth := flag.Bool("cliauth", false, "Performs client authentication (RequireAndVerifyClientCert used)") + arg_qr := flag.String("qr", "", "Enable quantum-resistant algorithms [c: Support classical and Quantum-Resistant, q: Enable Quantum-Resistant only]") flag.Parse() s.Address = *arg_addr @@ -162,6 +172,12 @@ func main() { s.TLS.ClientAuth = tls.RequireAndVerifyClientCert } + if *arg_qr == "c" { + enableQR(s, true) + } else if *arg_qr == "q" { + enableQR(s, false) + } + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { tlsConn := r.Context().Value(http.TLSConnContextKey).(*tls.Conn) diff --git a/_dev/tris-testclient/client.go b/_dev/tris-testclient/client.go index 21c22b4..9d7fdad 100644 --- a/_dev/tris-testclient/client.go +++ b/_dev/tris-testclient/client.go @@ -51,6 +51,17 @@ func (c *Client) setMinMaxTLS(ver uint16) { c.TLS.MaxVersion = ver } +func getQrAlgoId(qr string) tls.CurveID { + switch qr { + case "SIDH-P503-X25519": + return tls.HybridSidhP503Curve25519 + case "SIDH-P751-X448": + return tls.HybridSidhP751Curve448 + default: + return 0 + } +} + func (c *Client) run() { fmt.Printf("TLS %s with %s\n", tlsVersionToName[c.TLS.MinVersion], cipherSuiteIdToName[c.TLS.CipherSuites[0]]) @@ -78,8 +89,7 @@ func (c *Client) run() { failed++ return } - fmt.Printf("Read %d bytes\n", n) - + fmt.Printf("[TLS: %s] Read %d bytes\n", tlsVersionToName[con.ConnectionState().Version], n) fmt.Println("OK\n") } @@ -93,13 +103,14 @@ func result() { // Usage client args host:port func main() { - var keylog_file string + var keylog_file, qrAlgoName string var enable_rsa, enable_ecdsa, client_auth bool flag.StringVar(&keylog_file, "keylogfile", "", "Secrets will be logged here") flag.BoolVar(&enable_rsa, "rsa", true, "Whether to enable RSA cipher suites") flag.BoolVar(&enable_ecdsa, "ecdsa", true, "Whether to enable ECDSA cipher suites") flag.BoolVar(&client_auth, "cliauth", false, "Whether to enable client authentication") + flag.StringVar(&qrAlgoName, "qr", "", "Specifies qr algorithm from following list:\n[SIDH-P503-X25519, SIDH-P751-X448]") flag.Parse() if flag.NArg() != 1 { flag.Usage() @@ -124,6 +135,21 @@ func main() { log.Println("Enabled keylog") } + if len(qrAlgoName) > 0 { + id := getQrAlgoId(qrAlgoName) + if id == 0 { + log.Fatalf("Unknown QR algorithm: %s", qrAlgoName) + return + } + + client.TLS.CipherSuites = []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256} + client.TLS.CurvePreferences = []tls.CurveID{id} + client.setMinMaxTLS(tls.VersionTLS13) + client.run() + result() + return + } + if client_auth { var err error client_cert, err := tls.X509KeyPair([]byte(client_crt), []byte(client_key)) @@ -145,6 +171,7 @@ func main() { c.setMinMaxTLS(tls.VersionTLS12) c.run() } + if enable_ecdsa { // Sane cipher suite for TLS 1.2 with an ECDSA cert (as used by boringssl) c := client.clone() diff --git a/common.go b/common.go index 66288b8..471fd1d 100644 --- a/common.go +++ b/common.go @@ -116,10 +116,6 @@ const ( type CurveID uint16 const ( - // Unexported - sidhP503 CurveID = 0 - sidhP751 CurveID = 1 - // Exported IDs CurveP256 CurveID = 23 CurveP384 CurveID = 24 @@ -127,8 +123,12 @@ const ( X25519 CurveID = 29 // Experimental KEX - HybridSidhP503Curve25519 CurveID = 0x0105 + sidhP503 // HybridSIDH: X25519 + P503 - HybridSidhP751Curve448 CurveID = 0x0105 + sidhP751 // HybridSIDH: X448 + P751 + HybridSidhP503Curve25519 CurveID = 0x0105 + (sidhP503 & 0xFF) // HybridSIDH: X25519 + P503 + HybridSidhP751Curve448 CurveID = 0x0105 + (sidhP751 & 0xFF) // HybridSIDH: X448 + P751 + + // Internal usage. Deliberately not exported + sidhP503 CurveID = 0xFE00 + sidhP751 CurveID = 0xFE01 ) // TLS 1.3 Key Share