diff options
author | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-10-20 10:56:07 +0200 |
---|---|---|
committer | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-10-20 10:56:07 +0200 |
commit | 5d41f068b349fe0ddb2bd5eb4bd4af5ed0a7cff3 (patch) | |
tree | 491a3f1a4baa104a974eab5db16e36afbfbb822a /security-utils | |
parent | fe64bb97196040f633a9d1da5c83d808f9c324dd (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')
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:"; |