Skip to content

Commit d1116a1

Browse files
ext/sodium: Add crypto_kem (X-Wing) and crypto_kem_mlkem768 bindings (#23420)
* ext/sodium: Add crypto_kem (X-Wing) bindings Expose the libsodium 1.0.22 generic KEM API (X-Wing, hybrid ML-KEM768+X25519). Guarded so older libsodium builds are unaffected. * ext/sodium: Add crypto_kem_mlkem768 bindings Expose libsodium 1.0.22's ML-KEM768 (FIPS 203) KEM with the same shape as the generic crypto_kem API. * ext/sodium: Test the KEM internal-error paths An invalid ML-KEM public key encoding and a small-order X25519 ciphertext component deterministically fail inside libsodium, exercising the memzero-on-error branches. ML-KEM768 decapsulation never fails (implicit rejection), so it has no such test. Also drop the cross-family length check, which duplicated existing coverage.
1 parent 5b387b7 commit d1116a1

8 files changed

Lines changed: 814 additions & 1 deletion

File tree

‎NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ PHP NEWS
7171
. Fixed WSDL cache corruption when a soap:header defines headerfaults.
7272
(Ilia Alshanetsky)
7373

74+
- Sodium:
75+
. Added support for the libsodium 1.0.22 KEM APIs (X-Wing and ML-KEM768).
76+
(Zachary DuBois)
77+
7478
- Standard:
7579
. Fixed a segfault when a stream filter callback unsets StreamBucket::$data
7680
before re-attaching the bucket. (iliaal)

‎UPGRADING‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,20 @@ PHP 8.6 UPGRADE NOTES
768768
. snmp_set_output_option()
769769
. snmp_set_string_output_format()
770770

771+
- Sodium:
772+
. sodium_crypto_kem_keypair(), sodium_crypto_kem_seed_keypair(),
773+
sodium_crypto_kem_secretkey(), sodium_crypto_kem_publickey(),
774+
sodium_crypto_kem_enc() and sodium_crypto_kem_dec() expose the X-Wing
775+
KEM (hybrid ML-KEM768+X25519, libsodium's recommended KEM).
776+
Available when PHP is built against libsodium >= 1.0.22.
777+
. sodium_crypto_kem_mlkem768_keypair(),
778+
sodium_crypto_kem_mlkem768_seed_keypair(),
779+
sodium_crypto_kem_mlkem768_secretkey(),
780+
sodium_crypto_kem_mlkem768_publickey(),
781+
sodium_crypto_kem_mlkem768_enc() and sodium_crypto_kem_mlkem768_dec()
782+
expose the ML-KEM768 (FIPS 203) KEM.
783+
Available when PHP is built against libsodium >= 1.0.22.
784+
771785
- Standard:
772786
. clamp() returns the given value if in range, else returns the nearest
773787
bound.
@@ -922,6 +936,18 @@ PHP 8.6 UPGRADE NOTES
922936
. SODIUM_CRYPTO_XOF_TURBOSHAKE128_STATEBYTES.
923937
. SODIUM_CRYPTO_XOF_TURBOSHAKE256_BLOCKBYTES.
924938
. SODIUM_CRYPTO_XOF_TURBOSHAKE256_STATEBYTES.
939+
. SODIUM_CRYPTO_KEM_PUBLICKEYBYTES (libsodium >= 1.0.22).
940+
. SODIUM_CRYPTO_KEM_SECRETKEYBYTES (libsodium >= 1.0.22).
941+
. SODIUM_CRYPTO_KEM_CIPHERTEXTBYTES (libsodium >= 1.0.22).
942+
. SODIUM_CRYPTO_KEM_SHAREDSECRETBYTES (libsodium >= 1.0.22).
943+
. SODIUM_CRYPTO_KEM_SEEDBYTES (libsodium >= 1.0.22).
944+
. SODIUM_CRYPTO_KEM_KEYPAIRBYTES (libsodium >= 1.0.22).
945+
. SODIUM_CRYPTO_KEM_MLKEM768_PUBLICKEYBYTES (libsodium >= 1.0.22).
946+
. SODIUM_CRYPTO_KEM_MLKEM768_SECRETKEYBYTES (libsodium >= 1.0.22).
947+
. SODIUM_CRYPTO_KEM_MLKEM768_CIPHERTEXTBYTES (libsodium >= 1.0.22).
948+
. SODIUM_CRYPTO_KEM_MLKEM768_SHAREDSECRETBYTES (libsodium >= 1.0.22).
949+
. SODIUM_CRYPTO_KEM_MLKEM768_SEEDBYTES (libsodium >= 1.0.22).
950+
. SODIUM_CRYPTO_KEM_MLKEM768_KEYPAIRBYTES (libsodium >= 1.0.22).
925951

926952
- Standard:
927953
. ARRAY_FILTER_USE_VALUE.

‎ext/sodium/libsodium.c‎

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4830,3 +4830,311 @@ PHP_FUNCTION(sodium_crypto_xof_turboshake256_squeeze)
48304830
RETURN_NEW_STR(out);
48314831
}
48324832
#endif
4833+
4834+
#ifdef crypto_kem_PUBLICKEYBYTES
4835+
PHP_FUNCTION(sodium_crypto_kem_keypair)
4836+
{
4837+
zend_string *keypair;
4838+
size_t keypair_len;
4839+
4840+
ZEND_PARSE_PARAMETERS_NONE();
4841+
4842+
keypair_len = crypto_kem_SECRETKEYBYTES + crypto_kem_PUBLICKEYBYTES;
4843+
keypair = zend_string_alloc(keypair_len, 0);
4844+
if (crypto_kem_keypair((unsigned char *) ZSTR_VAL(keypair) +
4845+
crypto_kem_SECRETKEYBYTES,
4846+
(unsigned char *) ZSTR_VAL(keypair)) != 0) {
4847+
sodium_memzero(ZSTR_VAL(keypair), keypair_len);
4848+
zend_string_efree(keypair);
4849+
zend_throw_exception(sodium_exception_ce, "internal error", 0);
4850+
RETURN_THROWS();
4851+
}
4852+
ZSTR_VAL(keypair)[keypair_len] = 0;
4853+
4854+
RETURN_NEW_STR(keypair);
4855+
}
4856+
4857+
PHP_FUNCTION(sodium_crypto_kem_seed_keypair)
4858+
{
4859+
zend_string *keypair;
4860+
unsigned char *seed;
4861+
size_t keypair_len;
4862+
size_t seed_len;
4863+
4864+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
4865+
&seed, &seed_len) == FAILURE) {
4866+
sodium_remove_param_values_from_backtrace(EG(exception));
4867+
RETURN_THROWS();
4868+
}
4869+
if (seed_len != crypto_kem_SEEDBYTES) {
4870+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_SEEDBYTES bytes long");
4871+
RETURN_THROWS();
4872+
}
4873+
keypair_len = crypto_kem_SECRETKEYBYTES + crypto_kem_PUBLICKEYBYTES;
4874+
keypair = zend_string_alloc(keypair_len, 0);
4875+
if (crypto_kem_seed_keypair((unsigned char *) ZSTR_VAL(keypair) +
4876+
crypto_kem_SECRETKEYBYTES,
4877+
(unsigned char *) ZSTR_VAL(keypair),
4878+
seed) != 0) {
4879+
sodium_memzero(ZSTR_VAL(keypair), keypair_len);
4880+
zend_string_efree(keypair);
4881+
zend_throw_exception(sodium_exception_ce, "internal error", 0);
4882+
RETURN_THROWS();
4883+
}
4884+
ZSTR_VAL(keypair)[keypair_len] = 0;
4885+
4886+
RETURN_NEW_STR(keypair);
4887+
}
4888+
4889+
PHP_FUNCTION(sodium_crypto_kem_secretkey)
4890+
{
4891+
unsigned char *keypair;
4892+
size_t keypair_len;
4893+
4894+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
4895+
&keypair, &keypair_len) == FAILURE) {
4896+
sodium_remove_param_values_from_backtrace(EG(exception));
4897+
RETURN_THROWS();
4898+
}
4899+
if (keypair_len !=
4900+
crypto_kem_SECRETKEYBYTES + crypto_kem_PUBLICKEYBYTES) {
4901+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_KEYPAIRBYTES bytes long");
4902+
RETURN_THROWS();
4903+
}
4904+
RETURN_STRINGL((const char *) keypair, crypto_kem_SECRETKEYBYTES);
4905+
}
4906+
4907+
PHP_FUNCTION(sodium_crypto_kem_publickey)
4908+
{
4909+
unsigned char *keypair;
4910+
size_t keypair_len;
4911+
4912+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
4913+
&keypair, &keypair_len) == FAILURE) {
4914+
sodium_remove_param_values_from_backtrace(EG(exception));
4915+
RETURN_THROWS();
4916+
}
4917+
if (keypair_len !=
4918+
crypto_kem_SECRETKEYBYTES + crypto_kem_PUBLICKEYBYTES) {
4919+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_KEYPAIRBYTES bytes long");
4920+
RETURN_THROWS();
4921+
}
4922+
RETURN_STRINGL((const char *) keypair + crypto_kem_SECRETKEYBYTES, crypto_kem_PUBLICKEYBYTES);
4923+
}
4924+
4925+
PHP_FUNCTION(sodium_crypto_kem_enc)
4926+
{
4927+
unsigned char ciphertext[crypto_kem_CIPHERTEXTBYTES];
4928+
unsigned char shared_secret[crypto_kem_SHAREDSECRETBYTES];
4929+
unsigned char *publickey;
4930+
size_t publickey_len;
4931+
4932+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
4933+
&publickey, &publickey_len) == FAILURE) {
4934+
sodium_remove_param_values_from_backtrace(EG(exception));
4935+
RETURN_THROWS();
4936+
}
4937+
if (publickey_len != crypto_kem_PUBLICKEYBYTES) {
4938+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_PUBLICKEYBYTES bytes long");
4939+
RETURN_THROWS();
4940+
}
4941+
if (crypto_kem_enc(ciphertext, shared_secret, publickey) != 0) {
4942+
sodium_memzero(shared_secret, sizeof shared_secret);
4943+
zend_throw_exception(sodium_exception_ce, "internal error", 0);
4944+
RETURN_THROWS();
4945+
}
4946+
array_init(return_value);
4947+
add_next_index_stringl(return_value, (const char *) ciphertext, sizeof ciphertext);
4948+
add_next_index_stringl(return_value, (const char *) shared_secret, sizeof shared_secret);
4949+
sodium_memzero(shared_secret, sizeof shared_secret);
4950+
}
4951+
4952+
PHP_FUNCTION(sodium_crypto_kem_dec)
4953+
{
4954+
zend_string *shared_secret;
4955+
unsigned char *ciphertext;
4956+
unsigned char *secretkey;
4957+
size_t ciphertext_len;
4958+
size_t secretkey_len;
4959+
4960+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss",
4961+
&ciphertext, &ciphertext_len,
4962+
&secretkey, &secretkey_len) == FAILURE) {
4963+
sodium_remove_param_values_from_backtrace(EG(exception));
4964+
RETURN_THROWS();
4965+
}
4966+
if (ciphertext_len != crypto_kem_CIPHERTEXTBYTES) {
4967+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_CIPHERTEXTBYTES bytes long");
4968+
RETURN_THROWS();
4969+
}
4970+
if (secretkey_len != crypto_kem_SECRETKEYBYTES) {
4971+
zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_KEM_SECRETKEYBYTES bytes long");
4972+
RETURN_THROWS();
4973+
}
4974+
shared_secret = zend_string_alloc(crypto_kem_SHAREDSECRETBYTES, 0);
4975+
if (crypto_kem_dec((unsigned char *) ZSTR_VAL(shared_secret),
4976+
ciphertext, secretkey) != 0) {
4977+
sodium_memzero(ZSTR_VAL(shared_secret), crypto_kem_SHAREDSECRETBYTES);
4978+
zend_string_efree(shared_secret);
4979+
zend_throw_exception(sodium_exception_ce, "internal error", 0);
4980+
RETURN_THROWS();
4981+
}
4982+
ZSTR_VAL(shared_secret)[crypto_kem_SHAREDSECRETBYTES] = 0;
4983+
4984+
RETURN_NEW_STR(shared_secret);
4985+
}
4986+
#endif
4987+
4988+
#ifdef crypto_kem_mlkem768_PUBLICKEYBYTES
4989+
PHP_FUNCTION(sodium_crypto_kem_mlkem768_keypair)
4990+
{
4991+
zend_string *keypair;
4992+
size_t keypair_len;
4993+
4994+
ZEND_PARSE_PARAMETERS_NONE();
4995+
4996+
keypair_len = crypto_kem_mlkem768_SECRETKEYBYTES + crypto_kem_mlkem768_PUBLICKEYBYTES;
4997+
keypair = zend_string_alloc(keypair_len, 0);
4998+
if (crypto_kem_mlkem768_keypair((unsigned char *) ZSTR_VAL(keypair) +
4999+
crypto_kem_mlkem768_SECRETKEYBYTES,
5000+
(unsigned char *) ZSTR_VAL(keypair)) != 0) {
5001+
sodium_memzero(ZSTR_VAL(keypair), keypair_len);
5002+
zend_string_efree(keypair);
5003+
zend_throw_exception(sodium_exception_ce, "internal error", 0);
5004+
RETURN_THROWS();
5005+
}
5006+
ZSTR_VAL(keypair)[keypair_len] = 0;
5007+
5008+
RETURN_NEW_STR(keypair);
5009+
}
5010+
5011+
PHP_FUNCTION(sodium_crypto_kem_mlkem768_seed_keypair)
5012+
{
5013+
zend_string *keypair;
5014+
unsigned char *seed;
5015+
size_t keypair_len;
5016+
size_t seed_len;
5017+
5018+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
5019+
&seed, &seed_len) == FAILURE) {
5020+
sodium_remove_param_values_from_backtrace(EG(exception));
5021+
RETURN_THROWS();
5022+
}
5023+
if (seed_len != crypto_kem_mlkem768_SEEDBYTES) {
5024+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_MLKEM768_SEEDBYTES bytes long");
5025+
RETURN_THROWS();
5026+
}
5027+
keypair_len = crypto_kem_mlkem768_SECRETKEYBYTES + crypto_kem_mlkem768_PUBLICKEYBYTES;
5028+
keypair = zend_string_alloc(keypair_len, 0);
5029+
if (crypto_kem_mlkem768_seed_keypair((unsigned char *) ZSTR_VAL(keypair) +
5030+
crypto_kem_mlkem768_SECRETKEYBYTES,
5031+
(unsigned char *) ZSTR_VAL(keypair),
5032+
seed) != 0) {
5033+
sodium_memzero(ZSTR_VAL(keypair), keypair_len);
5034+
zend_string_efree(keypair);
5035+
zend_throw_exception(sodium_exception_ce, "internal error", 0);
5036+
RETURN_THROWS();
5037+
}
5038+
ZSTR_VAL(keypair)[keypair_len] = 0;
5039+
5040+
RETURN_NEW_STR(keypair);
5041+
}
5042+
5043+
PHP_FUNCTION(sodium_crypto_kem_mlkem768_secretkey)
5044+
{
5045+
unsigned char *keypair;
5046+
size_t keypair_len;
5047+
5048+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
5049+
&keypair, &keypair_len) == FAILURE) {
5050+
sodium_remove_param_values_from_backtrace(EG(exception));
5051+
RETURN_THROWS();
5052+
}
5053+
if (keypair_len !=
5054+
crypto_kem_mlkem768_SECRETKEYBYTES + crypto_kem_mlkem768_PUBLICKEYBYTES) {
5055+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_MLKEM768_KEYPAIRBYTES bytes long");
5056+
RETURN_THROWS();
5057+
}
5058+
RETURN_STRINGL((const char *) keypair, crypto_kem_mlkem768_SECRETKEYBYTES);
5059+
}
5060+
5061+
PHP_FUNCTION(sodium_crypto_kem_mlkem768_publickey)
5062+
{
5063+
unsigned char *keypair;
5064+
size_t keypair_len;
5065+
5066+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
5067+
&keypair, &keypair_len) == FAILURE) {
5068+
sodium_remove_param_values_from_backtrace(EG(exception));
5069+
RETURN_THROWS();
5070+
}
5071+
if (keypair_len !=
5072+
crypto_kem_mlkem768_SECRETKEYBYTES + crypto_kem_mlkem768_PUBLICKEYBYTES) {
5073+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_MLKEM768_KEYPAIRBYTES bytes long");
5074+
RETURN_THROWS();
5075+
}
5076+
RETURN_STRINGL((const char *) keypair + crypto_kem_mlkem768_SECRETKEYBYTES, crypto_kem_mlkem768_PUBLICKEYBYTES);
5077+
}
5078+
5079+
PHP_FUNCTION(sodium_crypto_kem_mlkem768_enc)
5080+
{
5081+
unsigned char ciphertext[crypto_kem_mlkem768_CIPHERTEXTBYTES];
5082+
unsigned char shared_secret[crypto_kem_mlkem768_SHAREDSECRETBYTES];
5083+
unsigned char *publickey;
5084+
size_t publickey_len;
5085+
5086+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
5087+
&publickey, &publickey_len) == FAILURE) {
5088+
sodium_remove_param_values_from_backtrace(EG(exception));
5089+
RETURN_THROWS();
5090+
}
5091+
if (publickey_len != crypto_kem_mlkem768_PUBLICKEYBYTES) {
5092+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_MLKEM768_PUBLICKEYBYTES bytes long");
5093+
RETURN_THROWS();
5094+
}
5095+
if (crypto_kem_mlkem768_enc(ciphertext, shared_secret, publickey) != 0) {
5096+
sodium_memzero(shared_secret, sizeof shared_secret);
5097+
zend_throw_exception(sodium_exception_ce, "internal error", 0);
5098+
RETURN_THROWS();
5099+
}
5100+
array_init(return_value);
5101+
add_next_index_stringl(return_value, (const char *) ciphertext, sizeof ciphertext);
5102+
add_next_index_stringl(return_value, (const char *) shared_secret, sizeof shared_secret);
5103+
sodium_memzero(shared_secret, sizeof shared_secret);
5104+
}
5105+
5106+
PHP_FUNCTION(sodium_crypto_kem_mlkem768_dec)
5107+
{
5108+
zend_string *shared_secret;
5109+
unsigned char *ciphertext;
5110+
unsigned char *secretkey;
5111+
size_t ciphertext_len;
5112+
size_t secretkey_len;
5113+
5114+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss",
5115+
&ciphertext, &ciphertext_len,
5116+
&secretkey, &secretkey_len) == FAILURE) {
5117+
sodium_remove_param_values_from_backtrace(EG(exception));
5118+
RETURN_THROWS();
5119+
}
5120+
if (ciphertext_len != crypto_kem_mlkem768_CIPHERTEXTBYTES) {
5121+
zend_argument_error(sodium_exception_ce, 1, "must be SODIUM_CRYPTO_KEM_MLKEM768_CIPHERTEXTBYTES bytes long");
5122+
RETURN_THROWS();
5123+
}
5124+
if (secretkey_len != crypto_kem_mlkem768_SECRETKEYBYTES) {
5125+
zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_KEM_MLKEM768_SECRETKEYBYTES bytes long");
5126+
RETURN_THROWS();
5127+
}
5128+
shared_secret = zend_string_alloc(crypto_kem_mlkem768_SHAREDSECRETBYTES, 0);
5129+
if (crypto_kem_mlkem768_dec((unsigned char *) ZSTR_VAL(shared_secret),
5130+
ciphertext, secretkey) != 0) {
5131+
sodium_memzero(ZSTR_VAL(shared_secret), crypto_kem_mlkem768_SHAREDSECRETBYTES);
5132+
zend_string_efree(shared_secret);
5133+
zend_throw_exception(sodium_exception_ce, "internal error", 0);
5134+
RETURN_THROWS();
5135+
}
5136+
ZSTR_VAL(shared_secret)[crypto_kem_mlkem768_SHAREDSECRETBYTES] = 0;
5137+
5138+
RETURN_NEW_STR(shared_secret);
5139+
}
5140+
#endif

0 commit comments

Comments
 (0)