Compare commits
9 Commits
aa61de33a2
...
919d4bcc95
| Author | SHA1 | Date |
|---|---|---|
|
|
919d4bcc95 | |
|
|
c9fd4542d7 | |
|
|
b1873b27be | |
|
|
3ea964d3d9 | |
|
|
cc72bb8919 | |
|
|
c82d472004 | |
|
|
58389ad685 | |
|
|
68836cfff4 | |
|
|
971f1aeaaa |
|
|
@ -1,4 +1,8 @@
|
|||
{
|
||||
"[rust]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnPaste": false
|
||||
},
|
||||
"cmake.configureOnOpen": false,
|
||||
"dart.analysisExcludedFolders": [
|
||||
"flutter_src",
|
||||
|
|
@ -15,6 +19,10 @@
|
|||
"AllBlockComments"
|
||||
],
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"cargo search": true
|
||||
"cargo search": true,
|
||||
"/^source \"\\$HOME/\\.cargo/env\" && cd /Volumes/LUM/source/letusmsg_proj/app/lum_ccc_rust && python3 -c \"\n# Self-test AES-GCM vector\nkey = 'feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308'\nnonce = 'cafebabefacedbaddecaf888'\naad = ''\npt = 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255'\nct_tag = '522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad'\n\nprint\\('Self-test AES-GCM:'\\)\nprint\\(f' PT bytes: \\{len\\(pt\\)//2\\}'\\)\nprint\\(f' CT_TAG bytes: \\{len\\(ct_tag\\)//2\\}'\\)\nprint\\(f' Expected \\(PT \\+ 16 tag\\): \\{len\\(pt\\)//2 \\+ 16\\}'\\)\nprint\\(f' ct_tag IS ciphertext-only \\(no tag\\): \\{len\\(ct_tag\\)//2 == len\\(pt\\)//2\\}'\\)\nprint\\(\\)\n\n# Conformance AES-GCM vector \\(different: has AAD, shorter PT\\)\npt2 = 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39'\nct_tag2 = '522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f66276fc6ece0f4e1768cddf8853bb2d551b'\nprint\\('Conformance AES-GCM TC16:'\\)\nprint\\(f' PT bytes: \\{len\\(pt2\\)//2\\}'\\)\nprint\\(f' CT_TAG bytes: \\{len\\(ct_tag2\\)//2\\}'\\)\nprint\\(f' CT_TAG == PT \\+ 16: \\{len\\(ct_tag2\\)//2 == len\\(pt2\\)//2 \\+ 16\\}'\\)\nprint\\(\\)\n\n# Self-test ChaCha vector\npt3 = '4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f6620273939'\nct_tag3 = 'd31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116'\nprint\\('Self-test ChaCha:'\\)\nprint\\(f' PT bytes: \\{len\\(pt3\\)//2\\}'\\)\nprint\\(f' CT_TAG bytes: \\{len\\(ct_tag3\\)//2\\}'\\)\nprint\\(f' CT_TAG == PT \\+ 16: \\{len\\(ct_tag3\\)//2 == len\\(pt3\\)//2 \\+ 16\\}'\\)\nprint\\(f' Ratio: CT_TAG is \\{len\\(ct_tag3\\)//2 - len\\(pt3\\)//2\\} bytes longer than PT'\\)\n\"\n$/": {
|
||||
"approve": true,
|
||||
"matchCommandLine": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
=============================================
|
||||
CCC Rust — Developer Build Guide
|
||||
=============================================
|
||||
|
||||
:Last Updated: 2026-03-11
|
||||
|
||||
This document covers everything needed to build, test, and cross-compile the
|
||||
``ccc_rust`` workspace on a development machine.
|
||||
|
||||
----
|
||||
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
The table below lists all required tools. Versions shown are known-good; newer
|
||||
patch releases should work.
|
||||
|
||||
+------------------+---------------+--------------------------------------------------+
|
||||
| Tool | Min Version | Purpose |
|
||||
+==================+===============+==================================================+
|
||||
| **Rust (rustup)**| stable 1.85+ | Compiler toolchain (pinned via ``rust-toolchain`` |
|
||||
| | | ``.toml``) |
|
||||
+------------------+---------------+--------------------------------------------------+
|
||||
| **CMake** | 3.24+ | Builds vendored wolfSSL from source |
|
||||
+------------------+---------------+--------------------------------------------------+
|
||||
| **Clang / LLVM** | 14+ | Required by ``bindgen`` for FFI generation |
|
||||
+------------------+---------------+--------------------------------------------------+
|
||||
| **Git** | 2.30+ | Submodule management |
|
||||
+------------------+---------------+--------------------------------------------------+
|
||||
| **task** (opt) | 3.x | Local task runner (``Taskfile.yaml``) |
|
||||
+------------------+---------------+--------------------------------------------------+
|
||||
| **Android NDK** | r25+ | Cross-compile for Android (optional on macOS) |
|
||||
| (optional) | | |
|
||||
+------------------+---------------+--------------------------------------------------+
|
||||
|
||||
Installing Dependencies
|
||||
-----------------------
|
||||
|
||||
macOS (Homebrew)
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Rust (via rustup — manages toolchains automatically)
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source "$HOME/.cargo/env"
|
||||
|
||||
# CMake and LLVM (clang for bindgen)
|
||||
brew install cmake llvm
|
||||
|
||||
# Task runner (optional — for Taskfile.yaml)
|
||||
brew install go-task
|
||||
|
||||
# Xcode Command Line Tools (required for iOS/macOS targets)
|
||||
xcode-select --install
|
||||
|
||||
Linux (apt)
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source "$HOME/.cargo/env"
|
||||
|
||||
sudo apt update && sudo apt install -y cmake clang libclang-dev git
|
||||
|
||||
# Task runner (optional)
|
||||
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
|
||||
|
||||
Windows
|
||||
~~~~~~~
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Install rustup from https://rustup.rs
|
||||
# Install CMake from https://cmake.org/download/ (add to PATH)
|
||||
# Install LLVM/Clang from https://releases.llvm.org/ (add to PATH)
|
||||
# Install Visual Studio Build Tools (C++ workload)
|
||||
|
||||
Android NDK (all platforms, optional)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Required only for ``aarch64-linux-android`` / ``x86_64-linux-android`` targets.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Via Android Studio SDK Manager, or:
|
||||
sdkmanager --install "ndk;28.2.13676358"
|
||||
|
||||
# Set environment variables (add to your shell profile):
|
||||
export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/27.0.12077973"
|
||||
export PATH="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH"
|
||||
|
||||
# On Linux, replace darwin-x86_64 with linux-x86_64.
|
||||
|
||||
----
|
||||
|
||||
First-Time Setup
|
||||
================
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Clone the repository
|
||||
git clone <repo-url> ccc_rust && cd ccc_rust
|
||||
|
||||
# Initialise the wolfSSL submodule (required — build fails without it)
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Verify the toolchain (rustup reads rust-toolchain.toml automatically)
|
||||
rustc --version # should print stable ≥ 1.85
|
||||
cmake --version # should print ≥ 3.24
|
||||
|
||||
----
|
||||
|
||||
Building
|
||||
========
|
||||
|
||||
Native Build (Host Machine)
|
||||
----------------------------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
cargo build --workspace # debug
|
||||
cargo build --workspace --release # release (optimised)
|
||||
|
||||
Cross-Compile Targets
|
||||
---------------------
|
||||
|
||||
Cargo aliases are defined in ``.cargo/config.toml``:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Apple
|
||||
cargo build-ios # aarch64-apple-ios
|
||||
cargo build-ios-sim # aarch64-apple-ios-sim
|
||||
cargo build-macos-arm64 # aarch64-apple-darwin
|
||||
cargo build-macos-x64 # x86_64-apple-darwin
|
||||
cargo build-all-apple # all three Apple targets at once
|
||||
|
||||
# Android (requires NDK — see above)
|
||||
cargo build-android-arm64 # aarch64-linux-android
|
||||
cargo build-android-x64 # x86_64-linux-android
|
||||
|
||||
# Release variants: append --release
|
||||
cargo build-ios --release
|
||||
cargo build-macos-arm64 --release
|
||||
|
||||
wolfSSL Build Overrides
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``build.rs`` script accepts two environment variables:
|
||||
|
||||
``WOLFSSL_SOURCE_DIR``
|
||||
Override the wolfSSL source path (default: ``vendors/wolfssl``).
|
||||
Useful in CI when the submodule is at a non-standard location.
|
||||
|
||||
``WOLFSSL_INSTALL_DIR``
|
||||
Point to a pre-installed wolfSSL (must contain ``include/wolfssl/``
|
||||
and ``lib/libwolfssl.a``). Skips the CMake build entirely.
|
||||
|
||||
Stub FFI (no C build)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For type-checking without building wolfSSL (CI lint jobs, docs):
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
cargo check -p ccc-crypto-wolfssl --features stub_ffi
|
||||
|
||||
----
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Unit tests (all crates)
|
||||
cargo test --workspace
|
||||
|
||||
# Conformance test binary (NIST / RFC vectors)
|
||||
cargo run -p ccc-conformance-tests
|
||||
|
||||
# Run a single test by name
|
||||
cargo test --workspace test_name
|
||||
|
||||
# Clippy (lint — treat warnings as errors)
|
||||
cargo clippy --workspace -- -D warnings
|
||||
|
||||
# Format check
|
||||
cargo fmt --check
|
||||
|
||||
----
|
||||
|
||||
Build Artifacts
|
||||
===============
|
||||
|
||||
All output lands under ``target/``. The directory structure is:
|
||||
|
||||
::
|
||||
|
||||
target/
|
||||
├── debug/ ← native host debug
|
||||
│ ├── conformance ← conformance test binary
|
||||
│ ├── libccc_crypto_core.rlib ← core crate (Rust lib)
|
||||
│ ├── libccc_crypto_wolfssl.rlib ← wolfSSL provider (Rust lib)
|
||||
│ └── build/ccc-crypto-wolfssl-*/out/
|
||||
│ ├── lib/libwolfssl.a ← wolfSSL static library
|
||||
│ └── include/wolfssl/ ← generated wolfSSL headers
|
||||
├── release/ ← native host release
|
||||
│ └── (same layout as debug/)
|
||||
├── aarch64-apple-ios/
|
||||
│ └── debug/ | release/ ← iOS artifacts
|
||||
├── aarch64-apple-darwin/
|
||||
│ └── debug/ | release/ ← macOS ARM64 artifacts
|
||||
├── x86_64-apple-darwin/
|
||||
│ └── debug/ | release/ ← macOS x86_64 artifacts
|
||||
├── aarch64-linux-android/
|
||||
│ └── debug/ | release/ ← Android ARM64 artifacts
|
||||
└── x86_64-linux-android/
|
||||
└── debug/ | release/ ← Android x86_64 artifacts
|
||||
|
||||
Key Artifacts per Target
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
+---------------------------------+--------------------------------------------------------------+
|
||||
| Artifact | Path |
|
||||
+=================================+==============================================================+
|
||||
| Core Rust library | ``target/<triple>/<profile>/libccc_crypto_core.rlib`` |
|
||||
+---------------------------------+--------------------------------------------------------------+
|
||||
| wolfSSL provider library | ``target/<triple>/<profile>/libccc_crypto_wolfssl.rlib`` |
|
||||
+---------------------------------+--------------------------------------------------------------+
|
||||
| wolfSSL static C library | ``target/<triple>/<profile>/build/ccc-crypto-wolfssl-*/out/`` |
|
||||
| | ``lib/libwolfssl.a`` |
|
||||
+---------------------------------+--------------------------------------------------------------+
|
||||
| Conformance test binary | ``target/<triple>/<profile>/conformance`` |
|
||||
+---------------------------------+--------------------------------------------------------------+
|
||||
|
||||
Where ``<triple>`` is the Rust target triple (e.g. ``aarch64-apple-ios``)
|
||||
and ``<profile>`` is ``debug`` or ``release``.
|
||||
|
||||
.. note::
|
||||
|
||||
These crates produce **rlib** (Rust library) output. The Milestone 2
|
||||
bridge crate (in the separate ``ccc_cryptography`` repository) adds
|
||||
``crate-type = ["cdylib", "staticlib"]`` to produce the ``.so`` /
|
||||
``.dylib`` / ``.a`` artifacts consumed by Flutter.
|
||||
|
||||
----
|
||||
|
||||
Publish
|
||||
=======
|
||||
|
||||
The Rust crypto library remains a pure Rust crate with no Flutter or FFI-specific code.
|
||||
|
||||
Option 1
|
||||
--------
|
||||
Option 1: Build from source via Cargokit (recommended for development)
|
||||
* Cargokit compiles the Rust code at build time for each platform
|
||||
* No precompiled binaries are needed
|
||||
|
||||
Option 2
|
||||
--------
|
||||
Option 2: Cargokit precompiled binaries via GitHub Releases (recommended for pub.dev)
|
||||
* The Rust crate is compiled into platform-specific binaries (staticlib for iOS/macOS, cdylib for Android)
|
||||
* Binaries are uploaded to GitHub Releases
|
||||
* Cargokit fetches the correct binary at build time based on the target platform
|
||||
* This allows pub.dev users to get native performance without needing Rust or a build environment
|
||||
|
||||
Workflow
|
||||
~~~~~~~~
|
||||
* CI builds the Rust crate for all 5 platform targets
|
||||
* CI uploads the .so/.dylib/.dll/.framework to a GitHub Release tagged with the Cargo package hash
|
||||
* When consumers run flutter build, Cargokit downloads the precompiled binary instead of compiling
|
||||
* If no precompiled binary exists, it falls back to source build
|
||||
* Pros: Fast builds for consumers, no Rust toolchain needed, no repo bloat, built into FRB, single repo
|
||||
* Cons: Requires CI pipeline to build + upload binaries
|
||||
* Best for: pub.dev releases, distributing to teams without Rust installed
|
||||
|
||||
----
|
||||
|
||||
Workspace Layout
|
||||
================
|
||||
|
||||
::
|
||||
|
||||
ccc_rust/
|
||||
├── Cargo.toml workspace manifest
|
||||
├── rust-toolchain.toml pinned stable toolchain + targets
|
||||
├── Taskfile.yaml local task runner (task CLI)
|
||||
├── ci.sh CI/CD build script
|
||||
├── .cargo/config.toml cross-compile aliases + linker config
|
||||
├── vendors/
|
||||
│ ├── README.md submodule pin rationale
|
||||
│ └── wolfssl/ git submodule → wolfSSL v5.7.2-stable
|
||||
├── crates/
|
||||
│ ├── ccc-crypto-core/ shared traits, enums, registry
|
||||
│ └── ccc-crypto-wolfssl/ wolfSSL/wolfCrypt provider
|
||||
├── tests/
|
||||
│ └── conformance/ NIST/RFC conformance test binary
|
||||
└── docs/ architecture + design documents
|
||||
|
||||
----
|
||||
|
||||
Troubleshooting
|
||||
===============
|
||||
|
||||
wolfSSL source not found
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
wolfSSL source not found at .../vendors/wolfssl.
|
||||
Run `git submodule update --init --recursive` to fetch it.
|
||||
|
||||
The wolfSSL submodule was not initialised. Run the command shown in the error.
|
||||
|
||||
Linker errors on Apple (undefined symbols)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Ensure the ``-lc++`` link arg is set for Apple targets. This is configured
|
||||
in ``.cargo/config.toml`` and should work automatically. If you see linker
|
||||
errors about C++ symbols, verify Xcode Command Line Tools are installed:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
xcode-select --install
|
||||
|
||||
Android linker not found
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
linker `aarch64-linux-android21-clang` not found
|
||||
|
||||
The Android NDK toolchain is not in your PATH. See the "Android NDK" section
|
||||
above.
|
||||
|
||||
bindgen fails / libclang not found
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``bindgen`` needs ``libclang``. On macOS, install LLVM via Homebrew:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
brew install llvm
|
||||
export LIBCLANG_PATH="$(brew --prefix llvm)/lib"
|
||||
|
||||
On Linux:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo apt install libclang-dev
|
||||
|
||||
----
|
||||
|
||||
Plugin Integration Notes
|
||||
=========================
|
||||
|
||||
When building the Milestone 2 Flutter plugin (``ccc_cryptography``):
|
||||
|
||||
1. The bridge crate depends on these crates via git tag::
|
||||
|
||||
[dependencies]
|
||||
ccc-crypto-core = { git = "...", tag = "v0.1.0" }
|
||||
ccc-crypto-wolfssl = { git = "...", tag = "v0.1.0" }
|
||||
|
||||
2. The bridge crate sets ``crate-type = ["cdylib", "staticlib"]``.
|
||||
3. Call ``ccc_crypto_wolfssl::init()`` from the bridge's ``ccc_init()``.
|
||||
4. ``flutter_rust_bridge`` and all Dart/Flutter dependencies live exclusively
|
||||
in the plugin repo — never in this one.
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
# https://taskfile.dev
|
||||
version: "3"
|
||||
|
||||
vars:
|
||||
APPLE_TARGETS: "aarch64-apple-ios aarch64-apple-darwin x86_64-apple-darwin"
|
||||
|
||||
tasks:
|
||||
|
||||
# ── Setup ────────────────────────────────────────────────────────────────
|
||||
|
||||
setup:
|
||||
desc: Initialise submodules and verify toolchain
|
||||
cmds:
|
||||
- git submodule update --init --recursive
|
||||
- rustc --version
|
||||
- cmd: cmake --version | head -1
|
||||
platforms: [darwin, linux]
|
||||
- cmd: cmake --version
|
||||
platforms: [windows]
|
||||
status:
|
||||
- test -f vendors/wolfssl/CMakeLists.txt
|
||||
|
||||
# ── Build ────────────────────────────────────────────────────────────────
|
||||
|
||||
build:
|
||||
desc: Build all crates (debug)
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo build --workspace
|
||||
|
||||
release:
|
||||
desc: Build all crates (release)
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo build --workspace --release
|
||||
|
||||
build:ios:
|
||||
desc: Cross-compile for iOS (aarch64-apple-ios, release)
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo build-ios --release
|
||||
|
||||
build:macos-arm64:
|
||||
desc: Cross-compile for macOS ARM64 (release)
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo build-macos-arm64 --release
|
||||
|
||||
build:macos-x64:
|
||||
desc: Cross-compile for macOS x86_64 (release)
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo build-macos-x64 --release
|
||||
|
||||
build:apple:
|
||||
desc: Cross-compile all Apple targets (iOS + macOS, release)
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo build-ios --release
|
||||
- cargo build-macos-arm64 --release
|
||||
- cargo build-macos-x64 --release
|
||||
|
||||
build:android-arm64:
|
||||
desc: Cross-compile for Android ARM64 (release)
|
||||
deps: [setup]
|
||||
preconditions:
|
||||
- sh: command -v aarch64-linux-android21-clang
|
||||
msg: "Android NDK not in PATH. Set ANDROID_NDK_HOME and add toolchain bin/ to PATH."
|
||||
cmds:
|
||||
- cargo build-android-arm64 --release
|
||||
|
||||
build:android-x64:
|
||||
desc: Cross-compile for Android x86_64 (release)
|
||||
deps: [setup]
|
||||
preconditions:
|
||||
- sh: command -v x86_64-linux-android21-clang
|
||||
msg: "Android NDK not in PATH. Set ANDROID_NDK_HOME and add toolchain bin/ to PATH."
|
||||
cmds:
|
||||
- cargo build-android-x64 --release
|
||||
|
||||
build:android:
|
||||
desc: Cross-compile all Android targets (release)
|
||||
cmds:
|
||||
- task: build:android-arm64
|
||||
- task: build:android-x64
|
||||
|
||||
# ── Windows (run on Windows only) ─────────────────────────────────────
|
||||
|
||||
build:windows-x64:
|
||||
desc: Build for Windows x86_64 (release, run on Windows)
|
||||
platforms: [windows]
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo build --workspace --release --target x86_64-pc-windows-msvc
|
||||
|
||||
ci:windows:
|
||||
desc: Full CI pipeline for Windows
|
||||
platforms: [windows]
|
||||
cmds:
|
||||
- task: lint
|
||||
- task: test
|
||||
- task: conformance
|
||||
- task: build:windows-x64
|
||||
|
||||
# ── Linux ────────────────────────────────────────────────────────────────
|
||||
|
||||
build:linux-x64:
|
||||
desc: Build for Linux x86_64 (release, run on Linux)
|
||||
platforms: [linux]
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo build --workspace --release --target x86_64-unknown-linux-gnu
|
||||
|
||||
ci:linux:
|
||||
desc: Full CI pipeline for Linux (lint + test + conformance + build + Android)
|
||||
platforms: [linux]
|
||||
cmds:
|
||||
- task: lint
|
||||
- task: test
|
||||
- task: conformance
|
||||
- task: build:linux-x64
|
||||
- task: build:android
|
||||
|
||||
# ── Aggregate ────────────────────────────────────────────────────────────
|
||||
|
||||
build:all:
|
||||
desc: Build for all targets (native + Apple + Android)
|
||||
cmds:
|
||||
- task: release
|
||||
- task: build:apple
|
||||
- task: build:android
|
||||
|
||||
# ── Test ─────────────────────────────────────────────────────────────────
|
||||
|
||||
test:
|
||||
desc: Run all unit tests
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo test --workspace
|
||||
|
||||
conformance:
|
||||
desc: Run NIST/RFC conformance test vectors
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo run -p ccc-conformance-tests
|
||||
|
||||
test:all:
|
||||
desc: Run unit tests + conformance tests
|
||||
cmds:
|
||||
- task: test
|
||||
- task: conformance
|
||||
|
||||
# ── Lint ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fmt:
|
||||
desc: Format code with rustfmt
|
||||
cmds:
|
||||
- cargo fmt
|
||||
|
||||
fmt:check:
|
||||
desc: Check formatting (no changes)
|
||||
cmds:
|
||||
- cargo fmt --check
|
||||
|
||||
clippy:
|
||||
desc: Run clippy with strict warnings
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo clippy --workspace -- -D warnings
|
||||
|
||||
lint:
|
||||
desc: Full lint (fmt check + clippy)
|
||||
cmds:
|
||||
- task: fmt:check
|
||||
- task: clippy
|
||||
|
||||
# ── CI Pipeline ─────────────────────────────────────────────────────────
|
||||
|
||||
ci:
|
||||
desc: Full CI pipeline (lint → test → conformance → release build)
|
||||
cmds:
|
||||
- task: lint
|
||||
- task: test
|
||||
- task: conformance
|
||||
- task: release
|
||||
|
||||
ci:full:
|
||||
desc: Full CI + all cross-compile targets (platform-aware)
|
||||
cmds:
|
||||
- task: ci
|
||||
- task: build:apple
|
||||
- task: build:android
|
||||
|
||||
ci:macos:
|
||||
desc: Full CI pipeline for macOS (lint + test + conformance + Apple targets)
|
||||
platforms: [darwin]
|
||||
cmds:
|
||||
- task: lint
|
||||
- task: test
|
||||
- task: conformance
|
||||
- task: release
|
||||
- task: build:apple
|
||||
|
||||
# ── Utility ─────────────────────────────────────────────────────────────
|
||||
|
||||
clean:
|
||||
desc: Remove all build artifacts
|
||||
cmds:
|
||||
- cargo clean
|
||||
|
||||
check:
|
||||
desc: Type-check without building (fast)
|
||||
deps: [setup]
|
||||
cmds:
|
||||
- cargo check --workspace
|
||||
|
||||
check:stub:
|
||||
desc: Type-check wolfSSL crate with stub_ffi (no C build)
|
||||
cmds:
|
||||
- cargo check -p ccc-crypto-wolfssl --features stub_ffi
|
||||
|
||||
artifacts:
|
||||
desc: Show artifact locations for all built targets
|
||||
platforms: [darwin, linux]
|
||||
cmds:
|
||||
- |
|
||||
echo "=== Build artifacts ==="
|
||||
for dir in target/debug target/release target/aarch64-apple-ios/release target/aarch64-apple-darwin/release target/x86_64-apple-darwin/release target/aarch64-linux-android/release target/x86_64-linux-android/release target/x86_64-unknown-linux-gnu/release; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo ""
|
||||
echo "── $dir ──"
|
||||
find "$dir" -maxdepth 1 \( -name "*.rlib" -o -name "*.a" -o -name "*.dylib" -o -name "*.so" -o -name "conformance" \) 2>/dev/null | sort
|
||||
find "$dir/build" -name "libwolfssl.a" 2>/dev/null | head -1
|
||||
fi
|
||||
done
|
||||
|
||||
artifacts:windows:
|
||||
desc: Show artifact locations (Windows)
|
||||
platforms: [windows]
|
||||
cmds:
|
||||
- cmd: |
|
||||
echo === Build artifacts ===
|
||||
for %%d in (target\debug target\release target\x86_64-pc-windows-msvc\release) do (
|
||||
if exist "%%d" (
|
||||
echo.
|
||||
echo -- %%d --
|
||||
dir /b "%%d\*.rlib" "%%d\*.lib" "%%d\*.dll" "%%d\*.exe" 2>nul
|
||||
)
|
||||
)
|
||||
|
||||
##### ##### ##### ##### ##### ##### ##### ##### ##### #####
|
||||
# Default
|
||||
#
|
||||
# if we are executed with option of "--taskfile", and no task, so this default...that is why we pass it along
|
||||
# --taskfile {{.TASKFILE}}
|
||||
# we spent several hours fixing that out, because of PEBKAC
|
||||
default:
|
||||
cmds:
|
||||
- task --list --taskfile {{.TASKFILE}}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env bash
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# ci.sh — CI/CD build script for ccc_rust
|
||||
#
|
||||
# Usage:
|
||||
# ./ci.sh Run the full CI pipeline (lint + test + cross-build)
|
||||
# ./ci.sh lint Lint only (fmt + clippy)
|
||||
# ./ci.sh test Unit tests + conformance vectors
|
||||
# ./ci.sh build Native debug build
|
||||
# ./ci.sh release Native release build
|
||||
# ./ci.sh apple Cross-compile all Apple targets (iOS + macOS)
|
||||
# ./ci.sh android Cross-compile Android targets (requires NDK)
|
||||
# ./ci.sh all Full pipeline + all cross targets
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
# Source cargo env if not already in PATH.
|
||||
if ! command -v cargo &>/dev/null; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$HOME/.cargo/env"
|
||||
fi
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
step() { echo -e "\n${CYAN}${BOLD}── $1 ──${RESET}"; }
|
||||
pass() { echo -e "${GREEN}✓ $1${RESET}"; }
|
||||
fail() { echo -e "${RED}✗ $1${RESET}"; exit 1; }
|
||||
|
||||
# ── Submodule check ──────────────────────────────────────────────────────────
|
||||
|
||||
check_submodule() {
|
||||
if [ ! -f "vendors/wolfssl/CMakeLists.txt" ]; then
|
||||
step "Initialising wolfSSL submodule"
|
||||
git submodule update --init --recursive
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Stages ───────────────────────────────────────────────────────────────────
|
||||
|
||||
do_lint() {
|
||||
step "Format check"
|
||||
cargo fmt --check || fail "cargo fmt --check failed"
|
||||
pass "Format OK"
|
||||
|
||||
step "Clippy"
|
||||
cargo clippy --workspace -- -D warnings || fail "clippy failed"
|
||||
pass "Clippy OK"
|
||||
}
|
||||
|
||||
do_test() {
|
||||
step "Unit tests"
|
||||
cargo test --workspace || fail "cargo test failed"
|
||||
pass "Unit tests passed"
|
||||
|
||||
step "Conformance vectors"
|
||||
cargo run -p ccc-conformance-tests || fail "conformance tests failed"
|
||||
pass "Conformance tests passed"
|
||||
}
|
||||
|
||||
do_build() {
|
||||
step "Build (debug)"
|
||||
cargo build --workspace || fail "debug build failed"
|
||||
pass "Debug build OK"
|
||||
}
|
||||
|
||||
do_release() {
|
||||
step "Build (release)"
|
||||
cargo build --workspace --release || fail "release build failed"
|
||||
pass "Release build OK"
|
||||
}
|
||||
|
||||
do_apple() {
|
||||
step "Build iOS (aarch64-apple-ios)"
|
||||
cargo build-ios --release || fail "iOS build failed"
|
||||
pass "iOS build OK"
|
||||
|
||||
step "Build macOS ARM64 (aarch64-apple-darwin)"
|
||||
cargo build-macos-arm64 --release || fail "macOS ARM64 build failed"
|
||||
pass "macOS ARM64 build OK"
|
||||
|
||||
step "Build macOS x64 (x86_64-apple-darwin)"
|
||||
cargo build-macos-x64 --release || fail "macOS x64 build failed"
|
||||
pass "macOS x64 build OK"
|
||||
}
|
||||
|
||||
do_android() {
|
||||
if ! command -v aarch64-linux-android21-clang &>/dev/null; then
|
||||
echo -e "${RED}Android NDK not in PATH — skipping Android targets.${RESET}"
|
||||
echo "Set ANDROID_NDK_HOME and add the toolchain bin/ to PATH."
|
||||
return 1
|
||||
fi
|
||||
|
||||
step "Build Android ARM64 (aarch64-linux-android)"
|
||||
cargo build-android-arm64 --release || fail "Android ARM64 build failed"
|
||||
pass "Android ARM64 build OK"
|
||||
|
||||
step "Build Android x64 (x86_64-linux-android)"
|
||||
cargo build-android-x64 --release || fail "Android x64 build failed"
|
||||
pass "Android x64 build OK"
|
||||
}
|
||||
|
||||
do_full() {
|
||||
do_lint
|
||||
do_test
|
||||
do_release
|
||||
}
|
||||
|
||||
do_linux() {
|
||||
step "Build Linux x64 (x86_64-unknown-linux-gnu)"
|
||||
cargo build --workspace --release --target x86_64-unknown-linux-gnu \
|
||||
|| fail "Linux x64 build failed"
|
||||
pass "Linux x64 build OK"
|
||||
}
|
||||
|
||||
do_windows() {
|
||||
step "Build Windows x64 (x86_64-pc-windows-msvc)"
|
||||
cargo build --workspace --release --target x86_64-pc-windows-msvc \
|
||||
|| fail "Windows x64 build failed"
|
||||
pass "Windows x64 build OK"
|
||||
}
|
||||
|
||||
do_all() {
|
||||
do_full
|
||||
do_apple
|
||||
do_android || true # Don't fail the pipeline if NDK is absent.
|
||||
}
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||
|
||||
summary() {
|
||||
echo ""
|
||||
step "Artifact locations"
|
||||
echo " Native (release): target/release/"
|
||||
echo " iOS: target/aarch64-apple-ios/release/"
|
||||
echo " macOS ARM64: target/aarch64-apple-darwin/release/"
|
||||
echo " macOS x64: target/x86_64-apple-darwin/release/"
|
||||
echo " Linux x64: target/x86_64-unknown-linux-gnu/release/"
|
||||
echo " Windows x64: target/x86_64-pc-windows-msvc/release/"
|
||||
echo " Android ARM64: target/aarch64-linux-android/release/"
|
||||
echo " Android x64: target/x86_64-linux-android/release/"
|
||||
echo ""
|
||||
pass "CI pipeline complete"
|
||||
}
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
check_submodule
|
||||
|
||||
case "${1:-}" in
|
||||
lint) do_lint ;;
|
||||
test) do_test ;;
|
||||
build) do_build ;;
|
||||
release) do_release ;;
|
||||
apple) do_apple && summary ;;
|
||||
android) do_android && summary ;;
|
||||
linux) do_linux && summary ;;
|
||||
windows) do_windows && summary ;;
|
||||
all) do_all && summary ;;
|
||||
"") do_full && summary ;;
|
||||
*)
|
||||
echo "Usage: $0 {lint|test|build|release|apple|android|linux|windows|all}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -110,11 +110,7 @@ pub trait MacProvider {
|
|||
/// Cryptographic hash operations.
|
||||
pub trait HashProvider {
|
||||
/// Compute a digest of `data` using the specified algorithm.
|
||||
fn hash(
|
||||
&self,
|
||||
algo: HashAlgorithm,
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError>;
|
||||
fn hash(&self, algo: HashAlgorithm, data: &[u8]) -> Result<Vec<u8>, CryptoError>;
|
||||
}
|
||||
|
||||
/// Key encapsulation mechanism operations.
|
||||
|
|
@ -124,10 +120,7 @@ pub trait HashProvider {
|
|||
/// functions separately and looks up a `KemProvider` from the registry.
|
||||
pub trait KemProvider {
|
||||
/// Generate a new KEM key pair.
|
||||
fn generate_keypair(
|
||||
&self,
|
||||
algo: KemAlgorithm,
|
||||
) -> Result<KemKeyPair, CryptoError>;
|
||||
fn generate_keypair(&self, algo: KemAlgorithm) -> Result<KemKeyPair, CryptoError>;
|
||||
|
||||
/// Encapsulate a shared secret for the holder of `public_key`.
|
||||
///
|
||||
|
|
@ -170,7 +163,9 @@ pub trait KemProvider {
|
|||
/// `ProviderRegistry::global().register(name, Box::new(provider))`.
|
||||
/// 5. Call `ccc_crypto_<name>::init()` from `ccc-flutter-bridge/src/lib.rs`.
|
||||
/// 6. Add NIST test vectors for every algorithm you expose.
|
||||
pub trait CryptoProvider: AeadProvider + KdfProvider + MacProvider + HashProvider + Send + Sync {
|
||||
pub trait CryptoProvider:
|
||||
AeadProvider + KdfProvider + MacProvider + HashProvider + Send + Sync
|
||||
{
|
||||
/// The provider's canonical name (e.g. `"wolfssl"`).
|
||||
///
|
||||
/// Must be lowercase, no spaces, match the `CccCryptoProvider` enum value
|
||||
|
|
|
|||
|
|
@ -94,144 +94,6 @@ impl ProviderRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Tests
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! Registry unit tests use a stub provider to avoid any native dependency.
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, MacAlgorithm},
|
||||
capabilities::ProviderCapabilities,
|
||||
error::CryptoError,
|
||||
provider::{AeadProvider, CryptoProvider, HashProvider, KdfProvider, MacProvider},
|
||||
types::{AlgoBenchResult, AlgoTestResult, BenchmarkReport, SelfTestReport},
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
// ── Stub provider ──────────────────────────────────────────────────────────
|
||||
|
||||
struct StubProvider;
|
||||
|
||||
impl AeadProvider for StubProvider {
|
||||
fn encrypt_aead(
|
||||
&self, _a: AeadAlgorithm, _k: &[u8], _n: &[u8], pt: &[u8], _aad: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
Ok(pt.to_vec())
|
||||
}
|
||||
fn decrypt_aead(
|
||||
&self, _a: AeadAlgorithm, _k: &[u8], _n: &[u8], ct: &[u8], _aad: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
Ok(ct.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl KdfProvider for StubProvider {
|
||||
fn derive_key(
|
||||
&self, _a: KdfAlgorithm, _ikm: &[u8], _salt: &[u8], _info: &[u8], length: usize,
|
||||
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
|
||||
Ok(Zeroizing::new(vec![0u8; length]))
|
||||
}
|
||||
}
|
||||
|
||||
impl MacProvider for StubProvider {
|
||||
fn compute_mac(
|
||||
&self, _a: MacAlgorithm, _k: &[u8], _data: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
Ok(vec![0u8; 32])
|
||||
}
|
||||
fn verify_mac(
|
||||
&self, _a: MacAlgorithm, _k: &[u8], _data: &[u8], _mac: &[u8],
|
||||
) -> Result<bool, CryptoError> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl HashProvider for StubProvider {
|
||||
fn hash(
|
||||
&self, _a: HashAlgorithm, data: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
Ok(data.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl CryptoProvider for StubProvider {
|
||||
fn provider_name(&self) -> &'static str { "stub" }
|
||||
fn capabilities(&self) -> ProviderCapabilities {
|
||||
ProviderCapabilities::empty("stub")
|
||||
}
|
||||
fn self_test(&self) -> SelfTestReport {
|
||||
SelfTestReport::finalise("stub", vec![AlgoTestResult {
|
||||
algo_id: 12, algo_name: "stub".into(), passed: true, error_message: None,
|
||||
}])
|
||||
}
|
||||
fn benchmark(&self) -> BenchmarkReport {
|
||||
BenchmarkReport {
|
||||
provider_name: "stub".into(),
|
||||
results: vec![AlgoBenchResult {
|
||||
algo_id: 12, algo_name: "stub".into(),
|
||||
throughput_mbps: 999.0, efficiency_score: 100,
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn fresh_registry() -> ProviderRegistry {
|
||||
ProviderRegistry { inner: Mutex::new(HashMap::new()) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_and_get() {
|
||||
let reg = fresh_registry();
|
||||
assert!(reg.get("stub").is_none());
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
assert!(reg.get("stub").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_providers() {
|
||||
let reg = fresh_registry();
|
||||
assert!(reg.list().is_empty());
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
let names = reg.list();
|
||||
assert_eq!(names.len(), 1);
|
||||
assert_eq!(names[0], "stub");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_and_unregister() {
|
||||
let reg = fresh_registry();
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
assert!(reg.contains("stub"));
|
||||
reg.unregister("stub");
|
||||
assert!(!reg.contains("stub"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_replaces_existing() {
|
||||
let reg = fresh_registry();
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
reg.register("stub", Box::new(StubProvider)); // replace
|
||||
assert_eq!(reg.list().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stub_provider_roundtrip() {
|
||||
let reg = fresh_registry();
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
let provider = reg.get("stub").unwrap();
|
||||
let plaintext = b"hello world";
|
||||
let ct = provider
|
||||
.encrypt_aead(AeadAlgorithm::AesGcm256, &[0u8; 32], &[0u8; 12], plaintext, b"")
|
||||
.unwrap();
|
||||
let pt = provider
|
||||
.decrypt_aead(AeadAlgorithm::AesGcm256, &[0u8; 32], &[0u8; 12], &ct, b"")
|
||||
.unwrap();
|
||||
assert_eq!(pt, plaintext);
|
||||
}
|
||||
}
|
||||
#[path = "registry_tests.rs"]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
//! Registry unit tests — use a stub provider to avoid any native dependency.
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, MacAlgorithm},
|
||||
capabilities::ProviderCapabilities,
|
||||
error::CryptoError,
|
||||
provider::{AeadProvider, CryptoProvider, HashProvider, KdfProvider, MacProvider},
|
||||
types::{AlgoBenchResult, AlgoTestResult, BenchmarkReport, SelfTestReport},
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
// ── Stub provider ──────────────────────────────────────────────────────────
|
||||
|
||||
struct StubProvider;
|
||||
|
||||
impl AeadProvider for StubProvider {
|
||||
fn encrypt_aead(
|
||||
&self,
|
||||
_a: AeadAlgorithm,
|
||||
_k: &[u8],
|
||||
_n: &[u8],
|
||||
pt: &[u8],
|
||||
_aad: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
Ok(pt.to_vec())
|
||||
}
|
||||
fn decrypt_aead(
|
||||
&self,
|
||||
_a: AeadAlgorithm,
|
||||
_k: &[u8],
|
||||
_n: &[u8],
|
||||
ct: &[u8],
|
||||
_aad: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
Ok(ct.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl KdfProvider for StubProvider {
|
||||
fn derive_key(
|
||||
&self,
|
||||
_a: KdfAlgorithm,
|
||||
_ikm: &[u8],
|
||||
_salt: &[u8],
|
||||
_info: &[u8],
|
||||
length: usize,
|
||||
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
|
||||
Ok(Zeroizing::new(vec![0u8; length]))
|
||||
}
|
||||
}
|
||||
|
||||
impl MacProvider for StubProvider {
|
||||
fn compute_mac(
|
||||
&self,
|
||||
_a: MacAlgorithm,
|
||||
_k: &[u8],
|
||||
_data: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
Ok(vec![0u8; 32])
|
||||
}
|
||||
fn verify_mac(
|
||||
&self,
|
||||
_a: MacAlgorithm,
|
||||
_k: &[u8],
|
||||
_data: &[u8],
|
||||
_mac: &[u8],
|
||||
) -> Result<bool, CryptoError> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl HashProvider for StubProvider {
|
||||
fn hash(&self, _a: HashAlgorithm, data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
Ok(data.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl CryptoProvider for StubProvider {
|
||||
fn provider_name(&self) -> &'static str {
|
||||
"stub"
|
||||
}
|
||||
fn capabilities(&self) -> ProviderCapabilities {
|
||||
ProviderCapabilities::empty("stub")
|
||||
}
|
||||
fn self_test(&self) -> SelfTestReport {
|
||||
SelfTestReport::finalise(
|
||||
"stub",
|
||||
vec![AlgoTestResult {
|
||||
algo_id: 12,
|
||||
algo_name: "stub".into(),
|
||||
passed: true,
|
||||
error_message: None,
|
||||
}],
|
||||
)
|
||||
}
|
||||
fn benchmark(&self) -> BenchmarkReport {
|
||||
BenchmarkReport {
|
||||
provider_name: "stub".into(),
|
||||
results: vec![AlgoBenchResult {
|
||||
algo_id: 12,
|
||||
algo_name: "stub".into(),
|
||||
throughput_mbps: 999.0,
|
||||
efficiency_score: 100,
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn fresh_registry() -> ProviderRegistry {
|
||||
ProviderRegistry {
|
||||
inner: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_and_get() {
|
||||
let reg = fresh_registry();
|
||||
assert!(reg.get("stub").is_none());
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
assert!(reg.get("stub").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_providers() {
|
||||
let reg = fresh_registry();
|
||||
assert!(reg.list().is_empty());
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
let names = reg.list();
|
||||
assert_eq!(names.len(), 1);
|
||||
assert_eq!(names[0], "stub");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_and_unregister() {
|
||||
let reg = fresh_registry();
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
assert!(reg.contains("stub"));
|
||||
reg.unregister("stub");
|
||||
assert!(!reg.contains("stub"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_replaces_existing() {
|
||||
let reg = fresh_registry();
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
reg.register("stub", Box::new(StubProvider)); // replace
|
||||
assert_eq!(reg.list().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stub_provider_roundtrip() {
|
||||
let reg = fresh_registry();
|
||||
reg.register("stub", Box::new(StubProvider));
|
||||
let provider = reg.get("stub").unwrap();
|
||||
let plaintext = b"hello world";
|
||||
let ct = provider
|
||||
.encrypt_aead(
|
||||
AeadAlgorithm::AesGcm256,
|
||||
&[0u8; 32],
|
||||
&[0u8; 12],
|
||||
plaintext,
|
||||
b"",
|
||||
)
|
||||
.unwrap();
|
||||
let pt = provider
|
||||
.decrypt_aead(AeadAlgorithm::AesGcm256, &[0u8; 32], &[0u8; 12], &ct, b"")
|
||||
.unwrap();
|
||||
assert_eq!(pt, plaintext);
|
||||
}
|
||||
|
|
@ -14,7 +14,10 @@
|
|||
//! a pre-installed wolfSSL at this path. The path must contain
|
||||
//! `include/wolfssl/` and `lib/libwolfssl.a`.
|
||||
|
||||
use std::{env, path::PathBuf};
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
// When the `stub_ffi` feature is enabled (e.g. for type-checking without
|
||||
|
|
@ -34,11 +37,13 @@ fn main() {
|
|||
|
||||
// ── 1. Locate wolfSSL ────────────────────────────────────────────────────
|
||||
|
||||
let (include_dir, lib_dir) =
|
||||
if let Ok(install_dir) = env::var("WOLFSSL_INSTALL_DIR") {
|
||||
let (include_dir, lib_dir) = if let Ok(install_dir) = env::var("WOLFSSL_INSTALL_DIR") {
|
||||
// Use a pre-installed wolfSSL — skip the CMake build.
|
||||
let p = PathBuf::from(&install_dir);
|
||||
println!("cargo:warning=Using pre-installed wolfSSL at {}", install_dir);
|
||||
println!(
|
||||
"cargo:warning=Using pre-installed wolfSSL at {}",
|
||||
install_dir
|
||||
);
|
||||
(p.join("include"), p.join("lib"))
|
||||
} else {
|
||||
// Build from source (default — uses our git submodule).
|
||||
|
|
@ -86,7 +91,7 @@ fn main() {
|
|||
}
|
||||
|
||||
/// Build wolfSSL from source using CMake. Returns `(include_dir, lib_dir)`.
|
||||
fn build_wolfssl_cmake(source_dir: &PathBuf, _out_dir: &PathBuf) -> (PathBuf, PathBuf) {
|
||||
fn build_wolfssl_cmake(source_dir: &Path, _out_dir: &Path) -> (PathBuf, PathBuf) {
|
||||
println!(
|
||||
"cargo:warning=Building wolfSSL from source at {}",
|
||||
source_dir.display()
|
||||
|
|
@ -166,8 +171,34 @@ fn build_wolfssl_cmake(source_dir: &PathBuf, _out_dir: &PathBuf) -> (PathBuf, Pa
|
|||
(include_dir, lib_dir)
|
||||
}
|
||||
|
||||
/// Parse the CMake-generated `options.h` to extract all `#define` directives.
|
||||
/// Returns a list of `-DNAME` clang args.
|
||||
fn parse_options_defines(include_dir: &Path) -> Vec<String> {
|
||||
let options_h = include_dir.join("wolfssl/options.h");
|
||||
if !options_h.exists() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&options_h).expect("failed to read options.h");
|
||||
let mut defines = Vec::new();
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
if !line.starts_with("#define ") {
|
||||
continue;
|
||||
}
|
||||
let rest = &line["#define ".len()..];
|
||||
let name = rest.split_whitespace().next().unwrap_or("");
|
||||
// Skip the header guard macro.
|
||||
if name.is_empty() || name == "WOLFSSL_OPTIONS_H" {
|
||||
continue;
|
||||
}
|
||||
defines.push(format!("-D{}", name));
|
||||
}
|
||||
defines
|
||||
}
|
||||
|
||||
/// Run bindgen over the wolfCrypt headers we actually use.
|
||||
fn generate_bindings(include_dir: &PathBuf, out_dir: &PathBuf) {
|
||||
fn generate_bindings(include_dir: &Path, out_dir: &Path) {
|
||||
let header = include_dir.join("wolfssl/wolfcrypt/aes.h");
|
||||
|
||||
if !header.exists() {
|
||||
|
|
@ -182,7 +213,17 @@ fn generate_bindings(include_dir: &PathBuf, out_dir: &PathBuf) {
|
|||
return;
|
||||
}
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
// Parse the CMake-generated options.h so that bindgen's clang sees the
|
||||
// exact same preprocessor flags as the compiled wolfSSL library. This is
|
||||
// essential for correct struct layout — e.g. GCM_TABLE_4BIT adds a 512-byte
|
||||
// M0 table to the Gcm struct that changes the size of Aes.
|
||||
let options_defines = parse_options_defines(include_dir);
|
||||
|
||||
// For cross-compilation, tell bindgen's clang the target triple so that
|
||||
// platform-specific types (pointer size, alignment, builtins) are correct.
|
||||
let target = env::var("TARGET").unwrap_or_default();
|
||||
|
||||
let mut builder = bindgen::Builder::default()
|
||||
// Include all wolfCrypt headers we need.
|
||||
.header(include_dir.join("wolfssl/wolfcrypt/aes.h").to_str().unwrap())
|
||||
.header(include_dir.join("wolfssl/wolfcrypt/chacha20_poly1305.h").to_str().unwrap())
|
||||
|
|
@ -196,31 +237,33 @@ fn generate_bindings(include_dir: &PathBuf, out_dir: &PathBuf) {
|
|||
.header(include_dir.join("wolfssl/wolfcrypt/curve25519.h").to_str().unwrap())
|
||||
.header(include_dir.join("wolfssl/wolfcrypt/curve448.h").to_str().unwrap())
|
||||
.header(include_dir.join("wolfssl/wolfcrypt/random.h").to_str().unwrap())
|
||||
.clang_arg(format!("-I{}", include_dir.display()))
|
||||
// Pass all required preprocessor defines explicitly so that bindgen's
|
||||
// libclang sees the same feature flags as the C build, regardless of
|
||||
// whether wolfSSL's options.h is correctly resolved by clang.
|
||||
//
|
||||
// AES-GCM (wc_AesGcmSetKey etc. are behind #ifdef HAVE_AESGCM)
|
||||
.clang_arg("-DHAVE_AESGCM")
|
||||
// ChaCha20 / Poly1305 / XChaCha20
|
||||
.clang_arg("-DHAVE_CHACHA")
|
||||
.clang_arg("-DHAVE_POLY1305")
|
||||
.clang_arg(format!("-I{}", include_dir.display()));
|
||||
|
||||
// Pass all defines from the generated options.h. This includes critical
|
||||
// struct-affecting flags like GCM_TABLE_4BIT, HAVE___UINT128_T,
|
||||
// WOLFSSL_USE_ALIGN, etc. that change the layout of Gcm, Aes, and other types.
|
||||
for def in &options_defines {
|
||||
builder = builder.clang_arg(def);
|
||||
}
|
||||
|
||||
// Pass additional defines that are added via .cflag() in the CMake build
|
||||
// (not cmake options, so they don't appear in options.h).
|
||||
builder = builder
|
||||
.clang_arg("-DHAVE_XCHACHA")
|
||||
// BLAKE2 is not a cmake option — enable via clang preprocessor flags
|
||||
// so that the headers expose the BLAKE2b/BLAKE2s function prototypes.
|
||||
.clang_arg("-DHAVE_BLAKE2")
|
||||
.clang_arg("-DHAVE_BLAKE2B")
|
||||
// Required so sha512.h exposes SHA384 typedef + functions.
|
||||
.clang_arg("-DWOLFSSL_SHA384")
|
||||
.clang_arg("-DWOLFSSL_SHA512")
|
||||
// SHA3 functions (wc_InitSha3_256, etc.)
|
||||
.clang_arg("-DWOLFSSL_SHA3")
|
||||
// HKDF via TLS-1.3 primitives (wc_Tls13_HKDF_Extract/Expand_Label)
|
||||
.clang_arg("-DHAVE_HKDF")
|
||||
// Curve25519 / Curve448 need HAVE_CURVE25519 / HAVE_CURVE448
|
||||
.clang_arg("-DHAVE_CURVE25519")
|
||||
.clang_arg("-DHAVE_CURVE448")
|
||||
.clang_arg("-DHAVE_BLAKE2B");
|
||||
|
||||
// Set the cross-compilation target if it differs from the host.
|
||||
if !target.is_empty() {
|
||||
// Map Rust target triples to clang-compatible triples.
|
||||
let clang_target = match target.as_str() {
|
||||
"aarch64-apple-ios-sim" => "arm64-apple-ios-simulator".to_string(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
builder = builder.clang_arg(format!("--target={}", clang_target));
|
||||
}
|
||||
|
||||
let bindings = builder
|
||||
// Only generate bindings for the types/functions we use.
|
||||
.allowlist_function("wc_Aes.*")
|
||||
.allowlist_function("wc_AesGcm.*")
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ pub fn encrypt(
|
|||
AeadAlgorithm::AesGcm256 => aes_gcm_256_encrypt(key, nonce, plaintext, aad),
|
||||
AeadAlgorithm::ChaCha20Poly1305 => chacha20_poly1305_encrypt(key, nonce, plaintext, aad),
|
||||
AeadAlgorithm::XChaCha20Poly1305 => xchacha20_poly1305_encrypt(key, nonce, plaintext, aad),
|
||||
AeadAlgorithm::Ascon128a =>
|
||||
Err(CryptoError::FeatureNotCompiled("Ascon-128a".into())),
|
||||
AeadAlgorithm::Ascon128a => Err(CryptoError::FeatureNotCompiled("Ascon-128a".into())),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,10 +47,13 @@ pub fn decrypt(
|
|||
validate_key_nonce(algo, key, nonce)?;
|
||||
match algo {
|
||||
AeadAlgorithm::AesGcm256 => aes_gcm_256_decrypt(key, nonce, ciphertext_and_tag, aad),
|
||||
AeadAlgorithm::ChaCha20Poly1305 => chacha20_poly1305_decrypt(key, nonce, ciphertext_and_tag, aad),
|
||||
AeadAlgorithm::XChaCha20Poly1305 => xchacha20_poly1305_decrypt(key, nonce, ciphertext_and_tag, aad),
|
||||
AeadAlgorithm::Ascon128a =>
|
||||
Err(CryptoError::FeatureNotCompiled("Ascon-128a".into())),
|
||||
AeadAlgorithm::ChaCha20Poly1305 => {
|
||||
chacha20_poly1305_decrypt(key, nonce, ciphertext_and_tag, aad)
|
||||
}
|
||||
AeadAlgorithm::XChaCha20Poly1305 => {
|
||||
xchacha20_poly1305_decrypt(key, nonce, ciphertext_and_tag, aad)
|
||||
}
|
||||
AeadAlgorithm::Ascon128a => Err(CryptoError::FeatureNotCompiled("Ascon-128a".into())),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,13 +67,17 @@ fn validate_key_nonce(algo: AeadAlgorithm, key: &[u8], nonce: &[u8]) -> Result<(
|
|||
if key.len() != expected_key {
|
||||
return Err(CryptoError::InvalidKey(format!(
|
||||
"{}: expected {}-byte key, got {}",
|
||||
algo.name(), expected_key, key.len()
|
||||
algo.name(),
|
||||
expected_key,
|
||||
key.len()
|
||||
)));
|
||||
}
|
||||
if nonce.len() != expected_nonce {
|
||||
return Err(CryptoError::InvalidNonce(format!(
|
||||
"{}: expected {}-byte nonce, got {}",
|
||||
algo.name(), expected_nonce, nonce.len()
|
||||
algo.name(),
|
||||
expected_nonce,
|
||||
nonce.len()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -97,11 +103,7 @@ fn aes_gcm_256_encrypt(
|
|||
// alignment so the C code's SIMD operations work correctly on ARM64.
|
||||
use std::alloc::{alloc_zeroed, dealloc, Layout};
|
||||
|
||||
// sizeof(Aes) = 288 bytes; alignment = 16.
|
||||
let layout = Layout::from_size_align(
|
||||
std::mem::size_of::<crate::sys::Aes>().max(288),
|
||||
16,
|
||||
)
|
||||
let layout = Layout::from_size_align(std::mem::size_of::<crate::sys::Aes>(), 16)
|
||||
.expect("layout must be valid");
|
||||
|
||||
let aes_ptr = alloc_zeroed(layout) as *mut crate::sys::Aes;
|
||||
|
|
@ -116,18 +118,20 @@ fn aes_gcm_256_encrypt(
|
|||
);
|
||||
if ret != 0 {
|
||||
dealloc(aes_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InternalError(format!("wc_AesInit returned {}", ret)));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_AesInit returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_AesGcmSetKey(
|
||||
aes_ptr,
|
||||
key.as_ptr(),
|
||||
key.len() as u32,
|
||||
);
|
||||
let ret = crate::sys::wc_AesGcmSetKey(aes_ptr, key.as_ptr(), key.len() as u32);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_AesFree(aes_ptr);
|
||||
dealloc(aes_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InvalidKey(format!("wc_AesGcmSetKey returned {}", ret)));
|
||||
return Err(CryptoError::InvalidKey(format!(
|
||||
"wc_AesGcmSetKey returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_AesGcmEncrypt(
|
||||
|
|
@ -139,13 +143,20 @@ fn aes_gcm_256_encrypt(
|
|||
nonce.len() as u32,
|
||||
tag_buf.as_mut_ptr(),
|
||||
TAG_LEN as u32,
|
||||
if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() },
|
||||
if aad.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
aad.as_ptr()
|
||||
},
|
||||
aad.len() as u32,
|
||||
);
|
||||
crate::sys::wc_AesFree(aes_ptr);
|
||||
dealloc(aes_ptr as *mut u8, layout);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_AesGcmEncrypt returned {}", ret)));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_AesGcmEncrypt returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +171,7 @@ fn aes_gcm_256_decrypt(
|
|||
) -> Result<Vec<u8>, CryptoError> {
|
||||
if ciphertext_and_tag.len() < TAG_LEN {
|
||||
return Err(CryptoError::InvalidInput(
|
||||
"AES-256-GCM: ciphertext too short (no room for tag)".into()
|
||||
"AES-256-GCM: ciphertext too short (no room for tag)".into(),
|
||||
));
|
||||
}
|
||||
let ct_len = ciphertext_and_tag.len() - TAG_LEN;
|
||||
|
|
@ -172,10 +183,7 @@ fn aes_gcm_256_decrypt(
|
|||
// alignment so the C code's SIMD operations work correctly on ARM64.
|
||||
use std::alloc::{alloc_zeroed, dealloc, Layout};
|
||||
|
||||
let layout = Layout::from_size_align(
|
||||
std::mem::size_of::<crate::sys::Aes>().max(288),
|
||||
16,
|
||||
)
|
||||
let layout = Layout::from_size_align(std::mem::size_of::<crate::sys::Aes>(), 16)
|
||||
.expect("layout must be valid");
|
||||
|
||||
let aes_ptr = alloc_zeroed(layout) as *mut crate::sys::Aes;
|
||||
|
|
@ -190,18 +198,20 @@ fn aes_gcm_256_decrypt(
|
|||
);
|
||||
if ret != 0 {
|
||||
dealloc(aes_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InternalError(format!("wc_AesInit returned {}", ret)));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_AesInit returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_AesGcmSetKey(
|
||||
aes_ptr,
|
||||
key.as_ptr(),
|
||||
key.len() as u32,
|
||||
);
|
||||
let ret = crate::sys::wc_AesGcmSetKey(aes_ptr, key.as_ptr(), key.len() as u32);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_AesFree(aes_ptr);
|
||||
dealloc(aes_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InvalidKey(format!("wc_AesGcmSetKey returned {}", ret)));
|
||||
return Err(CryptoError::InvalidKey(format!(
|
||||
"wc_AesGcmSetKey returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_AesGcmDecrypt(
|
||||
|
|
@ -213,7 +223,11 @@ fn aes_gcm_256_decrypt(
|
|||
nonce.len() as u32,
|
||||
tag.as_ptr(),
|
||||
TAG_LEN as u32,
|
||||
if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() },
|
||||
if aad.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
aad.as_ptr()
|
||||
},
|
||||
aad.len() as u32,
|
||||
);
|
||||
crate::sys::wc_AesFree(aes_ptr);
|
||||
|
|
@ -245,7 +259,11 @@ fn chacha20_poly1305_encrypt(
|
|||
let ret = crate::sys::wc_ChaCha20Poly1305_Encrypt(
|
||||
key.as_ptr(),
|
||||
nonce.as_ptr(),
|
||||
if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() },
|
||||
if aad.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
aad.as_ptr()
|
||||
},
|
||||
aad.len() as u32,
|
||||
plaintext.as_ptr(),
|
||||
plaintext.len() as u32,
|
||||
|
|
@ -253,9 +271,10 @@ fn chacha20_poly1305_encrypt(
|
|||
tag_buf.as_mut_ptr(),
|
||||
);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_ChaCha20Poly1305_Encrypt returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_ChaCha20Poly1305_Encrypt returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +289,7 @@ fn chacha20_poly1305_decrypt(
|
|||
) -> Result<Vec<u8>, CryptoError> {
|
||||
if ciphertext_and_tag.len() < TAG_LEN {
|
||||
return Err(CryptoError::InvalidInput(
|
||||
"ChaCha20-Poly1305: ciphertext too short".into()
|
||||
"ChaCha20-Poly1305: ciphertext too short".into(),
|
||||
));
|
||||
}
|
||||
let ct_len = ciphertext_and_tag.len() - TAG_LEN;
|
||||
|
|
@ -282,7 +301,11 @@ fn chacha20_poly1305_decrypt(
|
|||
let ret = crate::sys::wc_ChaCha20Poly1305_Decrypt(
|
||||
key.as_ptr(),
|
||||
nonce.as_ptr(),
|
||||
if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() },
|
||||
if aad.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
aad.as_ptr()
|
||||
},
|
||||
aad.len() as u32,
|
||||
ciphertext.as_ptr(),
|
||||
ct_len as u32,
|
||||
|
|
@ -325,7 +348,11 @@ fn xchacha20_poly1305_encrypt(
|
|||
dst.len(),
|
||||
plaintext.as_ptr(),
|
||||
plaintext.len(),
|
||||
if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() },
|
||||
if aad.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
aad.as_ptr()
|
||||
},
|
||||
aad.len(),
|
||||
nonce.as_ptr(),
|
||||
nonce.len(),
|
||||
|
|
@ -333,9 +360,10 @@ fn xchacha20_poly1305_encrypt(
|
|||
key.len(),
|
||||
);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_XChaCha20Poly1305_Encrypt returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_XChaCha20Poly1305_Encrypt returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +378,7 @@ fn xchacha20_poly1305_decrypt(
|
|||
) -> Result<Vec<u8>, CryptoError> {
|
||||
if ciphertext_and_tag.len() < TAG_LEN {
|
||||
return Err(CryptoError::InvalidInput(
|
||||
"XChaCha20-Poly1305: ciphertext too short".into()
|
||||
"XChaCha20-Poly1305: ciphertext too short".into(),
|
||||
));
|
||||
}
|
||||
let pt_len = ciphertext_and_tag.len() - TAG_LEN;
|
||||
|
|
@ -364,7 +392,11 @@ fn xchacha20_poly1305_decrypt(
|
|||
dst.len(),
|
||||
ciphertext_and_tag.as_ptr(),
|
||||
ciphertext_and_tag.len(),
|
||||
if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() },
|
||||
if aad.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
aad.as_ptr()
|
||||
},
|
||||
aad.len(),
|
||||
nonce.as_ptr(),
|
||||
nonce.len(),
|
||||
|
|
|
|||
|
|
@ -43,86 +43,130 @@ pub fn probe_capabilities(benchmarks: Option<&BenchmarkReport>) -> ProviderCapab
|
|||
.map(|r| r.efficiency_score)
|
||||
.unwrap_or(0);
|
||||
|
||||
caps = caps.with_aead(algo, AlgorithmCapability {
|
||||
caps = caps.with_aead(
|
||||
algo,
|
||||
AlgorithmCapability {
|
||||
available,
|
||||
deterministic_io: true, // wolfSSL output is deterministic for same key/nonce
|
||||
efficiency_score: efficiency,
|
||||
reliability_score: if available { 100 } else { 0 },
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Ascon-128a is not in this wolfSSL build.
|
||||
caps = caps.with_aead(AeadAlgorithm::Ascon128a, AlgorithmCapability::unavailable());
|
||||
|
||||
// ── KDF ──────────────────────────────────────────────────────────────────
|
||||
for algo in [KdfAlgorithm::Sha256, KdfAlgorithm::Sha384, KdfAlgorithm::Sha512] {
|
||||
for algo in [
|
||||
KdfAlgorithm::Sha256,
|
||||
KdfAlgorithm::Sha384,
|
||||
KdfAlgorithm::Sha512,
|
||||
] {
|
||||
let available = crate::kdf::derive_key(algo, b"key", b"salt", b"info", 32).is_ok();
|
||||
caps = caps.with_kdf(algo, AlgorithmCapability {
|
||||
caps = caps.with_kdf(
|
||||
algo,
|
||||
AlgorithmCapability {
|
||||
available,
|
||||
deterministic_io: true,
|
||||
efficiency_score: 80, // HKDF is fast; benchmark not run for KDF
|
||||
reliability_score: if available { 100 } else { 0 },
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
for algo in [KdfAlgorithm::Blake2b512, KdfAlgorithm::Argon2id] {
|
||||
let salt: &[u8] = if algo == KdfAlgorithm::Argon2id { b"saltsalt" } else { b"" };
|
||||
let salt: &[u8] = if algo == KdfAlgorithm::Argon2id {
|
||||
b"saltsalt"
|
||||
} else {
|
||||
b""
|
||||
};
|
||||
let available = crate::kdf::derive_key(algo, b"key", salt, b"", 32).is_ok();
|
||||
caps = caps.with_kdf(algo, AlgorithmCapability {
|
||||
caps = caps.with_kdf(
|
||||
algo,
|
||||
AlgorithmCapability {
|
||||
available,
|
||||
deterministic_io: true,
|
||||
efficiency_score: if algo == KdfAlgorithm::Argon2id { 10 } else { 70 },
|
||||
efficiency_score: if algo == KdfAlgorithm::Argon2id {
|
||||
10
|
||||
} else {
|
||||
70
|
||||
},
|
||||
reliability_score: if available { 100 } else { 0 },
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
caps = caps.with_kdf(KdfAlgorithm::Kmac256, AlgorithmCapability::unavailable());
|
||||
|
||||
// ── MAC ──────────────────────────────────────────────────────────────────
|
||||
for algo in [MacAlgorithm::HmacSha256, MacAlgorithm::HmacSha384, MacAlgorithm::HmacSha512] {
|
||||
for algo in [
|
||||
MacAlgorithm::HmacSha256,
|
||||
MacAlgorithm::HmacSha384,
|
||||
MacAlgorithm::HmacSha512,
|
||||
] {
|
||||
let available = crate::mac::compute_mac(algo, b"key", b"data").is_ok();
|
||||
caps = caps.with_mac(algo, AlgorithmCapability {
|
||||
caps = caps.with_mac(
|
||||
algo,
|
||||
AlgorithmCapability {
|
||||
available,
|
||||
deterministic_io: true,
|
||||
efficiency_score: 85,
|
||||
reliability_score: if available { 100 } else { 0 },
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
{
|
||||
let available = crate::mac::compute_mac(MacAlgorithm::Blake2bMac, b"key-that-is-16-by", b"data").is_ok();
|
||||
caps = caps.with_mac(MacAlgorithm::Blake2bMac, AlgorithmCapability {
|
||||
let available =
|
||||
crate::mac::compute_mac(MacAlgorithm::Blake2bMac, b"key-that-is-16-by", b"data")
|
||||
.is_ok();
|
||||
caps = caps.with_mac(
|
||||
MacAlgorithm::Blake2bMac,
|
||||
AlgorithmCapability {
|
||||
available,
|
||||
deterministic_io: true,
|
||||
efficiency_score: 90,
|
||||
reliability_score: if available { 100 } else { 0 },
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
caps = caps.with_mac(MacAlgorithm::Poly1305, AlgorithmCapability::unavailable());
|
||||
|
||||
// ── Hash ─────────────────────────────────────────────────────────────────
|
||||
for algo in [
|
||||
HashAlgorithm::Sha256, HashAlgorithm::Sha384, HashAlgorithm::Sha512,
|
||||
HashAlgorithm::Blake2b512, HashAlgorithm::Sha3_256, HashAlgorithm::Sha3_512,
|
||||
HashAlgorithm::Sha256,
|
||||
HashAlgorithm::Sha384,
|
||||
HashAlgorithm::Sha512,
|
||||
HashAlgorithm::Blake2b512,
|
||||
HashAlgorithm::Sha3_256,
|
||||
HashAlgorithm::Sha3_512,
|
||||
] {
|
||||
let available = crate::hash::hash(algo, b"probe").is_ok();
|
||||
caps = caps.with_hash(algo, AlgorithmCapability {
|
||||
caps = caps.with_hash(
|
||||
algo,
|
||||
AlgorithmCapability {
|
||||
available,
|
||||
deterministic_io: true,
|
||||
efficiency_score: 90,
|
||||
reliability_score: if available { 100 } else { 0 },
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ── KEM ──────────────────────────────────────────────────────────────────
|
||||
for algo in [KemAlgorithm::X25519, KemAlgorithm::X448] {
|
||||
let available = crate::kem::generate_keypair(algo).is_ok();
|
||||
caps = caps.with_kem(algo, AlgorithmCapability {
|
||||
caps = caps.with_kem(
|
||||
algo,
|
||||
AlgorithmCapability {
|
||||
available,
|
||||
deterministic_io: false, // key generation is randomised
|
||||
efficiency_score: 80,
|
||||
reliability_score: if available { 100 } else { 0 },
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
for algo in [
|
||||
KemAlgorithm::MlKem768, KemAlgorithm::MlKem1024,
|
||||
KemAlgorithm::MlKem768,
|
||||
KemAlgorithm::MlKem1024,
|
||||
KemAlgorithm::ClassicMcEliece460896,
|
||||
] {
|
||||
caps = caps.with_kem(algo, AlgorithmCapability::unavailable());
|
||||
|
|
|
|||
|
|
@ -42,10 +42,8 @@ fn sha256(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|||
use std::alloc::{alloc_zeroed, dealloc, Layout};
|
||||
|
||||
// Size reported by C's sizeof(wc_Sha256) = 120 bytes; alignment = 16.
|
||||
let layout = Layout::from_size_align(
|
||||
std::mem::size_of::<crate::sys::wc_Sha256>().max(120),
|
||||
16,
|
||||
)
|
||||
let layout =
|
||||
Layout::from_size_align(std::mem::size_of::<crate::sys::wc_Sha256>().max(120), 16)
|
||||
.expect("layout must be valid");
|
||||
|
||||
let sha_ptr = alloc_zeroed(layout) as *mut crate::sys::wc_Sha256;
|
||||
|
|
@ -56,23 +54,33 @@ fn sha256(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|||
let ret = crate::sys::wc_InitSha256(sha_ptr);
|
||||
if ret != 0 {
|
||||
dealloc(sha_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InternalError(format!("wc_InitSha256 returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_InitSha256 returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha256Update(
|
||||
sha_ptr,
|
||||
if data.is_empty() { std::ptr::null() } else { data.as_ptr() },
|
||||
if data.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
data.as_ptr()
|
||||
},
|
||||
data.len() as u32,
|
||||
);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_Sha256Free(sha_ptr);
|
||||
dealloc(sha_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha256Update returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha256Update returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha256Final(sha_ptr, out.as_mut_ptr());
|
||||
crate::sys::wc_Sha256Free(sha_ptr);
|
||||
dealloc(sha_ptr as *mut u8, layout);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha256Final returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha256Final returned {ret}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
|
@ -85,17 +93,23 @@ fn sha384(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|||
let mut sha: crate::sys::wc_Sha512 = std::mem::zeroed();
|
||||
let ret = crate::sys::wc_InitSha384(&mut sha);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_InitSha384 returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_InitSha384 returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha384Update(&mut sha, data.as_ptr(), data.len() as u32);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_Sha384Free(&mut sha);
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha384Update returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha384Update returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha384Final(&mut sha, out.as_mut_ptr());
|
||||
crate::sys::wc_Sha384Free(&mut sha);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha384Final returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha384Final returned {ret}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
|
@ -107,17 +121,23 @@ fn sha512(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|||
let mut sha: crate::sys::wc_Sha512 = std::mem::zeroed();
|
||||
let ret = crate::sys::wc_InitSha512(&mut sha);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_InitSha512 returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_InitSha512 returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha512Update(&mut sha, data.as_ptr(), data.len() as u32);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_Sha512Free(&mut sha);
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha512Update returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha512Update returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha512Final(&mut sha, out.as_mut_ptr());
|
||||
crate::sys::wc_Sha512Free(&mut sha);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha512Final returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha512Final returned {ret}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
|
@ -135,17 +155,23 @@ fn sha3_256(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|||
let mut sha: crate::sys::wc_Sha3 = std::mem::zeroed();
|
||||
let ret = crate::sys::wc_InitSha3_256(&mut sha, std::ptr::null_mut(), -2);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_InitSha3_256 returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_InitSha3_256 returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha3_256_Update(&mut sha, data.as_ptr(), data.len() as u32);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_Sha3_256_Free(&mut sha);
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha3_256_Update returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha3_256_Update returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha3_256_Final(&mut sha, out.as_mut_ptr());
|
||||
crate::sys::wc_Sha3_256_Free(&mut sha);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha3_256_Final returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha3_256_Final returned {ret}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
|
@ -157,17 +183,23 @@ fn sha3_512(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|||
let mut sha: crate::sys::wc_Sha3 = std::mem::zeroed();
|
||||
let ret = crate::sys::wc_InitSha3_512(&mut sha, std::ptr::null_mut(), -2);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_InitSha3_512 returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_InitSha3_512 returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha3_512_Update(&mut sha, data.as_ptr(), data.len() as u32);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_Sha3_512_Free(&mut sha);
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha3_512_Update returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha3_512_Update returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Sha3_512_Final(&mut sha, out.as_mut_ptr());
|
||||
crate::sys::wc_Sha3_512_Free(&mut sha);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_Sha3_512_Final returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Sha3_512_Final returned {ret}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
|
@ -187,16 +219,22 @@ pub fn blake2b_512(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|||
let mut b2b: crate::sys::Blake2b = std::mem::zeroed();
|
||||
let ret = crate::sys::wc_InitBlake2b(&mut b2b, OUT_LEN);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_InitBlake2b returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_InitBlake2b returned {ret}"
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_Blake2bUpdate(&mut b2b, data.as_ptr(), data.len() as u32);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_Blake2bUpdate returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Blake2bUpdate returned {ret}"
|
||||
)));
|
||||
}
|
||||
// requestSz = OUT_LEN → produce the full 64-byte digest.
|
||||
let ret = crate::sys::wc_Blake2bFinal(&mut b2b, out.as_mut_ptr(), OUT_LEN);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_Blake2bFinal returned {ret}")));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Blake2bFinal returned {ret}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
|
|
|||
|
|
@ -54,8 +54,7 @@ pub fn derive_key(
|
|||
KdfAlgorithm::Sha512 => hkdf(WC_HASH_TYPE_SHA512, ikm, salt, info, length),
|
||||
KdfAlgorithm::Blake2b512 => blake2b_kdf(ikm, salt, info, length),
|
||||
KdfAlgorithm::Argon2id => argon2id(ikm, salt, length),
|
||||
KdfAlgorithm::Kmac256 =>
|
||||
Err(CryptoError::FeatureNotCompiled("KMAC256 (Phase 5+)".into())),
|
||||
KdfAlgorithm::Kmac256 => Err(CryptoError::FeatureNotCompiled("KMAC256 (Phase 5+)".into())),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,16 +102,20 @@ fn hkdf_extract(
|
|||
unsafe {
|
||||
let ret = crate::sys::wc_Tls13_HKDF_Extract(
|
||||
prk.as_mut_ptr(),
|
||||
if salt.is_empty() { std::ptr::null() } else { salt.as_ptr() },
|
||||
if salt.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
salt.as_ptr()
|
||||
},
|
||||
salt.len() as u32,
|
||||
ikm_buf.as_mut_ptr(),
|
||||
ikm_buf.len() as u32,
|
||||
hash_type,
|
||||
);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_Tls13_HKDF_Extract returned {ret}")
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Tls13_HKDF_Extract returned {ret}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(prk)
|
||||
|
|
@ -133,9 +136,9 @@ fn hkdf_expand(
|
|||
// Maximum HKDF output is 255 * HashLen (RFC 5869 §2.3).
|
||||
let max_len = 255 * hash_len;
|
||||
if length > max_len {
|
||||
return Err(CryptoError::InvalidInput(
|
||||
format!("HKDF: requested length {length} exceeds maximum {max_len}")
|
||||
));
|
||||
return Err(CryptoError::InvalidInput(format!(
|
||||
"HKDF: requested length {length} exceeds maximum {max_len}"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut okm = Zeroizing::new(vec![0u8; length]);
|
||||
|
|
@ -156,9 +159,9 @@ fn hkdf_expand(
|
|||
let copy_len = (length - pos).min(hash_len);
|
||||
okm[pos..pos + copy_len].copy_from_slice(&t[..copy_len]);
|
||||
pos += copy_len;
|
||||
counter = counter.checked_add(1).ok_or_else(|| {
|
||||
CryptoError::InternalError("HKDF counter overflow".into())
|
||||
})?;
|
||||
counter = counter
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| CryptoError::InternalError("HKDF counter overflow".into()))?;
|
||||
}
|
||||
|
||||
Ok(okm)
|
||||
|
|
@ -194,7 +197,7 @@ fn blake2b_kdf(
|
|||
// callers should use HKDF-SHA512 instead.
|
||||
if length > digest.len() {
|
||||
return Err(CryptoError::InvalidInput(
|
||||
"BLAKE2b-512 KDF: requested length exceeds 64 bytes; use HKDF-SHA512".into()
|
||||
"BLAKE2b-512 KDF: requested length exceeds 64 bytes; use HKDF-SHA512".into(),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +224,7 @@ fn argon2id(
|
|||
// Argon2 requires at least 8 bytes of salt.
|
||||
if salt.len() < 8 {
|
||||
return Err(CryptoError::InvalidInput(
|
||||
"Argon2id requires at least 8-byte salt".into()
|
||||
"Argon2id requires at least 8-byte salt".into(),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,13 +21,11 @@ pub fn generate_keypair(algo: KemAlgorithm) -> Result<KemKeyPair, CryptoError> {
|
|||
match algo {
|
||||
KemAlgorithm::X25519 => x25519_generate(),
|
||||
KemAlgorithm::X448 => x448_generate(),
|
||||
KemAlgorithm::MlKem768 | KemAlgorithm::MlKem1024 =>
|
||||
Err(CryptoError::FeatureNotCompiled(
|
||||
"ML-KEM is deferred to Phase 5 (requires wolfSSL PQ build)".into()
|
||||
KemAlgorithm::MlKem768 | KemAlgorithm::MlKem1024 => Err(CryptoError::FeatureNotCompiled(
|
||||
"ML-KEM is deferred to Phase 5 (requires wolfSSL PQ build)".into(),
|
||||
)),
|
||||
KemAlgorithm::ClassicMcEliece460896 =>
|
||||
Err(CryptoError::FeatureNotCompiled(
|
||||
"Classic-McEliece is deferred to Phase 5".into()
|
||||
KemAlgorithm::ClassicMcEliece460896 => Err(CryptoError::FeatureNotCompiled(
|
||||
"Classic-McEliece is deferred to Phase 5".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -45,13 +43,11 @@ pub fn encapsulate(
|
|||
match algo {
|
||||
KemAlgorithm::X25519 => x25519_encapsulate(recipient_public_key),
|
||||
KemAlgorithm::X448 => x448_encapsulate(recipient_public_key),
|
||||
KemAlgorithm::MlKem768 | KemAlgorithm::MlKem1024 =>
|
||||
Err(CryptoError::FeatureNotCompiled(
|
||||
"ML-KEM is deferred to Phase 5".into()
|
||||
KemAlgorithm::MlKem768 | KemAlgorithm::MlKem1024 => Err(CryptoError::FeatureNotCompiled(
|
||||
"ML-KEM is deferred to Phase 5".into(),
|
||||
)),
|
||||
KemAlgorithm::ClassicMcEliece460896 =>
|
||||
Err(CryptoError::FeatureNotCompiled(
|
||||
"Classic-McEliece is deferred to Phase 5".into()
|
||||
KemAlgorithm::ClassicMcEliece460896 => Err(CryptoError::FeatureNotCompiled(
|
||||
"Classic-McEliece is deferred to Phase 5".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -66,13 +62,11 @@ pub fn decapsulate(
|
|||
match algo {
|
||||
KemAlgorithm::X25519 => x25519_decapsulate(private_key, peer_ephemeral_public_key),
|
||||
KemAlgorithm::X448 => x448_decapsulate(private_key, peer_ephemeral_public_key),
|
||||
KemAlgorithm::MlKem768 | KemAlgorithm::MlKem1024 =>
|
||||
Err(CryptoError::FeatureNotCompiled(
|
||||
"ML-KEM is deferred to Phase 5".into()
|
||||
KemAlgorithm::MlKem768 | KemAlgorithm::MlKem1024 => Err(CryptoError::FeatureNotCompiled(
|
||||
"ML-KEM is deferred to Phase 5".into(),
|
||||
)),
|
||||
KemAlgorithm::ClassicMcEliece460896 =>
|
||||
Err(CryptoError::FeatureNotCompiled(
|
||||
"Classic-McEliece is deferred to Phase 5".into()
|
||||
KemAlgorithm::ClassicMcEliece460896 => Err(CryptoError::FeatureNotCompiled(
|
||||
"Classic-McEliece is deferred to Phase 5".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -96,15 +90,19 @@ fn x25519_generate() -> Result<KemKeyPair, CryptoError> {
|
|||
|
||||
let ret = crate::sys::wc_InitRng(&mut rng);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_InitRng returned {}", ret)));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_InitRng returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_curve25519_make_key(&mut rng, 32, &mut key);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_FreeRng(&mut rng);
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve25519_make_key returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve25519_make_key returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let mut pub_sz = public_key.len() as u32;
|
||||
|
|
@ -114,23 +112,29 @@ fn x25519_generate() -> Result<KemKeyPair, CryptoError> {
|
|||
// material is in RFC 7748 canonical format.
|
||||
let ret = crate::sys::wc_curve25519_export_key_raw_ex(
|
||||
&mut key,
|
||||
private_key.as_mut_ptr(), &mut priv_sz,
|
||||
public_key.as_mut_ptr(), &mut pub_sz,
|
||||
private_key.as_mut_ptr(),
|
||||
&mut priv_sz,
|
||||
public_key.as_mut_ptr(),
|
||||
&mut pub_sz,
|
||||
X25519_LE,
|
||||
);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_curve25519_free(&mut key);
|
||||
crate::sys::wc_FreeRng(&mut rng);
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve25519_export_key_raw_ex returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve25519_export_key_raw_ex returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
crate::sys::wc_curve25519_free(&mut key);
|
||||
crate::sys::wc_FreeRng(&mut rng);
|
||||
}
|
||||
|
||||
Ok(KemKeyPair { public_key, private_key })
|
||||
Ok(KemKeyPair {
|
||||
public_key,
|
||||
private_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn x25519_encapsulate(recipient_pub: &[u8]) -> Result<KemEncapResult, CryptoError> {
|
||||
|
|
@ -163,7 +167,7 @@ fn x25519_decapsulate(
|
|||
fn x25519_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
|
||||
if private_key.len() != 32 || public_key.len() != 32 {
|
||||
return Err(CryptoError::InvalidKey(
|
||||
"X25519: keys must be 32 bytes each".into()
|
||||
"X25519: keys must be 32 bytes each".into(),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -176,44 +180,50 @@ fn x25519_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>
|
|||
// Initialise both key structs before use (sets dp, calls fe_init, etc.).
|
||||
let ret = crate::sys::wc_curve25519_init(&mut local_key);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve25519_init (local) returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve25519_init (local) returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_curve25519_init(&mut remote_key);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_curve25519_free(&mut local_key);
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve25519_init (remote) returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve25519_init (remote) returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
// Import private key in little-endian (EC25519_LITTLE_ENDIAN = 0).
|
||||
let ret = crate::sys::wc_curve25519_import_private_ex(
|
||||
private_key.as_ptr(), 32,
|
||||
private_key.as_ptr(),
|
||||
32,
|
||||
&mut local_key,
|
||||
X25519_LE,
|
||||
);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_curve25519_free(&mut local_key);
|
||||
crate::sys::wc_curve25519_free(&mut remote_key);
|
||||
return Err(CryptoError::InvalidKey(
|
||||
format!("wc_curve25519_import_private_ex returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InvalidKey(format!(
|
||||
"wc_curve25519_import_private_ex returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
// Import remote public key in little-endian.
|
||||
let ret = crate::sys::wc_curve25519_import_public_ex(
|
||||
public_key.as_ptr(), 32,
|
||||
public_key.as_ptr(),
|
||||
32,
|
||||
&mut remote_key,
|
||||
X25519_LE,
|
||||
);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_curve25519_free(&mut local_key);
|
||||
crate::sys::wc_curve25519_free(&mut remote_key);
|
||||
return Err(CryptoError::InvalidKey(
|
||||
format!("wc_curve25519_import_public_ex returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InvalidKey(format!(
|
||||
"wc_curve25519_import_public_ex returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let mut shared_sz = 32u32;
|
||||
|
|
@ -231,9 +241,10 @@ fn x25519_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>
|
|||
crate::sys::wc_curve25519_free(&mut remote_key);
|
||||
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve25519_shared_secret_ex returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve25519_shared_secret_ex returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,15 +270,19 @@ fn x448_generate() -> Result<KemKeyPair, CryptoError> {
|
|||
|
||||
let ret = crate::sys::wc_InitRng(&mut rng);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(format!("wc_InitRng returned {}", ret)));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_InitRng returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_curve448_make_key(&mut rng, 56, &mut key);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_FreeRng(&mut rng);
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve448_make_key returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve448_make_key returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let mut pub_sz = public_key.len() as u32;
|
||||
|
|
@ -276,23 +291,29 @@ fn x448_generate() -> Result<KemKeyPair, CryptoError> {
|
|||
// Export in little-endian (EC448_LITTLE_ENDIAN=0), RFC 7748 canonical format.
|
||||
let ret = crate::sys::wc_curve448_export_key_raw_ex(
|
||||
&mut key,
|
||||
private_key.as_mut_ptr(), &mut priv_sz,
|
||||
public_key.as_mut_ptr(), &mut pub_sz,
|
||||
private_key.as_mut_ptr(),
|
||||
&mut priv_sz,
|
||||
public_key.as_mut_ptr(),
|
||||
&mut pub_sz,
|
||||
X448_LE,
|
||||
);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_curve448_free(&mut key);
|
||||
crate::sys::wc_FreeRng(&mut rng);
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve448_export_key_raw_ex returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve448_export_key_raw_ex returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
crate::sys::wc_curve448_free(&mut key);
|
||||
crate::sys::wc_FreeRng(&mut rng);
|
||||
}
|
||||
|
||||
Ok(KemKeyPair { public_key, private_key })
|
||||
Ok(KemKeyPair {
|
||||
public_key,
|
||||
private_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn x448_encapsulate(recipient_pub: &[u8]) -> Result<KemEncapResult, CryptoError> {
|
||||
|
|
@ -319,7 +340,9 @@ fn x448_decapsulate(
|
|||
/// pattern) before importing keys.
|
||||
fn x448_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
|
||||
if private_key.len() != 56 || public_key.len() != 56 {
|
||||
return Err(CryptoError::InvalidKey("X448: keys must be 56 bytes each".into()));
|
||||
return Err(CryptoError::InvalidKey(
|
||||
"X448: keys must be 56 bytes each".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut shared = Zeroizing::new(vec![0u8; 56]);
|
||||
|
|
@ -331,40 +354,50 @@ fn x448_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>,
|
|||
// Initialise both key structs before use.
|
||||
let ret = crate::sys::wc_curve448_init(&mut local_key);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve448_init (local) returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve448_init (local) returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
let ret = crate::sys::wc_curve448_init(&mut remote_key);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_curve448_free(&mut local_key);
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve448_init (remote) returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve448_init (remote) returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
// Import private key in little-endian (EC448_LITTLE_ENDIAN = 0).
|
||||
let ret = crate::sys::wc_curve448_import_private_ex(
|
||||
private_key.as_ptr(), 56, &mut local_key, X448_LE,
|
||||
private_key.as_ptr(),
|
||||
56,
|
||||
&mut local_key,
|
||||
X448_LE,
|
||||
);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_curve448_free(&mut local_key);
|
||||
crate::sys::wc_curve448_free(&mut remote_key);
|
||||
return Err(CryptoError::InvalidKey(
|
||||
format!("wc_curve448_import_private_ex returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InvalidKey(format!(
|
||||
"wc_curve448_import_private_ex returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
// Import remote public key in little-endian.
|
||||
let ret = crate::sys::wc_curve448_import_public_ex(
|
||||
public_key.as_ptr(), 56, &mut remote_key, X448_LE,
|
||||
public_key.as_ptr(),
|
||||
56,
|
||||
&mut remote_key,
|
||||
X448_LE,
|
||||
);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_curve448_free(&mut local_key);
|
||||
crate::sys::wc_curve448_free(&mut remote_key);
|
||||
return Err(CryptoError::InvalidKey(
|
||||
format!("wc_curve448_import_public_ex returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InvalidKey(format!(
|
||||
"wc_curve448_import_public_ex returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let mut shared_sz = 56u32;
|
||||
|
|
@ -381,9 +414,10 @@ fn x448_dh(private_key: &[u8], public_key: &[u8]) -> Result<Zeroizing<Vec<u8>>,
|
|||
crate::sys::wc_curve448_free(&mut remote_key);
|
||||
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_curve448_shared_secret_ex returned {}", ret)
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_curve448_shared_secret_ex returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,17 @@ pub mod kdf;
|
|||
pub mod kem;
|
||||
pub mod mac;
|
||||
pub mod provider;
|
||||
mod self_test;
|
||||
|
||||
// FFI bindings generated by bindgen in build.rs.
|
||||
// When wolfSSL headers are not present (e.g. docs.rs), use a stub.
|
||||
#[allow(non_camel_case_types, non_snake_case, non_upper_case_globals, dead_code, clippy::all)]
|
||||
#[allow(
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
non_upper_case_globals,
|
||||
dead_code,
|
||||
clippy::all
|
||||
)]
|
||||
mod sys {
|
||||
// ── Manual ABI-critical type definitions ────────────────────────────────
|
||||
//
|
||||
|
|
@ -112,48 +119,98 @@ mod sys {
|
|||
|
||||
// ── AES-GCM ─────────────────────────────────────────────────────────
|
||||
|
||||
pub unsafe fn wc_AesGcmSetKey(aes: *mut Aes, key: *const c_uchar, len: c_uint) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_AesGcmSetKey(aes: *mut Aes, key: *const c_uchar, len: c_uint) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_AesGcmEncrypt(
|
||||
aes: *mut Aes, out: *mut c_uchar, in_: *const c_uchar, sz: c_uint,
|
||||
iv: *const c_uchar, iv_sz: c_uint, auth_tag: *mut c_uchar, auth_tag_sz: c_uint,
|
||||
auth_in: *const c_uchar, auth_in_sz: c_uint,
|
||||
) -> c_int { unreachable!() }
|
||||
aes: *mut Aes,
|
||||
out: *mut c_uchar,
|
||||
in_: *const c_uchar,
|
||||
sz: c_uint,
|
||||
iv: *const c_uchar,
|
||||
iv_sz: c_uint,
|
||||
auth_tag: *mut c_uchar,
|
||||
auth_tag_sz: c_uint,
|
||||
auth_in: *const c_uchar,
|
||||
auth_in_sz: c_uint,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_AesGcmDecrypt(
|
||||
aes: *mut Aes, out: *mut c_uchar, in_: *const c_uchar, sz: c_uint,
|
||||
iv: *const c_uchar, iv_sz: c_uint, auth_tag: *const c_uchar, auth_tag_sz: c_uint,
|
||||
auth_in: *const c_uchar, auth_in_sz: c_uint,
|
||||
) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_AesInit(aes: *mut Aes, heap: *mut c_void, dev_id: c_int) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_AesFree(aes: *mut Aes) { unreachable!() }
|
||||
aes: *mut Aes,
|
||||
out: *mut c_uchar,
|
||||
in_: *const c_uchar,
|
||||
sz: c_uint,
|
||||
iv: *const c_uchar,
|
||||
iv_sz: c_uint,
|
||||
auth_tag: *const c_uchar,
|
||||
auth_tag_sz: c_uint,
|
||||
auth_in: *const c_uchar,
|
||||
auth_in_sz: c_uint,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_AesInit(aes: *mut Aes, heap: *mut c_void, dev_id: c_int) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_AesFree(aes: *mut Aes) {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
// ── ChaCha20-Poly1305 ────────────────────────────────────────────────
|
||||
|
||||
pub unsafe fn wc_ChaCha20Poly1305_Encrypt(
|
||||
key: *const c_uchar, iv: *const c_uchar,
|
||||
aad: *const c_uchar, aad_sz: c_uint,
|
||||
in_: *const c_uchar, in_sz: c_uint,
|
||||
out: *mut c_uchar, auth_tag: *mut c_uchar,
|
||||
) -> c_int { unreachable!() }
|
||||
key: *const c_uchar,
|
||||
iv: *const c_uchar,
|
||||
aad: *const c_uchar,
|
||||
aad_sz: c_uint,
|
||||
in_: *const c_uchar,
|
||||
in_sz: c_uint,
|
||||
out: *mut c_uchar,
|
||||
auth_tag: *mut c_uchar,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_ChaCha20Poly1305_Decrypt(
|
||||
key: *const c_uchar, iv: *const c_uchar,
|
||||
aad: *const c_uchar, aad_sz: c_uint,
|
||||
in_: *const c_uchar, in_sz: c_uint,
|
||||
auth_tag: *const c_uchar, out: *mut c_uchar,
|
||||
) -> c_int { unreachable!() }
|
||||
key: *const c_uchar,
|
||||
iv: *const c_uchar,
|
||||
aad: *const c_uchar,
|
||||
aad_sz: c_uint,
|
||||
in_: *const c_uchar,
|
||||
in_sz: c_uint,
|
||||
auth_tag: *const c_uchar,
|
||||
out: *mut c_uchar,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_XChaCha20Poly1305_Encrypt(
|
||||
dst: *mut c_uchar, dst_space: usize,
|
||||
src: *const c_uchar, src_len: usize,
|
||||
ad: *const c_uchar, ad_len: usize,
|
||||
nonce: *const c_uchar, nonce_len: usize,
|
||||
key: *const c_uchar, key_len: usize,
|
||||
) -> c_int { unreachable!() }
|
||||
dst: *mut c_uchar,
|
||||
dst_space: usize,
|
||||
src: *const c_uchar,
|
||||
src_len: usize,
|
||||
ad: *const c_uchar,
|
||||
ad_len: usize,
|
||||
nonce: *const c_uchar,
|
||||
nonce_len: usize,
|
||||
key: *const c_uchar,
|
||||
key_len: usize,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_XChaCha20Poly1305_Decrypt(
|
||||
dst: *mut c_uchar, dst_space: usize,
|
||||
src: *const c_uchar, src_len: usize,
|
||||
ad: *const c_uchar, ad_len: usize,
|
||||
nonce: *const c_uchar, nonce_len: usize,
|
||||
key: *const c_uchar, key_len: usize,
|
||||
) -> c_int { unreachable!() }
|
||||
dst: *mut c_uchar,
|
||||
dst_space: usize,
|
||||
src: *const c_uchar,
|
||||
src_len: usize,
|
||||
ad: *const c_uchar,
|
||||
ad_len: usize,
|
||||
nonce: *const c_uchar,
|
||||
nonce_len: usize,
|
||||
key: *const c_uchar,
|
||||
key_len: usize,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
// ── SHA-256 (init/update/final) ──────────────────────────────────────
|
||||
|
||||
|
|
@ -195,76 +252,161 @@ mod sys {
|
|||
|
||||
pub unsafe fn wc_InitBlake2b(b2b: *mut Blake2b, digest_sz: c_uint) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_InitBlake2b_WithKey(
|
||||
b2b: *mut Blake2b, digest_sz: c_uint,
|
||||
key: *const c_uchar, key_sz: c_uint,
|
||||
) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_Blake2bUpdate(b2b: *mut Blake2b, in_: *const c_uchar, in_sz: c_uint) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_Blake2bFinal(b2b: *mut Blake2b, final_: *mut c_uchar, request_sz: c_uint) -> c_int { unreachable!() }
|
||||
b2b: *mut Blake2b,
|
||||
digest_sz: c_uint,
|
||||
key: *const c_uchar,
|
||||
key_sz: c_uint,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_Blake2bUpdate(
|
||||
b2b: *mut Blake2b,
|
||||
in_: *const c_uchar,
|
||||
in_sz: c_uint,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_Blake2bFinal(
|
||||
b2b: *mut Blake2b,
|
||||
final_: *mut c_uchar,
|
||||
request_sz: c_uint,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
// ── HMAC ────────────────────────────────────────────────────────────
|
||||
|
||||
pub unsafe fn wc_HmacInit(hmac: *mut Hmac, heap: *mut c_void, dev_id: c_int) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_HmacSetKey(hmac: *mut Hmac, type_: c_int, key: *const c_uchar, key_sz: c_uint) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_HmacUpdate(hmac: *mut Hmac, in_: *const c_uchar, in_sz: c_uint) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_HmacFinal(hmac: *mut Hmac, out: *mut c_uchar) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_HmacFree(hmac: *mut Hmac) { unreachable!() }
|
||||
pub unsafe fn wc_HmacSetKey(
|
||||
hmac: *mut Hmac,
|
||||
type_: c_int,
|
||||
key: *const c_uchar,
|
||||
key_sz: c_uint,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
// ── HKDF (TLS-1.3 Extract — RFC 5869 Extract step) ──────────────────
|
||||
|
||||
pub unsafe fn wc_Tls13_HKDF_Extract(
|
||||
prk: *mut c_uchar,
|
||||
salt: *const c_uchar, salt_len: c_uint,
|
||||
ikm: *mut c_uchar, ikm_len: c_uint,
|
||||
salt: *const c_uchar,
|
||||
salt_len: c_uint,
|
||||
ikm: *mut c_uchar,
|
||||
ikm_len: c_uint,
|
||||
digest: c_int,
|
||||
) -> c_int { unreachable!() }
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
// ── RNG ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub unsafe fn wc_InitRng(rng: *mut WC_RNG) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_RNG_GenerateBlock(rng: *mut WC_RNG, buf: *mut c_uchar, sz: c_uint) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_FreeRng(rng: *mut WC_RNG) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_RNG_GenerateBlock(
|
||||
rng: *mut WC_RNG,
|
||||
buf: *mut c_uchar,
|
||||
sz: c_uint,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
// ── Curve25519 ───────────────────────────────────────────────────────
|
||||
|
||||
pub unsafe fn wc_curve25519_make_key(rng: *mut WC_RNG, key_sz: c_int, key: *mut curve25519_key) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_curve25519_init(key: *mut curve25519_key) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_curve25519_make_key(
|
||||
rng: *mut WC_RNG,
|
||||
key_sz: c_int,
|
||||
key: *mut curve25519_key,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve25519_export_key_raw(
|
||||
key: *mut curve25519_key,
|
||||
priv_: *mut c_uchar, priv_sz: *mut c_uint,
|
||||
pub_: *mut c_uchar, pub_sz: *mut c_uint,
|
||||
) -> c_int { unreachable!() }
|
||||
priv_: *mut c_uchar,
|
||||
priv_sz: *mut c_uint,
|
||||
pub_: *mut c_uchar,
|
||||
pub_sz: *mut c_uint,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve25519_import_private(
|
||||
priv_: *const c_uchar, priv_sz: c_uint, key: *mut curve25519_key,
|
||||
) -> c_int { unreachable!() }
|
||||
priv_: *const c_uchar,
|
||||
priv_sz: c_uint,
|
||||
key: *mut curve25519_key,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve25519_import_public(
|
||||
pub_: *const c_uchar, pub_sz: c_uint, key: *mut curve25519_key,
|
||||
) -> c_int { unreachable!() }
|
||||
pub_: *const c_uchar,
|
||||
pub_sz: c_uint,
|
||||
key: *mut curve25519_key,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve25519_shared_secret_ex(
|
||||
local: *mut curve25519_key, remote: *mut curve25519_key,
|
||||
out: *mut c_uchar, out_sz: *mut c_uint, endian: c_int,
|
||||
) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_curve25519_free(key: *mut curve25519_key) { unreachable!() }
|
||||
local: *mut curve25519_key,
|
||||
remote: *mut curve25519_key,
|
||||
out: *mut c_uchar,
|
||||
out_sz: *mut c_uint,
|
||||
endian: c_int,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve25519_free(key: *mut curve25519_key) {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
// ── Curve448 ─────────────────────────────────────────────────────────
|
||||
|
||||
pub unsafe fn wc_curve448_make_key(rng: *mut WC_RNG, key_sz: c_int, key: *mut curve448_key) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_curve448_init(key: *mut curve448_key) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_curve448_make_key(
|
||||
rng: *mut WC_RNG,
|
||||
key_sz: c_int,
|
||||
key: *mut curve448_key,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve448_init(key: *mut curve448_key) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve448_export_key_raw(
|
||||
key: *mut curve448_key,
|
||||
priv_: *mut c_uchar, priv_sz: *mut c_uint,
|
||||
pub_: *mut c_uchar, pub_sz: *mut c_uint,
|
||||
) -> c_int { unreachable!() }
|
||||
priv_: *mut c_uchar,
|
||||
priv_sz: *mut c_uint,
|
||||
pub_: *mut c_uchar,
|
||||
pub_sz: *mut c_uint,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve448_import_private(
|
||||
priv_: *const c_uchar, priv_sz: c_uint, key: *mut curve448_key,
|
||||
) -> c_int { unreachable!() }
|
||||
priv_: *const c_uchar,
|
||||
priv_sz: c_uint,
|
||||
key: *mut curve448_key,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve448_import_public(
|
||||
pub_: *const c_uchar, pub_sz: c_uint, key: *mut curve448_key,
|
||||
) -> c_int { unreachable!() }
|
||||
pub_: *const c_uchar,
|
||||
pub_sz: c_uint,
|
||||
key: *mut curve448_key,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve448_shared_secret_ex(
|
||||
local: *mut curve448_key, remote: *mut curve448_key,
|
||||
out: *mut c_uchar, out_sz: *mut c_uint, endian: c_int,
|
||||
) -> c_int { unreachable!() }
|
||||
pub unsafe fn wc_curve448_free(key: *mut curve448_key) { unreachable!() }
|
||||
local: *mut curve448_key,
|
||||
remote: *mut curve448_key,
|
||||
out: *mut c_uchar,
|
||||
out_sz: *mut c_uint,
|
||||
endian: c_int,
|
||||
) -> c_int {
|
||||
unreachable!()
|
||||
}
|
||||
pub unsafe fn wc_curve448_free(key: *mut curve448_key) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,7 +418,6 @@ pub use provider::WolfSslProvider;
|
|||
/// Call this once from `ccc_init()` in the bridge crate.
|
||||
pub fn init() {
|
||||
let provider = WolfSslProvider::new();
|
||||
ccc_crypto_core::ProviderRegistry::global()
|
||||
.register("wolfssl", Box::new(provider));
|
||||
ccc_crypto_core::ProviderRegistry::global().register("wolfssl", Box::new(provider));
|
||||
log::info!("[ccc-crypto-wolfssl] provider registered");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,19 +17,14 @@ const WC_HASH_TYPE_SHA512: i32 = 8;
|
|||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compute a MAC tag over `data` using the specified algorithm and `key`.
|
||||
pub fn compute_mac(
|
||||
algo: MacAlgorithm,
|
||||
key: &[u8],
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
pub fn compute_mac(algo: MacAlgorithm, key: &[u8], data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
match algo {
|
||||
MacAlgorithm::HmacSha256 => hmac(WC_HASH_TYPE_SHA256, key, data, 32),
|
||||
MacAlgorithm::HmacSha384 => hmac(WC_HASH_TYPE_SHA384, key, data, 48),
|
||||
MacAlgorithm::HmacSha512 => hmac(WC_HASH_TYPE_SHA512, key, data, 64),
|
||||
MacAlgorithm::Blake2bMac => blake2b_mac(key, data),
|
||||
MacAlgorithm::Poly1305 =>
|
||||
Err(CryptoError::UnsupportedAlgorithm(
|
||||
"Poly1305 standalone MAC is not exposed in this Phase".into()
|
||||
MacAlgorithm::Poly1305 => Err(CryptoError::UnsupportedAlgorithm(
|
||||
"Poly1305 standalone MAC is not exposed in this Phase".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -71,10 +66,7 @@ pub(crate) fn hmac(
|
|||
use std::alloc::{alloc_zeroed, dealloc, Layout};
|
||||
|
||||
// sizeof(Hmac) = 456 bytes; alignment = 16 (from embedded wc_Sha256).
|
||||
let layout = Layout::from_size_align(
|
||||
std::mem::size_of::<crate::sys::Hmac>().max(456),
|
||||
16,
|
||||
)
|
||||
let layout = Layout::from_size_align(std::mem::size_of::<crate::sys::Hmac>().max(456), 16)
|
||||
.expect("layout must be valid");
|
||||
|
||||
let hmac_ptr = alloc_zeroed(layout) as *mut crate::sys::Hmac;
|
||||
|
|
@ -90,40 +82,40 @@ pub(crate) fn hmac(
|
|||
);
|
||||
if ret != 0 {
|
||||
dealloc(hmac_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InternalError(format!("wc_HmacInit returned {}", ret)));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_HmacInit returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_HmacSetKey(
|
||||
hmac_ptr,
|
||||
hash_type,
|
||||
key.as_ptr(),
|
||||
key.len() as u32,
|
||||
);
|
||||
let ret = crate::sys::wc_HmacSetKey(hmac_ptr, hash_type, key.as_ptr(), key.len() as u32);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_HmacFree(hmac_ptr);
|
||||
dealloc(hmac_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InvalidKey(format!("wc_HmacSetKey returned {}", ret)));
|
||||
return Err(CryptoError::InvalidKey(format!(
|
||||
"wc_HmacSetKey returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_HmacUpdate(
|
||||
hmac_ptr,
|
||||
data.as_ptr(),
|
||||
data.len() as u32,
|
||||
);
|
||||
let ret = crate::sys::wc_HmacUpdate(hmac_ptr, data.as_ptr(), data.len() as u32);
|
||||
if ret != 0 {
|
||||
crate::sys::wc_HmacFree(hmac_ptr);
|
||||
dealloc(hmac_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InternalError(format!("wc_HmacUpdate returned {}", ret)));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_HmacUpdate returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_HmacFinal(
|
||||
hmac_ptr,
|
||||
out.as_mut_ptr(),
|
||||
);
|
||||
let ret = crate::sys::wc_HmacFinal(hmac_ptr, out.as_mut_ptr());
|
||||
if ret != 0 {
|
||||
crate::sys::wc_HmacFree(hmac_ptr);
|
||||
dealloc(hmac_ptr as *mut u8, layout);
|
||||
return Err(CryptoError::InternalError(format!("wc_HmacFinal returned {}", ret)));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_HmacFinal returned {}",
|
||||
ret
|
||||
)));
|
||||
}
|
||||
|
||||
crate::sys::wc_HmacFree(hmac_ptr);
|
||||
|
|
@ -146,7 +138,7 @@ pub(crate) fn hmac(
|
|||
fn blake2b_mac(key: &[u8], data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
if key.is_empty() || key.len() > 64 {
|
||||
return Err(CryptoError::InvalidKey(
|
||||
"BLAKE2b-MAC: key must be 1–64 bytes".into()
|
||||
"BLAKE2b-MAC: key must be 1–64 bytes".into(),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -157,30 +149,26 @@ fn blake2b_mac(key: &[u8], data: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|||
let mut b2b: crate::sys::Blake2b = std::mem::zeroed();
|
||||
|
||||
// Initialise with digest size and key for keyed (MAC) mode.
|
||||
let ret = crate::sys::wc_InitBlake2b_WithKey(
|
||||
&mut b2b,
|
||||
OUT_LEN,
|
||||
key.as_ptr(),
|
||||
key.len() as u32,
|
||||
);
|
||||
let ret =
|
||||
crate::sys::wc_InitBlake2b_WithKey(&mut b2b, OUT_LEN, key.as_ptr(), key.len() as u32);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_InitBlake2b_WithKey returned {ret}")
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_InitBlake2b_WithKey returned {ret}"
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_Blake2bUpdate(&mut b2b, data.as_ptr(), data.len() as u32);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_Blake2bUpdate returned {ret}")
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Blake2bUpdate returned {ret}"
|
||||
)));
|
||||
}
|
||||
|
||||
let ret = crate::sys::wc_Blake2bFinal(&mut b2b, out.as_mut_ptr(), OUT_LEN);
|
||||
if ret != 0 {
|
||||
return Err(CryptoError::InternalError(
|
||||
format!("wc_Blake2bFinal returned {ret}")
|
||||
));
|
||||
return Err(CryptoError::InternalError(format!(
|
||||
"wc_Blake2bFinal returned {ret}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,26 +189,13 @@ fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
|||
}
|
||||
// Accumulate XOR differences; the branch is on the final accumulated value
|
||||
// only, not on any individual byte.
|
||||
let diff: u8 = a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y));
|
||||
let diff: u8 = a
|
||||
.iter()
|
||||
.zip(b.iter())
|
||||
.fold(0u8, |acc, (x, y)| acc | (x ^ y));
|
||||
diff == 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::constant_time_eq;
|
||||
|
||||
#[test]
|
||||
fn ct_eq_same() {
|
||||
assert!(constant_time_eq(b"hello", b"hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ct_eq_different() {
|
||||
assert!(!constant_time_eq(b"hello", b"Hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ct_eq_different_lengths() {
|
||||
assert!(!constant_time_eq(b"hi", b"hello"));
|
||||
}
|
||||
}
|
||||
#[path = "mac_tests.rs"]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
//! Unit tests for constant-time comparison in the MAC module.
|
||||
|
||||
use super::constant_time_eq;
|
||||
|
||||
#[test]
|
||||
fn ct_eq_same() {
|
||||
assert!(constant_time_eq(b"hello", b"hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ct_eq_different() {
|
||||
assert!(!constant_time_eq(b"hello", b"Hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ct_eq_different_lengths() {
|
||||
assert!(!constant_time_eq(b"hi", b"hello"));
|
||||
}
|
||||
|
|
@ -12,56 +12,11 @@ use ccc_crypto_core::{
|
|||
algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm},
|
||||
capabilities::ProviderCapabilities,
|
||||
error::CryptoError,
|
||||
provider::{
|
||||
AeadProvider, CryptoProvider, HashProvider, KdfProvider, KemProvider, MacProvider,
|
||||
},
|
||||
types::{AlgoTestResult, BenchmarkReport, KemEncapResult, KemKeyPair, SelfTestReport},
|
||||
provider::{AeadProvider, CryptoProvider, HashProvider, KdfProvider, KemProvider, MacProvider},
|
||||
types::{BenchmarkReport, KemEncapResult, KemKeyPair, SelfTestReport},
|
||||
};
|
||||
|
||||
use crate::{aead, capabilities, hash, kdf, kem, mac};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Embedded NIST / RFC test vectors for self_test()
|
||||
// These are checked at runtime; failures gate the provider from being marked
|
||||
// `available` in the capability catalog.
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
struct AeadVector {
|
||||
algo: AeadAlgorithm,
|
||||
key: &'static str,
|
||||
nonce: &'static str,
|
||||
aad: &'static str,
|
||||
pt: &'static str,
|
||||
ct_tag: &'static str, // ciphertext || tag, hex-encoded
|
||||
}
|
||||
|
||||
/// NIST SP 800-38D and RFC 8439 test vectors.
|
||||
static AEAD_VECTORS: &[AeadVector] = &[
|
||||
// ── AES-256-GCM: NIST CAVS test case GCM-256/96/128 (test case 1) ───────
|
||||
AeadVector {
|
||||
algo: AeadAlgorithm::AesGcm256,
|
||||
key: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308",
|
||||
nonce: "cafebabefacedbaddecaf888",
|
||||
aad: "",
|
||||
pt: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72\
|
||||
1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255",
|
||||
ct_tag: "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa\
|
||||
8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad",
|
||||
},
|
||||
// ── ChaCha20-Poly1305: RFC 8439 §2.8.2 test vector ──────────────────────
|
||||
AeadVector {
|
||||
algo: AeadAlgorithm::ChaCha20Poly1305,
|
||||
key: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
|
||||
nonce: "070000004041424344454647",
|
||||
aad: "50515253c0c1c2c3c4c5c6c7",
|
||||
pt: "4c616469657320616e642047656e746c656d656e206f662074686520636c617373\
|
||||
206f6620273939",
|
||||
ct_tag: "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63\
|
||||
dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b369\
|
||||
2ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3\
|
||||
ff4def08e4b7a9de576d26586cec64b6116",
|
||||
},
|
||||
];
|
||||
use crate::{aead, capabilities, hash, kdf, kem, mac, self_test};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// WolfSslProvider
|
||||
|
|
@ -97,14 +52,19 @@ impl WolfSslProvider {
|
|||
self.bench.get_or_init(|| {
|
||||
log::info!("[ccc-crypto-wolfssl] running throughput benchmark…");
|
||||
let report = capabilities::run_benchmark();
|
||||
log::info!("[ccc-crypto-wolfssl] benchmark complete ({} algos)", report.results.len());
|
||||
log::info!(
|
||||
"[ccc-crypto-wolfssl] benchmark complete ({} algos)",
|
||||
report.results.len()
|
||||
);
|
||||
report
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WolfSslProvider {
|
||||
fn default() -> Self { Self::new() }
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -113,15 +73,23 @@ impl Default for WolfSslProvider {
|
|||
|
||||
impl AeadProvider for WolfSslProvider {
|
||||
fn encrypt_aead(
|
||||
&self, algo: AeadAlgorithm, key: &[u8], nonce: &[u8],
|
||||
plaintext: &[u8], aad: &[u8],
|
||||
&self,
|
||||
algo: AeadAlgorithm,
|
||||
key: &[u8],
|
||||
nonce: &[u8],
|
||||
plaintext: &[u8],
|
||||
aad: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
aead::encrypt(algo, key, nonce, plaintext, aad)
|
||||
}
|
||||
|
||||
fn decrypt_aead(
|
||||
&self, algo: AeadAlgorithm, key: &[u8], nonce: &[u8],
|
||||
ciphertext_and_tag: &[u8], aad: &[u8],
|
||||
&self,
|
||||
algo: AeadAlgorithm,
|
||||
key: &[u8],
|
||||
nonce: &[u8],
|
||||
ciphertext_and_tag: &[u8],
|
||||
aad: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
aead::decrypt(algo, key, nonce, ciphertext_and_tag, aad)
|
||||
}
|
||||
|
|
@ -129,7 +97,12 @@ impl AeadProvider for WolfSslProvider {
|
|||
|
||||
impl KdfProvider for WolfSslProvider {
|
||||
fn derive_key(
|
||||
&self, algo: KdfAlgorithm, ikm: &[u8], salt: &[u8], info: &[u8], length: usize,
|
||||
&self,
|
||||
algo: KdfAlgorithm,
|
||||
ikm: &[u8],
|
||||
salt: &[u8],
|
||||
info: &[u8],
|
||||
length: usize,
|
||||
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
|
||||
kdf::derive_key(algo, ikm, salt, info, length)
|
||||
}
|
||||
|
|
@ -137,13 +110,20 @@ impl KdfProvider for WolfSslProvider {
|
|||
|
||||
impl MacProvider for WolfSslProvider {
|
||||
fn compute_mac(
|
||||
&self, algo: MacAlgorithm, key: &[u8], data: &[u8],
|
||||
&self,
|
||||
algo: MacAlgorithm,
|
||||
key: &[u8],
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
mac::compute_mac(algo, key, data)
|
||||
}
|
||||
|
||||
fn verify_mac(
|
||||
&self, algo: MacAlgorithm, key: &[u8], data: &[u8], mac_bytes: &[u8],
|
||||
&self,
|
||||
algo: MacAlgorithm,
|
||||
key: &[u8],
|
||||
data: &[u8],
|
||||
mac_bytes: &[u8],
|
||||
) -> Result<bool, CryptoError> {
|
||||
mac::verify_mac(algo, key, data, mac_bytes)
|
||||
}
|
||||
|
|
@ -162,12 +142,17 @@ impl KemProvider for WolfSslProvider {
|
|||
kem::generate_keypair(algo)
|
||||
}
|
||||
fn encapsulate(
|
||||
&self, algo: KemAlgorithm, public_key: &[u8],
|
||||
&self,
|
||||
algo: KemAlgorithm,
|
||||
public_key: &[u8],
|
||||
) -> Result<KemEncapResult, CryptoError> {
|
||||
kem::encapsulate(algo, public_key)
|
||||
}
|
||||
fn decapsulate(
|
||||
&self, algo: KemAlgorithm, private_key: &[u8], ciphertext: &[u8],
|
||||
&self,
|
||||
algo: KemAlgorithm,
|
||||
private_key: &[u8],
|
||||
ciphertext: &[u8],
|
||||
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
|
||||
kem::decapsulate(algo, private_key, ciphertext)
|
||||
}
|
||||
|
|
@ -178,25 +163,21 @@ impl KemProvider for WolfSslProvider {
|
|||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
impl CryptoProvider for WolfSslProvider {
|
||||
fn provider_name(&self) -> &'static str { "wolfssl" }
|
||||
fn provider_name(&self) -> &'static str {
|
||||
"wolfssl"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ProviderCapabilities {
|
||||
self.caps.get_or_init(|| {
|
||||
self.caps
|
||||
.get_or_init(|| {
|
||||
let bench = self.get_or_run_benchmark();
|
||||
capabilities::probe_capabilities(Some(bench))
|
||||
}).clone()
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn self_test(&self) -> SelfTestReport {
|
||||
let mut results: Vec<AlgoTestResult> = Vec::new();
|
||||
|
||||
// Run AEAD test vectors.
|
||||
for v in AEAD_VECTORS {
|
||||
let result = run_aead_vector(v);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
SelfTestReport::finalise("wolfssl", results)
|
||||
self_test::run()
|
||||
}
|
||||
|
||||
fn benchmark(&self) -> BenchmarkReport {
|
||||
|
|
@ -204,47 +185,4 @@ impl CryptoProvider for WolfSslProvider {
|
|||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Test vector runner
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn run_aead_vector(v: &AeadVector) -> AlgoTestResult {
|
||||
let test_name = format!("{} NIST/RFC vector", v.algo.name());
|
||||
|
||||
let decode = |hex: &str| -> Vec<u8> {
|
||||
(0..hex.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap_or(0))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let key = decode(v.key);
|
||||
let nonce = decode(v.nonce);
|
||||
let aad = decode(v.aad);
|
||||
let pt = decode(v.pt);
|
||||
let expected = decode(v.ct_tag);
|
||||
|
||||
match aead::encrypt(v.algo, &key, &nonce, &pt, &aad) {
|
||||
Ok(ct_tag) if ct_tag == expected => AlgoTestResult {
|
||||
algo_id: v.algo as u32,
|
||||
algo_name: test_name,
|
||||
passed: true,
|
||||
error_message: None,
|
||||
},
|
||||
Ok(ct_tag) => AlgoTestResult {
|
||||
algo_id: v.algo as u32,
|
||||
algo_name: test_name.clone(),
|
||||
passed: false,
|
||||
error_message: Some(format!(
|
||||
"output mismatch: got {} bytes, expected {} bytes",
|
||||
ct_tag.len(), expected.len()
|
||||
)),
|
||||
},
|
||||
Err(e) => AlgoTestResult {
|
||||
algo_id: v.algo as u32,
|
||||
algo_name: test_name,
|
||||
passed: false,
|
||||
error_message: Some(e.to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
//! Embedded NIST / RFC test vectors for the power-on self-test.
|
||||
//!
|
||||
//! These are checked at runtime by [`WolfSslProvider::self_test()`]; failures
|
||||
//! gate the provider from being marked `available` in the capability catalog.
|
||||
|
||||
use ccc_crypto_core::{
|
||||
algorithms::AeadAlgorithm,
|
||||
types::{AlgoTestResult, SelfTestReport},
|
||||
};
|
||||
|
||||
use crate::aead;
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// AEAD vectors
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
struct AeadVector {
|
||||
algo: AeadAlgorithm,
|
||||
key: &'static str,
|
||||
nonce: &'static str,
|
||||
aad: &'static str,
|
||||
pt: &'static str,
|
||||
ct_tag: &'static str, // ciphertext || tag, hex-encoded
|
||||
}
|
||||
|
||||
/// NIST SP 800-38D and RFC 8439 test vectors.
|
||||
static AEAD_VECTORS: &[AeadVector] = &[
|
||||
// ── AES-256-GCM: NIST SP 800-38D Test Case 16 ───────────────────────
|
||||
// Key: feffe9...308308 × 2 (AES-256)
|
||||
// Nonce: cafebabefacedbaddecaf888
|
||||
// AAD: feedfacedeadbeeffeedfacedeadbeefabaddad2 (20 bytes)
|
||||
// PT: d9313225...637b39 (60 bytes)
|
||||
// ct_tag = ciphertext (60 bytes) || GHASH tag (16 bytes) = 76 bytes
|
||||
AeadVector {
|
||||
algo: AeadAlgorithm::AesGcm256,
|
||||
key: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308",
|
||||
nonce: "cafebabefacedbaddecaf888",
|
||||
aad: "feedfacedeadbeeffeedfacedeadbeefabaddad2",
|
||||
pt: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72\
|
||||
1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39",
|
||||
ct_tag: "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa\
|
||||
8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662\
|
||||
76fc6ece0f4e1768cddf8853bb2d551b",
|
||||
},
|
||||
// ── ChaCha20-Poly1305: RFC 8439 §2.8.2 ──────────────────────────────
|
||||
// PT: "Ladies and Gentlemen of the class of '99" (40 bytes)
|
||||
// ct_tag = ciphertext (40 bytes) || Poly1305 tag (16 bytes) = 56 bytes
|
||||
AeadVector {
|
||||
algo: AeadAlgorithm::ChaCha20Poly1305,
|
||||
key: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
|
||||
nonce: "070000004041424344454647",
|
||||
aad: "50515253c0c1c2c3c4c5c6c7",
|
||||
pt: "4c616469657320616e642047656e746c656d656e206f662074686520636c617373\
|
||||
206f6620273939",
|
||||
ct_tag: "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6\
|
||||
3dbea45e8ca96712f180d4e9016c65a7dde15e3106075ebd",
|
||||
},
|
||||
];
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Runner
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Run all embedded self-test vectors and return a [`SelfTestReport`].
|
||||
pub(crate) fn run() -> SelfTestReport {
|
||||
let mut results: Vec<AlgoTestResult> = Vec::new();
|
||||
for v in AEAD_VECTORS {
|
||||
results.push(run_aead_vector(v));
|
||||
}
|
||||
SelfTestReport::finalise("wolfssl", results)
|
||||
}
|
||||
|
||||
fn run_aead_vector(v: &AeadVector) -> AlgoTestResult {
|
||||
let test_name = format!("{} NIST/RFC vector", v.algo.name());
|
||||
|
||||
let decode = |hex: &str| -> Vec<u8> {
|
||||
(0..hex.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap_or(0))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let key = decode(v.key);
|
||||
let nonce = decode(v.nonce);
|
||||
let aad = decode(v.aad);
|
||||
let pt = decode(v.pt);
|
||||
let expected = decode(v.ct_tag);
|
||||
|
||||
match aead::encrypt(v.algo, &key, &nonce, &pt, &aad) {
|
||||
Ok(ct_tag) if ct_tag == expected => AlgoTestResult {
|
||||
algo_id: v.algo as u32,
|
||||
algo_name: test_name,
|
||||
passed: true,
|
||||
error_message: None,
|
||||
},
|
||||
Ok(ct_tag) => AlgoTestResult {
|
||||
algo_id: v.algo as u32,
|
||||
algo_name: test_name.clone(),
|
||||
passed: false,
|
||||
error_message: Some(format!(
|
||||
"output mismatch: got {} bytes, expected {} bytes",
|
||||
ct_tag.len(),
|
||||
expected.len()
|
||||
)),
|
||||
},
|
||||
Err(e) => AlgoTestResult {
|
||||
algo_id: v.algo as u32,
|
||||
algo_name: test_name,
|
||||
passed: false,
|
||||
error_message: Some(e.to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -311,8 +311,8 @@ static XCHACHA20_PROBES: &[XChaChaProbe] = &[
|
|||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn from_hex(s: &str) -> Vec<u8> {
|
||||
let s = s.replace(' ', "").replace('\n', "");
|
||||
assert!(s.len() % 2 == 0, "odd-length hex: {s}");
|
||||
let s = s.replace([' ', '\n'], "");
|
||||
assert!(s.len().is_multiple_of(2), "odd-length hex: {s}");
|
||||
(0..s.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
|
||||
|
|
@ -347,12 +347,24 @@ fn run_aead(p: &WolfSslProvider, failures: &mut usize) {
|
|||
// Verify round-trip decrypt as well.
|
||||
match p.decrypt_aead(v.algo, &key, &nonce, &ct_tag, &aad) {
|
||||
Ok(recovered) if recovered == pt => pass(v.name),
|
||||
Ok(_) => { *failures += 1; println!(" [FAIL] {} (decrypt mismatch)", v.name); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} (decrypt error: {e})", v.name); }
|
||||
Ok(_) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (decrypt mismatch)", v.name);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (decrypt error: {e})", v.name);
|
||||
}
|
||||
}
|
||||
Ok(ct_tag) => { *failures += 1; fail(v.name, &ct_tag, &expected); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} (encrypt error: {e})", v.name); }
|
||||
}
|
||||
Ok(ct_tag) => {
|
||||
*failures += 1;
|
||||
fail(v.name, &ct_tag, &expected);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (encrypt error: {e})", v.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -367,8 +379,14 @@ fn run_kdf(p: &WolfSslProvider, failures: &mut usize) {
|
|||
|
||||
match p.derive_key(v.algo, &ikm, &salt, &info, v.length) {
|
||||
Ok(out) if out.as_slice() == expected.as_slice() => pass(v.name),
|
||||
Ok(out) => { *failures += 1; fail(v.name, &out, &expected); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", v.name); }
|
||||
Ok(out) => {
|
||||
*failures += 1;
|
||||
fail(v.name, &out, &expected);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} ({e})", v.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -385,12 +403,24 @@ fn run_mac(p: &WolfSslProvider, failures: &mut usize) {
|
|||
// Also verify constant-time path.
|
||||
match p.verify_mac(v.algo, &key, &data, &tag) {
|
||||
Ok(true) => pass(v.name),
|
||||
Ok(false) => { *failures += 1; println!(" [FAIL] {} (verify false)", v.name); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} (verify error: {e})", v.name); }
|
||||
Ok(false) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (verify false)", v.name);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (verify error: {e})", v.name);
|
||||
}
|
||||
}
|
||||
Ok(tag) => { *failures += 1; fail(v.name, &tag, &expected); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", v.name); }
|
||||
}
|
||||
Ok(tag) => {
|
||||
*failures += 1;
|
||||
fail(v.name, &tag, &expected);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} ({e})", v.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -403,8 +433,14 @@ fn run_hash(p: &WolfSslProvider, failures: &mut usize) {
|
|||
|
||||
match p.hash(v.algo, &data) {
|
||||
Ok(digest) if digest == expected => pass(v.name),
|
||||
Ok(digest) => { *failures += 1; fail(v.name, &digest, &expected); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", v.name); }
|
||||
Ok(digest) => {
|
||||
*failures += 1;
|
||||
fail(v.name, &digest, &expected);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} ({e})", v.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -431,16 +467,28 @@ fn run_kem(p: &WolfSslProvider, failures: &mut usize) {
|
|||
let test_a = format!("{} (Alice→Bob)", v.name);
|
||||
match p.decapsulate(v.algo, &alice_priv, &bob_pub) {
|
||||
Ok(shared) if shared.as_slice() == expected.as_slice() => pass(&test_a),
|
||||
Ok(shared) => { *failures += 1; fail(&test_a, &shared, &expected); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", test_a); }
|
||||
Ok(shared) => {
|
||||
*failures += 1;
|
||||
fail(&test_a, &shared, &expected);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} ({e})", test_a);
|
||||
}
|
||||
}
|
||||
|
||||
// Bob→Alice direction (symmetric).
|
||||
let test_b = format!("{} (Bob→Alice)", v.name);
|
||||
match p.decapsulate(v.algo, &bob_priv, &alice_pub) {
|
||||
Ok(shared) if shared.as_slice() == expected.as_slice() => pass(&test_b),
|
||||
Ok(shared) => { *failures += 1; fail(&test_b, &shared, &expected); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", test_b); }
|
||||
Ok(shared) => {
|
||||
*failures += 1;
|
||||
fail(&test_b, &shared, &expected);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} ({e})", test_b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -455,13 +503,21 @@ fn run_kem_roundtrip(p: &WolfSslProvider, failures: &mut usize) {
|
|||
for algo in [KemAlgorithm::X25519, KemAlgorithm::X448] {
|
||||
let name = format!("{:?} ephemeral roundtrip", algo);
|
||||
match p.generate_keypair(algo) {
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} (keygen: {e})", name); continue; }
|
||||
Ok(kp) => {
|
||||
match p.encapsulate(algo, &kp.public_key) {
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} (encap: {e})", name); }
|
||||
Ok(encap) => {
|
||||
match p.decapsulate(algo, &kp.private_key, &encap.ciphertext) {
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} (decap: {e})", name); }
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (keygen: {e})", name);
|
||||
continue;
|
||||
}
|
||||
Ok(kp) => match p.encapsulate(algo, &kp.public_key) {
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (encap: {e})", name);
|
||||
}
|
||||
Ok(encap) => match p.decapsulate(algo, &kp.private_key, &encap.ciphertext) {
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (decap: {e})", name);
|
||||
}
|
||||
Ok(decap_secret) => {
|
||||
if decap_secret.as_slice() == encap.shared_secret.as_slice() {
|
||||
pass(&name);
|
||||
|
|
@ -472,10 +528,8 @@ fn run_kem_roundtrip(p: &WolfSslProvider, failures: &mut usize) {
|
|||
println!(" decap: {}", hex::encode(&decap_secret));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -511,10 +565,22 @@ fn run_xchacha20_kat(p: &WolfSslProvider, failures: &mut usize) {
|
|||
// Print the ct_tag for pinning purposes.
|
||||
println!(" [INFO] {} ct_tag = {}", v.name, hex::encode(&ct_tag));
|
||||
|
||||
match p.decrypt_aead(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &ct_tag, &aad) {
|
||||
match p.decrypt_aead(
|
||||
AeadAlgorithm::XChaCha20Poly1305,
|
||||
&key,
|
||||
&nonce,
|
||||
&ct_tag,
|
||||
&aad,
|
||||
) {
|
||||
Ok(recovered) if recovered == pt => pass(&rt_name),
|
||||
Ok(_) => { *failures += 1; println!(" [FAIL] {} (decrypt mismatch)", rt_name); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} (decrypt error: {e})", rt_name); }
|
||||
Ok(_) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (decrypt mismatch)", rt_name);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (decrypt error: {e})", rt_name);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auth-failure check ────────────────────────────────────
|
||||
|
|
@ -522,10 +588,24 @@ fn run_xchacha20_kat(p: &WolfSslProvider, failures: &mut usize) {
|
|||
let auth_name = format!("{} [auth-fail]", v.name);
|
||||
let mut tampered = ct_tag.clone();
|
||||
*tampered.last_mut().unwrap() ^= 0xff;
|
||||
match p.decrypt_aead(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &tampered, &aad) {
|
||||
Err(ccc_crypto_core::error::CryptoError::AuthenticationFailed) => pass(&auth_name),
|
||||
Ok(_) => { *failures += 1; println!(" [FAIL] {} (expected auth failure, got Ok)", auth_name); }
|
||||
Err(e) => { *failures += 1; println!(" [FAIL] {} (wrong error type: {e})", auth_name); }
|
||||
match p.decrypt_aead(
|
||||
AeadAlgorithm::XChaCha20Poly1305,
|
||||
&key,
|
||||
&nonce,
|
||||
&tampered,
|
||||
&aad,
|
||||
) {
|
||||
Err(ccc_crypto_core::error::CryptoError::AuthenticationFailed) => {
|
||||
pass(&auth_name)
|
||||
}
|
||||
Ok(_) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (expected auth failure, got Ok)", auth_name);
|
||||
}
|
||||
Err(e) => {
|
||||
*failures += 1;
|
||||
println!(" [FAIL] {} (wrong error type: {e})", auth_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue