From 77d3c1182fa1950c303a5a0936547d22c24bd0c2 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 6 May 2026 10:14:12 -0400 Subject: [PATCH 1/3] feat: add SLH-DSA (FIPS 205) sign/verify support (#1007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements all 12 SLH-DSA variants: SHA2-{128,192,256}{s,f} and SHAKE-{128,192,256}{s,f}. - New HybridSlhDsaKeyPair (mirrors HybridMlDsaKeyPair) with EVP-based generate/sign/verify. - Extends KeyObjectHandle for SLH-DSA in AKP/PQC paths: JWK kty=AKP import/export with NIST alg names, raw-public / raw-seed import via initPqcRaw, and getAsymmetricKeyType. - Extends sign/verify handle one-shot detection for the 12 SLH-DSA EVP_PKEY types. - Subtle: generateKey, importKey, exportKey (spki/pkcs8/raw-public/ raw-seed/jwk), and sign/verify route SLH-DSA through the AKP machinery shared with ML-DSA. - Top-level crypto.generateKeyPair('slh-dsa-...') support. - Tests cover round-trip sign/verify, export→import→verify, tampered signature rejection, and SHA2/SHAKE cross-variant rejection. --- .docs/implementation-coverage.md | 441 +++++++++++------- docs/content/docs/api/index.mdx | 4 +- docs/content/docs/api/pqc.mdx | 167 +++++-- docs/content/docs/api/subtle.mdx | 248 +++++----- example/src/tests/subtle/sign_verify.ts | 193 ++++++++ .../android/CMakeLists.txt | 2 + .../cpp/keys/HybridKeyObjectHandle.cpp | 79 +++- .../cpp/sign/HybridSignHandle.cpp | 26 +- .../cpp/sign/HybridVerifyHandle.cpp | 26 +- .../cpp/slhdsa/HybridSlhDsaKeyPair.cpp | 245 ++++++++++ .../cpp/slhdsa/HybridSlhDsaKeyPair.hpp | 48 ++ packages/react-native-quick-crypto/nitro.json | 3 + .../android/QuickCrypto+autolinking.cmake | 1 + .../generated/android/QuickCryptoOnLoad.cpp | 10 + .../generated/ios/QuickCryptoAutolinking.mm | 10 + .../shared/c++/AsymmetricKeyType.hpp | 48 ++ .../shared/c++/HybridSlhDsaKeyPairSpec.cpp | 29 ++ .../shared/c++/HybridSlhDsaKeyPairSpec.hpp | 72 +++ .../src/keys/generateKeyPair.ts | 78 ++++ .../react-native-quick-crypto/src/slhdsa.ts | 146 ++++++ .../src/specs/slhDsaKeyPair.nitro.ts | 29 ++ .../react-native-quick-crypto/src/subtle.ts | 153 +++++- .../src/utils/types.ts | 40 +- 23 files changed, 1744 insertions(+), 354 deletions(-) create mode 100644 packages/react-native-quick-crypto/cpp/slhdsa/HybridSlhDsaKeyPair.cpp create mode 100644 packages/react-native-quick-crypto/cpp/slhdsa/HybridSlhDsaKeyPair.hpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.cpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.hpp create mode 100644 packages/react-native-quick-crypto/src/slhdsa.ts create mode 100644 packages/react-native-quick-crypto/src/specs/slhDsaKeyPair.nitro.ts diff --git a/.docs/implementation-coverage.md b/.docs/implementation-coverage.md index e05742a4..b7f67f85 100644 --- a/.docs/implementation-coverage.md +++ b/.docs/implementation-coverage.md @@ -14,6 +14,7 @@ This document attempts to describe the implementation status of Crypto APIs/Inte - **ML-DSA** (Module Lattice Digital Signature Algorithm, FIPS 204) - ML-DSA-44, ML-DSA-65, ML-DSA-87 - **ML-KEM** (Module Lattice Key Encapsulation Mechanism, FIPS 203) - ML-KEM-512, ML-KEM-768, ML-KEM-1024 +- **SLH-DSA** (Stateless Hash-Based Digital Signature, FIPS 205) - SLH-DSA-SHA2-{128,192,256}{s,f}, SLH-DSA-SHAKE-{128,192,256}{s,f} These algorithms provide quantum-resistant cryptography. @@ -184,31 +185,55 @@ These algorithms provide quantum-resistant cryptography. ## `crypto.generateKeyPair` -| type | Status | -| --------- | :----: | -| `rsa` | ✅ | -| `rsa-pss` | ✅ | -| `dsa` | ✅ | -| `ec` | ✅ | -| `ed25519` | ✅ | -| `ed448` | ✅ | -| `x25519` | ✅ | -| `x448` | ✅ | -| `dh` | ✅ | +| type | Status | +| -------------------- | :----: | +| `rsa` | ✅ | +| `rsa-pss` | ✅ | +| `dsa` | ✅ | +| `ec` | ✅ | +| `ed25519` | ✅ | +| `ed448` | ✅ | +| `x25519` | ✅ | +| `x448` | ✅ | +| `dh` | ✅ | +| `slh-dsa-sha2-128s` | ✅ | +| `slh-dsa-sha2-128f` | ✅ | +| `slh-dsa-sha2-192s` | ✅ | +| `slh-dsa-sha2-192f` | ✅ | +| `slh-dsa-sha2-256s` | ✅ | +| `slh-dsa-sha2-256f` | ✅ | +| `slh-dsa-shake-128s` | ✅ | +| `slh-dsa-shake-128f` | ✅ | +| `slh-dsa-shake-192s` | ✅ | +| `slh-dsa-shake-192f` | ✅ | +| `slh-dsa-shake-256s` | ✅ | +| `slh-dsa-shake-256f` | ✅ | ## `crypto.generateKeyPairSync` -| type | Status | -| --------- | :----: | -| `rsa` | ✅ | -| `rsa-pss` | ✅ | -| `dsa` | ✅ | -| `ec` | ✅ | -| `ed25519` | ✅ | -| `ed448` | ✅ | -| `x25519` | ✅ | -| `x448` | ✅ | -| `dh` | ✅ | +| type | Status | +| -------------------- | :----: | +| `rsa` | ✅ | +| `rsa-pss` | ✅ | +| `dsa` | ✅ | +| `ec` | ✅ | +| `ed25519` | ✅ | +| `ed448` | ✅ | +| `x25519` | ✅ | +| `x448` | ✅ | +| `dh` | ✅ | +| `slh-dsa-sha2-128s` | ✅ | +| `slh-dsa-sha2-128f` | ✅ | +| `slh-dsa-sha2-192s` | ✅ | +| `slh-dsa-sha2-192f` | ✅ | +| `slh-dsa-sha2-256s` | ✅ | +| `slh-dsa-sha2-256f` | ✅ | +| `slh-dsa-shake-128s` | ✅ | +| `slh-dsa-shake-128f` | ✅ | +| `slh-dsa-shake-192s` | ✅ | +| `slh-dsa-shake-192f` | ✅ | +| `slh-dsa-shake-256s` | ✅ | +| `slh-dsa-shake-256f` | ✅ | ## `crypto.generateKeySync` @@ -219,25 +244,49 @@ These algorithms provide quantum-resistant cryptography. ## `crypto.sign` -| Algorithm | Status | -| ------------------- | :----: | -| `RSASSA-PKCS1-v1_5` | ✅ | -| `RSA-PSS` | ✅ | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | +| Algorithm | Status | +| -------------------- | :----: | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `RSA-PSS` | ✅ | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | +| `SLH-DSA-SHA2-128s` | ✅ | +| `SLH-DSA-SHA2-128f` | ✅ | +| `SLH-DSA-SHA2-192s` | ✅ | +| `SLH-DSA-SHA2-192f` | ✅ | +| `SLH-DSA-SHA2-256s` | ✅ | +| `SLH-DSA-SHA2-256f` | ✅ | +| `SLH-DSA-SHAKE-128s` | ✅ | +| `SLH-DSA-SHAKE-128f` | ✅ | +| `SLH-DSA-SHAKE-192s` | ✅ | +| `SLH-DSA-SHAKE-192f` | ✅ | +| `SLH-DSA-SHAKE-256s` | ✅ | +| `SLH-DSA-SHAKE-256f` | ✅ | ## `crypto.verify` -| Algorithm | Status | -| ------------------- | :----: | -| `RSASSA-PKCS1-v1_5` | ✅ | -| `RSA-PSS` | ✅ | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | +| Algorithm | Status | +| -------------------- | :----: | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `RSA-PSS` | ✅ | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | +| `SLH-DSA-SHA2-128s` | ✅ | +| `SLH-DSA-SHA2-128f` | ✅ | +| `SLH-DSA-SHA2-192s` | ✅ | +| `SLH-DSA-SHA2-192f` | ✅ | +| `SLH-DSA-SHA2-256s` | ✅ | +| `SLH-DSA-SHA2-256f` | ✅ | +| `SLH-DSA-SHAKE-128s` | ✅ | +| `SLH-DSA-SHAKE-128f` | ✅ | +| `SLH-DSA-SHAKE-192s` | ✅ | +| `SLH-DSA-SHAKE-192f` | ✅ | +| `SLH-DSA-SHAKE-256s` | ✅ | +| `SLH-DSA-SHAKE-256f` | ✅ | ## Extended Ciphers (Beyond Node.js API) @@ -356,30 +405,42 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs ## `subtle.exportKey` -| Key Type | `spki` | `pkcs8` | `jwk` | `raw` | `raw-secret` | `raw-public` | `raw-seed` | -| ------------------- | :----: | :-----: | :---: | :---: | :----------: | :----------: | :--------: | -| `AES-CBC` | | | ✅ | ✅ | ✅ | | | -| `AES-CTR` | | | ✅ | ✅ | ✅ | | | -| `AES-GCM` | | | ✅ | ✅ | ✅ | | | -| `AES-KW` | | | ✅ | ✅ | ✅ | | | -| `AES-OCB` | | | ✅ | ✅ | ✅ | | | -| `ChaCha20-Poly1305` | | | ✅ | | ✅ | | | -| `ECDH` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `ECDSA` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `Ed25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `Ed448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `HMAC` | | | ✅ | ✅ | ✅ | | | -| `KMAC128` | | | ✅ | ✅ | ✅ | | | -| `KMAC256` | | | ✅ | ✅ | ✅ | | | -| `ML-DSA-44` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-DSA-65` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-DSA-87` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-KEM-512` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-KEM-768` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-KEM-1024` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `RSA-OAEP` | ✅ | ✅ | ✅ | | | | | -| `RSA-PSS` | ✅ | ✅ | ✅ | | | | | -| `RSASSA-PKCS1-v1_5` | ✅ | ✅ | ✅ | | | | | +| Key Type | `spki` | `pkcs8` | `jwk` | `raw` | `raw-secret` | `raw-public` | `raw-seed` | +| -------------------- | :----: | :-----: | :---: | :---: | :----------: | :----------: | :--------: | +| `AES-CBC` | | | ✅ | ✅ | ✅ | | | +| `AES-CTR` | | | ✅ | ✅ | ✅ | | | +| `AES-GCM` | | | ✅ | ✅ | ✅ | | | +| `AES-KW` | | | ✅ | ✅ | ✅ | | | +| `AES-OCB` | | | ✅ | ✅ | ✅ | | | +| `ChaCha20-Poly1305` | | | ✅ | | ✅ | | | +| `ECDH` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `ECDSA` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `Ed25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `Ed448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `HMAC` | | | ✅ | ✅ | ✅ | | | +| `KMAC128` | | | ✅ | ✅ | ✅ | | | +| `KMAC256` | | | ✅ | ✅ | ✅ | | | +| `ML-DSA-44` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-DSA-65` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-DSA-87` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-KEM-512` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-KEM-768` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-KEM-1024` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `RSA-OAEP` | ✅ | ✅ | ✅ | | | | | +| `RSA-PSS` | ✅ | ✅ | ✅ | | | | | +| `RSASSA-PKCS1-v1_5` | ✅ | ✅ | ✅ | | | | | +| `SLH-DSA-SHA2-128s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-128f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-192s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-192f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-256s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-256f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-128s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-128f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-192s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-192f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-256s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-256f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | - ` ` - not implemented in Node - ❌ - implemented in Node, not RNQC @@ -389,23 +450,35 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs ### `CryptoKeyPair` algorithms -| Algorithm | Status | -| ------------------- | :----: | -| `ECDH` | ✅ | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `ML-DSA-44` | ✅ | -| `ML-DSA-65` | ✅ | -| `ML-DSA-87` | ✅ | -| `ML-KEM-512` | ✅ | -| `ML-KEM-768` | ✅ | -| `ML-KEM-1024` | ✅ | -| `RSA-OAEP` | ✅ | -| `RSA-PSS` | ✅ | -| `RSASSA-PKCS1-v1_5` | ✅ | -| `X25519` | ✅ | -| `X448` | ✅ | +| Algorithm | Status | +| -------------------- | :----: | +| `ECDH` | ✅ | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `ML-DSA-44` | ✅ | +| `ML-DSA-65` | ✅ | +| `ML-DSA-87` | ✅ | +| `ML-KEM-512` | ✅ | +| `ML-KEM-768` | ✅ | +| `ML-KEM-1024` | ✅ | +| `RSA-OAEP` | ✅ | +| `RSA-PSS` | ✅ | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `SLH-DSA-SHA2-128s` | ✅ | +| `SLH-DSA-SHA2-128f` | ✅ | +| `SLH-DSA-SHA2-192s` | ✅ | +| `SLH-DSA-SHA2-192f` | ✅ | +| `SLH-DSA-SHA2-256s` | ✅ | +| `SLH-DSA-SHA2-256f` | ✅ | +| `SLH-DSA-SHAKE-128s` | ✅ | +| `SLH-DSA-SHAKE-128f` | ✅ | +| `SLH-DSA-SHAKE-192s` | ✅ | +| `SLH-DSA-SHAKE-192f` | ✅ | +| `SLH-DSA-SHAKE-256s` | ✅ | +| `SLH-DSA-SHAKE-256f` | ✅ | +| `X25519` | ✅ | +| `X448` | ✅ | ### `CryptoKey` algorithms @@ -423,53 +496,77 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs ## `subtle.importKey` -| Key Type | `spki` | `pkcs8` | `jwk` | `raw` | `raw-secret` | `raw-public` | `raw-seed` | -| ------------------- | :----: | :-----: | :---: | :---: | :----------: | :----------: | :--------: | -| `Argon2d` | | | | | ✅ | | | -| `Argon2i` | | | | | ✅ | | | -| `Argon2id` | | | | | ✅ | | | -| `AES-CBC` | | | ✅ | ✅ | ✅ | | | -| `AES-CTR` | | | ✅ | ✅ | ✅ | | | -| `AES-GCM` | | | ✅ | ✅ | ✅ | | | -| `AES-KW` | | | ✅ | ✅ | ✅ | | | -| `AES-OCB` | | | ✅ | ✅ | ✅ | | | -| `ChaCha20-Poly1305` | | | ✅ | | ✅ | | | -| `ECDH` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `ECDSA` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `Ed25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `Ed448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `HKDF` | | | | ✅ | ✅ | | | -| `HMAC` | | | ✅ | ✅ | ✅ | | | -| `KMAC128` | | | ✅ | ✅ | ✅ | | | -| `KMAC256` | | | ✅ | ✅ | ✅ | | | -| `ML-DSA-44` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-DSA-65` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-DSA-87` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-KEM-512` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-KEM-768` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `ML-KEM-1024` | ✅ | ✅ | ❌ | | | ✅ | ✅ | -| `PBKDF2` | | | | ✅ | ✅ | | | -| `RSA-OAEP` | ✅ | ✅ | ✅ | | | | | -| `RSA-PSS` | ✅ | ✅ | ✅ | | | | | -| `RSASSA-PKCS1-v1_5` | ✅ | ✅ | ✅ | | | | | -| `X25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `X448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| Key Type | `spki` | `pkcs8` | `jwk` | `raw` | `raw-secret` | `raw-public` | `raw-seed` | +| -------------------- | :----: | :-----: | :---: | :---: | :----------: | :----------: | :--------: | +| `Argon2d` | | | | | ✅ | | | +| `Argon2i` | | | | | ✅ | | | +| `Argon2id` | | | | | ✅ | | | +| `AES-CBC` | | | ✅ | ✅ | ✅ | | | +| `AES-CTR` | | | ✅ | ✅ | ✅ | | | +| `AES-GCM` | | | ✅ | ✅ | ✅ | | | +| `AES-KW` | | | ✅ | ✅ | ✅ | | | +| `AES-OCB` | | | ✅ | ✅ | ✅ | | | +| `ChaCha20-Poly1305` | | | ✅ | | ✅ | | | +| `ECDH` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `ECDSA` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `Ed25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `Ed448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `HKDF` | | | | ✅ | ✅ | | | +| `HMAC` | | | ✅ | ✅ | ✅ | | | +| `KMAC128` | | | ✅ | ✅ | ✅ | | | +| `KMAC256` | | | ✅ | ✅ | ✅ | | | +| `ML-DSA-44` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-DSA-65` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-DSA-87` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-KEM-512` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-KEM-768` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `ML-KEM-1024` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `PBKDF2` | | | | ✅ | ✅ | | | +| `RSA-OAEP` | ✅ | ✅ | ✅ | | | | | +| `RSA-PSS` | ✅ | ✅ | ✅ | | | | | +| `RSASSA-PKCS1-v1_5` | ✅ | ✅ | ✅ | | | | | +| `SLH-DSA-SHA2-128s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-128f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-192s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-192f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-256s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHA2-256f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-128s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-128f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-192s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-192f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-256s` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `SLH-DSA-SHAKE-256f` | ✅ | ✅ | ❌ | | | ✅ | ✅ | +| `X25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `X448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | ## `subtle.sign` -| Algorithm | Status | -| ------------------- | :----: | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | -| `KMAC128` | ✅ | -| `KMAC256` | ✅ | -| `ML-DSA-44` | ✅ | -| `ML-DSA-65` | ✅ | -| `ML-DSA-87` | ✅ | -| `RSA-PSS` | ✅ | -| `RSASSA-PKCS1-v1_5` | ✅ | +| Algorithm | Status | +| -------------------- | :----: | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | +| `KMAC128` | ✅ | +| `KMAC256` | ✅ | +| `ML-DSA-44` | ✅ | +| `ML-DSA-65` | ✅ | +| `ML-DSA-87` | ✅ | +| `RSA-PSS` | ✅ | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `SLH-DSA-SHA2-128s` | ✅ | +| `SLH-DSA-SHA2-128f` | ✅ | +| `SLH-DSA-SHA2-192s` | ✅ | +| `SLH-DSA-SHA2-192f` | ✅ | +| `SLH-DSA-SHA2-256s` | ✅ | +| `SLH-DSA-SHA2-256f` | ✅ | +| `SLH-DSA-SHAKE-128s` | ✅ | +| `SLH-DSA-SHAKE-128f` | ✅ | +| `SLH-DSA-SHAKE-192s` | ✅ | +| `SLH-DSA-SHAKE-192f` | ✅ | +| `SLH-DSA-SHAKE-256s` | ✅ | +| `SLH-DSA-SHAKE-256f` | ✅ | ## `subtle.unwrapKey` @@ -487,46 +584,70 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs ### unwrapped key algorithms -| Algorithm | Status | -| ------------------- | :----: | -| `AES-CBC` | ✅ | -| `AES-CTR` | ✅ | -| `AES-GCM` | ✅ | -| `AES-KW` | ✅ | -| `AES-OCB` | ✅ | -| `ChaCha20-Poly1305` | ✅ | -| `ECDH` | ✅ | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | -| `ML-DSA-44` | ✅ | -| `ML-DSA-65` | ✅ | -| `ML-DSA-87` | ✅ | -| `ML-KEM-512` | ✅ | -| `ML-KEM-768` | ✅ | -| `ML-KEM-1024` | ✅ | -| `RSA-OAEP` | ✅ | -| `RSA-PSS` | ✅ | -| `RSASSA-PKCS1-v1_5` | ✅ | -| `X25519` | ✅ | -| `X448` | ✅ | +| Algorithm | Status | +| -------------------- | :----: | +| `AES-CBC` | ✅ | +| `AES-CTR` | ✅ | +| `AES-GCM` | ✅ | +| `AES-KW` | ✅ | +| `AES-OCB` | ✅ | +| `ChaCha20-Poly1305` | ✅ | +| `ECDH` | ✅ | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | +| `ML-DSA-44` | ✅ | +| `ML-DSA-65` | ✅ | +| `ML-DSA-87` | ✅ | +| `ML-KEM-512` | ✅ | +| `ML-KEM-768` | ✅ | +| `ML-KEM-1024` | ✅ | +| `RSA-OAEP` | ✅ | +| `RSA-PSS` | ✅ | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `SLH-DSA-SHA2-128s` | ✅ | +| `SLH-DSA-SHA2-128f` | ✅ | +| `SLH-DSA-SHA2-192s` | ✅ | +| `SLH-DSA-SHA2-192f` | ✅ | +| `SLH-DSA-SHA2-256s` | ✅ | +| `SLH-DSA-SHA2-256f` | ✅ | +| `SLH-DSA-SHAKE-128s` | ✅ | +| `SLH-DSA-SHAKE-128f` | ✅ | +| `SLH-DSA-SHAKE-192s` | ✅ | +| `SLH-DSA-SHAKE-192f` | ✅ | +| `SLH-DSA-SHAKE-256s` | ✅ | +| `SLH-DSA-SHAKE-256f` | ✅ | +| `X25519` | ✅ | +| `X448` | ✅ | ## `subtle.verify` -| Algorithm | Status | -| ------------------- | :----: | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | -| `KMAC128` | ✅ | -| `KMAC256` | ✅ | -| `ML-DSA-44` | ✅ | -| `ML-DSA-65` | ✅ | -| `ML-DSA-87` | ✅ | -| `RSA-PSS` | ✅ | -| `RSASSA-PKCS1-v1_5` | ✅ | +| Algorithm | Status | +| -------------------- | :----: | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | +| `KMAC128` | ✅ | +| `KMAC256` | ✅ | +| `ML-DSA-44` | ✅ | +| `ML-DSA-65` | ✅ | +| `ML-DSA-87` | ✅ | +| `RSA-PSS` | ✅ | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `SLH-DSA-SHA2-128s` | ✅ | +| `SLH-DSA-SHA2-128f` | ✅ | +| `SLH-DSA-SHA2-192s` | ✅ | +| `SLH-DSA-SHA2-192f` | ✅ | +| `SLH-DSA-SHA2-256s` | ✅ | +| `SLH-DSA-SHA2-256f` | ✅ | +| `SLH-DSA-SHAKE-128s` | ✅ | +| `SLH-DSA-SHAKE-128f` | ✅ | +| `SLH-DSA-SHAKE-192s` | ✅ | +| `SLH-DSA-SHAKE-192f` | ✅ | +| `SLH-DSA-SHAKE-256s` | ✅ | +| `SLH-DSA-SHAKE-256f` | ✅ | ## `subtle.wrapKey` diff --git a/docs/content/docs/api/index.mdx b/docs/content/docs/api/index.mdx index 9ddfff82..b902698b 100644 --- a/docs/content/docs/api/index.mdx +++ b/docs/content/docs/api/index.mdx @@ -22,7 +22,7 @@ RNQC mirrors the Node.js `crypto` API while adding specialized high-performance Keyed-hash message authentication. - + CSPRNG, UUIDs, and random integers. @@ -77,7 +77,7 @@ RNQC mirrors the Node.js `crypto` API while adding specialized high-performance - ML-DSA signatures and ML-KEM key encapsulation. + ML-DSA / SLH-DSA signatures and ML-KEM key encapsulation. Keccak Message Authentication Code (KMAC128/KMAC256). diff --git a/docs/content/docs/api/pqc.mdx b/docs/content/docs/api/pqc.mdx index 38e30e0a..643d8041 100644 --- a/docs/content/docs/api/pqc.mdx +++ b/docs/content/docs/api/pqc.mdx @@ -1,6 +1,6 @@ --- title: Post-Quantum Cryptography -description: Quantum-resistant algorithms (ML-DSA, ML-KEM) +description: Quantum-resistant algorithms (ML-DSA, ML-KEM, SLH-DSA) --- import { Callout } from 'fumadocs-ui/components/callout'; @@ -9,7 +9,9 @@ import { TypeTable } from 'fumadocs-ui/components/type-table'; **Post-Quantum Cryptography (PQC)** provides cryptographic algorithms that are secure against both classical and quantum computers. RNQC implements the NIST standardized lattice-based algorithms via OpenSSL 3.6+. - Quantum computers threaten RSA, ECDSA, and ECDH. The PQC algorithms below are NIST-standardized replacements designed to resist quantum attacks while running efficiently on classical hardware. + Quantum computers threaten RSA, ECDSA, and ECDH. The PQC algorithms below are + NIST-standardized replacements designed to resist quantum attacks while + running efficiently on classical hardware. ## Table of Contents @@ -17,6 +19,7 @@ import { TypeTable } from 'fumadocs-ui/components/type-table'; - [Algorithms](#algorithms) - [ML-DSA (Digital Signatures)](#ml-dsa-digital-signatures) - [ML-KEM (Key Encapsulation)](#ml-kem-key-encapsulation) +- [SLH-DSA (Hash-Based Digital Signatures)](#slh-dsa-hash-based-digital-signatures) - [WebCrypto API](#webcrypto-api) - [Real-World Examples](#real-world-examples) @@ -26,21 +29,41 @@ import { TypeTable } from 'fumadocs-ui/components/type-table'; Module Lattice Digital Signature Algorithm. Replacement for RSA and ECDSA signatures. -| Parameter Set | Security Level | Public Key | Signature | Use Case | -|:-------------|:--------------|:-----------|:----------|:---------| -| `ML-DSA-44` | NIST Level 2 | 1,312 B | 2,420 B | General purpose | -| `ML-DSA-65` | NIST Level 3 | 1,952 B | 3,309 B | Recommended | -| `ML-DSA-87` | NIST Level 5 | 2,592 B | 4,627 B | Maximum security | +| Parameter Set | Security Level | Public Key | Signature | Use Case | +| :------------ | :------------- | :--------- | :-------- | :--------------- | +| `ML-DSA-44` | NIST Level 2 | 1,312 B | 2,420 B | General purpose | +| `ML-DSA-65` | NIST Level 3 | 1,952 B | 3,309 B | Recommended | +| `ML-DSA-87` | NIST Level 5 | 2,592 B | 4,627 B | Maximum security | ### ML-KEM (FIPS 203) Module Lattice Key Encapsulation Mechanism. Replacement for ECDH key exchange. | Parameter Set | Security Level | Public Key | Ciphertext | Shared Secret | -|:-------------|:--------------|:-----------|:-----------|:-------------| -| `ML-KEM-512` | NIST Level 1 | 800 B | 768 B | 32 B | -| `ML-KEM-768` | NIST Level 3 | 1,184 B | 1,088 B | 32 B | -| `ML-KEM-1024` | NIST Level 5 | 1,568 B | 1,568 B | 32 B | +| :------------ | :------------- | :--------- | :--------- | :------------ | +| `ML-KEM-512` | NIST Level 1 | 800 B | 768 B | 32 B | +| `ML-KEM-768` | NIST Level 3 | 1,184 B | 1,088 B | 32 B | +| `ML-KEM-1024` | NIST Level 5 | 1,568 B | 1,568 B | 32 B | + +### SLH-DSA (FIPS 205) + +Stateless Hash-Based Digital Signature Algorithm (formerly SPHINCS+). A hash-based alternative to ML-DSA — security relies only on the underlying hash function, making it a conservative choice when lattice assumptions are a concern. Each parameter set comes in `s` (small signature, slow) and `f` (fast, larger signature) variants. + +| Parameter Set | Security Level | Public Key | Signature | Notes | +| :----------------------------------------- | :------------- | :--------- | :-------- | :-------- | +| `SLH-DSA-SHA2-128s` / `SLH-DSA-SHAKE-128s` | NIST Level 1 | 32 B | 7,856 B | Small sig | +| `SLH-DSA-SHA2-128f` / `SLH-DSA-SHAKE-128f` | NIST Level 1 | 32 B | 17,088 B | Fast sign | +| `SLH-DSA-SHA2-192s` / `SLH-DSA-SHAKE-192s` | NIST Level 3 | 48 B | 16,224 B | Small sig | +| `SLH-DSA-SHA2-192f` / `SLH-DSA-SHAKE-192f` | NIST Level 3 | 48 B | 35,664 B | Fast sign | +| `SLH-DSA-SHA2-256s` / `SLH-DSA-SHAKE-256s` | NIST Level 5 | 64 B | 29,792 B | Small sig | +| `SLH-DSA-SHA2-256f` / `SLH-DSA-SHAKE-256f` | NIST Level 5 | 64 B | 49,856 B | Fast sign | + + + The `s` variants produce smaller signatures but signing is markedly slower + than ML-DSA. The `f` variants sign faster but emit signatures 4–6× larger. For + most applications ML-DSA is preferable; reach for SLH-DSA when a hash-only + security assumption is required. + --- @@ -51,11 +74,7 @@ Module Lattice Key Encapsulation Mechanism. Replacement for ECDH key exchange. Generate ML-DSA key pairs and sign/verify using the standard `crypto` API: ```ts -import { - generateKeyPairSync, - sign, - verify -} from 'react-native-quick-crypto'; +import { generateKeyPairSync, sign, verify } from 'react-native-quick-crypto'; // Generate ML-DSA-65 key pair const { publicKey, privateKey } = generateKeyPairSync('ml-dsa-65'); @@ -87,7 +106,7 @@ import { createPublicKey, createPrivateKey } from 'react-native-quick-crypto'; const imported = createPublicKey({ key: pubDer, format: 'der', - type: 'spki' + type: 'spki', }); ``` @@ -103,7 +122,7 @@ ML-KEM uses **encapsulation** rather than key exchange. One party encapsulates a import { generateKeyPairSync, encapsulate, - decapsulate + decapsulate, } from 'react-native-quick-crypto'; // Generate ML-KEM-768 key pair @@ -120,6 +139,29 @@ console.log(sharedSecret.equals(recovered)); // true --- +## SLH-DSA (Hash-Based Digital Signatures) + +### Node.js API + +Generate SLH-DSA key pairs and sign/verify using the standard `crypto` API. Twelve parameter sets are available: `slh-dsa-{sha2,shake}-{128,192,256}{s,f}`. + +```ts +import { generateKeyPairSync, sign, verify } from 'react-native-quick-crypto'; + +// Generate SLH-DSA-SHA2-128f key pair (fast variant) +const { publicKey, privateKey } = generateKeyPairSync('slh-dsa-sha2-128f'); + +// Sign +const message = Buffer.from('hash-based, quantum-safe message'); +const signature = sign(null, message, privateKey); + +// Verify +const isValid = verify(null, message, publicKey, signature); +console.log('Valid:', isValid); // true +``` + +--- + ## WebCrypto API PQC algorithms are fully supported through the SubtleCrypto interface. @@ -130,18 +172,17 @@ PQC algorithms are fully supported through the SubtleCrypto interface. import { subtle } from 'react-native-quick-crypto'; // Generate key pair -const keyPair = await subtle.generateKey( - { name: 'ML-DSA-65' }, - true, - ['sign', 'verify'] -); +const keyPair = await subtle.generateKey({ name: 'ML-DSA-65' }, true, [ + 'sign', + 'verify', +]); // Sign const data = new TextEncoder().encode('quantum-safe data'); const signature = await subtle.sign( { name: 'ML-DSA-65' }, keyPair.privateKey, - data + data, ); // Verify @@ -149,7 +190,7 @@ const isValid = await subtle.verify( { name: 'ML-DSA-65' }, keyPair.publicKey, signature, - data + data, ); ``` @@ -159,23 +200,22 @@ const isValid = await subtle.verify( import { subtle } from 'react-native-quick-crypto'; // Generate encapsulation key pair -const keyPair = await subtle.generateKey( - { name: 'ML-KEM-768' }, - true, - ['deriveBits', 'deriveKey'] -); +const keyPair = await subtle.generateKey({ name: 'ML-KEM-768' }, true, [ + 'deriveBits', + 'deriveKey', +]); // Encapsulate: get shared secret bits + ciphertext const { sharedSecret, ciphertext } = await subtle.encapsulateBits( { name: 'ML-KEM-768' }, - keyPair.publicKey + keyPair.publicKey, ); // Decapsulate: recover shared secret const recovered = await subtle.decapsulateBits( { name: 'ML-KEM-768' }, keyPair.privateKey, - ciphertext + ciphertext, ); // Or derive a key directly from encapsulation @@ -184,7 +224,7 @@ const { key: aesKey, ciphertext: ct } = await subtle.encapsulateKey( keyPair.publicKey, { name: 'AES-GCM', length: 256 }, true, - ['encrypt', 'decrypt'] + ['encrypt', 'decrypt'], ); // Decapsulate to get the same AES key @@ -194,16 +234,43 @@ const recoveredKey = await subtle.decapsulateKey( ct, { name: 'AES-GCM', length: 256 }, true, - ['encrypt', 'decrypt'] + ['encrypt', 'decrypt'], +); +``` + +### SLH-DSA via SubtleCrypto + +```ts +import { subtle } from 'react-native-quick-crypto'; + +// Generate key pair (use the canonical FIPS 205 name) +const keyPair = await subtle.generateKey({ name: 'SLH-DSA-SHA2-128f' }, true, [ + 'sign', + 'verify', +]); + +const data = new TextEncoder().encode('signed with hash-based PQC'); +const signature = await subtle.sign( + { name: 'SLH-DSA-SHA2-128f' }, + keyPair.privateKey, + data, +); + +const isValid = await subtle.verify( + { name: 'SLH-DSA-SHA2-128f' }, + keyPair.publicKey, + signature, + data, ); ``` ### Key Export Formats -| Algorithm | `spki` | `pkcs8` | `jwk` | `raw-public` | `raw-seed` | -|:----------|:------:|:-------:|:-----:|:------------:|:----------:| -| ML-DSA-44/65/87 | ✅ | ✅ | ✅ | ✅ | ✅ | -| ML-KEM-512/768/1024 | ✅ | ✅ | ✅ | ✅ | ✅ | +| Algorithm | `spki` | `pkcs8` | `jwk` | `raw-public` | `raw-seed` | +| :-------------------------------------- | :----: | :-----: | :---: | :----------: | :--------: | +| ML-DSA-44/65/87 | ✅ | ✅ | ✅ | ✅ | ✅ | +| ML-KEM-512/768/1024 | ✅ | ✅ | ✅ | ✅ | ✅ | +| SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f} | ✅ | ✅ | ✅ | ✅ | ✅ | ```ts // Export ML-DSA public key as JWK @@ -221,7 +288,7 @@ const imported = await subtle.importKey( rawPub, { name: 'ML-DSA-65' }, true, - ['verify'] + ['verify'], ); ``` @@ -234,11 +301,7 @@ const imported = await subtle.importKey( Combine Ed25519 with ML-DSA for defense-in-depth during the quantum transition: ```ts -import { - generateKeyPairSync, - sign, - verify -} from 'react-native-quick-crypto'; +import { generateKeyPairSync, sign, verify } from 'react-native-quick-crypto'; function hybridSign(message: Buffer) { const ed = generateKeyPairSync('ed25519'); @@ -250,18 +313,22 @@ function hybridSign(message: Buffer) { return { message, signatures: { ed25519: edSig, mlDsa65: pqcSig }, - publicKeys: { ed25519: ed.publicKey, mlDsa65: pqc.publicKey } + publicKeys: { ed25519: ed.publicKey, mlDsa65: pqc.publicKey }, }; } function hybridVerify(signed: ReturnType): boolean { const edValid = verify( - null, signed.message, - signed.publicKeys.ed25519, signed.signatures.ed25519 + null, + signed.message, + signed.publicKeys.ed25519, + signed.signatures.ed25519, ); const pqcValid = verify( - null, signed.message, - signed.publicKeys.mlDsa65, signed.signatures.mlDsa65 + null, + signed.message, + signed.publicKeys.mlDsa65, + signed.signatures.mlDsa65, ); // Both must pass @@ -281,7 +348,7 @@ import { createCipheriv, createDecipheriv, createHash, - randomBytes + randomBytes, } from 'react-native-quick-crypto'; // Server publishes its ML-KEM public key diff --git a/docs/content/docs/api/subtle.mdx b/docs/content/docs/api/subtle.mdx index 6fdd69fb..5bfeb51b 100644 --- a/docs/content/docs/api/subtle.mdx +++ b/docs/content/docs/api/subtle.mdx @@ -35,9 +35,12 @@ Encrypts data. @@ -51,7 +54,7 @@ Decrypts data. Supports the same algorithms as `encrypt`. Generates a digital signature. -**Supported algorithms:** `ECDSA`, `Ed25519`, `Ed448`, `HMAC`, `KMAC128`, `KMAC256`, `ML-DSA-44`, `ML-DSA-65`, `ML-DSA-87`, `RSA-PSS`, `RSASSA-PKCS1-v1_5` +**Supported algorithms:** `ECDSA`, `Ed25519`, `Ed448`, `HMAC`, `KMAC128`, `KMAC256`, `ML-DSA-44`, `ML-DSA-65`, `ML-DSA-87`, `RSA-PSS`, `RSASSA-PKCS1-v1_5`, `SLH-DSA-SHA2-{128,192,256}{s,f}`, `SLH-DSA-SHAKE-{128,192,256}{s,f}` ### verify(algorithm, key, signature, data) @@ -64,7 +67,9 @@ Generates a hash digest. **Supported algorithms:** `SHA-1`, `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`, `cSHAKE128`, `cSHAKE256` - `cSHAKE128` and `cSHAKE256` provide SHAKE XOF (Extendable Output Function) support. The `length` parameter (in bytes) is required to specify the output length. + `cSHAKE128` and `cSHAKE256` provide SHAKE XOF (Extendable Output Function) + support. The `length` parameter (in bytes) is required to specify the output + length. ```ts @@ -72,10 +77,7 @@ Generates a hash digest. const hash = await subtle.digest('SHA3-256', data); // cSHAKE256 with custom output length -const xof = await subtle.digest( - { name: 'cSHAKE256', length: 64 }, - data -); +const xof = await subtle.digest({ name: 'cSHAKE256', length: 64 }, data); ``` ### generateKey(algorithm, extractable, keyUsages) @@ -87,13 +89,13 @@ Generates a new key or key pair. ```ts const keyPair = await subtle.generateKey( { - name: "RSA-OAEP", + name: 'RSA-OAEP', modulusLength: 4096, publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256" + hash: 'SHA-256', }, true, - ["encrypt", "decrypt"] + ['encrypt', 'decrypt'], ); ``` @@ -113,9 +115,15 @@ Derives raw bits from a key using a key derivation algorithm. @@ -128,7 +136,7 @@ const passwordKey = await subtle.importKey( new TextEncoder().encode('password'), 'PBKDF2', false, - ['deriveBits'] + ['deriveBits'], ); const bits = await subtle.deriveBits( @@ -136,47 +144,41 @@ const bits = await subtle.deriveBits( name: 'PBKDF2', salt: crypto.getRandomValues(new Uint8Array(16)), iterations: 600000, - hash: 'SHA-256' + hash: 'SHA-256', }, passwordKey, - 256 + 256, ); ``` ```ts // HKDF -const ikmKey = await subtle.importKey( - 'raw', - masterSecret, - 'HKDF', - false, - ['deriveBits'] -); +const ikmKey = await subtle.importKey('raw', masterSecret, 'HKDF', false, [ + 'deriveBits', +]); const derived = await subtle.deriveBits( { name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32), - info: new TextEncoder().encode('session-key') + info: new TextEncoder().encode('session-key'), }, ikmKey, - 256 + 256, ); ``` ```ts // X25519 ECDH -const aliceKey = await subtle.generateKey( - { name: 'X25519' }, - true, - ['deriveBits'] -); +const aliceKey = await subtle.generateKey({ name: 'X25519' }, true, [ + 'deriveBits', +]); const sharedBits = await subtle.deriveBits( { name: 'X25519', public: bobPublicKey }, aliceKey.privateKey, - 256 + 256, ); ``` @@ -191,12 +193,12 @@ const aesKey = await subtle.deriveKey( name: 'PBKDF2', salt: crypto.getRandomValues(new Uint8Array(16)), iterations: 600000, - hash: 'SHA-256' + hash: 'SHA-256', }, passwordKey, { name: 'AES-GCM', length: 256 }, true, - ['encrypt', 'decrypt'] + ['encrypt', 'decrypt'], ); ``` @@ -208,10 +210,16 @@ Exports a key and encrypts (wraps) it for safe transport. @@ -222,21 +230,18 @@ Exports a key and encrypts (wraps) it for safe transport. const wrappingKey = await subtle.generateKey( { name: 'AES-KW', length: 256 }, true, - ['wrapKey', 'unwrapKey'] + ['wrapKey', 'unwrapKey'], ); const keyToWrap = await subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, - ['encrypt', 'decrypt'] + ['encrypt', 'decrypt'], ); -const wrapped = await subtle.wrapKey( - 'raw', - keyToWrap, - wrappingKey, - { name: 'AES-KW' } -); +const wrapped = await subtle.wrapKey('raw', keyToWrap, wrappingKey, { + name: 'AES-KW', +}); ``` ### unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgo, unwrappedKeyAlgo, extractable, keyUsages) @@ -251,7 +256,7 @@ const unwrapped = await subtle.unwrapKey( { name: 'AES-KW' }, { name: 'AES-GCM', length: 256 }, true, - ['encrypt', 'decrypt'] + ['encrypt', 'decrypt'], ); ``` @@ -263,13 +268,10 @@ Extracts the public key from a private `CryptoKey`. const keyPair = await subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, - ['sign', 'verify'] + ['sign', 'verify'], ); -const publicOnly = await subtle.getPublicKey( - keyPair.privateKey, - ['verify'] -); +const publicOnly = await subtle.getPublicKey(keyPair.privateKey, ['verify']); ``` ### SubtleCrypto.supports(operation, algorithm[, lengthOrAdditionalAlgorithm]) @@ -296,48 +298,49 @@ Post-quantum key encapsulation methods for ML-KEM. See the [PQC page](/docs/api/ ### Encryption/Decryption -| Algorithm | Type | Notes | -|:----------|:-----|:------| -| `AES-CBC` | Symmetric | Block cipher, requires padding | -| `AES-CTR` | Symmetric | Stream-like, counter mode | -| `AES-GCM` | Symmetric | **Recommended** — authenticated encryption | -| `AES-OCB` | Symmetric | Authenticated, faster than GCM | -| `ChaCha20-Poly1305` | Symmetric | Authenticated, fast on mobile | -| `RSA-OAEP` | Asymmetric | For small payloads or key wrapping | +| Algorithm | Type | Notes | +| :------------------ | :--------- | :----------------------------------------- | +| `AES-CBC` | Symmetric | Block cipher, requires padding | +| `AES-CTR` | Symmetric | Stream-like, counter mode | +| `AES-GCM` | Symmetric | **Recommended** — authenticated encryption | +| `AES-OCB` | Symmetric | Authenticated, faster than GCM | +| `ChaCha20-Poly1305` | Symmetric | Authenticated, fast on mobile | +| `RSA-OAEP` | Asymmetric | For small payloads or key wrapping | ### Signing/Verification -| Algorithm | Type | Notes | -|:----------|:-----|:------| -| `ECDSA` | Asymmetric | P-256, P-384, P-521, secp256k1 | -| `Ed25519` | Asymmetric | Fast, deterministic | -| `Ed448` | Asymmetric | Higher security Ed curve | -| `HMAC` | Symmetric | Message authentication | -| `KMAC128` / `KMAC256` | Symmetric | Keccak-based MAC | -| `ML-DSA-44/65/87` | Post-Quantum | FIPS 204 lattice signatures | -| `RSA-PSS` | Asymmetric | Probabilistic RSA signatures | -| `RSASSA-PKCS1-v1_5` | Asymmetric | Legacy RSA signatures | +| Algorithm | Type | Notes | +| :---------------------------------------- | :----------- | :------------------------------------------- | +| `ECDSA` | Asymmetric | P-256, P-384, P-521, secp256k1 | +| `Ed25519` | Asymmetric | Fast, deterministic | +| `Ed448` | Asymmetric | Higher security Ed curve | +| `HMAC` | Symmetric | Message authentication | +| `KMAC128` / `KMAC256` | Symmetric | Keccak-based MAC | +| `ML-DSA-44/65/87` | Post-Quantum | FIPS 204 lattice signatures | +| `RSA-PSS` | Asymmetric | Probabilistic RSA signatures | +| `RSASSA-PKCS1-v1_5` | Asymmetric | Legacy RSA signatures | +| `SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f}` | Post-Quantum | FIPS 205 hash-based signatures (12 variants) | ### Key Derivation -| Algorithm | Input | Notes | -|:----------|:------|:------| -| `PBKDF2` | Password | Iterations-based, widely supported | -| `HKDF` | Key material | Extract-and-Expand (RFC 5869) | -| `Argon2d/i/id` | Password | Memory-hard, PHC winner | -| `ECDH` | Key pair | P-256, P-384, P-521, secp256k1 | -| `X25519` | Key pair | Modern ECDH | -| `X448` | Key pair | Higher security ECDH | +| Algorithm | Input | Notes | +| :------------- | :----------- | :--------------------------------- | +| `PBKDF2` | Password | Iterations-based, widely supported | +| `HKDF` | Key material | Extract-and-Expand (RFC 5869) | +| `Argon2d/i/id` | Password | Memory-hard, PHC winner | +| `ECDH` | Key pair | P-256, P-384, P-521, secp256k1 | +| `X25519` | Key pair | Modern ECDH | +| `X448` | Key pair | Higher security ECDH | ### Key Wrapping -| Algorithm | Notes | -|:----------|:------| -| `AES-KW` | RFC 3394, purpose-built key wrapping | -| `AES-GCM` | Authenticated wrapping | -| `AES-CBC` / `AES-CTR` / `AES-OCB` | Alternative wrapping | -| `ChaCha20-Poly1305` | Authenticated wrapping | -| `RSA-OAEP` | Asymmetric wrapping | +| Algorithm | Notes | +| :-------------------------------- | :----------------------------------- | +| `AES-KW` | RFC 3394, purpose-built key wrapping | +| `AES-GCM` | Authenticated wrapping | +| `AES-CBC` / `AES-CTR` / `AES-OCB` | Alternative wrapping | +| `ChaCha20-Poly1305` | Authenticated wrapping | +| `RSA-OAEP` | Asymmetric wrapping | --- @@ -351,24 +354,23 @@ Symmetric encryption with a random key. import { subtle } from 'react-native-quick-crypto'; async function secureMessage(msg: string) { - // Generate Key - const key = await subtle.generateKey( - { name: "AES-GCM", length: 256 }, - true, - ["encrypt", "decrypt"] - ); - - // Encrypt - const iv = QuickCrypto.getRandomValues(new Uint8Array(12)); - const encoded = new TextEncoder().encode(msg); - - const ciphertext = await subtle.encrypt( - { name: "AES-GCM", iv: iv }, - key, - encoded - ); - - return { key, iv, ciphertext }; + // Generate Key + const key = await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [ + 'encrypt', + 'decrypt', + ]); + + // Encrypt + const iv = QuickCrypto.getRandomValues(new Uint8Array(12)); + const encoded = new TextEncoder().encode(msg); + + const ciphertext = await subtle.encrypt( + { name: 'AES-GCM', iv: iv }, + key, + encoded, + ); + + return { key, iv, ciphertext }; } ``` @@ -378,24 +380,24 @@ Importing a public key from a JWK to verify a token. ```ts async function verifyToken(token: string, jwk: any) { - const pubKey = await subtle.importKey( - "jwk", - jwk, - { name: "ECDSA", namedCurve: "P-256" }, - false, - ["verify"] - ); - - const [header, payload, sigBase64] = token.split('.'); - const data = new TextEncoder().encode(`${header}.${payload}`); - const signature = Buffer.from(sigBase64, 'base64'); - - return await subtle.verify( - { name: "ECDSA", hash: "SHA-256" }, - pubKey, - signature, - data - ); + const pubKey = await subtle.importKey( + 'jwk', + jwk, + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['verify'], + ); + + const [header, payload, sigBase64] = token.split('.'); + const data = new TextEncoder().encode(`${header}.${payload}`); + const signature = Buffer.from(sigBase64, 'base64'); + + return await subtle.verify( + { name: 'ECDSA', hash: 'SHA-256' }, + pubKey, + signature, + data, + ); } ``` @@ -413,7 +415,7 @@ async function encryptWithPassword(plaintext: string, password: string) { new TextEncoder().encode(password), 'PBKDF2', false, - ['deriveKey'] + ['deriveKey'], ); const aesKey = await subtle.deriveKey( @@ -421,13 +423,13 @@ async function encryptWithPassword(plaintext: string, password: string) { passwordKey, { name: 'AES-GCM', length: 256 }, false, - ['encrypt', 'decrypt'] + ['encrypt', 'decrypt'], ); const ciphertext = await subtle.encrypt( { name: 'AES-GCM', iv }, aesKey, - new TextEncoder().encode(plaintext) + new TextEncoder().encode(plaintext), ); return { ciphertext, salt, iv }; diff --git a/example/src/tests/subtle/sign_verify.ts b/example/src/tests/subtle/sign_verify.ts index 3a6ea388..4ee5fd7f 100644 --- a/example/src/tests/subtle/sign_verify.ts +++ b/example/src/tests/subtle/sign_verify.ts @@ -942,6 +942,199 @@ test(SUITE, 'ML-DSA cross-variant: 44 sig under 65 pub rejected', async () => { } }); +// --- SLH-DSA Tests (FIPS 205) --- +// +// SLH-DSA signature sizes (bytes), per FIPS 205 §11: +// 128s: 7856 128f: 17088 +// 192s: 16224 192f: 35664 +// 256s: 29792 256f: 49856 + +type SlhDsaVariant = + | 'SLH-DSA-SHA2-128s' + | 'SLH-DSA-SHA2-128f' + | 'SLH-DSA-SHA2-192s' + | 'SLH-DSA-SHA2-192f' + | 'SLH-DSA-SHA2-256s' + | 'SLH-DSA-SHA2-256f' + | 'SLH-DSA-SHAKE-128s' + | 'SLH-DSA-SHAKE-128f' + | 'SLH-DSA-SHAKE-192s' + | 'SLH-DSA-SHAKE-192f' + | 'SLH-DSA-SHAKE-256s' + | 'SLH-DSA-SHAKE-256f'; + +const SLHDSA_SIGNATURE_SIZES: Record = { + 'SLH-DSA-SHA2-128s': 7856, + 'SLH-DSA-SHA2-128f': 17088, + 'SLH-DSA-SHA2-192s': 16224, + 'SLH-DSA-SHA2-192f': 35664, + 'SLH-DSA-SHA2-256s': 29792, + 'SLH-DSA-SHA2-256f': 49856, + 'SLH-DSA-SHAKE-128s': 7856, + 'SLH-DSA-SHAKE-128f': 17088, + 'SLH-DSA-SHAKE-192s': 16224, + 'SLH-DSA-SHAKE-192f': 35664, + 'SLH-DSA-SHAKE-256s': 29792, + 'SLH-DSA-SHAKE-256f': 49856, +}; + +// SLH-DSA keygen for the larger parameter sets is fast, but signing — +// especially the `s` (small-signature) variants — is significantly slower +// than ML-DSA. Tests are intentionally minimal per variant: one round-trip +// each, plus a single tampered-signature check on the smallest variant to +// guard the negative path. +const SLHDSA_VARIANTS: SlhDsaVariant[] = [ + 'SLH-DSA-SHA2-128s', + 'SLH-DSA-SHA2-128f', + 'SLH-DSA-SHA2-192s', + 'SLH-DSA-SHA2-192f', + 'SLH-DSA-SHA2-256s', + 'SLH-DSA-SHA2-256f', + 'SLH-DSA-SHAKE-128s', + 'SLH-DSA-SHAKE-128f', + 'SLH-DSA-SHAKE-192s', + 'SLH-DSA-SHAKE-192f', + 'SLH-DSA-SHAKE-256s', + 'SLH-DSA-SHAKE-256f', +]; + +for (const variant of SLHDSA_VARIANTS) { + test(SUITE, `${variant} sign/verify`, async () => { + const keyPair = await generateKeyPairChecked({ name: variant }, true, [ + 'sign', + 'verify', + ]); + + const signature = await subtle.sign( + { name: variant }, + keyPair.privateKey, + testData, + ); + + expect(signature.byteLength).to.equal(SLHDSA_SIGNATURE_SIZES[variant]); + + const isValid = await subtle.verify( + { name: variant }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); + }); + + test(SUITE, `${variant} export → import → sign+verify`, async () => { + const original = await generateKeyPairChecked({ name: variant }, true, [ + 'sign', + 'verify', + ]); + + const pkcs8 = (await subtle.exportKey( + 'pkcs8', + original.privateKey, + )) as ArrayBuffer; + const spki = (await subtle.exportKey( + 'spki', + original.publicKey, + )) as ArrayBuffer; + + const importedPriv = await subtle.importKey( + 'pkcs8', + pkcs8, + { name: variant }, + true, + ['sign'], + ); + const importedPub = await subtle.importKey( + 'spki', + spki, + { name: variant }, + true, + ['verify'], + ); + + const sig = await subtle.sign({ name: variant }, importedPriv, testData); + expect(sig.byteLength).to.equal(SLHDSA_SIGNATURE_SIZES[variant]); + + const ok = await subtle.verify( + { name: variant }, + importedPub, + sig, + testData, + ); + expect(ok).to.equal(true); + }); +} + +test(SUITE, 'SLH-DSA-SHA2-128f verify rejects tampered signature', async () => { + const variant: SlhDsaVariant = 'SLH-DSA-SHA2-128f'; + const keyPair = await generateKeyPairChecked({ name: variant }, true, [ + 'sign', + 'verify', + ]); + + const signature = await subtle.sign( + { name: variant }, + keyPair.privateKey, + testData, + ); + + const tampered = new Uint8Array(signature); + tampered[0] = (tampered[0] ?? 0) ^ 0xff; + + const ok = await subtle.verify( + { name: variant }, + keyPair.publicKey, + tampered, + testData, + ); + expect(ok).to.equal(false); +}); + +// Cross-variant: an SLH-DSA-SHA2-128f signature must not verify under an +// SLH-DSA-SHAKE-128f public key — the parameter sets share signature length +// but use different hash families and unrelated keys. +test( + SUITE, + 'SLH-DSA cross-variant: sha2-128f sig under shake-128f pub rejected', + async () => { + const kpSha2 = await generateKeyPairChecked( + { name: 'SLH-DSA-SHA2-128f' }, + true, + ['sign', 'verify'], + ); + const kpShake = await generateKeyPairChecked( + { name: 'SLH-DSA-SHAKE-128f' }, + true, + ['sign', 'verify'], + ); + + const sig = await subtle.sign( + { name: 'SLH-DSA-SHA2-128f' }, + kpSha2.privateKey, + testData, + ); + + let result: boolean | Error; + try { + result = await subtle.verify( + { name: 'SLH-DSA-SHAKE-128f' }, + kpShake.publicKey, + sig, + testData, + ); + } catch (e) { + result = e as Error; + } + + if (typeof result === 'boolean') { + expect(result).to.equal(false); + } else { + expect(result).to.be.instanceOf(Error); + } + }, +); + // --- Key Import/Export and Sign/Verify --- test(SUITE, 'Sign with imported Ed25519 key', async () => { diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 948c0d65..fe293980 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -52,6 +52,7 @@ add_library( ../cpp/keys/KeyObjectData.cpp ../cpp/mldsa/HybridMlDsaKeyPair.cpp ../cpp/mlkem/HybridMlKemKeyPair.cpp + ../cpp/slhdsa/HybridSlhDsaKeyPair.cpp ../cpp/pbkdf2/HybridPbkdf2.cpp ../cpp/prime/HybridPrime.cpp ../cpp/random/HybridRandom.cpp @@ -92,6 +93,7 @@ include_directories( "../cpp/keys" "../cpp/mldsa" "../cpp/mlkem" + "../cpp/slhdsa" "../cpp/pbkdf2" "../cpp/prime" "../cpp/random" diff --git a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp index 86eb18e1..f38b3739 100644 --- a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp +++ b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp @@ -30,6 +30,7 @@ static void configurePqcOutputFormats() { [](OSSL_PROVIDER* provider, void*) -> int { OSSL_PROVIDER_add_conf_parameter(provider, "ml-kem.output_formats", "seed-only,priv-only"); OSSL_PROVIDER_add_conf_parameter(provider, "ml-dsa.output_formats", "seed-only,priv-only"); + OSSL_PROVIDER_add_conf_parameter(provider, "slh-dsa.output_formats", "seed-only,priv-only"); return 1; }, nullptr); @@ -174,7 +175,7 @@ std::shared_ptr HybridKeyObjectHandle::exportKey(std::optional= 0x30500000L - // EVP_PKEY_id returns -1 for provider-only key types (e.g. ML-KEM) + // EVP_PKEY_id returns -1 for provider-only key types (e.g. ML-KEM, SLH-DSA) // Fall back to string-based type name comparison const char* typeName = EVP_PKEY_get0_type_name(pkey.get()); if (typeName != nullptr) { @@ -481,6 +482,30 @@ AsymmetricKeyType HybridKeyObjectHandle::getAsymmetricKeyType() { return AsymmetricKeyType::ML_KEM_768; if (name == "ML-KEM-1024") return AsymmetricKeyType::ML_KEM_1024; + if (name == "SLH-DSA-SHA2-128s") + return AsymmetricKeyType::SLH_DSA_SHA2_128S; + if (name == "SLH-DSA-SHA2-128f") + return AsymmetricKeyType::SLH_DSA_SHA2_128F; + if (name == "SLH-DSA-SHA2-192s") + return AsymmetricKeyType::SLH_DSA_SHA2_192S; + if (name == "SLH-DSA-SHA2-192f") + return AsymmetricKeyType::SLH_DSA_SHA2_192F; + if (name == "SLH-DSA-SHA2-256s") + return AsymmetricKeyType::SLH_DSA_SHA2_256S; + if (name == "SLH-DSA-SHA2-256f") + return AsymmetricKeyType::SLH_DSA_SHA2_256F; + if (name == "SLH-DSA-SHAKE-128s") + return AsymmetricKeyType::SLH_DSA_SHAKE_128S; + if (name == "SLH-DSA-SHAKE-128f") + return AsymmetricKeyType::SLH_DSA_SHAKE_128F; + if (name == "SLH-DSA-SHAKE-192s") + return AsymmetricKeyType::SLH_DSA_SHAKE_192S; + if (name == "SLH-DSA-SHAKE-192f") + return AsymmetricKeyType::SLH_DSA_SHAKE_192F; + if (name == "SLH-DSA-SHAKE-256s") + return AsymmetricKeyType::SLH_DSA_SHAKE_256S; + if (name == "SLH-DSA-SHAKE-256f") + return AsymmetricKeyType::SLH_DSA_SHAKE_256F; } #endif @@ -790,6 +815,30 @@ std::optional HybridKeyObjectHandle::initJwk(const JWK& keyData, std::o nid = EVP_PKEY_ML_KEM_768; else if (alg == "ML-KEM-1024") nid = EVP_PKEY_ML_KEM_1024; + else if (alg == "SLH-DSA-SHA2-128s") + nid = EVP_PKEY_SLH_DSA_SHA2_128S; + else if (alg == "SLH-DSA-SHA2-128f") + nid = EVP_PKEY_SLH_DSA_SHA2_128F; + else if (alg == "SLH-DSA-SHA2-192s") + nid = EVP_PKEY_SLH_DSA_SHA2_192S; + else if (alg == "SLH-DSA-SHA2-192f") + nid = EVP_PKEY_SLH_DSA_SHA2_192F; + else if (alg == "SLH-DSA-SHA2-256s") + nid = EVP_PKEY_SLH_DSA_SHA2_256S; + else if (alg == "SLH-DSA-SHA2-256f") + nid = EVP_PKEY_SLH_DSA_SHA2_256F; + else if (alg == "SLH-DSA-SHAKE-128s") + nid = EVP_PKEY_SLH_DSA_SHAKE_128S; + else if (alg == "SLH-DSA-SHAKE-128f") + nid = EVP_PKEY_SLH_DSA_SHAKE_128F; + else if (alg == "SLH-DSA-SHAKE-192s") + nid = EVP_PKEY_SLH_DSA_SHAKE_192S; + else if (alg == "SLH-DSA-SHAKE-192f") + nid = EVP_PKEY_SLH_DSA_SHAKE_192F; + else if (alg == "SLH-DSA-SHAKE-256s") + nid = EVP_PKEY_SLH_DSA_SHAKE_256S; + else if (alg == "SLH-DSA-SHAKE-256f") + nid = EVP_PKEY_SLH_DSA_SHAKE_256F; else throw std::runtime_error("Unsupported JWK AKP \"alg\": " + alg); @@ -964,6 +1013,30 @@ bool HybridKeyObjectHandle::initPqcRaw(const std::string& algorithmName, const s nid = EVP_PKEY_ML_DSA_65; else if (algorithmName == "ML-DSA-87") nid = EVP_PKEY_ML_DSA_87; + else if (algorithmName == "SLH-DSA-SHA2-128s") + nid = EVP_PKEY_SLH_DSA_SHA2_128S; + else if (algorithmName == "SLH-DSA-SHA2-128f") + nid = EVP_PKEY_SLH_DSA_SHA2_128F; + else if (algorithmName == "SLH-DSA-SHA2-192s") + nid = EVP_PKEY_SLH_DSA_SHA2_192S; + else if (algorithmName == "SLH-DSA-SHA2-192f") + nid = EVP_PKEY_SLH_DSA_SHA2_192F; + else if (algorithmName == "SLH-DSA-SHA2-256s") + nid = EVP_PKEY_SLH_DSA_SHA2_256S; + else if (algorithmName == "SLH-DSA-SHA2-256f") + nid = EVP_PKEY_SLH_DSA_SHA2_256F; + else if (algorithmName == "SLH-DSA-SHAKE-128s") + nid = EVP_PKEY_SLH_DSA_SHAKE_128S; + else if (algorithmName == "SLH-DSA-SHAKE-128f") + nid = EVP_PKEY_SLH_DSA_SHAKE_128F; + else if (algorithmName == "SLH-DSA-SHAKE-192s") + nid = EVP_PKEY_SLH_DSA_SHAKE_192S; + else if (algorithmName == "SLH-DSA-SHAKE-192f") + nid = EVP_PKEY_SLH_DSA_SHAKE_192F; + else if (algorithmName == "SLH-DSA-SHAKE-256s") + nid = EVP_PKEY_SLH_DSA_SHAKE_256S; + else if (algorithmName == "SLH-DSA-SHAKE-256f") + nid = EVP_PKEY_SLH_DSA_SHAKE_256F; else throw std::runtime_error("Unknown PQC algorithm: " + algorithmName); diff --git a/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp b/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp index f8c88a3c..13d57c21 100644 --- a/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp +++ b/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp @@ -73,12 +73,32 @@ void HybridSignHandle::update(const std::shared_ptr& data) { } } -// Check if key type requires one-shot signing (Ed25519, Ed448, ML-DSA) +// Check if key type requires one-shot signing (Ed25519, Ed448, ML-DSA, SLH-DSA) static bool isOneShotVariant(EVP_PKEY* pkey) { int type = EVP_PKEY_id(pkey); #if RNQC_HAS_ML_DSA - return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448 || type == EVP_PKEY_ML_DSA_44 || type == EVP_PKEY_ML_DSA_65 || - type == EVP_PKEY_ML_DSA_87; + switch (type) { + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + case EVP_PKEY_SLH_DSA_SHA2_128S: + case EVP_PKEY_SLH_DSA_SHA2_128F: + case EVP_PKEY_SLH_DSA_SHA2_192S: + case EVP_PKEY_SLH_DSA_SHA2_192F: + case EVP_PKEY_SLH_DSA_SHA2_256S: + case EVP_PKEY_SLH_DSA_SHA2_256F: + case EVP_PKEY_SLH_DSA_SHAKE_128S: + case EVP_PKEY_SLH_DSA_SHAKE_128F: + case EVP_PKEY_SLH_DSA_SHAKE_192S: + case EVP_PKEY_SLH_DSA_SHAKE_192F: + case EVP_PKEY_SLH_DSA_SHAKE_256S: + case EVP_PKEY_SLH_DSA_SHAKE_256F: + return true; + default: + return false; + } #else return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448; #endif diff --git a/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp b/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp index 52739353..18fce058 100644 --- a/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp +++ b/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp @@ -73,12 +73,32 @@ void HybridVerifyHandle::update(const std::shared_ptr& data) { } } -// Check if key type requires one-shot verification (Ed25519, Ed448, ML-DSA) +// Check if key type requires one-shot verification (Ed25519, Ed448, ML-DSA, SLH-DSA) static bool isOneShotVariant(EVP_PKEY* pkey) { int type = EVP_PKEY_id(pkey); #if RNQC_HAS_ML_DSA - return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448 || type == EVP_PKEY_ML_DSA_44 || type == EVP_PKEY_ML_DSA_65 || - type == EVP_PKEY_ML_DSA_87; + switch (type) { + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + case EVP_PKEY_SLH_DSA_SHA2_128S: + case EVP_PKEY_SLH_DSA_SHA2_128F: + case EVP_PKEY_SLH_DSA_SHA2_192S: + case EVP_PKEY_SLH_DSA_SHA2_192F: + case EVP_PKEY_SLH_DSA_SHA2_256S: + case EVP_PKEY_SLH_DSA_SHA2_256F: + case EVP_PKEY_SLH_DSA_SHAKE_128S: + case EVP_PKEY_SLH_DSA_SHAKE_128F: + case EVP_PKEY_SLH_DSA_SHAKE_192S: + case EVP_PKEY_SLH_DSA_SHAKE_192F: + case EVP_PKEY_SLH_DSA_SHAKE_256S: + case EVP_PKEY_SLH_DSA_SHAKE_256F: + return true; + default: + return false; + } #else return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448; #endif diff --git a/packages/react-native-quick-crypto/cpp/slhdsa/HybridSlhDsaKeyPair.cpp b/packages/react-native-quick-crypto/cpp/slhdsa/HybridSlhDsaKeyPair.cpp new file mode 100644 index 00000000..ef9db32d --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/slhdsa/HybridSlhDsaKeyPair.cpp @@ -0,0 +1,245 @@ +#include "HybridSlhDsaKeyPair.hpp" + +#include +#include +#include +#include + +#include + +#include "QuickCryptoUtils.hpp" + +#if OPENSSL_VERSION_NUMBER >= 0x30500000L +#define RNQC_HAS_SLH_DSA 1 +#else +#define RNQC_HAS_SLH_DSA 0 +#endif + +namespace margelo::nitro::crypto { + +using EVP_MD_CTX_ptr = std::unique_ptr; +using EVP_PKEY_CTX_ptr = std::unique_ptr; + +#if RNQC_HAS_SLH_DSA +static constexpr std::array kSlhDsaVariants{ + "SLH-DSA-SHA2-128s", "SLH-DSA-SHA2-128f", "SLH-DSA-SHA2-192s", "SLH-DSA-SHA2-192f", "SLH-DSA-SHA2-256s", "SLH-DSA-SHA2-256f", + "SLH-DSA-SHAKE-128s", "SLH-DSA-SHAKE-128f", "SLH-DSA-SHAKE-192s", "SLH-DSA-SHAKE-192f", "SLH-DSA-SHAKE-256s", "SLH-DSA-SHAKE-256f", +}; + +static bool isValidSlhDsaVariant(const std::string& variant) { + for (const char* v : kSlhDsaVariants) { + if (variant == v) { + return true; + } + } + return false; +} +#endif + +void HybridSlhDsaKeyPair::setVariant(const std::string& variant) { +#if !RNQC_HAS_SLH_DSA + throw std::runtime_error("SLH-DSA requires OpenSSL 3.5+"); +#else + if (!isValidSlhDsaVariant(variant)) { + throw std::runtime_error("Invalid SLH-DSA variant: " + variant); + } + variant_ = variant; +#endif +} + +std::shared_ptr> HybridSlhDsaKeyPair::generateKeyPair(double publicFormat, double publicType, double privateFormat, + double privateType) { + auto self = this->shared_cast(); + return Promise::async([self, publicFormat, publicType, privateFormat, privateType]() { + self->generateKeyPairSync(publicFormat, publicType, privateFormat, privateType); + }); +} + +void HybridSlhDsaKeyPair::generateKeyPairSync(double publicFormat, double publicType, double privateFormat, double privateType) { +#if !RNQC_HAS_SLH_DSA + throw std::runtime_error("SLH-DSA requires OpenSSL 3.5+"); +#else + clearOpenSSLErrors(); + + if (variant_.empty()) { + throw std::runtime_error("SLH-DSA variant not set. Call setVariant() first."); + } + + publicFormat_ = static_cast(publicFormat); + publicType_ = static_cast(publicType); + privateFormat_ = static_cast(privateFormat); + privateType_ = static_cast(privateType); + + pkey_.reset(); + + EVP_PKEY_CTX_ptr pctx(EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr), EVP_PKEY_CTX_free); + if (pctx == nullptr) { + throw std::runtime_error("Failed to create key context for " + variant_ + ": " + getOpenSSLError()); + } + + if (EVP_PKEY_keygen_init(pctx.get()) <= 0) { + throw std::runtime_error("Failed to initialize keygen: " + getOpenSSLError()); + } + + EVP_PKEY* raw = nullptr; + if (EVP_PKEY_keygen(pctx.get(), &raw) <= 0) { + throw std::runtime_error("Failed to generate SLH-DSA key pair: " + getOpenSSLError()); + } + pkey_.reset(raw); +#endif +} + +std::shared_ptr HybridSlhDsaKeyPair::getPublicKey() { +#if !RNQC_HAS_SLH_DSA + throw std::runtime_error("SLH-DSA requires OpenSSL 3.5+"); +#else + checkKeyPair(); + + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("Failed to create BIO for public key export"); + } + + int result; + if (publicFormat_ == 1) { + result = PEM_write_bio_PUBKEY(bio, pkey_.get()); + } else { + result = i2d_PUBKEY_bio(bio, pkey_.get()); + } + + if (result != 1) { + BIO_free(bio); + throw std::runtime_error("Failed to export public key: " + getOpenSSLError()); + } + + BUF_MEM* bptr; + BIO_get_mem_ptr(bio, &bptr); + + size_t len = bptr->length; + auto buf = std::make_unique(len); + memcpy(buf.get(), bptr->data, len); + + BIO_free(bio); + + uint8_t* raw_ptr = buf.get(); + return std::make_shared(buf.release(), len, [raw_ptr]() { delete[] raw_ptr; }); +#endif +} + +std::shared_ptr HybridSlhDsaKeyPair::getPrivateKey() { +#if !RNQC_HAS_SLH_DSA + throw std::runtime_error("SLH-DSA requires OpenSSL 3.5+"); +#else + checkKeyPair(); + + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("Failed to create BIO for private key export"); + } + + int result; + if (privateFormat_ == 1) { + result = PEM_write_bio_PrivateKey(bio, pkey_.get(), nullptr, nullptr, 0, nullptr, nullptr); + } else { + result = i2d_PKCS8PrivateKey_bio(bio, pkey_.get(), nullptr, nullptr, 0, nullptr, nullptr); + } + + if (result != 1) { + BIO_free(bio); + throw std::runtime_error("Failed to export private key: " + getOpenSSLError()); + } + + BUF_MEM* bptr; + BIO_get_mem_ptr(bio, &bptr); + + size_t len = bptr->length; + auto buf = std::make_unique(len); + memcpy(buf.get(), bptr->data, len); + + secureZero(bptr->data, bptr->length); + BIO_free(bio); + + uint8_t* raw_ptr = buf.get(); + return std::make_shared(buf.release(), len, [raw_ptr]() { delete[] raw_ptr; }); +#endif +} + +std::shared_ptr>> HybridSlhDsaKeyPair::sign(const std::shared_ptr& message) { + auto nativeMessage = ToNativeArrayBuffer(message); + auto self = this->shared_cast(); + return Promise>::async([self, nativeMessage]() { return self->signSync(nativeMessage); }); +} + +std::shared_ptr HybridSlhDsaKeyPair::signSync(const std::shared_ptr& message) { +#if !RNQC_HAS_SLH_DSA + throw std::runtime_error("SLH-DSA requires OpenSSL 3.5+"); +#else + clearOpenSSLErrors(); + checkKeyPair(); + + EVP_MD_CTX_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (md_ctx == nullptr) { + throw std::runtime_error("Failed to create signing context"); + } + + if (EVP_DigestSignInit(md_ctx.get(), nullptr, nullptr, nullptr, pkey_.get()) <= 0) { + throw std::runtime_error("Failed to initialize signing: " + getOpenSSLError()); + } + + size_t sig_len = 0; + if (EVP_DigestSign(md_ctx.get(), nullptr, &sig_len, message->data(), message->size()) <= 0) { + throw std::runtime_error("Failed to calculate signature size: " + getOpenSSLError()); + } + + auto sig = std::make_unique(sig_len); + + if (EVP_DigestSign(md_ctx.get(), sig.get(), &sig_len, message->data(), message->size()) <= 0) { + throw std::runtime_error("Failed to sign message: " + getOpenSSLError()); + } + + uint8_t* raw_ptr = sig.get(); + return std::make_shared(sig.release(), sig_len, [raw_ptr]() { delete[] raw_ptr; }); +#endif +} + +std::shared_ptr> HybridSlhDsaKeyPair::verify(const std::shared_ptr& signature, + const std::shared_ptr& message) { + auto nativeSignature = ToNativeArrayBuffer(signature); + auto nativeMessage = ToNativeArrayBuffer(message); + auto self = this->shared_cast(); + return Promise::async([self, nativeSignature, nativeMessage]() { return self->verifySync(nativeSignature, nativeMessage); }); +} + +bool HybridSlhDsaKeyPair::verifySync(const std::shared_ptr& signature, const std::shared_ptr& message) { +#if !RNQC_HAS_SLH_DSA + throw std::runtime_error("SLH-DSA requires OpenSSL 3.5+"); +#else + clearOpenSSLErrors(); + checkKeyPair(); + + EVP_MD_CTX_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (md_ctx == nullptr) { + throw std::runtime_error("Failed to create verify context"); + } + + if (EVP_DigestVerifyInit(md_ctx.get(), nullptr, nullptr, nullptr, pkey_.get()) <= 0) { + throw std::runtime_error("Failed to initialize verification: " + getOpenSSLError()); + } + + int result = EVP_DigestVerify(md_ctx.get(), signature->data(), signature->size(), message->data(), message->size()); + + if (result < 0) { + throw std::runtime_error("Verification error: " + getOpenSSLError()); + } + + return result == 1; +#endif +} + +void HybridSlhDsaKeyPair::checkKeyPair() { + if (!pkey_) { + throw std::runtime_error("Key pair not initialized"); + } +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/slhdsa/HybridSlhDsaKeyPair.hpp b/packages/react-native-quick-crypto/cpp/slhdsa/HybridSlhDsaKeyPair.hpp new file mode 100644 index 00000000..66423835 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/slhdsa/HybridSlhDsaKeyPair.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +#include "HybridSlhDsaKeyPairSpec.hpp" + +namespace margelo::nitro::crypto { + +class HybridSlhDsaKeyPair : public HybridSlhDsaKeyPairSpec { + public: + HybridSlhDsaKeyPair() : HybridObject(TAG) {} + ~HybridSlhDsaKeyPair() override = default; + + std::shared_ptr> generateKeyPair(double publicFormat, double publicType, double privateFormat, double privateType) override; + + void generateKeyPairSync(double publicFormat, double publicType, double privateFormat, double privateType) override; + + std::shared_ptr getPublicKey() override; + std::shared_ptr getPrivateKey() override; + + std::shared_ptr>> sign(const std::shared_ptr& message) override; + + std::shared_ptr signSync(const std::shared_ptr& message) override; + + std::shared_ptr> verify(const std::shared_ptr& signature, + const std::shared_ptr& message) override; + + bool verifySync(const std::shared_ptr& signature, const std::shared_ptr& message) override; + + void setVariant(const std::string& variant) override; + + private: + using EVP_PKEY_ptr = std::unique_ptr; + + std::string variant_; + EVP_PKEY_ptr pkey_{nullptr, EVP_PKEY_free}; + + int publicFormat_ = -1; + int publicType_ = -1; + int privateFormat_ = -1; + int privateType_ = -1; + + void checkKeyPair(); +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index 16bd5f41..91c49343 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -62,6 +62,9 @@ "MlKemKeyPair": { "cpp": "HybridMlKemKeyPair" }, + "SlhDsaKeyPair": { + "cpp": "HybridSlhDsaKeyPair" + }, "Pbkdf2": { "cpp": "HybridPbkdf2" }, diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake index 966c56cd..f15a6a85 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake @@ -59,6 +59,7 @@ target_sources( ../nitrogen/generated/shared/c++/HybridScryptSpec.cpp ../nitrogen/generated/shared/c++/HybridSignHandleSpec.cpp ../nitrogen/generated/shared/c++/HybridVerifyHandleSpec.cpp + ../nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.cpp ../nitrogen/generated/shared/c++/HybridUtilsSpec.cpp ../nitrogen/generated/shared/c++/HybridX509CertificateHandleSpec.cpp # Android-specific Nitrogen C++ sources diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp index 44ef262c..5483c34b 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -33,6 +33,7 @@ #include "HybridKeyObjectHandle.hpp" #include "HybridMlDsaKeyPair.hpp" #include "HybridMlKemKeyPair.hpp" +#include "HybridSlhDsaKeyPair.hpp" #include "HybridPbkdf2.hpp" #include "HybridPrime.hpp" #include "HybridRandom.hpp" @@ -218,6 +219,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "SlhDsaKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridSlhDsaKeyPair\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); HybridObjectRegistry::registerHybridObjectConstructor( "Pbkdf2", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm index 43237e32..f8f8a507 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -28,6 +28,7 @@ #include "HybridKeyObjectHandle.hpp" #include "HybridMlDsaKeyPair.hpp" #include "HybridMlKemKeyPair.hpp" +#include "HybridSlhDsaKeyPair.hpp" #include "HybridPbkdf2.hpp" #include "HybridPrime.hpp" #include "HybridRandom.hpp" @@ -210,6 +211,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "SlhDsaKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridSlhDsaKeyPair\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); HybridObjectRegistry::registerHybridObjectConstructor( "Pbkdf2", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp index d9d42ae8..e2349aa9 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp @@ -44,6 +44,18 @@ namespace margelo::nitro::crypto { ML_KEM_512 SWIFT_NAME(mlKem512) = 12, ML_KEM_768 SWIFT_NAME(mlKem768) = 13, ML_KEM_1024 SWIFT_NAME(mlKem1024) = 14, + SLH_DSA_SHA2_128S SWIFT_NAME(slhDsaSha2128s) = 15, + SLH_DSA_SHA2_128F SWIFT_NAME(slhDsaSha2128f) = 16, + SLH_DSA_SHA2_192S SWIFT_NAME(slhDsaSha2192s) = 17, + SLH_DSA_SHA2_192F SWIFT_NAME(slhDsaSha2192f) = 18, + SLH_DSA_SHA2_256S SWIFT_NAME(slhDsaSha2256s) = 19, + SLH_DSA_SHA2_256F SWIFT_NAME(slhDsaSha2256f) = 20, + SLH_DSA_SHAKE_128S SWIFT_NAME(slhDsaShake128s) = 21, + SLH_DSA_SHAKE_128F SWIFT_NAME(slhDsaShake128f) = 22, + SLH_DSA_SHAKE_192S SWIFT_NAME(slhDsaShake192s) = 23, + SLH_DSA_SHAKE_192F SWIFT_NAME(slhDsaShake192f) = 24, + SLH_DSA_SHAKE_256S SWIFT_NAME(slhDsaShake256s) = 25, + SLH_DSA_SHAKE_256F SWIFT_NAME(slhDsaShake256f) = 26, } CLOSED_ENUM; } // namespace margelo::nitro::crypto @@ -71,6 +83,18 @@ namespace margelo::nitro { case hashString("ml-kem-512"): return margelo::nitro::crypto::AsymmetricKeyType::ML_KEM_512; case hashString("ml-kem-768"): return margelo::nitro::crypto::AsymmetricKeyType::ML_KEM_768; case hashString("ml-kem-1024"): return margelo::nitro::crypto::AsymmetricKeyType::ML_KEM_1024; + case hashString("slh-dsa-sha2-128s"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_128S; + case hashString("slh-dsa-sha2-128f"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_128F; + case hashString("slh-dsa-sha2-192s"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_192S; + case hashString("slh-dsa-sha2-192f"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_192F; + case hashString("slh-dsa-sha2-256s"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_256S; + case hashString("slh-dsa-sha2-256f"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_256F; + case hashString("slh-dsa-shake-128s"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_128S; + case hashString("slh-dsa-shake-128f"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_128F; + case hashString("slh-dsa-shake-192s"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_192S; + case hashString("slh-dsa-shake-192f"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_192F; + case hashString("slh-dsa-shake-256s"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_256S; + case hashString("slh-dsa-shake-256f"): return margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_256F; default: [[unlikely]] throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum AsymmetricKeyType - invalid value!"); } @@ -92,6 +116,18 @@ namespace margelo::nitro { case margelo::nitro::crypto::AsymmetricKeyType::ML_KEM_512: return JSIConverter::toJSI(runtime, "ml-kem-512"); case margelo::nitro::crypto::AsymmetricKeyType::ML_KEM_768: return JSIConverter::toJSI(runtime, "ml-kem-768"); case margelo::nitro::crypto::AsymmetricKeyType::ML_KEM_1024: return JSIConverter::toJSI(runtime, "ml-kem-1024"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_128S: return JSIConverter::toJSI(runtime, "slh-dsa-sha2-128s"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_128F: return JSIConverter::toJSI(runtime, "slh-dsa-sha2-128f"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_192S: return JSIConverter::toJSI(runtime, "slh-dsa-sha2-192s"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_192F: return JSIConverter::toJSI(runtime, "slh-dsa-sha2-192f"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_256S: return JSIConverter::toJSI(runtime, "slh-dsa-sha2-256s"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHA2_256F: return JSIConverter::toJSI(runtime, "slh-dsa-sha2-256f"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_128S: return JSIConverter::toJSI(runtime, "slh-dsa-shake-128s"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_128F: return JSIConverter::toJSI(runtime, "slh-dsa-shake-128f"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_192S: return JSIConverter::toJSI(runtime, "slh-dsa-shake-192s"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_192F: return JSIConverter::toJSI(runtime, "slh-dsa-shake-192f"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_256S: return JSIConverter::toJSI(runtime, "slh-dsa-shake-256s"); + case margelo::nitro::crypto::AsymmetricKeyType::SLH_DSA_SHAKE_256F: return JSIConverter::toJSI(runtime, "slh-dsa-shake-256f"); default: [[unlikely]] throw std::invalid_argument("Cannot convert AsymmetricKeyType to JS - invalid value: " + std::to_string(static_cast(arg)) + "!"); @@ -118,6 +154,18 @@ namespace margelo::nitro { case hashString("ml-kem-512"): case hashString("ml-kem-768"): case hashString("ml-kem-1024"): + case hashString("slh-dsa-sha2-128s"): + case hashString("slh-dsa-sha2-128f"): + case hashString("slh-dsa-sha2-192s"): + case hashString("slh-dsa-sha2-192f"): + case hashString("slh-dsa-sha2-256s"): + case hashString("slh-dsa-sha2-256f"): + case hashString("slh-dsa-shake-128s"): + case hashString("slh-dsa-shake-128f"): + case hashString("slh-dsa-shake-192s"): + case hashString("slh-dsa-shake-192f"): + case hashString("slh-dsa-shake-256s"): + case hashString("slh-dsa-shake-256f"): return true; default: return false; diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.cpp new file mode 100644 index 00000000..79d95548 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.cpp @@ -0,0 +1,29 @@ +/// +/// HybridSlhDsaKeyPairSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridSlhDsaKeyPairSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridSlhDsaKeyPairSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("generateKeyPair", &HybridSlhDsaKeyPairSpec::generateKeyPair); + prototype.registerHybridMethod("generateKeyPairSync", &HybridSlhDsaKeyPairSpec::generateKeyPairSync); + prototype.registerHybridMethod("getPublicKey", &HybridSlhDsaKeyPairSpec::getPublicKey); + prototype.registerHybridMethod("getPrivateKey", &HybridSlhDsaKeyPairSpec::getPrivateKey); + prototype.registerHybridMethod("sign", &HybridSlhDsaKeyPairSpec::sign); + prototype.registerHybridMethod("signSync", &HybridSlhDsaKeyPairSpec::signSync); + prototype.registerHybridMethod("verify", &HybridSlhDsaKeyPairSpec::verify); + prototype.registerHybridMethod("verifySync", &HybridSlhDsaKeyPairSpec::verifySync); + prototype.registerHybridMethod("setVariant", &HybridSlhDsaKeyPairSpec::setVariant); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.hpp new file mode 100644 index 00000000..43725476 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.hpp @@ -0,0 +1,72 @@ +/// +/// HybridSlhDsaKeyPairSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `SlhDsaKeyPair` + * Inherit this class to create instances of `HybridSlhDsaKeyPairSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridSlhDsaKeyPair: public HybridSlhDsaKeyPairSpec { + * public: + * HybridSlhDsaKeyPair(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridSlhDsaKeyPairSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridSlhDsaKeyPairSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridSlhDsaKeyPairSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr> generateKeyPair(double publicFormat, double publicType, double privateFormat, double privateType) = 0; + virtual void generateKeyPairSync(double publicFormat, double publicType, double privateFormat, double privateType) = 0; + virtual std::shared_ptr getPublicKey() = 0; + virtual std::shared_ptr getPrivateKey() = 0; + virtual std::shared_ptr>> sign(const std::shared_ptr& message) = 0; + virtual std::shared_ptr signSync(const std::shared_ptr& message) = 0; + virtual std::shared_ptr> verify(const std::shared_ptr& signature, const std::shared_ptr& message) = 0; + virtual bool verifySync(const std::shared_ptr& signature, const std::shared_ptr& message) = 0; + virtual void setVariant(const std::string& variant) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "SlhDsaKeyPair"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts index 0d2fefb7..9cec8de2 100644 --- a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts +++ b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts @@ -6,6 +6,7 @@ import { dh_generateKeyPairNode, dh_generateKeyPairNodeSync, } from '../dhKeyPair'; +import { SlhDsa, type SlhDsaVariant } from '../slhdsa'; import { kEmptyObject, validateFunction, @@ -17,9 +18,79 @@ import { type KeyPairGenConfig, type KeyPairKey, type KeyPairType, + type SlhDsaKeyPairType, + KFormatType, + KeyEncoding, } from '../utils'; +import { + KeyObject, + type PublicKeyObject, + type PrivateKeyObject, +} from './classes'; import { parsePrivateKeyEncoding, parsePublicKeyEncoding } from './utils'; +const SLH_DSA_TYPE_TO_VARIANT: Readonly< + Record +> = { + 'slh-dsa-sha2-128s': 'SLH-DSA-SHA2-128s', + 'slh-dsa-sha2-128f': 'SLH-DSA-SHA2-128f', + 'slh-dsa-sha2-192s': 'SLH-DSA-SHA2-192s', + 'slh-dsa-sha2-192f': 'SLH-DSA-SHA2-192f', + 'slh-dsa-sha2-256s': 'SLH-DSA-SHA2-256s', + 'slh-dsa-sha2-256f': 'SLH-DSA-SHA2-256f', + 'slh-dsa-shake-128s': 'SLH-DSA-SHAKE-128s', + 'slh-dsa-shake-128f': 'SLH-DSA-SHAKE-128f', + 'slh-dsa-shake-192s': 'SLH-DSA-SHAKE-192s', + 'slh-dsa-shake-192f': 'SLH-DSA-SHAKE-192f', + 'slh-dsa-shake-256s': 'SLH-DSA-SHAKE-256s', + 'slh-dsa-shake-256f': 'SLH-DSA-SHAKE-256f', +}; + +function isSlhDsaType(type: string): type is SlhDsaKeyPairType { + return type in SLH_DSA_TYPE_TO_VARIANT; +} + +function slhDsaGenerateKeyPairNodeSync(type: SlhDsaKeyPairType): { + publicKey: PublicKeyObject; + privateKey: PrivateKeyObject; +} { + const slhdsa = new SlhDsa(SLH_DSA_TYPE_TO_VARIANT[type]); + slhdsa.generateKeyPairSync(); + const publicKey = KeyObject.createKeyObject( + 'public', + slhdsa.getPublicKey(), + KFormatType.DER, + KeyEncoding.SPKI, + ) as PublicKeyObject; + const privateKey = KeyObject.createKeyObject( + 'private', + slhdsa.getPrivateKey(), + KFormatType.DER, + KeyEncoding.PKCS8, + ) as PrivateKeyObject; + return { publicKey, privateKey }; +} + +async function slhDsaGenerateKeyPairNode( + type: SlhDsaKeyPairType, +): Promise<{ publicKey: PublicKeyObject; privateKey: PrivateKeyObject }> { + const slhdsa = new SlhDsa(SLH_DSA_TYPE_TO_VARIANT[type]); + await slhdsa.generateKeyPair(); + const publicKey = KeyObject.createKeyObject( + 'public', + slhdsa.getPublicKey(), + KFormatType.DER, + KeyEncoding.SPKI, + ) as PublicKeyObject; + const privateKey = KeyObject.createKeyObject( + 'private', + slhdsa.getPrivateKey(), + KFormatType.DER, + KeyEncoding.PKCS8, + ) as PrivateKeyObject; + return { publicKey, privateKey }; +} + export const generateKeyPair = ( type: KeyPairType, options: GenerateKeyPairOptions, @@ -147,6 +218,9 @@ function internalGenerateKeyPair( case 'dh': break; default: { + if (isSlhDsaType(type)) { + break; + } const err = new Error(` Invalid Argument options: '${type}' scheme not supported for generateKeyPair(). Currently not all encryption methods are supported in @@ -168,6 +242,8 @@ function internalGenerateKeyPair( result = await dsa_generateKeyPairNode(options, encoding); } else if (type === 'dh') { result = await dh_generateKeyPairNode(options, encoding); + } else if (isSlhDsaType(type)) { + result = await slhDsaGenerateKeyPairNode(type); } else { throw new Error(`Unsupported key type: ${type}`); } @@ -198,6 +274,8 @@ function internalGenerateKeyPair( result = dsa_generateKeyPairNodeSync(options, encoding); } else if (type === 'dh') { result = dh_generateKeyPairNodeSync(options, encoding); + } else if (isSlhDsaType(type)) { + result = slhDsaGenerateKeyPairNodeSync(type); } else { throw new Error(`Unsupported key type: ${type}`); } diff --git a/packages/react-native-quick-crypto/src/slhdsa.ts b/packages/react-native-quick-crypto/src/slhdsa.ts new file mode 100644 index 00000000..90b96323 --- /dev/null +++ b/packages/react-native-quick-crypto/src/slhdsa.ts @@ -0,0 +1,146 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { SlhDsaKeyPair } from './specs/slhDsaKeyPair.nitro'; +import { + CryptoKey, + KeyObject, + PublicKeyObject, + PrivateKeyObject as PrivateKeyObjectClass, +} from './keys'; +import type { + CryptoKeyPair, + KeyUsage, + SubtleAlgorithm, + SlhDsaAlgorithm, +} from './utils'; +import { + hasAnyNotIn, + lazyDOMException, + getUsagesUnion, + KFormatType, + KeyEncoding, +} from './utils'; + +export type SlhDsaVariant = SlhDsaAlgorithm; + +export const SLH_DSA_VARIANTS: readonly SlhDsaVariant[] = [ + 'SLH-DSA-SHA2-128s', + 'SLH-DSA-SHA2-128f', + 'SLH-DSA-SHA2-192s', + 'SLH-DSA-SHA2-192f', + 'SLH-DSA-SHA2-256s', + 'SLH-DSA-SHA2-256f', + 'SLH-DSA-SHAKE-128s', + 'SLH-DSA-SHAKE-128f', + 'SLH-DSA-SHAKE-192s', + 'SLH-DSA-SHAKE-192f', + 'SLH-DSA-SHAKE-256s', + 'SLH-DSA-SHAKE-256f', +] as const; + +export class SlhDsa { + variant: SlhDsaVariant; + native: SlhDsaKeyPair; + + constructor(variant: SlhDsaVariant) { + this.variant = variant; + this.native = + NitroModules.createHybridObject('SlhDsaKeyPair'); + this.native.setVariant(variant); + } + + async generateKeyPair(): Promise { + await this.native.generateKeyPair( + KFormatType.DER, + KeyEncoding.SPKI, + KFormatType.DER, + KeyEncoding.PKCS8, + ); + } + + generateKeyPairSync(): void { + this.native.generateKeyPairSync( + KFormatType.DER, + KeyEncoding.SPKI, + KFormatType.DER, + KeyEncoding.PKCS8, + ); + } + + getPublicKey(): ArrayBuffer { + return this.native.getPublicKey(); + } + + getPrivateKey(): ArrayBuffer { + return this.native.getPrivateKey(); + } + + async sign(message: ArrayBuffer): Promise { + return this.native.sign(message); + } + + signSync(message: ArrayBuffer): ArrayBuffer { + return this.native.signSync(message); + } + + async verify(signature: ArrayBuffer, message: ArrayBuffer): Promise { + return this.native.verify(signature, message); + } + + verifySync(signature: ArrayBuffer, message: ArrayBuffer): boolean { + return this.native.verifySync(signature, message); + } +} + +export async function slhdsa_generateKeyPairWebCrypto( + variant: SlhDsaVariant, + extractable: boolean, + keyUsages: KeyUsage[], +): Promise { + if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { + throw lazyDOMException( + `Unsupported key usage for ${variant}`, + 'SyntaxError', + ); + } + + const publicUsages = getUsagesUnion(keyUsages, 'verify'); + const privateUsages = getUsagesUnion(keyUsages, 'sign'); + + if (privateUsages.length === 0) { + throw lazyDOMException('Usages cannot be empty', 'SyntaxError'); + } + + const slhdsa = new SlhDsa(variant); + await slhdsa.generateKeyPair(); + + const publicKeyData = slhdsa.getPublicKey(); + const privateKeyData = slhdsa.getPrivateKey(); + + const pub = KeyObject.createKeyObject( + 'public', + publicKeyData, + KFormatType.DER, + KeyEncoding.SPKI, + ) as PublicKeyObject; + const publicKey = new CryptoKey( + pub, + { name: variant } as SubtleAlgorithm, + publicUsages, + true, + ); + + const priv = KeyObject.createKeyObject( + 'private', + privateKeyData, + KFormatType.DER, + KeyEncoding.PKCS8, + ) as PrivateKeyObjectClass; + const privateKey = new CryptoKey( + priv, + { name: variant } as SubtleAlgorithm, + privateUsages, + extractable, + ); + + return { publicKey, privateKey }; +} diff --git a/packages/react-native-quick-crypto/src/specs/slhDsaKeyPair.nitro.ts b/packages/react-native-quick-crypto/src/specs/slhDsaKeyPair.nitro.ts new file mode 100644 index 00000000..d954be05 --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/slhDsaKeyPair.nitro.ts @@ -0,0 +1,29 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface SlhDsaKeyPair + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + generateKeyPair( + publicFormat: number, + publicType: number, + privateFormat: number, + privateType: number, + ): Promise; + + generateKeyPairSync( + publicFormat: number, + publicType: number, + privateFormat: number, + privateType: number, + ): void; + + getPublicKey(): ArrayBuffer; + getPrivateKey(): ArrayBuffer; + + sign(message: ArrayBuffer): Promise; + signSync(message: ArrayBuffer): ArrayBuffer; + + verify(signature: ArrayBuffer, message: ArrayBuffer): Promise; + verifySync(signature: ArrayBuffer, message: ArrayBuffer): boolean; + + setVariant(variant: string): void; +} diff --git a/packages/react-native-quick-crypto/src/subtle.ts b/packages/react-native-quick-crypto/src/subtle.ts index 83d4bd64..39d36a3c 100644 --- a/packages/react-native-quick-crypto/src/subtle.ts +++ b/packages/react-native-quick-crypto/src/subtle.ts @@ -62,6 +62,11 @@ import { Ed, } from './ed'; import { mldsa_generateKeyPairWebCrypto, type MlDsaVariant } from './mldsa'; +import { + slhdsa_generateKeyPairWebCrypto, + SLH_DSA_VARIANTS, + type SlhDsaVariant, +} from './slhdsa'; import { mlkem_generateKeyPairWebCrypto, type MlKemVariant, @@ -1435,13 +1440,25 @@ export const PQC_SEEDLESS_PKCS8_LENGTHS: Readonly> = { // Map from PQC algorithm name to display family. Used to render the // import-rejection error message in the same form Node emits. -const PQC_FAMILY: Readonly> = { +const PQC_FAMILY: Readonly> = { 'ML-DSA-44': 'ML-DSA', 'ML-DSA-65': 'ML-DSA', 'ML-DSA-87': 'ML-DSA', 'ML-KEM-512': 'ML-KEM', 'ML-KEM-768': 'ML-KEM', 'ML-KEM-1024': 'ML-KEM', + 'SLH-DSA-SHA2-128s': 'SLH-DSA', + 'SLH-DSA-SHA2-128f': 'SLH-DSA', + 'SLH-DSA-SHA2-192s': 'SLH-DSA', + 'SLH-DSA-SHA2-192f': 'SLH-DSA', + 'SLH-DSA-SHA2-256s': 'SLH-DSA', + 'SLH-DSA-SHA2-256f': 'SLH-DSA', + 'SLH-DSA-SHAKE-128s': 'SLH-DSA', + 'SLH-DSA-SHAKE-128f': 'SLH-DSA', + 'SLH-DSA-SHAKE-192s': 'SLH-DSA', + 'SLH-DSA-SHAKE-192f': 'SLH-DSA', + 'SLH-DSA-SHAKE-256s': 'SLH-DSA', + 'SLH-DSA-SHAKE-256f': 'SLH-DSA', }; function pqcImportKeyObject( @@ -1620,6 +1637,16 @@ function mldsaImportKey( return new CryptoKey(keyObject, { name }, keyUsages, extractable); } +function slhdsaImportKey( + format: ImportFormat, + data: BufferLike | JWK, + algorithm: SubtleAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[], +): CryptoKey { + return mldsaImportKey(format, data, algorithm, extractable, keyUsages); +} + function mlkemImportKey( format: ImportFormat, data: BufferLike | JWK, @@ -1685,8 +1712,21 @@ const exportKeySpki = async ( case 'ML-DSA-65': // Fall through case 'ML-DSA-87': + // Fall through + case 'SLH-DSA-SHA2-128s': + case 'SLH-DSA-SHA2-128f': + case 'SLH-DSA-SHA2-192s': + case 'SLH-DSA-SHA2-192f': + case 'SLH-DSA-SHA2-256s': + case 'SLH-DSA-SHA2-256f': + case 'SLH-DSA-SHAKE-128s': + case 'SLH-DSA-SHAKE-128f': + case 'SLH-DSA-SHAKE-192s': + case 'SLH-DSA-SHAKE-192f': + case 'SLH-DSA-SHAKE-256s': + case 'SLH-DSA-SHAKE-256f': if (key.type === 'public') { - // Export ML-DSA key in SPKI DER format + // Export ML-DSA / SLH-DSA key in SPKI DER format return bufferLikeToArrayBuffer( key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI), ); @@ -1771,6 +1811,24 @@ const exportKeyPkcs8 = async ( return ab; } break; + case 'SLH-DSA-SHA2-128s': + case 'SLH-DSA-SHA2-128f': + case 'SLH-DSA-SHA2-192s': + case 'SLH-DSA-SHA2-192f': + case 'SLH-DSA-SHA2-256s': + case 'SLH-DSA-SHA2-256f': + case 'SLH-DSA-SHAKE-128s': + case 'SLH-DSA-SHAKE-128f': + case 'SLH-DSA-SHAKE-192s': + case 'SLH-DSA-SHAKE-192f': + case 'SLH-DSA-SHAKE-256s': + case 'SLH-DSA-SHAKE-256f': + if (key.type === 'private') { + return bufferLikeToArrayBuffer( + key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8), + ); + } + break; } throw new Error( @@ -1847,8 +1905,20 @@ const exportKeyRaw = ( case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - // ML-DSA / ML-KEM keys do not recognize plain 'raw' (Node webcrypto.js - // lines 488-510). + case 'SLH-DSA-SHA2-128s': + case 'SLH-DSA-SHA2-128f': + case 'SLH-DSA-SHA2-192s': + case 'SLH-DSA-SHA2-192f': + case 'SLH-DSA-SHA2-256s': + case 'SLH-DSA-SHA2-256f': + case 'SLH-DSA-SHAKE-128s': + case 'SLH-DSA-SHAKE-128f': + case 'SLH-DSA-SHAKE-192s': + case 'SLH-DSA-SHAKE-192f': + case 'SLH-DSA-SHAKE-256s': + case 'SLH-DSA-SHAKE-256f': + // ML-DSA / ML-KEM / SLH-DSA keys do not recognize plain 'raw' (Node + // webcrypto.js lines 488-510). if (!isPublic) return fail(); if (format === 'raw-public') return exportRawPublic(); return fail(); @@ -1908,6 +1978,19 @@ const exportKeyJWK = (key: CryptoKey): ArrayBuffer | unknown => { case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': + // Fall through + case 'SLH-DSA-SHA2-128s': + case 'SLH-DSA-SHA2-128f': + case 'SLH-DSA-SHA2-192s': + case 'SLH-DSA-SHA2-192f': + case 'SLH-DSA-SHA2-256s': + case 'SLH-DSA-SHA2-256f': + case 'SLH-DSA-SHAKE-128s': + case 'SLH-DSA-SHAKE-128f': + case 'SLH-DSA-SHAKE-192s': + case 'SLH-DSA-SHAKE-192f': + case 'SLH-DSA-SHAKE-256s': + case 'SLH-DSA-SHAKE-256f': return jwk; case 'AES-CTR': // Fall through @@ -2289,6 +2372,18 @@ const signVerify = ( case 'ML-DSA-44': case 'ML-DSA-65': case 'ML-DSA-87': + case 'SLH-DSA-SHA2-128s': + case 'SLH-DSA-SHA2-128f': + case 'SLH-DSA-SHA2-192s': + case 'SLH-DSA-SHA2-192f': + case 'SLH-DSA-SHA2-256s': + case 'SLH-DSA-SHA2-256f': + case 'SLH-DSA-SHAKE-128s': + case 'SLH-DSA-SHAKE-128f': + case 'SLH-DSA-SHAKE-192s': + case 'SLH-DSA-SHAKE-192f': + case 'SLH-DSA-SHAKE-256s': + case 'SLH-DSA-SHAKE-256f': return mldsaSignVerify(key, data, signature); case 'KMAC128': case 'KMAC256': @@ -2364,6 +2459,7 @@ const SUPPORTED_ALGORITHMS: Record> = { 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', + ...SLH_DSA_VARIANTS, ]), verify: new Set([ 'RSASSA-PKCS1-v1_5', @@ -2377,6 +2473,7 @@ const SUPPORTED_ALGORITHMS: Record> = { 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', + ...SLH_DSA_VARIANTS, ]), digest: new Set([ 'SHA-1', @@ -2414,6 +2511,7 @@ const SUPPORTED_ALGORITHMS: Record> = { 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', + ...SLH_DSA_VARIANTS, ]), importKey: new Set([ 'RSASSA-PKCS1-v1_5', @@ -2445,6 +2543,7 @@ const SUPPORTED_ALGORITHMS: Record> = { 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', + ...SLH_DSA_VARIANTS, ]), exportKey: new Set([ 'RSASSA-PKCS1-v1_5', @@ -2471,6 +2570,7 @@ const SUPPORTED_ALGORITHMS: Record> = { 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', + ...SLH_DSA_VARIANTS, ]), deriveBits: new Set([ 'PBKDF2', @@ -2506,7 +2606,7 @@ const SUPPORTED_ALGORITHMS: Record> = { decapsulateKey: new Set(['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']), }; -const ASYMMETRIC_ALGORITHMS = new Set([ +const ASYMMETRIC_ALGORITHMS = new Set([ 'RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', @@ -2522,6 +2622,7 @@ const ASYMMETRIC_ALGORITHMS = new Set([ 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', + ...SLH_DSA_VARIANTS, ]); // Per-algorithm validators for deriveBits (mirrors Node's hkdf.js:141-149, @@ -2964,13 +3065,14 @@ export class Subtle { throw lazyDOMException('key is not extractable', 'InvalidAccessError'); if (format === 'raw-seed') { - const pqcAlgos = [ + const pqcAlgos: string[] = [ 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', + ...SLH_DSA_VARIANTS, ]; if (!pqcAlgos.includes(key.algorithm.name)) { throw lazyDOMException( @@ -3216,6 +3318,25 @@ export class Subtle { ); checkCryptoKeyPairUsages(result as CryptoKeyPair); break; + case 'SLH-DSA-SHA2-128s': + case 'SLH-DSA-SHA2-128f': + case 'SLH-DSA-SHA2-192s': + case 'SLH-DSA-SHA2-192f': + case 'SLH-DSA-SHA2-256s': + case 'SLH-DSA-SHA2-256f': + case 'SLH-DSA-SHAKE-128s': + case 'SLH-DSA-SHAKE-128f': + case 'SLH-DSA-SHAKE-192s': + case 'SLH-DSA-SHAKE-192f': + case 'SLH-DSA-SHAKE-256s': + case 'SLH-DSA-SHAKE-256f': + result = await slhdsa_generateKeyPairWebCrypto( + algorithm.name as SlhDsaVariant, + extractable, + keyUsages, + ); + checkCryptoKeyPairUsages(result as CryptoKeyPair); + break; case 'X25519': // Fall through case 'X448': @@ -3389,6 +3510,26 @@ export class Subtle { keyUsages, ); break; + case 'SLH-DSA-SHA2-128s': + case 'SLH-DSA-SHA2-128f': + case 'SLH-DSA-SHA2-192s': + case 'SLH-DSA-SHA2-192f': + case 'SLH-DSA-SHA2-256s': + case 'SLH-DSA-SHA2-256f': + case 'SLH-DSA-SHAKE-128s': + case 'SLH-DSA-SHAKE-128f': + case 'SLH-DSA-SHAKE-192s': + case 'SLH-DSA-SHAKE-192f': + case 'SLH-DSA-SHAKE-256s': + case 'SLH-DSA-SHAKE-256f': + result = slhdsaImportKey( + format, + data as BufferLike | JWK, + normalizedAlgorithm, + extractable, + keyUsages, + ); + break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': diff --git a/packages/react-native-quick-crypto/src/utils/types.ts b/packages/react-native-quick-crypto/src/utils/types.ts index 83562b94..f739a08c 100644 --- a/packages/react-native-quick-crypto/src/utils/types.ts +++ b/packages/react-native-quick-crypto/src/utils/types.ts @@ -74,20 +74,50 @@ export type ECKeyPairAlgorithm = 'ECDSA' | 'ECDH'; export type CFRGKeyPairAlgorithm = 'Ed25519' | 'Ed448' | 'X25519' | 'X448'; export type CFRGKeyPairType = 'ed25519' | 'ed448' | 'x25519' | 'x448'; +export type SlhDsaAlgorithm = + | 'SLH-DSA-SHA2-128s' + | 'SLH-DSA-SHA2-128f' + | 'SLH-DSA-SHA2-192s' + | 'SLH-DSA-SHA2-192f' + | 'SLH-DSA-SHA2-256s' + | 'SLH-DSA-SHA2-256f' + | 'SLH-DSA-SHAKE-128s' + | 'SLH-DSA-SHAKE-128f' + | 'SLH-DSA-SHAKE-192s' + | 'SLH-DSA-SHAKE-192f' + | 'SLH-DSA-SHAKE-256s' + | 'SLH-DSA-SHAKE-256f'; + +export type SlhDsaKeyPairType = + | 'slh-dsa-sha2-128s' + | 'slh-dsa-sha2-128f' + | 'slh-dsa-sha2-192s' + | 'slh-dsa-sha2-192f' + | 'slh-dsa-sha2-256s' + | 'slh-dsa-sha2-256f' + | 'slh-dsa-shake-128s' + | 'slh-dsa-shake-128f' + | 'slh-dsa-shake-192s' + | 'slh-dsa-shake-192f' + | 'slh-dsa-shake-256s' + | 'slh-dsa-shake-256f'; + export type PQCKeyPairAlgorithm = | 'ML-DSA-44' | 'ML-DSA-65' | 'ML-DSA-87' | 'ML-KEM-512' | 'ML-KEM-768' - | 'ML-KEM-1024'; + | 'ML-KEM-1024' + | SlhDsaAlgorithm; export type PQCKeyPairType = | 'ml-dsa-44' | 'ml-dsa-65' | 'ml-dsa-87' | 'ml-kem-512' | 'ml-kem-768' - | 'ml-kem-1024'; + | 'ml-kem-1024' + | SlhDsaKeyPairType; export type MlKemAlgorithm = 'ML-KEM-512' | 'ML-KEM-768' | 'ML-KEM-1024'; @@ -128,7 +158,8 @@ export type SignVerifyAlgorithm = | 'Ed448' | 'ML-DSA-44' | 'ML-DSA-65' - | 'ML-DSA-87'; + | 'ML-DSA-87' + | SlhDsaAlgorithm; export type Argon2Algorithm = 'Argon2d' | 'Argon2i' | 'Argon2id'; @@ -255,7 +286,8 @@ export type KeyPairType = | RSAKeyPairType | ECKeyPairType | DSAKeyPairType - | DHKeyPairType; + | DHKeyPairType + | SlhDsaKeyPairType; export type KeyUsage = | 'encrypt' From e919b95c97630b01df7b831db4e94d44e9c355b8 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 6 May 2026 12:12:46 -0400 Subject: [PATCH 2/3] fix: honor SLH-DSA key encoding options --- .../src/keys/generateKeyPair.ts | 89 ++++++++++++++----- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts index 9cec8de2..3c0320bb 100644 --- a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts +++ b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts @@ -1,4 +1,5 @@ import { ed_generateKeyPair } from '../ed'; +import { Buffer } from '@craftzdog/react-native-buffer'; import { rsa_generateKeyPairNode, rsa_generateKeyPairNodeSync } from '../rsa'; import { ec_generateKeyPairNode, ec_generateKeyPairNodeSync } from '../ec'; import { dsa_generateKeyPairNode, dsa_generateKeyPairNodeSync } from '../dsa'; @@ -50,12 +51,15 @@ function isSlhDsaType(type: string): type is SlhDsaKeyPairType { return type in SLH_DSA_TYPE_TO_VARIANT; } -function slhDsaGenerateKeyPairNodeSync(type: SlhDsaKeyPairType): { - publicKey: PublicKeyObject; - privateKey: PrivateKeyObject; +function slhDsaFormatKeyPairOutput( + slhdsa: SlhDsa, + encoding: KeyPairGenConfig, +): { + publicKey: PublicKeyObject | string | ArrayBuffer; + privateKey: PrivateKeyObject | string | ArrayBuffer; } { - const slhdsa = new SlhDsa(SLH_DSA_TYPE_TO_VARIANT[type]); - slhdsa.generateKeyPairSync(); + const { publicFormat, privateFormat, cipher, passphrase } = encoding; + const publicKey = KeyObject.createKeyObject( 'public', slhdsa.getPublicKey(), @@ -68,27 +72,68 @@ function slhDsaGenerateKeyPairNodeSync(type: SlhDsaKeyPairType): { KFormatType.DER, KeyEncoding.PKCS8, ) as PrivateKeyObject; - return { publicKey, privateKey }; + + let publicKeyOutput: PublicKeyObject | string | ArrayBuffer; + let privateKeyOutput: PrivateKeyObject | string | ArrayBuffer; + + if (publicFormat === -1) { + publicKeyOutput = publicKey; + } else { + const format = + publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; + const exported = publicKey.handle.exportKey(format, KeyEncoding.SPKI); + if (format === KFormatType.PEM) { + publicKeyOutput = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + publicKeyOutput = exported; + } + } + + if (privateFormat === -1) { + privateKeyOutput = privateKey; + } else { + const format = + privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; + const exported = privateKey.handle.exportKey( + format, + KeyEncoding.PKCS8, + cipher, + passphrase, + ); + if (format === KFormatType.PEM) { + privateKeyOutput = Buffer.from(new Uint8Array(exported)).toString( + 'utf-8', + ); + } else { + privateKeyOutput = exported; + } + } + + return { publicKey: publicKeyOutput, privateKey: privateKeyOutput }; +} + +function slhDsaGenerateKeyPairNodeSync( + type: SlhDsaKeyPairType, + encoding: KeyPairGenConfig, +): { + publicKey: PublicKeyObject | string | ArrayBuffer; + privateKey: PrivateKeyObject | string | ArrayBuffer; +} { + const slhdsa = new SlhDsa(SLH_DSA_TYPE_TO_VARIANT[type]); + slhdsa.generateKeyPairSync(); + return slhDsaFormatKeyPairOutput(slhdsa, encoding); } async function slhDsaGenerateKeyPairNode( type: SlhDsaKeyPairType, -): Promise<{ publicKey: PublicKeyObject; privateKey: PrivateKeyObject }> { + encoding: KeyPairGenConfig, +): Promise<{ + publicKey: PublicKeyObject | string | ArrayBuffer; + privateKey: PrivateKeyObject | string | ArrayBuffer; +}> { const slhdsa = new SlhDsa(SLH_DSA_TYPE_TO_VARIANT[type]); await slhdsa.generateKeyPair(); - const publicKey = KeyObject.createKeyObject( - 'public', - slhdsa.getPublicKey(), - KFormatType.DER, - KeyEncoding.SPKI, - ) as PublicKeyObject; - const privateKey = KeyObject.createKeyObject( - 'private', - slhdsa.getPrivateKey(), - KFormatType.DER, - KeyEncoding.PKCS8, - ) as PrivateKeyObject; - return { publicKey, privateKey }; + return slhDsaFormatKeyPairOutput(slhdsa, encoding); } export const generateKeyPair = ( @@ -243,7 +288,7 @@ function internalGenerateKeyPair( } else if (type === 'dh') { result = await dh_generateKeyPairNode(options, encoding); } else if (isSlhDsaType(type)) { - result = await slhDsaGenerateKeyPairNode(type); + result = await slhDsaGenerateKeyPairNode(type, encoding); } else { throw new Error(`Unsupported key type: ${type}`); } @@ -275,7 +320,7 @@ function internalGenerateKeyPair( } else if (type === 'dh') { result = dh_generateKeyPairNodeSync(options, encoding); } else if (isSlhDsaType(type)) { - result = slhDsaGenerateKeyPairNodeSync(type); + result = slhDsaGenerateKeyPairNodeSync(type, encoding); } else { throw new Error(`Unsupported key type: ${type}`); } From a1bfad32e1782ef4c9e4ef0c8bfa04c00958bc0f Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 6 May 2026 12:25:59 -0400 Subject: [PATCH 3/3] fix: resolve SLH-DSA CI failures --- docs/content/docs/api/pqc.mdx | 10 +++++----- packages/react-native-quick-crypto/src/slhdsa.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/content/docs/api/pqc.mdx b/docs/content/docs/api/pqc.mdx index 643d8041..add455f8 100644 --- a/docs/content/docs/api/pqc.mdx +++ b/docs/content/docs/api/pqc.mdx @@ -266,11 +266,11 @@ const isValid = await subtle.verify( ### Key Export Formats -| Algorithm | `spki` | `pkcs8` | `jwk` | `raw-public` | `raw-seed` | -| :-------------------------------------- | :----: | :-----: | :---: | :----------: | :--------: | -| ML-DSA-44/65/87 | ✅ | ✅ | ✅ | ✅ | ✅ | -| ML-KEM-512/768/1024 | ✅ | ✅ | ✅ | ✅ | ✅ | -| SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f} | ✅ | ✅ | ✅ | ✅ | ✅ | +| Algorithm | `spki` | `pkcs8` | `jwk` | `raw-public` | `raw-seed` | +| :---------------------------------------- | :----: | :-----: | :---: | :----------: | :--------: | +| ML-DSA-44/65/87 | ✅ | ✅ | ✅ | ✅ | ✅ | +| ML-KEM-512/768/1024 | ✅ | ✅ | ✅ | ✅ | ✅ | +| `SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f}` | ✅ | ✅ | ✅ | ✅ | ✅ | ```ts // Export ML-DSA public key as JWK diff --git a/packages/react-native-quick-crypto/src/slhdsa.ts b/packages/react-native-quick-crypto/src/slhdsa.ts index 90b96323..4af1f75d 100644 --- a/packages/react-native-quick-crypto/src/slhdsa.ts +++ b/packages/react-native-quick-crypto/src/slhdsa.ts @@ -5,7 +5,7 @@ import { KeyObject, PublicKeyObject, PrivateKeyObject as PrivateKeyObjectClass, -} from './keys'; +} from './keys/classes'; import type { CryptoKeyPair, KeyUsage,