Initial Contractless PHP crypto modules

This commit is contained in:
viraladmin 2026-07-25 13:07:35 -06:00
commit 40bdc3dd79
12 changed files with 859 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/native/
/vendor/
/interop/fixtures/
/interop/target/
/interop/*.exe

114
README.md Normal file
View File

@ -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.

57
build-linux.sh Executable file
View File

@ -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 <<EOF
Native extensions built:
$third_party/liboqs-php/build/oqsphp.so
$third_party/php-skein/modules/skein.so
Add both absolute paths to php.ini:
extension=$third_party/liboqs-php/build/oqsphp.so
extension=$third_party/php-skein/modules/skein.so
EOF

29
install-linux.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
oqs_module="$root/native/third-party/liboqs-php/build/oqsphp.so"
skein_module="$root/native/third-party/php-skein/modules/skein.so"
if [[ ! -f "$oqs_module" || ! -f "$skein_module" ]]; then
echo "The modules have not been built. Run ./build-linux.sh first." >&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."

283
interop/Cargo.lock generated Normal file
View File

@ -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",
]

11
interop/Cargo.toml Normal file
View File

@ -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"

38
interop/README.md Normal file
View File

@ -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.

103
interop/oqs_interop.c Normal file
View File

@ -0,0 +1,103 @@
#include <oqs/oqs.h>
#include <oqs/sha3.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
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;
}

54
interop/skein_interop.c Normal file
View File

@ -0,0 +1,54 @@
#include "skein.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
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;
}

91
interop/src/main.rs Normal file
View File

@ -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::<U32>::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);
}
}
}

31
oqsphp-contractless.patch Normal file
View File

@ -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 <string>
#include <stdexcept>
#include <exception>
@@ -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<uint8_t *>(output.data()),
+ output_length,
+ reinterpret_cast<const uint8_t *>(input.data()),
+ input.length()
+ );
+ return output;
+}
+%}
+
%inline %{
class CustomException : public std::exception {
public:

43
tests/native.php Normal file
View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
$fixtures = dirname(__DIR__) . '/interop/fixtures';
$message = file_get_contents("$fixtures/message.bin");
$publicKey = file_get_contents("$fixtures/public-key.bin");
$privateKey = file_get_contents("$fixtures/private-key.bin");
$rustSignature = file_get_contents("$fixtures/rust-signature.bin");
$rustSkein = file_get_contents("$fixtures/rust-skein256.bin");
if (in_array(false, [$message, $publicKey, $privateKey, $rustSignature, $rustSkein], true)) {
throw new RuntimeException(
'Interop fixtures are missing. Run cargo run --release inside interop first.',
);
}
if (!function_exists('skein_hash') || !function_exists('contractless_shake256')) {
throw new RuntimeException('The Contractless PHP crypto modules are not loaded.');
}
if (!class_exists('OQS_SIGNATURE')) {
throw new RuntimeException('The liboqs-php module is not loaded.');
}
$phpSkein = skein_hash($message, 256);
if (!is_string($phpSkein) || !hash_equals($rustSkein, $phpSkein)) {
throw new RuntimeException('PHP Skein-256 does not match Contractless Rust.');
}
$falcon = new OQS_SIGNATURE('Falcon-padded-512');
$adapted = contractless_shake256($publicKey, 64) . "\0\0" . $message;
if ($falcon->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";