From 40bdc3dd79391c9403c17b5bc3fc702801dc3178 Mon Sep 17 00:00:00 2001 From: viraladmin <00purple@gmail.com> Date: Sat, 25 Jul 2026 13:07:35 -0600 Subject: [PATCH] Initial Contractless PHP crypto modules --- .gitignore | 5 + README.md | 114 +++++++++++++++ build-linux.sh | 57 ++++++++ install-linux.sh | 29 ++++ interop/Cargo.lock | 283 ++++++++++++++++++++++++++++++++++++++ interop/Cargo.toml | 11 ++ interop/README.md | 38 +++++ interop/oqs_interop.c | 103 ++++++++++++++ interop/skein_interop.c | 54 ++++++++ interop/src/main.rs | 91 ++++++++++++ oqsphp-contractless.patch | 31 +++++ tests/native.php | 43 ++++++ 12 files changed, 859 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 build-linux.sh create mode 100755 install-linux.sh create mode 100644 interop/Cargo.lock create mode 100644 interop/Cargo.toml create mode 100644 interop/README.md create mode 100644 interop/oqs_interop.c create mode 100644 interop/skein_interop.c create mode 100644 interop/src/main.rs create mode 100644 oqsphp-contractless.patch create mode 100644 tests/native.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0db910e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/native/ +/vendor/ +/interop/fixtures/ +/interop/target/ +/interop/*.exe diff --git a/README.md b/README.md new file mode 100644 index 0000000..fbde84c --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# Contractless PHP Crypto Modules + +These native PHP modules provide the cryptography required by +`php-contractless-rpc`: + +- Skein-256 hashing through PHP-Skein-Hash +- Contractless-compatible Falcon-512 signing and verification through liboqs + +Contractless uses fixed 666-byte Falcon signatures and a specific FN-DSA raw +message format. The included liboqs-php patch adds the SHAKE256 operation needed +to reproduce that format exactly. + +## Supported Environment + +The automated installation currently supports 64-bit Debian and Ubuntu systems. +This includes Ubuntu running through WSL. + +Native modules are tied to the operating system, CPU architecture, and PHP +extension API. They should normally be compiled on the machine where they will +run. + +## Build Requirements + +```bash +sudo apt update +sudo apt install -y \ + php-cli php-dev \ + swig cmake ninja-build \ + build-essential git pkg-config libssl-dev +``` + +## Build + +```bash +chmod +x build-linux.sh install-linux.sh +./build-linux.sh +``` + +This downloads and compiles: + +- A minimal liboqs build containing `Falcon-padded-512` +- The patched liboqs-php extension +- The PHP-Skein-Hash extension + +Building does not install or enable the modules. + +## Install + +After a successful build: + +```bash +sudo ./install-linux.sh +``` + +The installer copies both modules into the active PHP extension directory, +creates `contractless-crypto.ini`, enables it through `phpenmod`, and verifies +that PHP can see both modules. + +Confirm the installation manually: + +```bash +php -m | grep -E '^(skein|oqsphp)$' +``` + +Expected output: + +```text +oqsphp +skein +``` + +Apache or PHP-FPM may need to be restarted: + +```bash +sudo systemctl restart apache2 +``` + +or: + +```bash +sudo systemctl restart php8.3-fpm +``` + +Use the PHP version installed on the server in the service name. + +## Compatibility Test + +The compatibility test proves that PHP and Contractless Rust produce matching +Skein hashes and mutually valid Falcon signatures: + +```bash +cd interop +cargo run --release +cd .. +php tests/native.php +``` + +The generated private key is a disposable test fixture. Never use it as an +application wallet. + +## Using The Modules + +Applications normally use these modules through `php-contractless-rpc` rather +than calling them directly. + +Direct Skein hashing: + +```php +$digest = skein_hash($data, 256); +``` + +Direct Contractless Falcon handling requires the public-key SHAKE256 prefix and +raw-message marker. `php-contractless-rpc` applies that format automatically, +so application developers should use its `NativeCrypto` class. diff --git a/build-linux.sh b/build-linux.sh new file mode 100755 index 0000000..cdea4a0 --- /dev/null +++ b/build-linux.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +third_party="$root/native/third-party" +mkdir -p "$third_party" + +if [[ ! -d "$third_party/liboqs/.git" ]]; then + git clone --depth 1 https://github.com/open-quantum-safe/liboqs.git \ + "$third_party/liboqs" +fi + +cmake -S "$third_party/liboqs" -B "$third_party/liboqs/build" -GNinja \ + -DBUILD_SHARED_LIBS=OFF \ + -DOQS_BUILD_ONLY_LIB=ON \ + -DOQS_MINIMAL_BUILD="SIG_falcon_padded_512" +ninja -C "$third_party/liboqs/build" + +if [[ ! -d "$third_party/liboqs-php/.git" ]]; then + git clone --depth 1 https://github.com/Muzosh/liboqs-php.git \ + "$third_party/liboqs-php" +fi + +( + cd "$third_party/liboqs-php" + if git apply --check "$root/oqsphp-contractless.patch"; then + git apply "$root/oqsphp-contractless.patch" + elif ! git apply --reverse --check "$root/oqsphp-contractless.patch"; then + echo "The Contractless liboqs-php patch cannot be applied cleanly." >&2 + exit 1 + fi + LIBOQS_ROOT="$third_party/liboqs" bash build.sh +) + +if [[ ! -d "$third_party/php-skein/.git" ]]; then + git clone --depth 1 https://github.com/jedisct1/PHP-Skein-Hash.git \ + "$third_party/php-skein" +fi + +( + cd "$third_party/php-skein" + phpize + ./configure + make +) + +cat <&2 + exit 1 +fi + +extension_dir="$(php-config --extension-dir)" +php_version="$(php -r 'echo PHP_MAJOR_VERSION . \".\" . PHP_MINOR_VERSION;')" +ini_dir="/etc/php/$php_version/mods-available" + +install -m 0755 "$oqs_module" "$extension_dir/oqsphp.so" +install -m 0755 "$skein_module" "$extension_dir/skein.so" +mkdir -p "$ini_dir" +printf '%s\n' 'extension=skein.so' 'extension=oqsphp.so' \ + > "$ini_dir/contractless-crypto.ini" + +if command -v phpenmod >/dev/null 2>&1; then + phpenmod contractless-crypto +fi + +php -m | grep -E '^(skein|oqsphp)$' +echo "Contractless PHP crypto modules installed and enabled for PHP $php_version." diff --git a/interop/Cargo.lock b/interop/Cargo.lock new file mode 100644 index 0000000..dc24f7f --- /dev/null +++ b/interop/Cargo.lock @@ -0,0 +1,283 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "contractless-crypto-interop" +version = "0.1.0" +dependencies = [ + "digest", + "fn-dsa", + "rand", + "skein", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fn-dsa" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb39d94be3f4538ccfdf23dc69a89904c9b802331b1975538fcc251973593f9" +dependencies = [ + "fn-dsa-comm", + "fn-dsa-kgen", + "fn-dsa-sign", + "fn-dsa-vrfy", +] + +[[package]] +name = "fn-dsa-comm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28104bba227c79bff8ffe34dff7c5a5631fd790dfd4d4f0a84b01952ef7c6f46" +dependencies = [ + "cpufeatures", + "rand_core", +] + +[[package]] +name = "fn-dsa-kgen" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a04fe82799cc1550d32778b415648d1962a7b4b4860ff5f2e6ca32494fc9aa" +dependencies = [ + "fn-dsa-comm", + "zeroize", +] + +[[package]] +name = "fn-dsa-sign" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffb7d9c94e56de7e64019849b3b6110b1495690da2e9e1decef8f1744b3bab4" +dependencies = [ + "fn-dsa-comm", + "zeroize", +] + +[[package]] +name = "fn-dsa-vrfy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc96b904b213160c8804cc2642ccd15cb968318a64648fc5ca3edab9153cda2b" +dependencies = [ + "fn-dsa-comm", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "skein" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a90a220ab98dbbfeabeae7c558a79f37839c10b9ef55c77082a741a463cab7" +dependencies = [ + "digest", + "threefish", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "threefish" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a693d0c8cf16973fac5a93fbe47b8c6452e7097d4fcac49f3d7a18e39c76e62e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/interop/Cargo.toml b/interop/Cargo.toml new file mode 100644 index 0000000..45c66b7 --- /dev/null +++ b/interop/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "contractless-crypto-interop" +version = "0.1.0" +edition = "2021" + +[dependencies] +fn-dsa = "0.3.0" +rand = "0.8" +skein = "0.1.0" +digest = "0.10" + diff --git a/interop/README.md b/interop/README.md new file mode 100644 index 0000000..075256e --- /dev/null +++ b/interop/README.md @@ -0,0 +1,38 @@ +# Contractless PHP Crypto Interoperability + +This harness verifies that the native libraries packaged by +`contractless-php-crypto` produce data compatible with Contractless. + +## Results + +- The C implementation wrapped by `jedisct1/PHP-Skein-Hash` produces the + same 32-byte Skein-256 output as the Rust `skein` crate used by + Contractless. +- Unmodified `Falcon-padded-512` message handling is not compatible with + Contractless `fn-dsa 0.3.0`. +- `Falcon-padded-512` keys and fixed signatures use the same Contractless + sizes: 897-byte public keys, 1,281-byte private keys, and 666-byte + signatures. +- Both signing implementations are compatible when liboqs receives the + message framing used by Contractless: + +```text +SHAKE256(public_key, 64 bytes) || 0x00 || 0x00 || original_message +``` + +The two zero bytes represent raw-message mode and an empty domain context. + +With that adapter: + +- Contractless Rust signatures verify through liboqs. +- Liboqs signatures verify through Contractless Rust. + +## Required liboqs algorithm + +Use `Falcon-padded-512`. Do not use variable-length `Falcon-512`. + +## Regression requirement + +Run these tests whenever Contractless changes its `fn-dsa`, `skein`, or +signature-framing dependencies. Matching key and signature lengths alone +does not establish compatibility. diff --git a/interop/oqs_interop.c b/interop/oqs_interop.c new file mode 100644 index 0000000..156c624 --- /dev/null +++ b/interop/oqs_interop.c @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include + +static uint8_t *read_file(const char *path, size_t *length) { + FILE *file = fopen(path, "rb"); + if (file == NULL) { + fprintf(stderr, "Could not open %s\n", path); + exit(1); + } + fseek(file, 0, SEEK_END); + long size = ftell(file); + rewind(file); + if (size < 0) { + fprintf(stderr, "Could not determine size of %s\n", path); + exit(1); + } + uint8_t *bytes = malloc((size_t)size); + if (bytes == NULL || fread(bytes, 1, (size_t)size, file) != (size_t)size) { + fprintf(stderr, "Could not read %s\n", path); + exit(1); + } + fclose(file); + *length = (size_t)size; + return bytes; +} + +static void write_file(const char *path, const uint8_t *bytes, size_t length) { + FILE *file = fopen(path, "wb"); + if (file == NULL || fwrite(bytes, 1, length, file) != length) { + fprintf(stderr, "Could not write %s\n", path); + exit(1); + } + fclose(file); +} + +int main(void) { + OQS_SIG *sig = OQS_SIG_new(OQS_SIG_alg_falcon_padded_512); + if (sig == NULL) { + fprintf(stderr, "Falcon-padded-512 is unavailable\n"); + return 1; + } + + size_t public_key_len; + size_t private_key_len; + size_t message_len; + size_t rust_signature_len; + uint8_t *public_key = read_file("fixtures/public-key.bin", &public_key_len); + uint8_t *private_key = read_file("fixtures/private-key.bin", &private_key_len); + uint8_t *message = read_file("fixtures/message.bin", &message_len); + uint8_t *rust_signature = + read_file("fixtures/rust-signature.bin", &rust_signature_len); + size_t adapted_message_len = 64 + 2 + message_len; + uint8_t *adapted_message = malloc(adapted_message_len); + OQS_SHA3_shake256(adapted_message, 64, public_key, public_key_len); + adapted_message[64] = 0; + adapted_message[65] = 0; + for (size_t i = 0; i < message_len; i++) { + adapted_message[66 + i] = message[i]; + } + + printf("liboqs public_key_bytes=%zu private_key_bytes=%zu signature_bytes=%zu\n", + sig->length_public_key, sig->length_secret_key, sig->length_signature); + + if (public_key_len != sig->length_public_key || + private_key_len != sig->length_secret_key || + rust_signature_len != sig->length_signature) { + fprintf(stderr, "Contractless and liboqs object sizes differ\n"); + return 1; + } + + OQS_STATUS rust_valid = OQS_SIG_verify( + sig, adapted_message, adapted_message_len, + rust_signature, rust_signature_len, public_key + ); + printf("Rust signature verified in liboqs: %s\n", + rust_valid == OQS_SUCCESS ? "true" : "false"); + + uint8_t *oqs_signature = malloc(sig->length_signature); + size_t oqs_signature_len = sig->length_signature; + OQS_STATUS signed_ok = OQS_SIG_sign( + sig, oqs_signature, &oqs_signature_len, + adapted_message, adapted_message_len, private_key + ); + if (signed_ok != OQS_SUCCESS) { + fprintf(stderr, "liboqs signing failed\n"); + return 1; + } + printf("liboqs produced signature_bytes=%zu\n", oqs_signature_len); + write_file("fixtures/oqs-signature.bin", oqs_signature, oqs_signature_len); + + free(oqs_signature); + free(adapted_message); + free(rust_signature); + free(message); + free(private_key); + free(public_key); + OQS_SIG_free(sig); + + return rust_valid == OQS_SUCCESS ? 0 : 2; +} diff --git a/interop/skein_interop.c b/interop/skein_interop.c new file mode 100644 index 0000000..5d02df9 --- /dev/null +++ b/interop/skein_interop.c @@ -0,0 +1,54 @@ +#include "skein.h" +#include +#include +#include + +static uint8_t *read_file(const char *path, size_t *length) { + FILE *file = fopen(path, "rb"); + if (file == NULL) { + return NULL; + } + fseek(file, 0, SEEK_END); + long size = ftell(file); + rewind(file); + uint8_t *bytes = malloc((size_t)size); + if (size < 0 || bytes == NULL || + fread(bytes, 1, (size_t)size, file) != (size_t)size) { + return NULL; + } + fclose(file); + *length = (size_t)size; + return bytes; +} + +int main(void) { + size_t message_len; + size_t rust_hash_len; + uint8_t *message = read_file("fixtures/message.bin", &message_len); + uint8_t *rust_hash = read_file("fixtures/rust-skein256.bin", &rust_hash_len); + if (message == NULL || rust_hash == NULL || rust_hash_len != 32) { + fprintf(stderr, "Could not read Skein fixtures\n"); + return 1; + } + + uint8_t extension_hash[32]; + Skein_256_Ctxt_t context; + Skein_256_Init(&context, 256); + Skein_256_Update(&context, message, message_len); + Skein_256_Final(&context, extension_hash); + + int equal = 1; + for (size_t i = 0; i < sizeof(extension_hash); i++) { + if (extension_hash[i] != rust_hash[i]) { + equal = 0; + break; + } + } + printf("PHP-Skein-Hash output matches Contractless Rust: %s\n", + equal ? "true" : "false"); + + free(rust_hash); + free(message); + return equal ? 0 : 2; +} + diff --git a/interop/src/main.rs b/interop/src/main.rs new file mode 100644 index 0000000..ef0715f --- /dev/null +++ b/interop/src/main.rs @@ -0,0 +1,91 @@ +use digest::Digest; +use fn_dsa::{ + signature_size, sign_key_size, vrfy_key_size, KeyPairGenerator, + KeyPairGeneratorStandard, SigningKey, SigningKeyStandard, VerifyingKey, + VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_512, HASH_ID_RAW, +}; +use rand::rngs::OsRng; +use skein::Skein256; +use digest::consts::U32; +use std::fs; +use std::path::Path; + +const MESSAGE: &[u8] = b"Contractless FN-DSA interoperability\x00binary\xffmessage"; + +fn main() { + let fixture_dir = Path::new("fixtures"); + fs::create_dir_all(fixture_dir).expect("create fixture directory"); + + let private_key_path = fixture_dir.join("private-key.bin"); + let public_key_path = fixture_dir.join("public-key.bin"); + let message_path = fixture_dir.join("message.bin"); + let rust_signature_path = fixture_dir.join("rust-signature.bin"); + let oqs_signature_path = fixture_dir.join("oqs-signature.bin"); + + if !private_key_path.exists() { + let mut generator = KeyPairGeneratorStandard::default(); + let mut private_key = vec![0u8; sign_key_size(FN_DSA_LOGN_512)]; + let mut public_key = vec![0u8; vrfy_key_size(FN_DSA_LOGN_512)]; + generator.keygen( + FN_DSA_LOGN_512, + &mut OsRng, + &mut private_key, + &mut public_key, + ); + fs::write(&private_key_path, private_key).expect("write private key"); + fs::write(&public_key_path, public_key).expect("write public key"); + fs::write(&message_path, MESSAGE).expect("write message"); + } + + let private_key = fs::read(&private_key_path).expect("read private key"); + let public_key = fs::read(&public_key_path).expect("read public key"); + let message = fs::read(&message_path).expect("read message"); + + let mut signing_key = + SigningKeyStandard::decode(&private_key).expect("decode Contractless private key"); + let mut rust_signature = vec![0u8; signature_size(FN_DSA_LOGN_512)]; + signing_key.sign( + &mut OsRng, + &DOMAIN_NONE, + &HASH_ID_RAW, + &message, + &mut rust_signature, + ); + fs::write(&rust_signature_path, &rust_signature).expect("write Rust signature"); + + let verifying_key = + VerifyingKeyStandard::decode(&public_key).expect("decode Contractless public key"); + assert!( + verifying_key.verify( + &rust_signature, + &DOMAIN_NONE, + &HASH_ID_RAW, + &message + ), + "Rust signature did not verify in Rust" + ); + + let mut hasher = Skein256::::new(); + hasher.update(&message); + let hash = hasher.finalize(); + fs::write(fixture_dir.join("rust-skein256.bin"), hash).expect("write Skein hash"); + + println!("Rust FN-DSA signature verified in Rust."); + println!("public_key_bytes={}", public_key.len()); + println!("private_key_bytes={}", private_key.len()); + println!("signature_bytes={}", rust_signature.len()); + + if oqs_signature_path.exists() { + let oqs_signature = fs::read(&oqs_signature_path).expect("read liboqs signature"); + let valid = verifying_key.verify( + &oqs_signature, + &DOMAIN_NONE, + &HASH_ID_RAW, + &message, + ); + println!("liboqs signature verified in Rust: {valid}"); + if !valid { + std::process::exit(2); + } + } +} diff --git a/oqsphp-contractless.patch b/oqsphp-contractless.patch new file mode 100644 index 0000000..ab74f25 --- /dev/null +++ b/oqsphp-contractless.patch @@ -0,0 +1,31 @@ +--- a/oqsphp.i ++++ b/oqsphp.i +@@ -25,6 +25,7 @@ + %module(directors="1") oqsphp + %{ + #include "oqs/oqs.h" ++#include "oqs/sha3.h" + #include + #include + #include +@@ -39,6 +40,20 @@ + %feature("director"); + %feature("php:type", "1"); + ++%inline %{ ++std::string contractless_shake256(const std::string &input, size_t output_length) { ++ std::string output; ++ output.resize(output_length); ++ OQS_SHA3_shake256( ++ reinterpret_cast(output.data()), ++ output_length, ++ reinterpret_cast(input.data()), ++ input.length() ++ ); ++ return output; ++} ++%} ++ + %inline %{ + class CustomException : public std::exception { + public: diff --git a/tests/native.php b/tests/native.php new file mode 100644 index 0000000..687708a --- /dev/null +++ b/tests/native.php @@ -0,0 +1,43 @@ +verify($adapted, $rustSignature, $publicKey) !== 0) { + throw new RuntimeException('PHP/liboqs did not verify the Contractless Rust signature.'); +} + +$phpSignature = ''; +if ($falcon->sign($phpSignature, $adapted, $privateKey) !== 0 + || strlen($phpSignature) !== 666 + || $falcon->verify($adapted, $phpSignature, $publicKey) !== 0 +) { + throw new RuntimeException('PHP/liboqs Falcon signing failed.'); +} + +echo "Native Skein and Falcon interoperability tests passed.\n";