For Security, Go Hybrid: Hybrid Public Key Encryption (HPKE) and RFC 9180

We love public key encryption. It allows us to securely exchange secrets and digitally sign data. But, it’s not that efficient when it…

For Security, Go Hybrid: Hybrid Public Key Encryption (HPKE) and RFC 9180

We love public key encryption. It allows us to securely exchange secrets and digitally sign data. But, it’s not that efficient when it comes to actually encrypting large amounts of data.

For this, Bob sends Alice his public key, and she encrypts the data with this and sends back the ciphertext. Bob then uses the associated private key to decrypt it. This works fairly well for small amounts of data — such as encrypting a 128-bit or a 256-bit encryption key — but becomes computationally expensive when encrypting large amounts of data. This is especially important when we are using a mobile device, as this increase in computing will often drain the battery. The core method we have at hand to encrypt data is to use RSA, but RSA is often a heavy method to implement with mobile devices.

So, what’s the solution? Can we combine the power of symmetric key with public key encryption? Well, RFC 9180 provides one solution with HPKE (Hybrid Public Key Encryption) [here]:

With this, we use a symmetric key to encrypt the data, and then encrypt the symmetric key with public key encryption. We can also sign our data with the private key, and which can be checked with the associated public key. In this way we can also integrate authentication into transmitted data, and where we can properly authenticate the sender of the data (using the sender’s public key).

And, so, Apple has just announced that their CryptoKit will now support HPKE in a Beta form:

Along with providing authenticated encryption with additional data (AEAD) algorithm, the library supports a key derivation function (KDF) to create shared keys:

  • HKDF_SHA256. This uses an HMAC-based key derivation function with SHA-256.
  • HKDF_SHA384. This uses an HMAC-based key derivation function with SHA-384.
  • HKDF_SHA512. This uses an HMAC-based key derivation function with SHA-512.

The key encapsulation mechanism (KEM) is used to pass the shared key. For efficiency it uses ECC (Elliptic Curve Cryptography) with either Curve 25519 or P256:

  • Curve25519_HKDF_SHA256. This uses X25519 with a SHA-256 hash.
  • P256_HKDF_SHA256. This uses the P256 (secp256r1) curve with a SHA-256 hash.
  • P384_HKDF_SHA384 . This uses the NIST P384 curve with a SHA-384 hash.
  • P521_HKDF_SHA512. This uses the NIST P521 curve with a SHA-512 hash, and has the strongest security.

For the symmetric key encryption, there are two main methods: AES with GCM mode and ChaCha20/Poly1305:

  • AES_GCM_128. This uses 128-bit AES with Galois/Counter Mode (GCM). This is a proven fast encryption mode for AES, and converts the block cipher into a stream cipher.
  • AES_GCM_256. This uses 256-bit AES with Galois/Counter Mode (GCM).
  • chaChaPoly. This uses the ChaCha20 stream cipher with the Poly1305 MAC (message authentication code).

Hybrid Encryption

Many other libraries have moved to adopt HPKE, including the CIRCL library:

https://asecuritysite.com/golang/go_hybrid

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 toward 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 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.

In this method, Alice generates a random private key (dA) and then takes a point on an elliptic curve (G) and then determines her public key (QA):

QA=dA×G

G and QA are thus points on an elliptic curve. Alice then sends QA to Bob. Next Bob will generate:

R=r×G

S=r×QA

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=dA×R

which is:

S=dA×(r×G)

S=r×(dA×G)

S=r×QA

and which is the same as the key that Bob generated. The method is illustrated here:

A sample run is [here[:

Public key type:	HPKE_KEM_P256_HKDF_SHA256
Params kem_id: 16 kdf_id: 1 aead_id: 1
Key exchange parameters:
Ciphersize: 65
EncapsulationSeedSize: 32
PrivateKeySize: 32
PublicKeySize: 65
SeedSize: 32
SharedKeySize: 32
Cipher parameters:
Key Length: 16
Key derivation function:
Extract size: 32
Message: Testing 123
Cipher: 343363373461353461343437666463613435653437383435666537653264666334306331623239653439303333646236646334323663
Decipher: Testing 123

The code is based on [here]:

package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"strconv"
"github.com/cloudflare/circl/hpke"
)
func main() {
kemID := int(hpke.KEM_P256_HKDF_SHA256)
kdfID := int(hpke.KDF_HKDF_SHA256)
aeadID := int(hpke.AEAD_AES128GCM)
msg := "Hello"
argCount := len(os.Args[1:])
if argCount > 0 {
msg = os.Args[1]
}
if argCount > 1 {
kemID, _ = strconv.Atoi(os.Args[2])
}
if argCount > 2 {
kdfID, _ = strconv.Atoi(os.Args[3])
}
if argCount > 3 {
aeadID, _ = strconv.Atoi(os.Args[4])
}
suite := hpke.NewSuite(hpke.KEM(kemID), hpke.KDF(kdfID), hpke.AEAD(aeadID))
info := []byte("Test")
Bob_pub, Bob_private, _ := hpke.KEM(kemID).Scheme().GenerateKeyPair()
Bob, _ := suite.NewReceiver(Bob_private, info)
Alice, _ := suite.NewSender(Bob_pub, info)
enc, sealer, _ := Alice.Setup(rand.Reader)
Alice_msg := []byte(msg)
aad := []byte("Additional data")
ct, _ := sealer.Seal(Alice_msg, aad)
opener, _ := Bob.Setup(enc)
Bob_msg, _ := opener.Open(ct, aad)
fmt.Printf("Public key type:\t%s\n", Bob_pub.Scheme().Name())
fmt.Printf(" Params\t%s\n", suite.String())
fmt.Printf("Key exchange parameters:\n")
fmt.Printf(" Ciphersize:\t%d\n", hpke.KEM(kemID).Scheme().CiphertextSize())
fmt.Printf(" EncapsulationSeedSize:\t%d\n", hpke.KEM(kemID).Scheme().EncapsulationSeedSize())
fmt.Printf(" PrivateKeySize:\t%d\n", hpke.KEM(kemID).Scheme().PrivateKeySize())
fmt.Printf(" PublicKeySize:\t%d\n", hpke.KEM(kemID).Scheme().PublicKeySize())
fmt.Printf(" SeedSize:\t%d\n", hpke.KEM(kemID).Scheme().SeedSize())
fmt.Printf(" SharedKeySize:\t%d\n", hpke.KEM(kemID).Scheme().SharedKeySize())
fmt.Printf("Cipher parameters:\n")
fmt.Printf(" Key Length:\t%d\n", hpke.AEAD(aeadID).KeySize())
fmt.Printf("Key derivation function:\n")
fmt.Printf(" Extract size:\t%d\n", hpke.KDF(kdfID).ExtractSize())
fmt.Printf("\nMessage:\t%s\n", Alice_msg)
fmt.Printf("Cipher:\t%x\n", hex.EncodeToString(ct))
fmt.Printf("Decipher:\t%s\n", Bob_msg)
}

Conclusions

While OpenSSL offers so many encryption methods, it can leave applications open to attack from the usage of legacy methods. Along with this, methods such as RSA encryption are heavy on the battery. Legacy hashing methods, such as MD5 and SHA-1 also expose applications to attack. And, so, RFC 9180 provides a way to use the best security around, and also methods that are efficient. So, go Hybrid!