HPKE (Hybrid Public Key Encryption) with Go and CIRCLWith ECIES (Elliptic Curve Integrated Encryption Scheme) we use the public key from Elliptic Curve Cryptography in order to derive a symmetric key. In this case we create a symmetric key from Elliptic Curve Cryptography, and then use this to encrypt with AES-GCM. In this case we will use the Cloudflare CIRCL library, and will implement a range of Key Exchange Mechanisms, including the PQC Hybrid Exchange method of Kyber 768 and X25519 key exchange. The Kyber method is PQC, and X25519 provides a traditional key exchange with elliptic curve methods (and which is not quantum robust). |
Theory
Outline
With ECC (Elliptic Curve Cryptography), we have an opportunity to use both the power of public-key encryption, with the speed and security of symmetric key encryption. And, so we slowly move to the best practice for encryption, where there’s an increasing consensus around:
- Public key encryption curve: P256, P384, P521, X25519 and X448.
- Hashing method for key derivation (HKDF): SHA256, SHA384 and SHA512.
- Symmetric key: 128-bit AES GCM and 256-bit AES GCM.
All of the above methods are compatible with most systems. For this Bob and Alice will pick a curve to define their key pair, and then use given hashing methods to derive an encryption key. This is normally achieved with HKDF (HMAC Key Derivation Function). For the actual encryption, we can use symmetric-key encryption, as this is the most efficient and much faster than public key encryption. Overall, with this, there is a general move towards using AEAD (Authenticated Encryption with Additional Data). A typical mode for this is GCM. So let’s build a hybrid encryption method with Golang.
Now, let’s say that Bob will send an encrypted message to Alice. Alice will then generate a key pair (a public key and a private key). She then sends her public key to Bob, and he then uses this to derive a symmetric key for the encryption (\(S\)). He then encrypts the message using \(K\) and with AES GCM. Bob receives the cipher (\(C\)) and a value of \(R\). From \(R\), she can then derive S from her private key. With this key, she can decrypt the cipher text to derive the plaintext message.
Theory
In this method Alice generates a random private key (\(d_A\)) and the takes a point on an elliptic curve (\(G\)) and then determines her public key (\(Q_A\)):
\(Q_A = d_A \times G\)
G and \(Q_A\) are thus points on an elliptic curve. Alice then sends \(Q_A\) to Bob. Next Bob will generate:
\(R = r \times G\)
\(S = r \times Q_A\)
and where r is a random number generated by Bob. The symmetric key (S) is then used to encrypt a message.
Alice will then receive the encrypted message along with \(R\). She is then able to determine the same encryption key with::
\(S = d_A \times R\)
which is:
\(S = d_A \times (r \times G)\)
\(S = r \times (d_A \times G)\)
\(S = r \times Q_A\)
and which is the same as the key that Bob generated.
The method is illustrated here:
Coding
The code is:
package main import ( "os" "fmt" "github.com/cloudflare/circl/hpke" "crypto/rand" ) func main() { msg:="Hello" inf:="Info" kem:="KEM_P256_HKDF_SHA256" kemID := hpke.KEM_P256_HKDF_SHA256 argCount := len(os.Args[1:]) if argCount > 0 { msg = (os.Args[1]) } if argCount >1 { kem = (os.Args[2]) } if (kem=="KEM_P256_HKDF_SHA256") { kemID = hpke.KEM_P256_HKDF_SHA256 } if (kem=="KEM_P384_HKDF_SHA384") { kemID = hpke.KEM_P384_HKDF_SHA384 } if (kem=="KEM_P521_HKDF_SHA512") { kemID = hpke.KEM_P521_HKDF_SHA512 } if (kem=="KEM_X25519_HKDF_SHA256") { kemID = hpke.KEM_X25519_HKDF_SHA256 } if (kem=="KEM_X448_HKDF_SHA512") { kemID = hpke.KEM_X448_HKDF_SHA512 } if (kem=="KEM_X25519_KYBER768_DRAFT00") { kemID = hpke.KEM_X25519_KYBER768_DRAFT00 } // HPKE suite is a domain parameter. kdfID := hpke.KDF_HKDF_SHA384 aeadID := hpke.AEAD_AES256GCM suite := hpke.NewSuite(kemID, kdfID, aeadID) info := []byte(inf) // Generate Bob's key pair publicBob, privateBob, _ := kemID.Scheme().GenerateKeyPair() Bob, _ := suite.NewReceiver(privateBob, info) // Send public key to Alice Alice, _ := suite.NewSender(publicBob, info) enc, sealer, _ := Alice.Setup(rand.Reader) // Alice encrypts and sends to Bob ptAlice := []byte(msg) aad := []byte("additional public data") ct, _ := sealer.Seal(ptAlice, aad) // Bob decrypts the ciphertext. opener, _ := Bob.Setup(enc) ptBob, _ := opener.Open(ct, aad) fmt.Printf("KEM:\t\t%v\n",kem) fmt.Printf("\nBob private:\t%v\n",privateBob) fmt.Printf("Bob public:\t%v\n",publicBob) fmt.Printf("\nAlice plaintext:\t%s\n",ptAlice) fmt.Printf("ALice ciphertext:\t%x\n",ct) fmt.Printf("Bob decrypt:\t\t%s\n",ptBob) }
For a test run using P256 for the public key method and a SHA-256 hash:
iKEM: KEM_P256_HKDF_SHA256 Bob private: ddd59571651ef0bb4a4bdf3ec9dec76ac231fc427112b670daeb5d4494e56809 Bob public: x: 3c02a58c49e63a0aa9065186f43d5365258b5f1d95a6139c99b4fb8421531126 y: 5bd23365a9297338718aacd7278d633f4d07b424033f4c5a753edcb10b9daa0 Alice plaintext: Testing 123 ALice ciphertext: ac565e440934e20b44b942b6733ae961211856ab91b58553e1eb8e Bob decrypt: Testing 123