diff options
Diffstat (limited to 'security-utils/src')
4 files changed, 195 insertions, 13 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/hpke/DHKemX25519HkdfSha256.java b/security-utils/src/main/java/com/yahoo/security/hpke/DHKemX25519HkdfSha256.java index 8f6dffcb9c2..91f92bf3b33 100644 --- a/security-utils/src/main/java/com/yahoo/security/hpke/DHKemX25519HkdfSha256.java +++ b/security-utils/src/main/java/com/yahoo/security/hpke/DHKemX25519HkdfSha256.java @@ -108,6 +108,40 @@ final class DHKemX25519HkdfSha256 implements Kem { * Section 4.1 DH-Based KEM (DHKEM): * * <pre> + * def AuthEncap(pkR, skS): + * skE, pkE = GenerateKeyPair() + * dh = concat(DH(skE, pkR), DH(skS, pkR)) + * enc = SerializePublicKey(pkE) + * + * pkRm = SerializePublicKey(pkR) + * pkSm = SerializePublicKey(pk(skS)) + * kem_context = concat(enc, pkRm, pkSm) + * + * shared_secret = ExtractAndExpand(dh, kem_context) + * return shared_secret, enc + * </pre> + */ + @Override + public EncapResult authEncap(XECPublicKey pkR, XECPrivateKey skS) { + var kpE = keyPairGen.get(); + var skE = (XECPrivateKey)kpE.getPrivate(); + var pkE = (XECPublicKey)kpE.getPublic(); + + byte[] dh = concat(KeyUtils.ecdh(skE, pkR), KeyUtils.ecdh(skS, pkR)); + byte[] enc = serializePublicKey(pkE); + + byte[] pkRm = serializePublicKey(pkR); + byte[] pkSm = serializePublicKey(KeyUtils.extractX25519PublicKey(skS)); + byte[] kemContext = concat(enc, pkRm, pkSm); + + byte[] sharedSecret = extractAndExpand(dh, kemContext); + return new EncapResult(sharedSecret, enc); + } + + /** + * Section 4.1 DH-Based KEM (DHKEM): + * + * <pre> * def Decap(enc, skR): * pkE = DeserializePublicKey(enc) * dh = DH(skR, pkE) @@ -130,4 +164,31 @@ final class DHKemX25519HkdfSha256 implements Kem { return extractAndExpand(dh, kemContext); } + /** + * Section 4.1 DH-Based KEM (DHKEM): + * + * <pre> + * def AuthDecap(enc, skR, pkS): + * pkE = DeserializePublicKey(enc) + * dh = concat(DH(skR, pkE), DH(skR, pkS)) + * + * pkRm = SerializePublicKey(pk(skR)) + * pkSm = SerializePublicKey(pkS) + * kem_context = concat(enc, pkRm, pkSm) + * + * shared_secret = ExtractAndExpand(dh, kem_context) + * return shared_secret + * </pre> + */ + public byte[] authDecap(byte[] enc, XECPrivateKey skR, XECPublicKey pkS) { + var pkE = deserializePublicKey(enc); + byte[] dh = concat(KeyUtils.ecdh(skR, pkE), KeyUtils.ecdh(skR, pkS)); + + byte[] pkRm = serializePublicKey(KeyUtils.extractX25519PublicKey(skR)); + byte[] pkSm = serializePublicKey(pkS); + byte[] kemContext = concat(enc, pkRm, pkSm); + + return extractAndExpand(dh, kemContext); + } + } diff --git a/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java b/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java index 51f41ab7da7..98dc739f039 100644 --- a/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java +++ b/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java @@ -39,9 +39,10 @@ import static com.yahoo.security.hpke.LabeledKdfUtils.labeledExtractForSuite; * <ul> * <li>Only the <code>DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM</code> ciphersuite is * implemented. This is expected to be a good default choice for any internal use of this class.</li> - * <li>Only the "base mode" (unauthenticated sender) is supported, i.e. no PSK support and no - * secret exporting. This implementation is only expected to be used for anonymous one-way - * encryption.</li> + * <li>Only the "base" (unauthenticated sender) and "auth" (authentication using an asymmetric + * key) modes are supported, i.e. no PSK support and no secret exporting. This implementation + * is only expected to be used for one-way encryption (possibly authenticated, if using the + * "auth" mode).</li> * <li>The API only offers single-shot encryption to keep anyone from being tempted to * use it to build their own multi-message protocol on top. This entirely avoids the * risk of nonce reuse caused by accidentally repeating sequence numbers.</li> @@ -253,6 +254,37 @@ public final class Hpke { return new ContextR(keySchedule(MODE_BASE, sharedSecret, info, DEFAULT_PSK, DEFAULT_PSK_ID)); } + /** + * Section 5.1.3. Authentication Using an Asymmetric Key: + * + * <pre> + * def SetupAuthS(pkR, info, skS): + * shared_secret, enc = AuthEncap(pkR, skS) + * return enc, KeyScheduleS(mode_auth, shared_secret, info, + * default_psk, default_psk_id) + * </pre> + */ + ContextS setupAuthS(XECPublicKey pkR, byte[] info, XECPrivateKey skS) { + var encapped = kem.authEncap(pkR, skS); + return new ContextS(encapped.enc(), + keySchedule(MODE_AUTH, encapped.sharedSecret(), info, DEFAULT_PSK, DEFAULT_PSK_ID)); + } + + /** + * Section 5.1.3. Authentication Using an Asymmetric Key: + * + * <pre> + * def SetupAuthR(enc, skR, info, pkS): + * shared_secret = AuthDecap(enc, skR, pkS) + * return KeyScheduleR(mode_auth, shared_secret, info, + * default_psk, default_psk_id) + * </pre> + */ + ContextR setupAuthR(byte[] enc, XECPrivateKey skR, byte[] info, XECPublicKey pkS) { + byte[] sharedSecret = kem.authDecap(enc, skR, pkS); + return new ContextR(keySchedule(MODE_AUTH, sharedSecret, info, DEFAULT_PSK, DEFAULT_PSK_ID)); + } + public record Sealed(byte[] enc, byte[] ciphertext) {} /** @@ -286,6 +318,13 @@ public final class Hpke { return new Sealed(encAndCtx.enc, ct); } + public Sealed sealAuth(XECPublicKey pkR, byte[] info, byte[] aad, byte[] pt, XECPrivateKey skS) { + var encAndCtx = setupAuthS(pkR, info, skS); + var base = encAndCtx.base; + byte[] ct = aead.seal(base.key(), base.nonce(), aad, pt); + return new Sealed(encAndCtx.enc, ct); + } + /** * Section 6.1 Encryption and Decryption: * @@ -316,4 +355,11 @@ public final class Hpke { return aead.open(base.key(), base.nonce(), aad, ct); } + public byte[] openAuth(byte[] enc, XECPrivateKey skR, byte[] info, byte[] aad, byte[] ct, XECPublicKey pkS) { + var ctx = setupAuthR(enc, skR, info, pkS); + var base = ctx.base; + // TODO wrap any exceptions in OpenError et al? + return aead.open(base.key(), base.nonce(), aad, ct); + } + } diff --git a/security-utils/src/main/java/com/yahoo/security/hpke/Kem.java b/security-utils/src/main/java/com/yahoo/security/hpke/Kem.java index 99c019a9d0b..55bb2e0e662 100644 --- a/security-utils/src/main/java/com/yahoo/security/hpke/Kem.java +++ b/security-utils/src/main/java/com/yahoo/security/hpke/Kem.java @@ -18,22 +18,42 @@ public interface Kem { /** * Section 4 Cryptographic Dependencies: - * + * <blockquote> * "Randomized algorithm to generate an ephemeral, fixed-length symmetric key * (the KEM shared secret) and a fixed-length encapsulation of that key that can * be decapsulated by the holder of the private key corresponding to <code>pkR</code>" + * </blockquote> */ EncapResult encap(XECPublicKey pkR); /** + * Section 4: Cryptographic Dependencies: + * <blockquote> + * "Same as <code>Encap()</code>, and the outputs encode an assurance that the KEM + * shared secret was generated by the holder of the private key <code>skS</code>." + * </blockquote> + */ + EncapResult authEncap(XECPublicKey pkR, XECPrivateKey skS); + + /** * Section 4 Cryptographic Dependencies: - * + * <blockquote> * "Deterministic algorithm using the private key <code>skR</code> to recover the * ephemeral symmetric key (the KEM shared secret) from its encapsulated * representation <code>enc</code>." + * </blockquote> */ byte[] decap(byte[] enc, XECPrivateKey skR); + /** + * Section 4 Cryptographic Dependencies: + * <blockquote> + * "Same as <code>Decap()</code>, and the recipient is assured that the KEM shared + * secret was generated by the holder of the private key <code>skS</code>." + * </blockquote> + */ + byte[] authDecap(byte[] enc, XECPrivateKey skR, XECPublicKey pkS); + /** The length in bytes of a KEM shared secret produced by this KEM. */ short nSecret(); /** The length in bytes of an encapsulated key produced by this KEM. */ diff --git a/security-utils/src/test/java/com/yahoo/security/HpkeTest.java b/security-utils/src/test/java/com/yahoo/security/HpkeTest.java index 0d5a4d53e2a..ee9127b73bb 100644 --- a/security-utils/src/test/java/com/yahoo/security/HpkeTest.java +++ b/security-utils/src/test/java/com/yahoo/security/HpkeTest.java @@ -24,18 +24,38 @@ import static org.junit.jupiter.api.Assertions.assertThrows; */ public class HpkeTest { - static KeyPair ephemeralRrfc9180TestVectorKeyPair() { + // Base mode key pairs + static KeyPair ephemeralRfc9180BaseTestVectorKeyPair() { var priv = KeyUtils.fromRawX25519PrivateKey(unhex("52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736")); var pub = KeyUtils.fromRawX25519PublicKey(unhex("37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431")); return new KeyPair(pub, priv); } - static KeyPair receiverRrfc9180TestVectorKeyPair() { + static KeyPair receiverRfc9180BaseTestVectorKeyPair() { var priv = KeyUtils.fromRawX25519PrivateKey(unhex("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8")); var pub = KeyUtils.fromRawX25519PublicKey(unhex("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")); return new KeyPair(pub, priv); } + // Auth mode key pairs + static KeyPair ephemeralRfc9180AuthTestVectorKeyPair() { + var priv = KeyUtils.fromRawX25519PrivateKey(unhex("ff4442ef24fbc3c1ff86375b0be1e77e88a0de1e79b30896d73411c5ff4c3518")); + var pub = KeyUtils.fromRawX25519PublicKey(unhex("23fb952571a14a25e3d678140cd0e5eb47a0961bb18afcf85896e5453c312e76")); + return new KeyPair(pub, priv); + } + + static KeyPair receiverRfc9180AuthTestVectorKeyPair() { + var priv = KeyUtils.fromRawX25519PrivateKey(unhex("fdea67cf831f1ca98d8e27b1f6abeb5b7745e9d35348b80fa407ff6958f9137e")); + var pub = KeyUtils.fromRawX25519PublicKey(unhex("1632d5c2f71c2b38d0a8fcc359355200caa8b1ffdf28618080466c909cb69b2e")); + return new KeyPair(pub, priv); + } + + static KeyPair senderRfc9180AuthTestVectorKeyPair() { + var priv = KeyUtils.fromRawX25519PrivateKey(unhex("dc4a146313cce60a278a5323d321f051c5707e9c45ba21a3479fecdf76fc69dd")); + var pub = KeyUtils.fromRawX25519PublicKey(unhex("8b0c70873dc5aecb7f9ee4e62406a397b350e57012be45cf53b7105ae731790b")); + return new KeyPair(pub, priv); + } + private static XECPublicKey pk(KeyPair kp) { return (XECPublicKey) kp.getPublic(); } @@ -45,7 +65,7 @@ public class HpkeTest { } /** - * https://www.rfc-editor.org/rfc/rfc9180.html test vector + * <a href="https://www.rfc-editor.org/rfc/rfc9180.html">RFC 9180</a> test vector, Base mode * * Appendix A.1.1 * @@ -54,13 +74,13 @@ public class HpkeTest { * Only tests first encryption, i.e. sequence number 0. */ @Test - void passes_rfc_9180_dhkem_x25519_hkdf_sha256_hkdf_sha256_aes_gcm_128_test_vector() { + void passes_rfc_9180_dhkem_x25519_hkdf_sha256_hkdf_sha256_aes_gcm_128_base_test_vector() { byte[] info = unhex("4f6465206f6e2061204772656369616e2055726e"); byte[] pt = unhex("4265617574792069732074727574682c20747275746820626561757479"); byte[] aad = unhex("436f756e742d30"); - var kpR = receiverRrfc9180TestVectorKeyPair(); + var kpR = receiverRfc9180BaseTestVectorKeyPair(); - var kem = Kem.dHKemX25519HkdfSha256(new Kem.UnsafeDeterminsticKeyPairOnlyUsedByTesting(ephemeralRrfc9180TestVectorKeyPair())); + var kem = Kem.dHKemX25519HkdfSha256(new Kem.UnsafeDeterminsticKeyPairOnlyUsedByTesting(ephemeralRfc9180BaseTestVectorKeyPair())); var ciphersuite = Ciphersuite.of(kem, Kdf.hkdfSha256(), Aead.aes128Gcm()); var hpke = Hpke.of(ciphersuite); @@ -78,12 +98,47 @@ public class HpkeTest { assertEquals(hex(pt), hex(openedPt)); } + /** + * <a href="https://www.rfc-editor.org/rfc/rfc9180.html">RFC 9180</a> test vector, Auth mode + * + * Appendix A.1.3 + * + * DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM + * + * Only tests first encryption, i.e. sequence number 0. + */ + @Test + void passes_rfc_9180_dhkem_x25519_hkdf_sha256_hkdf_sha256_aes_gcm_128_auth_test_vector() { + byte[] info = unhex("4f6465206f6e2061204772656369616e2055726e"); + byte[] pt = unhex("4265617574792069732074727574682c20747275746820626561757479"); + byte[] aad = unhex("436f756e742d30"); + var kpR = receiverRfc9180AuthTestVectorKeyPair(); + var kpS = senderRfc9180AuthTestVectorKeyPair(); + + var kem = Kem.dHKemX25519HkdfSha256(new Kem.UnsafeDeterminsticKeyPairOnlyUsedByTesting(ephemeralRfc9180AuthTestVectorKeyPair())); + var ciphersuite = Ciphersuite.of(kem, Kdf.hkdfSha256(), Aead.aes128Gcm()); + + var hpke = Hpke.of(ciphersuite); + var s = hpke.sealAuth(pk(kpR), info, aad, pt, sk(kpS)); + + // The "enc" output is the ephemeral public key + var expectedEnc = "23fb952571a14a25e3d678140cd0e5eb47a0961bb18afcf85896e5453c312e76"; + assertEquals(expectedEnc, hex(s.enc())); + + var expectedCiphertext = "5fd92cc9d46dbf8943e72a07e42f363ed5f721212cd90bcfd072bfd9f44e06b8" + + "0fd17824947496e21b680c141b"; + assertEquals(expectedCiphertext, hex(s.ciphertext())); + + byte[] openedPt = hpke.openAuth(s.enc(), sk(kpR), info, aad, s.ciphertext(), pk(kpS)); + assertEquals(hex(pt), hex(openedPt)); + } + @Test void sealing_creates_new_ephemeral_key_pair_per_invocation() { byte[] info = toUtf8Bytes("the finest info"); byte[] pt = toUtf8Bytes("seagulls attack at dawn"); byte[] aad = toUtf8Bytes("cool AAD"); - var kpR = receiverRrfc9180TestVectorKeyPair(); + var kpR = receiverRfc9180BaseTestVectorKeyPair(); var hpke = Hpke.of(Ciphersuite.defaultSuite()); @@ -103,7 +158,7 @@ public class HpkeTest { byte[] info = toUtf8Bytes("the finest info"); byte[] pt = toUtf8Bytes("seagulls attack at dawn"); byte[] aad = toUtf8Bytes("cool AAD"); - var kpR = receiverRrfc9180TestVectorKeyPair(); + var kpR = receiverRfc9180BaseTestVectorKeyPair(); var hpke = Hpke.of(Ciphersuite.defaultSuite()); var s = hpke.sealBase(pk(kpR), info, aad, pt); |