diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index 1f3298094..a1cea0b53 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -166,6 +166,7 @@ def find_openssl_library # added in 3.2.0 have_func("SSL_get0_group_name(NULL)", ssl_h) +have_header("openssl/hpke.h") # added in 3.4.0 have_func("TS_VERIFY_CTX_set0_certs(NULL, NULL)", ts_h) diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c index 5716e6f10..53807dab1 100644 --- a/ext/openssl/ossl.c +++ b/ext/openssl/ossl.c @@ -1148,6 +1148,7 @@ Init_openssl(void) Init_ossl_digest(); Init_ossl_engine(); Init_ossl_hmac(); + Init_ossl_hpke_ctx(); Init_ossl_kdf(); Init_ossl_ns_spki(); Init_ossl_ocsp(); diff --git a/ext/openssl/ossl.h b/ext/openssl/ossl.h index 0b479a720..b25199d6e 100644 --- a/ext/openssl/ossl.h +++ b/ext/openssl/ossl.h @@ -46,6 +46,9 @@ #include #include #include "openssl_missing.h" +#ifdef HAVE_OPENSSL_HPKE_H + #include +#endif #ifndef LIBRESSL_VERSION_NUMBER # define OSSL_IS_LIBRESSL 0 @@ -192,6 +195,7 @@ extern VALUE dOSSL; #include "ossl_digest.h" #include "ossl_engine.h" #include "ossl_hmac.h" +#include "ossl_hpke_ctx.h" #include "ossl_kdf.h" #include "ossl_ns_spki.h" #include "ossl_ocsp.h" diff --git a/ext/openssl/ossl_hpke_ctx.c b/ext/openssl/ossl_hpke_ctx.c new file mode 100644 index 000000000..85c331507 --- /dev/null +++ b/ext/openssl/ossl_hpke_ctx.c @@ -0,0 +1,315 @@ +/* + * Ruby/OpenSSL Project + * Copyright (C) 2026 Ruby/OpenSSL Project Authors + */ +#include "ossl.h" + +VALUE mHPKE; +VALUE cContext; +VALUE cSenderContext; +VALUE cReceiverContext; +VALUE eHPKEError; + +static void +ossl_hpke_ctx_free(void *ptr) +{ +#if OSSL_OPENSSL_PREREQ(3, 2, 0) + OSSL_HPKE_CTX_free(ptr); +#endif +} + +/* public */ +const rb_data_type_t ossl_hpke_ctx_type = { + "OpenSSL/HPKE_CTX", + { + 0, ossl_hpke_ctx_free, + }, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY +}; + +static VALUE +ossl_hpke_ctx_new_sender(VALUE self, VALUE mode, VALUE suite) +{ +#if !OSSL_OPENSSL_PREREQ(3, 2, 0) + ossl_raise(eHPKEError, "OpenSSL 3.2.0 required"); +#else + OSSL_HPKE_CTX *sctx; + VALUE kem_id, kdf_id, aead_id, mode_table, mode_id; + + if (RTYPEDDATA_DATA(self)) + ossl_raise(eHPKEError, "HPKE context is already initialized"); + + kem_id = rb_iv_get(suite, "@kem_id"); + kdf_id = rb_iv_get(suite, "@kdf_id"); + aead_id = rb_iv_get(suite, "@aead_id"); + + rb_iv_set(self, "@kem_id", kem_id); + rb_iv_set(self, "@kdf_id", kdf_id); + rb_iv_set(self, "@aead_id", aead_id); + + OSSL_HPKE_SUITE hpke_suite = { + NUM2INT(kem_id), NUM2INT(kdf_id), NUM2INT(aead_id) + }; + mode_table = rb_const_get_at(cContext, rb_intern("MODES")); + mode_id = rb_funcall(mode_table, rb_intern("[]"), 1, mode); + + const char *propq = EVP_default_properties_is_fips_enabled(NULL) ? "fips=yes" : NULL; + + if((sctx = OSSL_HPKE_CTX_new(NUM2INT(mode_id), hpke_suite, OSSL_HPKE_ROLE_SENDER, NULL, propq)) == NULL) { + ossl_raise(eHPKEError, "could not create ctx"); + } + + RTYPEDDATA_DATA(self) = sctx; + return self; +#endif +} + +static VALUE +ossl_hpke_ctx_new_receiver(VALUE self, VALUE mode, VALUE suite) +{ +#if !OSSL_OPENSSL_PREREQ(3, 2, 0) + ossl_raise(eHPKEError, "OpenSSL 3.2.0 required"); +#else + OSSL_HPKE_CTX *rctx; + VALUE kem_id, kdf_id, aead_id, mode_table, mode_id; + + if (RTYPEDDATA_DATA(self)) + ossl_raise(eHPKEError, "HPKE context is already initialized"); + + kem_id = rb_iv_get(suite, "@kem_id"); + kdf_id = rb_iv_get(suite, "@kdf_id"); + aead_id = rb_iv_get(suite, "@aead_id"); + + rb_iv_set(self, "@kem_id", kem_id); + rb_iv_set(self, "@kdf_id", kdf_id); + rb_iv_set(self, "@aead_id", aead_id); + + OSSL_HPKE_SUITE hpke_suite = { + NUM2INT(kem_id), NUM2INT(kdf_id), NUM2INT(aead_id) + }; + mode_table = rb_const_get_at(cContext, rb_intern("MODES")); + mode_id = rb_funcall(mode_table, rb_intern("[]"), 1, mode); + + const char *propq = EVP_default_properties_is_fips_enabled(NULL) ? "fips=yes" : NULL; + + if((rctx = OSSL_HPKE_CTX_new(NUM2INT(mode_id), hpke_suite, OSSL_HPKE_ROLE_RECEIVER, NULL, propq)) == NULL) { + ossl_raise(eHPKEError, "could not create ctx"); + } + + RTYPEDDATA_DATA(self) = rctx; + return self; +#endif +} + +static VALUE +ossl_hpke_encap(VALUE self, VALUE pub, VALUE info) +{ +#if !OSSL_OPENSSL_PREREQ(3, 2, 0) + ossl_raise(eHPKEError, "OpenSSL 3.2.0 required"); +#else + VALUE enc_obj; + size_t enclen; + OSSL_HPKE_CTX *sctx; + size_t publen; + size_t infolen; + OSSL_HPKE_SUITE suite = { + NUM2INT(rb_iv_get(self, "@kem_id")), + NUM2INT(rb_iv_get(self, "@kdf_id")), + NUM2INT(rb_iv_get(self, "@aead_id")) + }; + + GetHpkeCtx(self, sctx); + + StringValue(pub); + StringValue(info); + publen = RSTRING_LEN(pub); + infolen = RSTRING_LEN(info); + + enclen = OSSL_HPKE_get_public_encap_size(suite); + enc_obj = rb_str_new(0, enclen); + + if (OSSL_HPKE_encap(sctx, (unsigned char *)RSTRING_PTR(enc_obj), &enclen, (unsigned char*)RSTRING_PTR(pub), publen, (unsigned char*)RSTRING_PTR(info), infolen) != 1) { + if (EVP_default_properties_is_fips_enabled(NULL)) + ossl_raise(eHPKEError, "could not encap; HPKE is not supported by the FIPS provider"); + ossl_raise(eHPKEError, "could not encap"); + } + + rb_str_resize(enc_obj, enclen); + return enc_obj; +#endif +} + +static VALUE +ossl_hpke_seal(VALUE self, VALUE aad, VALUE pt) +{ +#if !OSSL_OPENSSL_PREREQ(3, 2, 0) + ossl_raise(eHPKEError, "OpenSSL 3.2.0 required"); +#else + VALUE ct_obj; + OSSL_HPKE_CTX *sctx; + OSSL_HPKE_SUITE suite = { + NUM2INT(rb_iv_get(self, "@kem_id")), + NUM2INT(rb_iv_get(self, "@kdf_id")), + NUM2INT(rb_iv_get(self, "@aead_id")) + }; + size_t ctlen, aadlen, ptlen; + + StringValue(aad); + StringValue(pt); + aadlen = RSTRING_LEN(aad); + ptlen = RSTRING_LEN(pt); + ctlen = OSSL_HPKE_get_ciphertext_size(suite, ptlen); + + ct_obj = rb_str_new(0, ctlen); + + GetHpkeCtx(self, sctx); + + if (OSSL_HPKE_seal(sctx, (unsigned char *)RSTRING_PTR(ct_obj), &ctlen, (unsigned char*)RSTRING_PTR(aad), aadlen, (unsigned char*)RSTRING_PTR(pt), ptlen) != 1) { + ossl_raise(eHPKEError, "could not seal"); + } + + return ct_obj; +#endif +} + +static VALUE +ossl_hpke_decap(VALUE self, VALUE enc, VALUE priv, VALUE info) +{ +#if !OSSL_OPENSSL_PREREQ(3, 2, 0) + ossl_raise(eHPKEError, "OpenSSL 3.2.0 required"); +#else + OSSL_HPKE_CTX *rctx; + EVP_PKEY *pkey; + size_t enclen; + size_t infolen; + + GetHpkeCtx(self, rctx); + GetPKey(priv, pkey); + + StringValue(enc); + StringValue(info); + enclen = RSTRING_LEN(enc); + infolen = RSTRING_LEN(info); + + if (OSSL_HPKE_decap(rctx, (unsigned char *)RSTRING_PTR(enc), enclen, pkey, (unsigned char *)RSTRING_PTR(info), infolen) != 1) { + if (EVP_default_properties_is_fips_enabled(NULL)) + ossl_raise(eHPKEError, "could not decap; HPKE is not supported by the FIPS provider"); + ossl_raise(eHPKEError, "could not decap"); + } + + return Qtrue; +#endif +} + +static VALUE +ossl_hpke_open(VALUE self, VALUE aad, VALUE ct) +{ +#if !OSSL_OPENSSL_PREREQ(3, 2, 0) + ossl_raise(eHPKEError, "OpenSSL 3.2.0 required"); +#else + VALUE pt_obj; + OSSL_HPKE_CTX *rctx; + size_t ptlen, aadlen, ctlen; + + StringValue(aad); + StringValue(ct); + aadlen = RSTRING_LEN(aad); + ctlen = RSTRING_LEN(ct); + ptlen = ctlen; + + pt_obj = rb_str_new(0, ptlen); + + GetHpkeCtx(self, rctx); + + if (OSSL_HPKE_open(rctx, (unsigned char *)RSTRING_PTR(pt_obj), &ptlen, (unsigned char*)RSTRING_PTR(aad), aadlen, (unsigned char*)RSTRING_PTR(ct), ctlen) != 1) { + ossl_raise(eHPKEError, "could not open"); + } + + rb_str_resize(pt_obj, ptlen); + + return pt_obj; +#endif +} + +static VALUE +ossl_hpke_export(VALUE self, VALUE secretlen, VALUE label) +{ +#if !OSSL_OPENSSL_PREREQ(3, 2, 0) + ossl_raise(eHPKEError, "OpenSSL 3.2.0 required"); +#else + VALUE secret_obj; + OSSL_HPKE_CTX *ctx; + size_t labellen; + + StringValue(label); + labellen = RSTRING_LEN(label); + + secret_obj = rb_str_new(0, NUM2INT(secretlen)); + + GetHpkeCtx(self, ctx); + if (OSSL_HPKE_export(ctx, (unsigned char *)RSTRING_PTR(secret_obj), NUM2INT(secretlen), (unsigned char*)RSTRING_PTR(label), labellen) != 1) { + ossl_raise(eHPKEError, "could not export"); + } + + return secret_obj; +#endif +} + +/* private */ +static VALUE +ossl_hpke_ctx_alloc(VALUE klass) +{ + return TypedData_Wrap_Struct(klass, &ossl_hpke_ctx_type, NULL); +} + +/* HPKE module method */ +static VALUE +ossl_hpke_keygen(VALUE self, VALUE kem_id, VALUE kdf_id, VALUE aead_id) +{ +#if !OSSL_OPENSSL_PREREQ(3, 2, 0) + ossl_raise(eHPKEError, "OpenSSL 3.2.0 required"); +#else + EVP_PKEY *pkey; + VALUE pkey_obj; + unsigned char pub[133]; // as per RFC9180 section 7.1, the maximum size of Npk possible is 133 + size_t publen; + OSSL_HPKE_SUITE hpke_suite = { + NUM2INT(kem_id), NUM2INT(kdf_id), NUM2INT(aead_id) + }; + publen = 133; // set it to maximum length first, it will shrink down upon call of OSSL_HPKE_keygen + + const char *propq = EVP_default_properties_is_fips_enabled(NULL) ? "fips=yes" : NULL; + + if(!OSSL_HPKE_keygen(hpke_suite, pub, &publen, &pkey, NULL, 0, NULL, propq)){ + ossl_raise(eHPKEError, "could not keygen"); + } + + pkey_obj = ossl_pkey_wrap(pkey); + + return pkey_obj; +#endif +} + +void +Init_ossl_hpke_ctx(void) +{ + mHPKE = rb_define_module_under(mOSSL, "HPKE"); + cContext = rb_define_class_under(mHPKE, "Context", rb_cObject); + cSenderContext = rb_define_class_under(cContext, "Sender", cContext); + cReceiverContext = rb_define_class_under(cContext, "Receiver", cContext); + eHPKEError = rb_define_class_under(mHPKE, "HPKEError", eOSSLError); + + rb_define_module_function(mHPKE, "keygen", ossl_hpke_keygen, 3); + + rb_define_method(cSenderContext, "initialize", ossl_hpke_ctx_new_sender, 2); + rb_define_method(cSenderContext, "encap", ossl_hpke_encap, 2); + rb_define_method(cSenderContext, "seal", ossl_hpke_seal, 2); + + rb_define_method(cReceiverContext, "initialize", ossl_hpke_ctx_new_receiver, 2); + rb_define_method(cReceiverContext, "decap", ossl_hpke_decap, 3); + rb_define_method(cReceiverContext, "open", ossl_hpke_open, 2); + + rb_define_method(cContext, "export", ossl_hpke_export, 2); + + rb_define_alloc_func(cContext, ossl_hpke_ctx_alloc); +} \ No newline at end of file diff --git a/ext/openssl/ossl_hpke_ctx.h b/ext/openssl/ossl_hpke_ctx.h new file mode 100644 index 000000000..ddfeeffce --- /dev/null +++ b/ext/openssl/ossl_hpke_ctx.h @@ -0,0 +1,23 @@ +/* + * Ruby/OpenSSL Project + * Copyright (C) 2026 Ruby/OpenSSL Project Authors + */ +#if !defined(OSSL_HPKE_CTX_H) +#define OSSL_HPKE_CTX_H + +extern VALUE mHPKE; +extern VALUE cContext; +extern const rb_data_type_t ossl_hpke_ctx_type; + +#if OSSL_OPENSSL_PREREQ(3, 2, 0) +#define GetHpkeCtx(obj, ctx) do {\ + TypedData_Get_Struct((obj), OSSL_HPKE_CTX, &ossl_hpke_ctx_type, (ctx)); \ + if (!(ctx)) { \ + rb_raise(rb_eRuntimeError, "OSSL_HPKE_CTX wasn't initialized!");\ + } \ +} while (0) +#endif + +void Init_ossl_hpke_ctx(void); + +#endif \ No newline at end of file diff --git a/lib/openssl.rb b/lib/openssl.rb index 98fa8d39f..233f1c048 100644 --- a/lib/openssl.rb +++ b/lib/openssl.rb @@ -21,6 +21,7 @@ require_relative 'openssl/ssl' require_relative 'openssl/version' require_relative 'openssl/x509' +require_relative 'openssl/hpke' module OpenSSL # :call-seq: diff --git a/lib/openssl/hpke.rb b/lib/openssl/hpke.rb new file mode 100644 index 000000000..cfcee79db --- /dev/null +++ b/lib/openssl/hpke.rb @@ -0,0 +1,51 @@ +module OpenSSL::HPKE + def self.keygen_with_suite(suite) + raise OpenSSL::HPKE::HPKEError, 'Invalid suite specified' unless suite.is_a?(OpenSSL::HPKE::Suite) + + keygen(suite.kem_id, suite.kdf_id, suite.aead_id) + end + + class Context + # supports only base mode for now + MODES = { + base: 0x00 + }.freeze + + attr_reader :kem_id, :kdf_id, :aead_id + end + + class Suite + attr_reader :kem_id, :kdf_id, :aead_id + + KEMS = { + dhkem_p256_hkdf_sha256: 0x0010, + dhkem_p384_hkdf_sha384: 0x0011, + dhkem_p521_hkdf_sha512: 0x0012, + dhkem_x25519_hkdf_sha256: 0x0020, + dhkem_x448_hkdf_sha512: 0x0021 + }.freeze + + KDFS = { + hkdf_sha256: 0x0001, + hkdf_sha384: 0x0002, + hkdf_sha512: 0x0003 + }.freeze + + AEADS = { + aes_128_gcm: 0x0001, + aes_256_gcm: 0x0002, + chacha20poly1305: 0x0003, + export_only: 0xffff + }.freeze + + def initialize(kem_id, kdf_id, aead_id) + @kem_id = kem_id + @kdf_id = kdf_id + @aead_id = aead_id + end + + def self.new_with_names(kem_name, kdf_name, aead_name) + new(KEMS[kem_name], KDFS[kdf_name], AEADS[aead_name]) if KEMS[kem_name] && KDFS[kdf_name] && AEADS[aead_name] + end + end +end diff --git a/test/openssl/test_hpke.rb b/test/openssl/test_hpke.rb new file mode 100644 index 000000000..f11ec3350 --- /dev/null +++ b/test/openssl/test_hpke.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true +require_relative 'utils' + +if defined?(OpenSSL) + +class OpenSSL::TestHPKE < OpenSSL::TestCase + def setup + super + # OpenSSL's FIPS provider does not implement the DHKEM KEM encapsulation + # used by HPKE, so no HPKE operation can complete a round-trip under FIPS. + # The whole feature is therefore omitted in FIPS mode. + omit_on_fips + # The HPKE API was added in OpenSSL 3.2.0. LibreSSL and AWS-LC do not + # provide it, and openssl? returns false for those. + unless openssl?(3, 2, 0) + omit "HPKE is only supported on OpenSSL >= 3.2.0" + end + end + + def test_suite_new_with_names + suite = OpenSSL::HPKE::Suite.new_with_names( + :dhkem_x25519_hkdf_sha256, :hkdf_sha256, :aes_128_gcm) + assert_equal(0x0020, suite.kem_id) + assert_equal(0x0001, suite.kdf_id) + assert_equal(0x0001, suite.aead_id) + end + + def test_suite_new_with_names_unknown_returns_nil + assert_nil(OpenSSL::HPKE::Suite.new_with_names(:bogus, :hkdf_sha256, :aes_128_gcm)) + assert_nil(OpenSSL::HPKE::Suite.new_with_names(:dhkem_x25519_hkdf_sha256, :bogus, :aes_128_gcm)) + assert_nil(OpenSSL::HPKE::Suite.new_with_names(:dhkem_x25519_hkdf_sha256, :hkdf_sha256, :bogus)) + end + + def test_suite_new_with_ids + suite = OpenSSL::HPKE::Suite.new(0x0020, 0x0001, 0x0001) + assert_equal(0x0020, suite.kem_id) + assert_equal(0x0001, suite.kdf_id) + assert_equal(0x0001, suite.aead_id) + end + + def test_keygen_returns_pkey + pkey = OpenSSL::HPKE.keygen_with_suite(x25519_suite) + assert_kind_of(OpenSSL::PKey::PKey, pkey) + end + + def test_keygen_for_all_kems + OpenSSL::HPKE::Suite::KEMS.each_key do |kem| + suite = OpenSSL::HPKE::Suite.new_with_names(kem, :hkdf_sha256, :aes_128_gcm) + assert_kind_of(OpenSSL::PKey::PKey, + OpenSSL::HPKE.keygen_with_suite(suite), + "keygen failed for KEM #{kem}") + end + end + + def test_keygen_with_suite_rejects_non_suite + assert_raise(OpenSSL::HPKE::HPKEError) do + OpenSSL::HPKE.keygen_with_suite("not a suite") + end + end + + def test_base_mode_roundtrip_x25519 + assert_hpke_roundtrip(x25519_suite) + end + + def test_base_mode_roundtrip_x448 + assert_hpke_roundtrip(OpenSSL::HPKE::Suite.new_with_names( + :dhkem_x448_hkdf_sha512, :hkdf_sha512, :aes_256_gcm)) + end + + def test_base_mode_roundtrip_p256 + assert_hpke_roundtrip(OpenSSL::HPKE::Suite.new_with_names( + :dhkem_p256_hkdf_sha256, :hkdf_sha256, :aes_128_gcm)) + end + + def test_base_mode_roundtrip_chacha20poly1305 + assert_hpke_roundtrip(OpenSSL::HPKE::Suite.new_with_names( + :dhkem_x25519_hkdf_sha256, :hkdf_sha256, :chacha20poly1305)) + end + + def test_seal_open_multiple_messages_in_order + sender, receiver = paired_contexts(x25519_suite) + messages = ["first", "second", "third"] + ciphertexts = messages.map { |m| sender.seal("aad", m) } + opened = ciphertexts.map { |c| receiver.open("aad", c) } + assert_equal(messages, opened) + end + + def test_open_fails_with_wrong_aad + sender, receiver = paired_contexts(x25519_suite) + ct = sender.seal("correct aad", "secret") + assert_raise(OpenSSL::HPKE::HPKEError) do + receiver.open("wrong aad", ct) + end + end + + def test_open_fails_on_tampered_ciphertext + sender, receiver = paired_contexts(x25519_suite) + ct = sender.seal("aad", "secret message") + tampered = ct.dup + tampered.setbyte(0, tampered.getbyte(0) ^ 0xff) + assert_raise(OpenSSL::HPKE::HPKEError) do + receiver.open("aad", tampered) + end + end + + def test_export_secret_agreement + sender, receiver = paired_contexts(x25519_suite) + sender_secret = sender.export(32, "context label") + receiver_secret = receiver.export(32, "context label") + assert_equal(32, sender_secret.bytesize) + assert_equal(sender_secret, receiver_secret) + end + + def test_export_different_labels_differ + sender, = paired_contexts(x25519_suite) + assert_not_equal(sender.export(32, "label a"), sender.export(32, "label b")) + end + + def test_export_only_suite + suite = OpenSSL::HPKE::Suite.new_with_names( + :dhkem_x25519_hkdf_sha256, :hkdf_sha256, :export_only) + sender, receiver = paired_contexts(suite) + assert_equal(sender.export(32, "label"), receiver.export(32, "label")) + # The export-only AEAD cannot seal or open. + assert_raise(OpenSSL::HPKE::HPKEError) { sender.seal("aad", "msg") } + end + + def test_context_cannot_be_reinitialized + suite = x25519_suite + sender = OpenSSL::HPKE::Context::Sender.new(:base, suite) + assert_raise(OpenSSL::HPKE::HPKEError) do + sender.send(:initialize, :base, suite) + end + + receiver = OpenSSL::HPKE::Context::Receiver.new(:base, suite) + assert_raise(OpenSSL::HPKE::HPKEError) do + receiver.send(:initialize, :base, suite) + end + end + + def test_string_arguments_are_required + suite = x25519_suite + pkey = OpenSSL::HPKE.keygen_with_suite(suite) + sender = OpenSSL::HPKE::Context::Sender.new(:base, suite) + assert_raise(TypeError) { sender.encap(12345, "info") } + assert_raise(TypeError) { sender.encap(public_key_bytes(pkey), 12345) } + end + + private + + def x25519_suite + OpenSSL::HPKE::Suite.new_with_names( + :dhkem_x25519_hkdf_sha256, :hkdf_sha256, :aes_128_gcm) + end + + # The KEM public key passed to #encap is the recipient's public key in the + # KEM's wire encoding: the raw key for X25519/X448, the uncompressed point + # for the NIST curves. + def public_key_bytes(pkey) + if pkey.is_a?(OpenSSL::PKey::EC) + pkey.public_key.to_octet_string(:uncompressed) + else + pkey.raw_public_key + end + end + + # Returns an established [sender, receiver] pair sharing the same context. + def paired_contexts(suite, info: "shared info") + pkey = OpenSSL::HPKE.keygen_with_suite(suite) + sender = OpenSSL::HPKE::Context::Sender.new(:base, suite) + enc = sender.encap(public_key_bytes(pkey), info) + receiver = OpenSSL::HPKE::Context::Receiver.new(:base, suite) + assert_equal(true, receiver.decap(enc, pkey, info)) + [sender, receiver] + end + + def assert_hpke_roundtrip(suite, info: "some info", aad: "some aad", message: "hello hpke") + pkey = OpenSSL::HPKE.keygen_with_suite(suite) + + sender = OpenSSL::HPKE::Context::Sender.new(:base, suite) + enc = sender.encap(public_key_bytes(pkey), info) + ct = sender.seal(aad, message) + + receiver = OpenSSL::HPKE::Context::Receiver.new(:base, suite) + assert_equal(true, receiver.decap(enc, pkey, info)) + assert_equal(message, receiver.open(aad, ct)) + end +end + +end