summaryrefslogtreecommitdiffstats
path: root/security-utils
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@yahooinc.com>2022-10-20 10:56:07 +0200
committerTor Brede Vekterli <vekterli@yahooinc.com>2022-10-20 10:56:07 +0200
commit5d41f068b349fe0ddb2bd5eb4bd4af5ed0a7cff3 (patch)
tree491a3f1a4baa104a974eab5db16e36afbfbb822a /security-utils
parentfe64bb97196040f633a9d1da5c83d808f9c324dd (diff)
Use HPKE instead of ECIES for shared single-use keys
Also use AES-128 instead of AES-256 for the one-time key since the underlying HPKE AEAD cipher protecting the key itself is AES-128.
Diffstat (limited to 'security-utils')
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java30
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java77
-rw-r--r--security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java46
3 files changed, 65 insertions, 88 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java
index 237c4976c7c..9c379b67a23 100644
--- a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java
+++ b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java
@@ -15,12 +15,12 @@ import java.util.Base64;
* This token representation is expected to be used as a convenient serialization
* form when communicating shared keys.
*/
-public record SealedSharedKey(int keyId, byte[] eciesPayload, byte[] iv) {
+public record SealedSharedKey(int keyId, byte[] enc, byte[] ciphertext) {
/** Current encoding version of opaque sealed key tokens. Must be less than 256. */
public static final int CURRENT_TOKEN_VERSION = 1;
- private static final int ECIES_AES_IV_LENGTH = SharedKeyGenerator.ECIES_AES_CBC_IV_BITS / 8;
+ private static final int MAX_ENC_CONTEXT_LENGTH = Short.MAX_VALUE;
/**
* Creates an opaque URL-safe string token that contains enough information to losslessly
@@ -30,14 +30,16 @@ public record SealedSharedKey(int keyId, byte[] eciesPayload, byte[] iv) {
if (keyId >= (1 << 24)) {
throw new IllegalArgumentException("Key id is too large to be encoded");
}
- if (iv.length != ECIES_AES_IV_LENGTH) {
- throw new IllegalStateException("Expected a %d byte IV, got %d bytes".formatted(ECIES_AES_IV_LENGTH, iv.length));
+ if (enc.length > MAX_ENC_CONTEXT_LENGTH) {
+ throw new IllegalArgumentException("Encryption context is too large to be encoded");
}
- ByteBuffer encoded = ByteBuffer.allocate(4 + ECIES_AES_IV_LENGTH + eciesPayload.length);
+ // i32 header || i16 length(enc) || enc || ciphertext
+ ByteBuffer encoded = ByteBuffer.allocate(4 + 2 + enc.length + ciphertext.length);
encoded.putInt((CURRENT_TOKEN_VERSION << 24) | keyId);
- encoded.put(iv);
- encoded.put(eciesPayload);
+ encoded.putShort((short)enc.length);
+ encoded.put(enc);
+ encoded.put(ciphertext);
encoded.flip();
byte[] encBytes = new byte[encoded.remaining()];
@@ -61,13 +63,17 @@ public record SealedSharedKey(int keyId, byte[] eciesPayload, byte[] iv) {
throw new IllegalArgumentException("Token had unexpected version. Expected %d, was %d"
.formatted(CURRENT_TOKEN_VERSION, version));
}
- byte[] iv = new byte[ECIES_AES_IV_LENGTH];
- decoded.get(iv);
- byte[] eciesPayload = new byte[decoded.remaining()];
- decoded.get(eciesPayload);
+ short encLen = decoded.getShort();
+ if (encLen <= 0) {
+ throw new IllegalArgumentException("Token encryption context does not have a valid length");
+ }
+ byte[] enc = new byte[encLen];
+ decoded.get(enc);
+ byte[] ciphertext = new byte[decoded.remaining()];
+ decoded.get(ciphertext);
int keyId = versionAndKeyId & 0xffffff;
- return new SealedSharedKey(keyId, eciesPayload, iv);
+ return new SealedSharedKey(keyId, enc, ciphertext);
}
}
diff --git a/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java b/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java
index 07e8243ec09..f9a16a78013 100644
--- a/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java
+++ b/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java
@@ -1,14 +1,16 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.security;
-import org.bouncycastle.jcajce.provider.util.BadBlockException;
-import org.bouncycastle.jce.spec.IESParameterSpec;
+import com.yahoo.security.hpke.Aead;
+import com.yahoo.security.hpke.Ciphersuite;
+import com.yahoo.security.hpke.Hpke;
+import com.yahoo.security.hpke.Kdf;
+import com.yahoo.security.hpke.Kem;
-import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
@@ -17,10 +19,12 @@ import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
+import java.security.interfaces.XECPrivateKey;
+import java.security.interfaces.XECPublicKey;
/**
* Implements both the sender and receiver sides of a secure, anonymous one-way
- * key generation and exchange protocol implemented using ECIES; a hybrid crypto
+ * key generation and exchange protocol implemented using HPKE; a hybrid crypto
* scheme built around elliptic curves.
*
* A shared key, once generated, may have its sealed component sent over a public
@@ -37,52 +41,38 @@ import java.security.SecureRandom;
*/
public class SharedKeyGenerator {
- private static final int AES_GCM_KEY_BITS = 256;
+ private static final int AES_GCM_KEY_BITS = 128;
private static final int AES_GCM_AUTH_TAG_BITS = 128;
private static final String AES_GCM_ALGO_SPEC = "AES/GCM/NoPadding";
- private static final String ECIES_CIPHER_NAME = "ECIESwithSHA256andAES-CBC";
- protected static final int ECIES_AES_CBC_IV_BITS = 128;
- private static final int ECIES_HMAC_BITS = 256;
- private static final int ECIES_AES_KEY_BITS = 256;
+ private static final byte[] EMPTY_BYTES = new byte[0];
private static final SecureRandom SHARED_CSPRNG = new SecureRandom();
+ // Since the HPKE ciphersuite is not provided in the token, we must be very explicit about what it always is
+ private static final Ciphersuite HPKE_CIPHERSUITE = Ciphersuite.of(Kem.dHKemX25519HkdfSha256(), Kdf.hkdfSha256(), Aead.aesGcm128());
+ private static final Hpke HPKE = Hpke.of(HPKE_CIPHERSUITE);
- public static SecretSharedKey generateForReceiverPublicKey(PublicKey receiverPublicKey, int keyId) {
+ private static SecretKey generateRandomSecretAesKey() {
try {
var keyGen = KeyGenerator.getInstance("AES");
keyGen.init(AES_GCM_KEY_BITS, SHARED_CSPRNG);
- var secretKey = keyGen.generateKey();
-
- var cipher = Cipher.getInstance(ECIES_CIPHER_NAME, BouncyCastleProviderHolder.getInstance());
- byte[] iv = new byte[ECIES_AES_CBC_IV_BITS / 8];
- SHARED_CSPRNG.nextBytes(iv);
- var iesParamSpec = new IESParameterSpec(null, null, ECIES_HMAC_BITS, ECIES_AES_KEY_BITS, iv);
-
- cipher.init(Cipher.ENCRYPT_MODE, receiverPublicKey, iesParamSpec);
- byte[] eciesPayload = cipher.doFinal(secretKey.getEncoded());
-
- var sealedSharedKey = new SealedSharedKey(keyId, eciesPayload, iv);
- return new SecretSharedKey(secretKey, sealedSharedKey);
- } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException
- | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
+ return keyGen.generateKey();
+ } catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
- public static SecretSharedKey fromSealedKey(SealedSharedKey sealedKey, PrivateKey receiverPrivateKey) {
- try {
- var cipher = Cipher.getInstance(ECIES_CIPHER_NAME, BouncyCastleProviderHolder.getInstance());
- var iesParamSpec = new IESParameterSpec(null, null, ECIES_HMAC_BITS, ECIES_AES_KEY_BITS, sealedKey.iv());
- cipher.init(Cipher.DECRYPT_MODE, receiverPrivateKey, iesParamSpec);
- byte[] secretKey = cipher.doFinal(sealedKey.eciesPayload());
+ public static SecretSharedKey generateForReceiverPublicKey(PublicKey receiverPublicKey, int keyId) {
+ var secretKey = generateRandomSecretAesKey();
+ // TODO do we want to tie the key ID to the sealing via AAD?
+ var sealed = HPKE.sealBase((XECPublicKey) receiverPublicKey, EMPTY_BYTES, EMPTY_BYTES, secretKey.getEncoded());
+ var sealedSharedKey = new SealedSharedKey(keyId, sealed.enc(), sealed.ciphertext());
+ return new SecretSharedKey(secretKey, sealedSharedKey);
+ }
- return new SecretSharedKey(new SecretKeySpec(secretKey, "AES"), sealedKey);
- } catch (BadBlockException e) {
- throw new IllegalArgumentException("Token integrity check failed; token is either corrupt or was " +
- "generated for a different public key");
- } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException
- | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
- throw new RuntimeException(e);
- }
+ public static SecretSharedKey fromSealedKey(SealedSharedKey sealedKey, PrivateKey receiverPrivateKey) {
+ // TODO do we want to tie the key ID to the opening via AAD?
+ byte[] secretKeyBytes = HPKE.openBase(sealedKey.enc(), (XECPrivateKey) receiverPrivateKey,
+ EMPTY_BYTES, EMPTY_BYTES, sealedKey.ciphertext());
+ return new SecretSharedKey(new SecretKeySpec(secretKeyBytes, "AES"), sealedKey);
}
// A given key+IV pair can only be used for one single encryption session, ever.
@@ -92,15 +82,14 @@ public class SharedKeyGenerator {
// token recipient (which would be the case if the IV were deterministically derived
// from the recipient key and ephemeral ECDH public key), as that would preclude
// support for delegated key forwarding.
- private static byte[] fixed96BitIvForSingleUseKey() {
- // Nothing up my sleeve!
- return new byte[] { 'h', 'e', 'r', 'e', 'B', 'd', 'r', 'a', 'g', 'o', 'n', 's' };
- }
+ private static final byte[] FIXED_96BIT_IV_FOR_SINGLE_USE_KEY = new byte[] {
+ 'h','e','r','e','B','d','r','a','g','o','n','s' // Nothing up my sleeve!
+ };
private static Cipher makeAesGcmCipher(SecretSharedKey secretSharedKey, int cipherMode) {
try {
var cipher = Cipher.getInstance(AES_GCM_ALGO_SPEC);
- var gcmSpec = new GCMParameterSpec(AES_GCM_AUTH_TAG_BITS, fixed96BitIvForSingleUseKey());
+ var gcmSpec = new GCMParameterSpec(AES_GCM_AUTH_TAG_BITS, FIXED_96BIT_IV_FOR_SINGLE_USE_KEY);
cipher.init(cipherMode, secretSharedKey.secretKey(), gcmSpec);
return cipher;
} catch (NoSuchAlgorithmException | NoSuchPaddingException
diff --git a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java
index 26a506015c5..e63c0c48e9d 100644
--- a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java
+++ b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java
@@ -11,7 +11,6 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
-import java.security.KeyPair;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -20,17 +19,17 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class SharedKeyTest {
@Test
- void generated_secret_key_is_256_bit_aes() {
- var receiverKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+ void generated_secret_key_is_128_bit_aes() {
+ var receiverKeyPair = KeyUtils.generateX25519KeyPair();
var shared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), 1);
var secret = shared.secretKey();
assertEquals(secret.getAlgorithm(), "AES");
- assertEquals(secret.getEncoded().length, 32);
+ assertEquals(secret.getEncoded().length, 16);
}
@Test
void sealed_shared_key_can_be_exchanged_via_token_and_computes_identical_secret_key_at_receiver() {
- var receiverKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+ var receiverKeyPair = KeyUtils.generateX25519KeyPair();
var myShared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), 1);
var publicToken = myShared.sealedSharedKey().toTokenString();
@@ -43,49 +42,32 @@ public class SharedKeyTest {
@Test
void token_v1_representation_is_stable() {
- var receiverPrivate = KeyUtils.fromPemEncodedPrivateKey(
- """
- -----BEGIN EC PRIVATE KEY-----
- MHcCAQEEIO+CkAccoU9jPjX64mwU54Ar9DNZSLBBTYRSINerSW8EoAoGCCqGSM49
- AwEHoUQDQgAE3FA2VSuOn0vVhtQgNe13H2UE0Vx5A41demyX8nkHTCO4BDXSEPca
- vejY7YaVcNSvFUbzDvia51X4pxbr1pe56g==
- -----END EC PRIVATE KEY-----
- """);
- var receiverPublic = KeyUtils.fromPemEncodedPublicKey(
- """
- -----BEGIN PUBLIC KEY-----
- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3FA2VSuOn0vVhtQgNe13H2UE0Vx5
- A41demyX8nkHTCO4BDXSEPcavejY7YaVcNSvFUbzDvia51X4pxbr1pe56g==
- -----END PUBLIC KEY-----
- """
- );
- var receiverKeyPair = new KeyPair(receiverPublic, receiverPrivate);
+ var receiverPrivate = KeyUtils.fromBase64EncodedX25519PrivateKey("4qGcntygFn_a3uqeBa1PbDlygQ-cpOuNznTPIz9ftWE");
+ var receiverPublic = KeyUtils.fromBase64EncodedX25519PublicKey( "ROAH_S862tNMpbJ49lu1dPXFCPHFIXZK30pSrMZEmEg");
// Token generated for the above receiver public key, with the below expected shared secret (in hex)
- var publicToken = "AQAAAUfvuJpugUV3knQXwyP7afgEpDXT4JxaF-x7Ykirty2iwUqJv5UsGx78is5Vu4Mdln_mOVbAUv4dj" +
- "da7hvzKYNC3IpSMjFrTQ8ab-bEkMpc5tjss_Z7DaJzY4fUlw31Lhx39BMB5yQX0pVLMdFGp5F-_8z8CE" +
- "-7d9lkCDP9hPKiD77besjrBt_mEBadCd4oNONqc6zzhuQj4O5T9k_RC5VRV";
- var expectedSharedSecret = "64e01295e736cb827e86cf0281385d5a0dcca217ec1b59f6609a06e2e9debf78";
+ var publicToken = "AQAAAQAgwyxd7bFNQB_2LdL3bw-xFlvrxXhs7WWNVCKZ4EFeNVtu42JMwM74bMN4E46v6mYcfQNPzcMGaP22Wl2cTnji0A";
+ var expectedSharedSecret = "85ac3c7c3a930a19334cb73e02779733";
var theirSealed = SealedSharedKey.fromTokenString(publicToken);
- var theirShared = SharedKeyGenerator.fromSealedKey(theirSealed, receiverKeyPair.getPrivate());
+ var theirShared = SharedKeyGenerator.fromSealedKey(theirSealed, receiverPrivate);
assertEquals(expectedSharedSecret, Hex.toHexString(theirShared.secretKey().getEncoded()));
}
@Test
void unrelated_private_key_cannot_decrypt_shared_secret_key() {
- var aliceKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
- var eveKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+ var aliceKeyPair = KeyUtils.generateX25519KeyPair();
+ var eveKeyPair = KeyUtils.generateX25519KeyPair();
var bobShared = SharedKeyGenerator.generateForReceiverPublicKey(aliceKeyPair.getPublic(), 1);
- assertThrows(IllegalArgumentException.class, // TODO consider distinct exception class
+ assertThrows(RuntimeException.class, // TODO consider distinct exception class
() -> SharedKeyGenerator.fromSealedKey(bobShared.sealedSharedKey(), eveKeyPair.getPrivate()));
}
@Test
void token_carries_key_id_as_metadata() {
int keyId = 12345;
- var keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+ var keyPair = KeyUtils.generateX25519KeyPair();
var myShared = SharedKeyGenerator.generateForReceiverPublicKey(keyPair.getPublic(), keyId);
var publicToken = myShared.sealedSharedKey().toTokenString();
var theirShared = SealedSharedKey.fromTokenString(publicToken);
@@ -124,7 +106,7 @@ public class SharedKeyTest {
@Test
void can_create_symmetric_ciphers_from_shared_secret_key_and_public_keys() throws Exception {
- var receiverKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC);
+ var receiverKeyPair = KeyUtils.generateX25519KeyPair();
var myShared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), 1);
String terrifyingSecret = "birds are not real D:";