summaryrefslogtreecommitdiffstats
path: root/security-utils
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@yahooinc.com>2023-03-23 13:28:52 +0100
committerTor Brede Vekterli <vekterli@yahooinc.com>2023-03-23 13:36:06 +0100
commitf2b21d5f030d9d99d3b5c4a4c84eedb11eed2a8a (patch)
treeb512088bc5f0ff63ecf92a7af5a1ebd598163d6c /security-utils
parent829f4a279b817b01144c5896de1a5c671804857d (diff)
Implement RFC 9180 HPKE sender asymmetric key authentication mode
We already have support for the `base` unauthenticated mode, so this just adds the `auth` mode where the sender's key pair is added to the ECDH shared key derivation mix. This ensures that a message may only be successfully opened if the sender was in possession of the private key (`skS`) corresponding to the expected public key (`pkS`).
Diffstat (limited to 'security-utils')
-rw-r--r--security-utils/src/main/java/com/yahoo/security/hpke/DHKemX25519HkdfSha256.java61
-rw-r--r--security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java52
-rw-r--r--security-utils/src/main/java/com/yahoo/security/hpke/Kem.java24
-rw-r--r--security-utils/src/test/java/com/yahoo/security/HpkeTest.java71
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);