From 776d9dca7216d5cbe515310bbaf0cb5b258e42e3 Mon Sep 17 00:00:00 2001 From: Mina Hamdi Date: Mon, 27 Apr 2026 09:00:47 +0300 Subject: [PATCH 1/5] Adding a readme describing the PoC. --- README.md | 588 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 520 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 1e8af75..9490b33 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,566 @@ +# Cryptoki -# C++ & Rust Bazel Template Repository +A PKCS#11 v3.0 software token written in Rust. Drop it in where you’d use a YubiKey or a Thales HSM — anything that speaks PKCS#11 will just work. The crypto operations are behind a trait, so the backend is whatever you plug in: a software library, a TPM, a real HSM. The repo ships an OpenSSL implementation to demo the layers, but that’s not the point. -This repository serves as a **template** for setting up **C++ and Rust projects** using **Bazel**. -It provides a **standardized project structure**, ensuring best practices for: +--- -- **Build configuration** with Bazel. -- **Testing** (unit and integration tests). -- **Documentation** setup. -- **CI/CD workflows**. -- **Development environment** configuration. +## What is this? ---- +Cryptoki is a **software HSM** — a shared library (`.so` / `.dylib`) that speaks the full PKCS#11 v3.0 C API. From the caller’s perspective it’s indistinguishable from a hardware token. From our perspective it’s a chance to do things the right way: safe Rust, auditable code, no surprises. -## 📂 Project Structure - -| File/Folder | Description | -| ----------------------------------- | ------------------------------------------------- | -| `README.md` | Short description & build instructions | -| `src/` | Source files for the module | -| `tests/` | Unit tests (UT) and integration tests (IT) | -| `examples/` | Example files used for guidance | -| `docs/` | Documentation (Doxygen for C++ / mdBook for Rust) | -| `.github/workflows/` | CI/CD pipelines | -| `.vscode/` | Recommended VS Code settings | -| `.bazelrc`, `MODULE.bazel`, `BUILD` | Bazel configuration & settings | -| `project_config.bzl` | Project-specific metadata for Bazel macros | -| `LICENSE.md` | Licensing information | -| `CONTRIBUTION.md` | Contribution guidelines | +A few things worth highlighting: + +* **All 92 functions.** Every `C_*` in the OASIS PKCS#11 v3.0 (2020) spec is implemented — not stubbed, not returning `CKR_FUNCTION_NOT_SUPPORTED`. +* **Both dispatch tables.** We expose the v2.40 `CK_FUNCTION_LIST` (68 slots) and the v3.0 `CK_FUNCTION_LIST_3_0` (24 extra slots), plus `C_GetInterface` / `C_GetInterfaceList` for clients that do capability discovery. +* **Fork-safe.** A `pthread_atfork` child handler closes inherited lock FDs and reseeds the CSPRNG — no parent/child RNG state sharing. +* **Pluggable backend.** The `CryptoProvider` trait is the only interface the PKCS#11 layer talks to. It doesn't know or care what's underneath — a software library, a TPM, an HSM, whatever. The included OpenSSL implementation is a working reference, not a dependency. --- -## 🚀 Getting Started +## Getting Started + +### Prerequisites -### 1️⃣ Clone the Repository +Stable Rust toolchain. If you're using the bundled OpenSSL reference backend, you'll also need the OpenSSL dev headers (`libssl-dev` on Debian/Ubuntu, `openssl-devel` on Fedora). A custom backend may have different requirements. -```sh -git clone https://github.com/eclipse-score/YOUR_PROJECT.git -cd YOUR_PROJECT +### Build + +```bash +cargo build --release ``` -### 2️⃣ Build the Examples of module +Output lands at `target/release/libcryptoki.so` (Linux) or `target/release/libcryptoki.dylib` (macOS). + +### Run the tests -> DISCLAIMER: Depending what module implements, it's possible that different -> configuration flags needs to be set on command line. +```bash +cargo test +``` -To build all targets of the module the following command can be used: +### Try the demo -```sh -bazel build //src/... +There's an end-to-end example that walks through the full v3.0 call sequence — initialization, sessions, key generation (AES, RSA, EC, Ed25519, ChaCha20), encrypt/decrypt, sign/verify, hashing, interface discovery, and cleanup: + +```bash +cargo run --example pkcs11_demo ``` -This command will instruct Bazel to build all targets that are under Bazel -package `src/`. The ideal solution is to provide single target that builds -artifacts, for example: +### Configuration + +* **Storage path:** defaults to `~/.cryptoki/token.json`. Override with `CRYPTOKI_STORE=/path/to/store.json`. +* **Legacy algorithms:** MD5 and SHA-1 are hidden by default. Set `CRYPTOKI_LEGACY=1` to expose them — but don't do this in production. + +--- + +## Cross-Compilation (QNX SDP 8.0) + +This project supports cross-compilation for QNX SDP 8.0 (aarch64) while maintaining default compatibility with Linux x86_64 hosts. -```sh -bazel build //src/:release_artifacts +**Build Configuration:** +QNX SDP 8.0 uses the `qcc` compiler wrapper, which interprets standard `gcc` flags differently (e.g., `-V`). The `.cargo/config.toml` is pre-configured to use the `qcc` linker, and a specialized `build_qnx.sh` script is provided to correctly invoke the QNX toolchain. + +### 1. Building the Rust Library +To cross-compile the PKCS#11 library and Rust examples for QNX, use the provided helper script: +```bash +# Build library for QNX +./build_qnx.sh build + +# Build tests for QNX +./build_qnx.sh test --no-run + +# Build demos for QNX +./build_qnx.sh examples +``` +*Artifacts:* `target/aarch64-unknown-nto-qnx800/release/libcryptoki.so` and `target/aarch64-unknown-nto-qnx800/release/examples/pkcs11_demo` + +### 2. Building the C++ Tests and Demo +The C++ frontend (tests and demo) interacts with the compiled library via `dlopen`. +```bash +mkdir -p cpp/build_qnx && cd cpp/build_qnx +source ~/qnx800/qnxsdp-env.sh # Source the QNX environment + +cmake .. \ + -DCMAKE_SYSTEM_NAME=QNX \ + -DCMAKE_SYSTEM_VERSION=8.0.0 \ + -DCMAKE_C_COMPILER=qcc \ + -DCMAKE_C_COMPILER_TARGET=gcc_ntoaarch64le \ + -DCMAKE_CXX_COMPILER=qcc \ + -DCMAKE_CXX_COMPILER_TARGET=gcc_ntoaarch64le_cxx + +make +``` +*Artifacts:* `cpp/build_qnx/demo` and `cpp/build_qnx/test_cpp` + +### 3. Deployment and Execution +Transfer the compiled artifacts (`libcryptoki.so`, `pkcs11_demo`, and `demo`) to your QNX target (e.g., `/tmp/pkcs11`). + +Configure the environment and run: +```bash +# Set the library search path +export LD_LIBRARY_PATH=/tmp/pkcs11:$LD_LIBRARY_PATH + +# Optional configurations +export CRYPTOKI_STORE=/tmp/pkcs11/token.json +export CRYPTOKI_LEGACY=1 + +# Run the demos +chmod +x pkcs11_demo demo +./pkcs11_demo +./demo ``` -where `:release_artifacts` is filegroup target that collects all release -artifacts of the module. +### 4. Testing on QNX +Since `cargo test` cannot automatically run binaries on the QNX target, you must compile them on the host and run them manually on the target. -> NOTE: This is just proposal, the final decision is on module maintainer how -> the module code needs to be built. +```bash +# 1. Compile test binaries without running them +./build_qnx.sh test --no-run +``` +Transfer the compiled test binaries from `target/aarch64-unknown-nto-qnx800/release/deps/` (e.g., `signing-`) to your QNX target and execute them directly, ensuring `LD_LIBRARY_PATH` is set. + +> **Note:** The default Cargo target remains the host. Always use `--target aarch64-unknown-nto-qnx800` or the provided `build_qnx.sh` script when targeting QNX. If you add C library dependencies, update the `rustflags` in `.cargo/config.toml` or your `build.rs` accordingly. -### 3️⃣ Run Tests +--- -```sh -bazel test //tests/... +## Architecture + +The library is strictly layered. The idea is that each layer only knows about the layer below it — the FFI boundary doesn't do crypto, the crypto layer doesn't know about sessions, and so on. + +```text +┌──────────────────────────────────────────────────────────────────────┐ +│ External caller │ +└────────────────────────────┬─────────────────────────────────────────┘ + │ C ABI (unsafe extern "C") + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ src/pkcs11/mod.rs — FFI boundary + orchestrator │ +│ │ +│ GlobalState: OnceLock>> │ +│ None → Some on C_Initialize, Some → None on C_Finalize │ +│ ck_try! macro: Pkcs11Error → CK_RV at every return site │ +│ │ +│ Orchestration order for all C_* ops: │ +│ 1. check_init() / require_rw_session() │ +│ 2. mechanisms.rs — tier gate (Standard / Legacy) │ +│ 3. session.rs — op context lookup, auth checks │ +│ 4. attribute_policy.rs — ratchets, immutability, access control │ +│ 5. object_store.rs — handle → KeyObject resolution │ +│ 6. backend.rs — crypto dispatch (crypto ops only) │ +│ 7. storage.rs — persist if token object (mutating ops only) │ +└──┬──────────┬────────────┬──────────────┬────────────┬──────────────┬┘ + │ │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ │ +┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────────────────────┐ └──────┐ +│session.rs│ │token.rs │ │mechanisms │ │attribute_policy.rs │ │ +│ │ │ │ │.rs │ │ │ │ +│Per-sess. │ │Per-slot │ │ │ │One-way ratchets: │ │ +│state: │ │token │ │Standard / │ │ CKA_SENSITIVE ↑ only │ │ +│ SignCtx │ │state: │ │Legacy / │ │ CKA_EXTRACTABLE ↓ only │ │ +│ CipherCtx│ │ label │ │ │ │ CKA_WRAP_WITH_TRUSTED ↑ │ │ +│ DigestCtx│ │ state │ │RSA < 1024 │ │Immutable attrs: │ │ +│ FindCtx │ │ machine │ │→ CKR_KEY_ │ │ class, key_type, modulus│ │ +│ MsgCtx │ │ Argon2id│ │SIZE_RANGE │ │Derived attrs: │ │ +│ │ │ PIN │ │ │ │ always_sensitive │ │ +│Login │ │ hashes │ │Legacy gate │ │ never_extractable │ │ +│state │ │ PIN │ │via env var │ │CKA_VALUE blocking │ │ +│always_ │ │ lockout │ │ │ │ │ │ +│auth │ │ counters│ │ │ │ │ │ +└──────────┘ └──────────┘ └────────────┘ └──────────────────────────┘ │ + │ + (mod.rs → object_store) + │ + ▼ + ┌───────────────────────────────────────────────────┐ + │ object_store.rs │ + │ │ + │ KeyObject { │ + │ handle, slot_id, key_type: KeyType, │ + │ key_ref: EngineKeyRef, ← Zeroizing> │ + │ attributes: HashMap>│ + │ local, always_sensitive, never_extractable, │ + │ always_authenticate, key_gen_mechanism │ + │ } │ + │ │ + │ Session objects: tagged by creating_session, │ + │ destroyed on CloseSession │ + │ Token objects: auto-persisted, survive Finalize │ + │ Profile objects: token lifetime, never to disk │ + └──────────────┬────────────────────────────────────┘ + │ + ┌──────────────┴──────────────┐ + │ │ + ▼ ▼ + ┌──────────────────────────┐ ┌───────────────────────────────────┐ + │ storage.rs │ │ backend.rs │ + │ │ │ │ + │ JSON persistence: │ │ Crypto dispatch (no OpenSSL): │ + │ • NamedTempFile → fsync │ │ sign / verify / digest │ + │ → rename (atomic) │ │ encrypt / decrypt (sym + asym) │ + │ • flock on .lock sidecar│ │ wrap / unwrap (AES Key Wrap) │ + │ • dir 0700, file 0600 │ │ message-based AEAD │ + │ • Argon2id PIN hashes │ │ HKDF derive │ + │ • LOCK_FILE_FD: AtomicI32 │ attribute extraction │ + │ for atfork safety │ │ │ + └──────────────────────────┘ └─────────────────┬─────────────────┘ + │ + ▼ + ┌─────────────────────────────────┐ + │ registry.rs │ + │ │ + │ global_slot_id → │ + │ (Arc, │ + │ internal_slot_id) │ + │ │ + │ for_each_engine(f): iterates │ + │ all engines (atfork handler) │ + └──────────────┬──────────────────┘ + │ + ▼ + ┌─────────────────────────────────┐ + │ traits.rs — CryptoProvider │ + │ │ + │ Object-safe. No generics. │ + │ No PKCS#11 knowledge. │ + │ │ + │ serialize_key / deserialize_key │ + │ key_value_for_digest (fail-safe)│ + │ post_fork_child() default no-op │ + │ mechanism_info() per mechanism │ + └──────────────┬──────────────────┘ + │ + ▼ + ┌─────────────────────────────────┐ + │ your_backend.rs │ + │ │ + │ Implements CryptoProvider. │ + │ Could be OpenSSL, a TPM, an │ + │ HSM — anything you plug in. │ + └─────────────────────────────────┘ ``` +### Layer Dependency Rules + +Hard rules — nothing reaches up or sideways: + +| Layer | May call | Must NOT call | +| --- | --- | --- | +| `mod.rs` | All layers | — | +| `session.rs` | — | backend, storage, object_store, registry | +| `token.rs` | — | backend, storage, object_store, registry | +| `mechanisms.rs` | — | all others | +| `attribute_policy.rs` | `object_store` types only | backend, storage, registry | +| `object_store.rs` | `storage`, `registry`, `token` | `backend` | +| `backend.rs` | `registry`, `traits` | `storage`, `session`, `object_store` | +| `storage.rs` | `traits` (Provider param only) | `backend`, `session`, `registry` | +| `registry.rs` | `traits` | all PKCS#11 layers | +| `your_backend.rs` | its own crypto deps | all PKCS#11 layers | + +--- + +## Security + +Key material handling was a first-class concern from the start, not an afterthought. Here's how the main pieces fit together: + +### 1. The PKCS#11 layer never sees key bytes + +All key material is an opaque `EngineKeyRef` (`Zeroizing>`). The orchestration code in `mod.rs` passes handles around without ever interpreting them — only the Provider (`openssl_provider.rs`) knows what the bytes mean. If a backend can't digest an opaque handle, `key_value_for_digest` fails closed with `CKR_MECHANISM_INVALID`. + +### 2. Buffers are zeroed on drop + +We use the `zeroize` crate throughout the call stack: + +* `KeyObject.key_ref` +* Decrypt outputs, HKDF derives, AES key unwraps +* Intermediate buffers inside the crypto backend + +### 3. PINs are Argon2id hashes, not passwords + +PIN management is entirely separate from the crypto backend — the `argon2` crate handles it directly. Parameters: 64 MiB memory, 3 iterations, 4 parallelism, 32-byte output. SO and User PINs are hashed independently; the PHC strings live in the token state. + +### 4. Writes are atomic + +Torn writes would corrupt the token state permanently. We use a 6-step sequence to prevent that: + +1. Serialize to JSON in memory. +2. Write to a `NamedTempFile` in the same directory as the target. +3. `chmod 0o600` the temp file. +4. `fsync` the file data. +5. `rename(2)` into place — atomic on POSIX. +6. `fsync` the parent directory. + +Cross-process exclusion is a `flock(LOCK_EX)` on a `.lock` sidecar. The FD lives in an `AtomicI32` (not a Mutex) so it's safe to close inside an `atfork` handler. + +### 5. Attribute ratchets + +Some attribute transitions are one-way and we enforce that strictly in `C_SetAttributeValue` and `C_CopyObject`: + +* `CKA_SENSITIVE` can only go `FALSE → TRUE` +* `CKA_EXTRACTABLE` can only go `TRUE → FALSE` +* Key type, modulus, and class are immutable once set +* `CKA_LOCAL`, `CKA_ALWAYS_SENSITIVE`, `CKA_KEY_GEN_MECHANISM` are computed by us — we never trust the caller's value + +### 6. Fork safety + +`C_Initialize` registers a `pthread_atfork` child handler. On fork, the child: + +1. Closes inherited lock FDs. +2. Reseeds the CSPRNG (`openssl::rand::rand_bytes`) — parent and child must not share RNG state. +3. Calls `post_fork_child()` on every registered Provider. + +### 7. Per-operation authentication + +Keys with `CKA_ALWAYS_AUTHENTICATE=TRUE` need a fresh `C_Login(CKU_CONTEXT_SPECIFIC)` before every crypto operation — not just at session open. `C_WrapKey` also enforces `CKA_WRAP=TRUE`, checks extractability, and honours `CKA_TRUSTED` / `CKA_WRAP_WITH_TRUSTED` ACLs. + --- -## 🛠 Tools & Linters +## Threat Model + +### What we protect against + +| Threat | Mitigation | +| --- | --- | +| Concurrent writes from multiple processes | `flock(LOCK_EX)` on `.lock` sidecar serializes all writers | +| Torn writes / crash during storage update | Atomic `NamedTempFile → rename(2)` — partial writes never visible | +| Weak RSA keys | Minimum 1024 bits enforced | +| Broken cryptography (MD5, SHA-1) | Legacy tier explicitly gated behind `CRYPTOKI_LEGACY=1` | +| Key material leakage via process memory | `Zeroizing>` zeros buffers on drop throughout the call stack | +| Fork without CSPRNG reseed | `pthread_atfork` child handler forces `rand_bytes` reseed | +| Unauthorized key extraction | `CKA_SENSITIVE` / `CKA_EXTRACTABLE` ratchets; `CKA_VALUE` returns `CKR_ATTRIBUTE_SENSITIVE` | +| Key attribute downgrade | One-way ratchets enforced on `C_SetAttributeValue` and `C_CopyObject` | +| Unattended operations on privileged keys | `CKA_ALWAYS_AUTHENTICATE` one-shot context login required per operation | +| Untrusted key wrapping | `CKA_WRAP_WITH_TRUSTED` / `CKA_TRUSTED` ACL on `C_WrapKey` | +| Session objects leaking to disk | `CKA_TOKEN=FALSE` objects are never written to storage | -The template integrates **tools and linters** from **centralized repositories** to ensure consistency across projects. +### Out of scope -- **C++:** `clang-tidy`, `cppcheck`, `Google Test` -- **Rust:** `clippy`, `rustfmt`, `Rust Unit Tests` -- **CI/CD:** GitHub Actions for automated builds and tests +* **Malicious callers.** PKCS#11 loads into the caller's process — we can't protect against the process itself. +* **Physical memory attacks.** We don't `mlock()` yet, so keys can be paged to disk. This is on the roadmap. +* **Encryption at rest.** The token file is plaintext JSON today. Argon2id covers the PINs but not the key material. See Roadmap. --- -## 📖 Documentation +## PKCS#11 v3.0 Compliance -- A **centralized docs structure** is planned. +All 92 functions are implemented. RW session enforcement is strict — anything that mutates persistent state requires a session opened with `CKF_RW_SESSION`. + +A few honest deviations: + +* `C_MessageSignInit` / `C_MessageVerifyInit` → `CKR_FUNCTION_NOT_SUPPORTED` (message-based signing is not yet implemented). +* Multi-part digest+crypto combinators (`C_DigestEncryptUpdate`, etc.) → `CKR_FUNCTION_NOT_SUPPORTED`. +* `C_OpenSession` without `CKF_SERIAL_SESSION` → rejected. Parallel sessions are not supported. +* `C_SeedRandom` → `CKR_RANDOM_SEED_NOT_SUPPORTED`. We use the OS entropy source directly. --- -## ⚙️ `project_config.bzl` +## Cryptographic Mechanisms Supported + +| Mechanism | `CKM_*` constant | Key type | Tier | +| --- | --- | --- | --- | +| RSA key pair generation | `CKM_RSA_PKCS_KEY_PAIR_GEN` | RSA ≥ 1024 bits | Standard | +| EC key pair generation | `CKM_EC_KEY_PAIR_GEN` | P-256 | Standard | +| EdDSA key pair generation | `CKM_EC_EDWARDS_KEY_PAIR_GEN` | Ed25519 | Standard | +| AES key generation | `CKM_AES_KEY_GEN` | AES 128/192/256 | Standard | +| ChaCha20 key generation | `CKM_CHACHA20_KEY_GEN` | ChaCha20 256 bit | Standard | +| RSA PKCS#1 v1.5 encrypt/decrypt | `CKM_RSA_PKCS` | RSA | Standard | +| RSA OAEP encrypt/decrypt | `CKM_RSA_PKCS_OAEP` | RSA | Standard | +| AES-CBC with PKCS#7 padding | `CKM_AES_CBC_PAD` | AES | Standard | +| AES-GCM | `CKM_AES_GCM` | AES | Standard | +| AES-CTR | `CKM_AES_CTR` | AES | Standard | +| ChaCha20-Poly1305 | `CKM_CHACHA20_POLY1305` | ChaCha20 | Standard | +| RSA PKCS#1 v1.5 sign/verify (SHA-256/384/512) | `CKM_SHA***_RSA_PKCS` | RSA | Standard | +| RSA-PSS sign/verify (SHA-256/384/512) | `CKM_SHA***_RSA_PKCS_PSS` | RSA | Standard | +| ECDSA (prehashed, SHA256/384/512) | `CKM_ECDSA_*` | EC | Standard | +| EdDSA (Ed25519) | `CKM_EDDSA` | EdDSA | Standard | +| SHA-2 / SHA-3 digests | `CKM_SHA***` / `CKM_SHA3_***` | — | Standard | +| HKDF key derivation | `CKM_HKDF_DERIVE` | AES/generic | Standard | +| AES Key Wrap (RFC 3394) | `CKM_AES_KEY_WRAP` | AES | Standard | +| MD5 / SHA-1 digest | `CKM_MD5` / `CKM_SHA_1` | — | Legacy | +| RSA PKCS#1 v1.5 / PSS sign/verify (SHA-1) | `CKM_SHA1_RSA_*` | RSA | Legacy | +| RSA keygen < 1024 bits | — | RSA | Rejected (`CKR_KEY_SIZE_RANGE`) | -This file defines project-specific metadata used by Bazel macros, such as `dash_license_checker`. +--- -### 📌 Purpose +## Adding a Backend -It provides structured configuration that helps determine behavior such as: +This is the whole point of the architecture. Implement the `CryptoProvider` trait in `src/traits.rs` and the entire PKCS#11 stack works on top of it — sessions, objects, attribute policy, PIN management, all of it. Nothing else needs to change. -- Source language type (used to determine license check file format) -- Safety level or other compliance info (e.g. ASIL level) +```rust +struct MyProvider { /* internal state */ } -### 📄 Example Content +impl CryptoProvider for MyProvider { + // 1. Slot metadata + fn slot_count(&self) -> u32 { 1 } + fn slot_description(&self, _slot: u32) -> String { "My Provider".into() } + fn token_model(&self, _slot: u32) -> String { "v1.0".into() } + fn supported_mechanisms(&self, _slot: u32) -> Vec { vec![…] } -```python -PROJECT_CONFIG = { - "asil_level": "QM", # or "ASIL-A", "ASIL-B", etc. - "source_code": ["cpp", "rust"] # Languages used in the module + // 2. Key serialization + fn serialize_key(&self, key_ref: &EngineKeyRef) -> Result, CryptoError> { … } + fn deserialize_key(&self, bytes: &[u8]) -> Result { … } + + // 3. Crypto operations (sign, verify, etc.) + fn sign(&self, …) -> Result, CryptoError> { … } + // ... } ``` -### 🔧 Use Case +Register it in `src/pkcs11/mod.rs`: + +```rust +let _ = crate::registry::register_engine(MyEngine::new()); +``` + +Multiple engines coexist fine — each gets sequential global slot IDs. + +--- + +## Test Coverage + +### Rust Integration Tests + +Every test goes through the C ABI function pointers (`fn_list()` / `fn_list_3_0()`) — the same path a real PKCS#11 consumer would take. No shortcuts. + +Current Status: 197 tests — 0 failures + +```text +[████████████████████] 197 / 197 100% +``` + +| Suite | Focus Area | +| --- | --- | +| `pkcs11_integration` | Full `C_*` path, session lifecycle, key generation, fallback, error casing. | +| `pkcs11_v3_integration` | v3.0 specific features (EdDSA, ChaCha20, SHA-3, discovery, session cancels). | +| `attribute_policy` | Ratchet enforcement, immutable attributes, secure keygen defaults. | +| `storage_atomic_writes` | Cross-process serialization, atomic temp-to-rename writes, permission sets. | +| `ro_session` | Ensures mutating ops strictly return `CKR_SESSION_READ_ONLY` on read-only sessions. | +| `engine_integration` | Direct `CryptoProvider` stress testing, tamper detection, error codes. | +| `always_authenticate` | Validates per-operation context logins and auth ticket consumption. | + +*(For full coverage details, see the inline module docs within the test suite).* + +### Google Test Conformance Suite + +On top of the Rust tests, we run a C++ Google Test suite (`cpp/pkcs11test/`) that loads the compiled `.so` as a black box and drives it through the public C ABI — no Rust internals, just raw `C_*` calls. It's the closest thing to a third-party integration test we have. + +Current Status: 206 tests from 15 suites — 0 failures + +```text +[████████████████████] 206 / 206 100% +``` + +| Suite | Count | Focus Area | +| --- | --- | --- | +| `PKCS11Test` | 32 | Token lifecycle: `C_InitToken`, `C_InitPIN`, `C_SetPIN`, `C_Login`/`C_Logout`. | +| `ReadOnlySessionTest` | 23 | Session state machine, R/O vs R/W enforcement, `C_GetSessionInfo`. | +| `ReadWriteSessionTest` | 21 | Object creation, copy, destroy, and attribute reads on R/W sessions. | +| `Digests/DigestTest` | 75 | All supported hash algorithms via single and multi-part `C_Digest`. | +| `Signatures/SignTest` | 18 | Sign/verify across RSA PKCS#1, RSA-PSS, ECDSA, and EdDSA. | +| `ROUserSessionTest` | 6 | User-login gate: operations requiring `CKU_USER` on R/O sessions. | +| `RWUserSessionTest` | 3 | User-login gate on R/W sessions. | +| `DataObjectTest` | 7 | `CKO_DATA` objects: create, set attributes, retrieve, destroy. | +| `RWSOSessionTest` | 2 | SO-login operations on R/W sessions. | +| `Init` | 11 | `C_Initialize` / `C_Finalize` sequencing and error codes. | +| `RNG` | 2 | `C_GenerateRandom` correctness and length contracts. | +| `BERDecode` | 3 | ASN.1/BER decode helpers used internally by the test harness. | + +> The `Ciphers*`, `HMACs*`, `Duals*`, `DES`, `MD5-RSA`, and `SHA1-RSA` suites are excluded from the standard run — they cover operations not yet in scope for this release. + +#### Fetching and Running the Conformance Suite + +Follow these steps to initialize the submodule, compile the libraries, and execute the tests: + +```bash +# 1. Fetch the Google Test Conformance Suite submodule +git submodule update --init --recursive + +# 2. Build the Rust shared library +cargo build + +# 3. Build the C++ test binary +cd cpp && mkdir -p build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Debug && make + +# 4. Clear the token storage to start fresh +rm -rf ~/.cryptoki/token.json + +# 5. Run the conformance suite against the Rust library +./pkcs11test -m libcryptoki.so -l ../../target/debug -s 0 -u 1234 -o so-pin -I --gtest_filter=-Ciphers*:HMACs*:Duals*:*DES*:*MD5-RSA*:*SHA1-RSA* +``` + +Optional flags: + +| Flag | Description | +| --- | --- | +| `-u ` | Override the User PIN (default: `1234`) | +| `-o ` | Override the SO PIN (default: `so-pin`) | +| `-s ` | Target a specific slot ID | +| `-v` | Verbose output | +| `-I` | Run `C_InitToken` at startup (reinitializes the token) | + +--- + +## Known Limitations & Roadmap + +Honest status of what's missing and what's next: + +1. **Encryption at rest.** `token.json` stores key bytes as plaintext. PINs are Argon2id hashes so they're safe, but the key material itself is not encrypted. The plan is envelope encryption — a random DEK wrapped by PIN-derived KEKs for both SO and User. +2. **Page locking.** Keys can be paged to disk because we don't call `mlock(2)` yet. It's on the list. +3. **Algorithm gaps.** Missing: P-384/P-521, Ed448, HMAC keygen, PBKDF2, and raw RSA PKCS#8 import via `C_CreateObject`. +4. **Tooling validation.** We haven't run against `pkcs11-tool` or `p11-kit` yet. Probably fine, but it needs to be verified. + +--- + +## Requirements Traceability + +**Coverage** `[████████████░░░░░░░░] 25 / 42 60%` + +| Requirement ID | Title | Type | Security | Safety | Status | Covered | Details | +|---|---|---|---|---|---|---|---| +| feat_req__sec_crypt__sym_symmetric_encrypt | Symmetric Encryption and Decryption | Functional | YES | QM | valid | YES | `C_Encrypt`, `C_Decrypt`, `src/pkcs11/backend.rs` | +| feat_req__sec_crypt__sym_symm_algo_aes_cbc | AES-CBC Support | Functional | YES | QM | valid | YES | `CKM_AES_CBC_PAD` | +| feat_req__sec_crypt__sym_sym_algo_aes_gcm | AES-GCM Support | Functional | YES | QM | valid | YES | `CKM_AES_GCM` | +| feat_req__sec_crypt__sym_sym_algo_aes_ccm | AES-CCM Support | Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__sym_algo_chacha20 | ChaCha20-Poly1305 Support | Functional | YES | QM | valid | YES | `CKM_CHACHA20_POLY1305` | +| feat_req__sec_crypt__asym_encryption | Asymmetric Encryption/Decryption | Functional | YES | QM | valid | YES | `C_Encrypt`, `C_Decrypt`, `CryptoProvider` | +| feat_req__sec_crypt__asym_algo_ecdh | ECDH Support | Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__sig_creation | Signature Creation | Functional | YES | QM | valid | YES | `C_Sign`, `CryptoProvider` | +| feat_req__sec_crypt__sig_verification | Signature Verification | Functional | YES | QM | valid | YES | `C_Verify`, `CryptoProvider` | +| feat_req__sec_crypt__sig_algo_ecdsa | ECDSA Support | Functional | YES | QM | valid | YES | `CKM_ECDSA` | +| feat_req__sec_crypt__mac | Message Authentication Code | Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__hashing | Hashing Functionality | Functional | YES | QM | valid | YES | `C_Digest`, `CryptoProvider` | +| feat_req__sec_crypt__hashing_algo_sha2 | SHA-2 Support | Functional | YES | QM | valid | YES | `CKM_SHA256`, etc. | +| feat_req__sec_crypt__hashing_algo_sha3 | SHA-3 Support | Functional | YES | QM | valid | YES | `CKM_SHA3_256`, etc. | +| feat_req__sec_crypt__kdf | Key Derivation | Functional | YES | QM | valid | YES | `C_DeriveKey`, `hkdf_derive` | +| feat_req__sec_crypt__rng | Entropy Source | Functional | YES | QM | valid | YES | `C_GenerateRandom`, `rand_bytes` | +| feat_req__sec_crypt__rng_algo_chacha20rng | ChaCha20Rng Support | Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__cert_management | Certificate Management | Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__key_generation | Secure Key Generation | Functional | YES | QM | valid | YES | `C_GenerateKey`, `C_GenerateKeyPair` | +| feat_req__sec_crypt__key_import | Secure Key Import | Functional | YES | QM | valid | YES | `C_CreateObject`: AES/secret via `CKA_VALUE`, EC public via `CKA_EC_PARAMS`+`CKA_EC_POINT`, RSA public/private and EC private by key type | +| feat_req__sec_crypt__key_storage | Secure Key Storage | Functional | YES | QM | valid | YES | `storage.rs`, `object_store.rs` | +| feat_req__sec_crypt__key_deletion | Secure Key Deletion | Functional | YES | QM | valid | YES | `C_DestroyObject`, `storage.rs` | +| feat_req__sec_crypt__flexible_api | API Algorithm Selection | Functional | YES | QM | valid | YES | `CryptoProvider`, `registry.rs` | +| feat_req__sec_crypt__tls_support | TLS Support | Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__performance_tooling | Benchmark tooling | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__algo_naming | Standardized Algorithm Naming | Non-Functional | YES | QM | valid | YES | `CKM_*` mappings | +| feat_req__sec_crypt__no_key_exposure | No Key Material Exposure | Non-Functional | YES | QM | valid | YES | `EngineKeyRef`, `attribute_policy.rs` | +| feat_req__sec_crypt__side_channel_mitigation| Side-Channel Attack Mitigation | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__api_lifecycle | API Lifecycle Management | Non-Functional | YES | QM | valid | YES | `C_Initialize`, `C_Finalize`, etc. | +| feat_req__sec_crypt__error_handling | Structured Error Handling | Non-Functional | YES | QM | valid | YES | `ck_try!`, `error.rs` | +| feat_req__sec_crypt__security_concept | Security Concept | Non-Functional | YES | QM | valid | YES | Security Architecture Documented | +| feat_req__sec_crypt__algo_updates | Crypto Algorithm Update Strategy | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__reverse_eng_protection | Reverse Engineering Protection | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__production_keys | Initial Production Key Handling | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__pqc_readiness | Post-Quantum Readiness | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__hw_acceleration | Hardware Acceleration Support | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__sw_fallback | Software Fallback | Non-Functional | YES | QM | valid | YES | Pluggable `CryptoProvider` (reference: `openssl_provider.rs`) | +| feat_req__sec_crypt__trusted_time | Trusted Time Source | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__os_protection | OS-Level Protection | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__access_control | Access Control | Non-Functional | YES | QM | valid | YES | `C_Login`, `attribute_policy.rs` | +| feat_req__sec_crypt__ids_integration | IDS Integration | Non-Functional | YES | QM | valid | NO | N/A | +| feat_req__sec_crypt__dos_mitigation | DoS Mitigation | Non-Functional | YES | QM | valid | NO | N/A | + +--- + +## License -When used with macros like `dash_license_checker`, it allows dynamic selection of file types - (e.g., `cargo`, `requirements`) based on the languages declared in `source_code`. +This project is licensed under the Apache License, Version 2.0. See the `NOTICE` file(s) distributed with this work for additional information regarding copyright ownership. From ca0eadce9f7736549bf18b9713db4dbe5d8d78a2 Mon Sep 17 00:00:00 2001 From: oeweda Date: Thu, 30 Apr 2026 21:06:20 +0300 Subject: [PATCH 2/5] Code Base For Cryptoki PKCS#11 Rust Implementaion --- .cargo/config.toml | 3 + .gitignore | 7 + .gitmodules | 3 + BUILD | 111 +- CONTRIBUTION.md | 38 +- Cargo.lock | 965 +++++++++++++++ Cargo.toml | 34 + LICENSE | 180 +-- MODULE.bazel | 71 +- NOTICE | 34 +- README.md | 170 ++- build_qnx.sh | 32 + cpp/BUILD | 35 + cpp/CMakeLists.txt | 24 + cpp/demo.cpp | 316 +++++ cpp/pkcs11.h | 421 +++++++ cpp/pkcs11_cpp.hpp | 815 +++++++++++++ cpp/test_cpp.cpp | 307 +++++ docs/BUILD | 11 + docs/conf.py | 60 +- docs/index.rst | 84 +- examples/BUILD | 31 +- examples/pkcs11_business_demo.rs | 289 +++++ examples/pkcs11_demo.rs | 739 ++++++++++++ extensions.bzl | 35 + project_config.bzl | 6 +- pyproject.toml | 6 +- src/BUILD | 29 + src/attributes.rs | 146 +++ src/error.rs | 123 ++ src/lib.rs | 61 + src/openssl_engine.rs | 860 +++++++++++++ src/openssl_provider.rs | 1073 +++++++++++++++++ src/pkcs11/attribute_policy.rs | 176 +++ src/pkcs11/backend.rs | 71 ++ src/pkcs11/backend/attributes.rs | 34 + src/pkcs11/backend/digest_random.rs | 43 + src/pkcs11/backend/keygen.rs | 198 +++ src/pkcs11/backend/message_aead.rs | 48 + src/pkcs11/backend/rsa_wrap_derive.rs | 68 ++ src/pkcs11/backend/sign_verify.rs | 99 ++ src/pkcs11/backend/symmetric.rs | 149 +++ src/pkcs11/constants.rs | 460 +++++++ src/pkcs11/error.rs | 151 +++ src/pkcs11/ffi_api_core.rs | 12 + .../keys_objects_attributes_find.rs | 503 ++++++++ .../ffi_api_core/lifecycle_and_slot_token.rs | 407 +++++++ src/pkcs11/ffi_api_core/session_and_login.rs | 249 ++++ src/pkcs11/ffi_api_crypto.rs | 18 + src/pkcs11/ffi_api_crypto/digest.rs | 148 +++ src/pkcs11/ffi_api_crypto/encrypt_decrypt.rs | 237 ++++ src/pkcs11/ffi_api_crypto/helpers.rs | 113 ++ src/pkcs11/ffi_api_crypto/key_wrap_derive.rs | 272 +++++ src/pkcs11/ffi_api_crypto/misc_v240.rs | 278 +++++ src/pkcs11/ffi_api_crypto/sign_verify.rs | 279 +++++ src/pkcs11/ffi_api_v3.rs | 14 + src/pkcs11/ffi_api_v3/interface_discovery.rs | 68 ++ .../ffi_api_v3/message_encrypt_decrypt.rs | 230 ++++ src/pkcs11/ffi_api_v3/message_sign_verify.rs | 160 +++ src/pkcs11/ffi_api_v3/session_user.rs | 109 ++ src/pkcs11/mechanisms.rs | 68 ++ src/pkcs11/mod.rs | 404 +++++++ src/pkcs11/object_store.rs | 425 +++++++ src/pkcs11/session.rs | 292 +++++ src/pkcs11/storage.rs | 31 + src/pkcs11/storage/helpers.rs | 22 + src/pkcs11/storage/io.rs | 89 ++ src/pkcs11/storage/locks.rs | 29 + src/pkcs11/storage/models.rs | 219 ++++ src/pkcs11/storage/path.rs | 21 + src/pkcs11/token.rs | 330 +++++ src/pkcs11/types.rs | 428 +++++++ src/registry.rs | 182 +++ src/traits.rs | 330 +++++ src/types.rs | 140 +++ tests/BUILD | 69 ++ tests/always_authenticate.rs | 254 ++++ tests/attribute_policy.rs | 425 +++++++ tests/common/mod.rs | 103 ++ tests/connect_disconnect.rs | 106 ++ tests/copy_object.rs | 248 ++++ tests/cpp/BUILD | 50 +- tests/cpp/run_cpp_tests.sh | 19 + tests/cpp/run_pkcs11test.sh | 15 + tests/cpp/run_test_cpp.sh | 8 + tests/cpp/test_main.cpp | 43 +- tests/encryption.rs | 423 +++++++ tests/engine_integration.rs | 396 ++++++ tests/hashing.rs | 377 ++++++ tests/lifecycle.rs | 287 +++++ tests/mechanism_policy.rs | 306 +++++ tests/message_api.rs | 471 ++++++++ tests/misc.rs | 113 ++ tests/object_management.rs | 410 +++++++ tests/persistence_integration.rs | 267 ++++ tests/pkcs11_integration.rs | 632 ++++++++++ tests/pkcs11_v3_integration.rs | 487 ++++++++ tests/profile_objects.rs | 246 ++++ tests/ro_session.rs | 285 +++++ tests/rust/BUILD | 37 +- tests/rust/run_rust_tests.sh | 7 + tests/rust/test_main.rs | 4 +- tests/session_vs_token_objects.rs | 343 ++++++ tests/signing.rs | 357 ++++++ tests/slots_and_tokens.rs | 133 ++ tests/storage_atomic_writes.rs | 307 +++++ tests/token_info.rs | 236 ++++ tests/wrap_acl.rs | 382 ++++++ 108 files changed, 22023 insertions(+), 546 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 .gitmodules create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100755 build_qnx.sh create mode 100644 cpp/BUILD create mode 100644 cpp/CMakeLists.txt create mode 100644 cpp/demo.cpp create mode 100644 cpp/pkcs11.h create mode 100644 cpp/pkcs11_cpp.hpp create mode 100644 cpp/test_cpp.cpp create mode 100644 docs/BUILD create mode 100644 examples/pkcs11_business_demo.rs create mode 100644 examples/pkcs11_demo.rs create mode 100644 extensions.bzl create mode 100644 src/attributes.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100755 src/openssl_engine.rs create mode 100644 src/openssl_provider.rs create mode 100644 src/pkcs11/attribute_policy.rs create mode 100644 src/pkcs11/backend.rs create mode 100644 src/pkcs11/backend/attributes.rs create mode 100644 src/pkcs11/backend/digest_random.rs create mode 100644 src/pkcs11/backend/keygen.rs create mode 100644 src/pkcs11/backend/message_aead.rs create mode 100644 src/pkcs11/backend/rsa_wrap_derive.rs create mode 100644 src/pkcs11/backend/sign_verify.rs create mode 100644 src/pkcs11/backend/symmetric.rs create mode 100644 src/pkcs11/constants.rs create mode 100644 src/pkcs11/error.rs create mode 100644 src/pkcs11/ffi_api_core.rs create mode 100644 src/pkcs11/ffi_api_core/keys_objects_attributes_find.rs create mode 100644 src/pkcs11/ffi_api_core/lifecycle_and_slot_token.rs create mode 100644 src/pkcs11/ffi_api_core/session_and_login.rs create mode 100644 src/pkcs11/ffi_api_crypto.rs create mode 100644 src/pkcs11/ffi_api_crypto/digest.rs create mode 100644 src/pkcs11/ffi_api_crypto/encrypt_decrypt.rs create mode 100644 src/pkcs11/ffi_api_crypto/helpers.rs create mode 100644 src/pkcs11/ffi_api_crypto/key_wrap_derive.rs create mode 100644 src/pkcs11/ffi_api_crypto/misc_v240.rs create mode 100644 src/pkcs11/ffi_api_crypto/sign_verify.rs create mode 100644 src/pkcs11/ffi_api_v3.rs create mode 100644 src/pkcs11/ffi_api_v3/interface_discovery.rs create mode 100644 src/pkcs11/ffi_api_v3/message_encrypt_decrypt.rs create mode 100644 src/pkcs11/ffi_api_v3/message_sign_verify.rs create mode 100644 src/pkcs11/ffi_api_v3/session_user.rs create mode 100644 src/pkcs11/mechanisms.rs create mode 100644 src/pkcs11/mod.rs create mode 100644 src/pkcs11/object_store.rs create mode 100644 src/pkcs11/session.rs create mode 100644 src/pkcs11/storage.rs create mode 100644 src/pkcs11/storage/helpers.rs create mode 100644 src/pkcs11/storage/io.rs create mode 100644 src/pkcs11/storage/locks.rs create mode 100644 src/pkcs11/storage/models.rs create mode 100644 src/pkcs11/storage/path.rs create mode 100644 src/pkcs11/token.rs create mode 100644 src/pkcs11/types.rs create mode 100644 src/registry.rs create mode 100644 src/traits.rs create mode 100644 src/types.rs create mode 100644 tests/BUILD create mode 100644 tests/always_authenticate.rs create mode 100644 tests/attribute_policy.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/connect_disconnect.rs create mode 100644 tests/copy_object.rs create mode 100755 tests/cpp/run_cpp_tests.sh create mode 100644 tests/cpp/run_pkcs11test.sh create mode 100644 tests/cpp/run_test_cpp.sh create mode 100644 tests/encryption.rs create mode 100644 tests/engine_integration.rs create mode 100644 tests/hashing.rs create mode 100644 tests/lifecycle.rs create mode 100644 tests/mechanism_policy.rs create mode 100644 tests/message_api.rs create mode 100644 tests/misc.rs create mode 100644 tests/object_management.rs create mode 100644 tests/persistence_integration.rs create mode 100644 tests/pkcs11_integration.rs create mode 100644 tests/pkcs11_v3_integration.rs create mode 100644 tests/profile_objects.rs create mode 100644 tests/ro_session.rs create mode 100755 tests/rust/run_rust_tests.sh create mode 100644 tests/session_vs_token_objects.rs create mode 100644 tests/signing.rs create mode 100644 tests/slots_and_tokens.rs create mode 100644 tests/storage_atomic_writes.rs create mode 100644 tests/token_info.rs create mode 100644 tests/wrap_acl.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..8226c77 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[target.aarch64-unknown-nto-qnx800] +linker = "qcc" +rustflags = ["-C", "link-arg=-Vgcc_ntoaarch64le"] diff --git a/.gitignore b/.gitignore index e7dc329..1ba1e5c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,10 @@ styles/ .venv __pycache__/ /.coverage + +# Ignore C++ native build directory +/cpp/build/* + +# Rust +target/ +**/*.rs.bk diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..450a057 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cpp/pkcs11test"] + path = cpp/pkcs11test + url = https://github.com/google/pkcs11test.git diff --git a/BUILD b/BUILD index 473b5d5..8037cd3 100644 --- a/BUILD +++ b/BUILD @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation +# Copyright (c) 2026 Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -11,38 +11,97 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@score_docs_as_code//:docs.bzl", "docs") -load("@score_tooling//:defs.bzl", "copyright_checker", "dash_license_checker", "setup_starpls", "use_format_targets") +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") load("//:project_config.bzl", "PROJECT_CONFIG") -setup_starpls( - name = "starpls_server", - visibility = ["//visibility:public"], -) +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "Cargo.toml", + "Cargo.lock", + "README.md", + "LICENSE", + "NOTICE", + "CONTRIBUTION.md", + "MODULE.bazel", + "project_config.bzl", + "pyproject.toml", +]) -copyright_checker( - name = "copyright", +filegroup( + name = "rust_srcs", srcs = [ - "src", - "tests", - "//:BUILD", - "//:MODULE.bazel", - ], - config = "@score_tooling//cr_checker/resources:config", - template = "@score_tooling//cr_checker/resources:templates", - visibility = ["//visibility:public"], + "Cargo.toml", + "Cargo.lock", + "//src:src", + "//examples:examples", + "//tests/rust:test_main.rs", + ] + glob([ + "tests/*.rs", + "tests/common/**/*.rs", + ], allow_empty = True), +) + +filegroup( + name = "cpp_srcs", + srcs = glob([ + "cpp/**/*.cpp", + "cpp/**/*.cc", + "cpp/**/*.h", + "cpp/**/*.hpp", + "cpp/CMakeLists.txt", + ]), +) + +sh_binary( + name = "cargo_build", + srcs = ["tools/bazel/cargo_build.sh"], + data = [":rust_srcs"], +) + +sh_binary( + name = "cargo_test", + srcs = ["tools/bazel/cargo_test.sh"], + data = [":rust_srcs"], +) + +# Top-level aliases for ergonomic `bazel build/test //:foo` invocations +alias( + name = "docs", + actual = "//docs:docs", ) -dash_license_checker( - src = "//examples:cargo_lock", - file_type = "", # let it auto-detect based on project_config - project_config = PROJECT_CONFIG, - visibility = ["//visibility:public"], +alias( + name = "rust_lib", + actual = "//src:cryptoki_lib", ) -# Add target for formatting checks -use_format_targets() +alias( + name = "rust_unit_smoke", + actual = "//tests/rust:rust_unit_smoke", +) + +alias( + name = "tests_rust", + actual = "//tests:integration_tests", +) + +alias( + name = "tests_cpp", + actual = "//tests/cpp:test_cpp", +) + +alias( + name = "tests_pkcs11_conformance", + actual = "//tests/cpp:pkcs11test", +) + +alias( + name = "example_pkcs11_demo", + actual = "//examples:pkcs11_demo", +) -docs( - source_dir = "docs", +alias( + name = "example_pkcs11_business_demo", + actual = "//examples:pkcs11_business_demo", ) diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index dcc54e6..b66a3c2 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -1,35 +1,15 @@ -# Eclipse Safe Open Vehicle Core (SCORE) -The [Eclipse Safe Open Vehicle Core](https://projects.eclipse.org/projects/automotive.score) project aims to develop an open-source core stack for Software Defined Vehicles (SDVs), specifically targeting embedded high-performance Electronic Control Units (ECUs). -Please check the [documentation](https://eclipse-score.github.io) for more information. -The source code is hosted at [GitHub](https://github.com/eclipse-score). +# Contribution Guide -The communication mainly takes place via the [`score-dev` mailing list](https://accounts.eclipse.org/mailing-list/score-dev) and GitHub issues & pull requests (PR). And we have a chatroom for community discussions here [Eclipse SCORE chatroom](https://chat.eclipse.org/#/room/#automotive.score:matrix.eclipse.org). +## Development workflows -Please note that for the project the [Eclipse Foundation’s Terms of Use](https://www.eclipse.org/legal/terms-of-use/) apply. -In addition, you need to sign the [ECA](https://www.eclipse.org/legal/ECA.php) and the [DCO](https://www.eclipse.org/legal/dco/) to contribute to the project. +This repository supports both workflows: -## Contributing -### Getting the source code & building the project -Please refer to the [README.md](README.md) for further information. +1. Cargo-first (`cargo build`, `cargo test`) +2. Bazel-orchestrated (`bazel run //:cargo_build`, `bazel test //tests/rust:rust_tests`) -### Getting involved +## Quality gates -#### Setup Phase -This phase is part of the eclipse Incubation Phase and shall establish all the processes needed for a safe development of functions. Only after this phase it will be possible to contribute code to the project. As the development in this project is driven by requirements, the processes and needed infrastructure incl. tooling will be established based on non-functional Stakeholder_Requirements. During setup phase the contributions are Bug Fixes and Improvements (both on processes and infrastructure). +Before submitting changes: -#### Bug Fixes and Improvements -Improvements are adding/changing processes and infrastructure, bug fixes can be also on development work products like code. -In case you want to fix a bug or contribute an improvement, please perform the following steps: -1) Create a PR by using the corresponding template ([Bugfix PR template](.github/PULL_REQUEST_TEMPLATE/bug_fix.md) or [Improvement PR template](.github/PULL_REQUEST_TEMPLATE/improvement.md)). Please mark your PR as draft until it's ready for review by the Committers (see the [Eclipse Foundation Project Handbook](https://www.eclipse.org/projects/handbook/#contributing-committers) for more information on the role definitions). Improvements are requested by the definition or modification of [Stakeholder Requirements](docs/stakeholder_requirements) or [Tool Requirements](docs/tool_requirements) and may be implemented after acceptance/merge of the request by a second Improvement PR. The needed reviews are automatically triggered via the [CODEOWNERS](.github/CODEOWNERS) file in the repository. -2) Initiate content review by opening a corresponding issue for the PR when it is ready for review. Review of the PR and final merge into the project repository is in responsibility of the Committers. Use the [Bugfix Issue template](.github/ISSUE_TEMPLATE/bug_fix.md) or [Improvement Issue template](.github/ISSUE_TEMPLATE/improvement.md) for this. - -Please check here for our Git Commit Rules in the [Configuration_Tool_Guidelines](https://eclipse-score.github.io/score/process_description/guidelines/index.html). - -Please use the [Stakeholder and Tool Requirements Template](https://eclipse-score.github.io/score/process_description/templates/index.html) when defining these requirements. - -![Contribution guide workflow](./docs/_assets/contribution_guide.svg "Contribution guide workflow") - -#### Additional Information -Please note, that all Git commit messages must adhere the rules described in the [Eclipse Foundation Project Handbook](https://www.eclipse.org/projects/handbook/#resources-commit). - -Please find process descriptions here: [process description](https://eclipse-score.github.io/score/process_description/). +1. `cargo clippy --all-targets --all-features -- -D warnings` +2. `cargo test` diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..72f7bbb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,965 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cryptoki" +version = "0.1.0" +dependencies = [ + "argon2", + "dirs", + "libc", + "once_cell", + "openssl", + "parking_lot", + "serde", + "serde_json", + "serial_test", + "tempfile", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serial_test" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..874f083 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "cryptoki" +version = "0.1.0" +edition = "2021" +description = "PKCS#11 v3.0 software token with a multi-provider registry and pluggable crypto backends" +license = "Apache-2.0" + +[lib] +name = "cryptoki" +path = "src/lib.rs" +crate-type = ["rlib", "cdylib"] + +[[example]] +name = "pkcs11_demo" +path = "examples/pkcs11_demo.rs" + +[[example]] +name = "pkcs11_business_demo" +path = "examples/pkcs11_business_demo.rs" + +[dependencies] +libc = "0.2" +openssl = "0.10" +parking_lot = "0.12" +once_cell = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +dirs = "5" +zeroize = { version = "1", features = ["derive"] } +argon2 = { version = "0.5", features = ["std"] } +tempfile = "3" + +[dev-dependencies] +serial_test = "3" diff --git a/LICENSE b/LICENSE index f433b1a..87cd10e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,177 +1,7 @@ +Apache License +Version 2.0, January 2004 +https://www.apache.org/licenses/LICENSE-2.0 - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +SPDX-License-Identifier: Apache-2.0 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS +For full license text see: https://www.apache.org/licenses/LICENSE-2.0 diff --git a/MODULE.bazel b/MODULE.bazel index befae8a..bad3c6a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation +# Copyright (c) 2026 Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -11,56 +11,31 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* module( - name = "cpp_rust_template_repository", - version = "1.0", + name = "cryptoki", + version = "0.1.0", ) -bazel_dep(name = "rules_python", version = "1.4.1", dev_dependency = True) - -# Python 3.12: Required for testing infrastructure and code generation tools -PYTHON_VERSION = "3.12" - -python = use_extension( - "@rules_python//python/extensions:python.bzl", - "python", - dev_dependency = True, -) -python.toolchain( - is_default = True, - python_version = PYTHON_VERSION, +# ========================================== +# 1. C++ & RUST SETUP +# ========================================== +bazel_dep(name = "rules_cc", version = "0.2.18") +bazel_dep(name = "rules_python", version = "2.0.0") +bazel_dep(name = "rules_rust", version = "0.70.0") + +crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate") +crate.from_cargo( + name = "crates", + cargo_lockfile = "//:Cargo.lock", + manifests = ["//:Cargo.toml"], ) -use_repo(python) - -# Add GoogleTest dependency -bazel_dep(name = "googletest", version = "1.17.0", dev_dependency = True) - -# Rust rules for Bazel -bazel_dep(name = "rules_rust", version = "0.63.0") +use_repo(crate, "crates") -# C/C++ rules for Bazel -bazel_dep(name = "rules_cc", version = "0.2.14") - -# LLVM Toolchains Rules - host configuration -bazel_dep(name = "toolchains_llvm", version = "1.5.0", dev_dependency = True) - -llvm = use_extension( - "@toolchains_llvm//toolchain/extensions:llvm.bzl", - "llvm", - dev_dependency = True, -) -llvm.toolchain( - cxx_standard = {"": "c++17"}, - llvm_version = "19.1.0", +rust = use_extension("@rules_rust//rust:extensions.bzl", "rust") +rust.toolchain( + edition = "2021", + versions = ["1.85.0"], ) -use_repo(llvm, "llvm_toolchain") -use_repo(llvm, "llvm_toolchain_llvm") - -register_toolchains("@llvm_toolchain//:all") - -# tooling -bazel_dep(name = "score_tooling", version = "1.0.4", dev_dependency = True) -bazel_dep(name = "aspect_rules_lint", version = "1.10.2", dev_dependency = True) -bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2", dev_dependency = True) +use_repo(rust, "rust_toolchains") -#docs-as-code -bazel_dep(name = "score_docs_as_code", version = "2.3.0", dev_dependency = True) +# Register the Rust toolchains +register_toolchains("@rust_toolchains//:all") diff --git a/NOTICE b/NOTICE index 5d111d4..78fe620 100644 --- a/NOTICE +++ b/NOTICE @@ -1,32 +1,4 @@ -# Notices for Eclipse Safe Open Vehicle Core +Cryptoki +Copyright (c) 2026 Valeo -This content is produced and maintained by the Eclipse Safe Open Vehicle Core project. - - * Project home: https://projects.eclipse.org/projects/automotive.score - -## Trademarks - -Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. - -## Copyright - -All content is the property of the respective authors or their employers. -For more information regarding authorship of content, please consult the -listed source code repository logs. - -## Declared Project Licenses - -This program and the accompanying materials are made available under the terms -of the Apache License Version 2.0 which is available at -https://www.apache.org/licenses/LICENSE-2.0. - -SPDX-License-Identifier: Apache-2.0 - -## Cryptography - -Content may contain encryption software. The country in which you are currently -may have restrictions on the import, possession, and use, and/or re-export to -another country, of encryption software. BEFORE using any encryption software, -please check the country's laws, regulations and policies concerning the import, -possession, or use, and re-export of encryption software, to see if this is -permitted. +This product includes software developed by the Cryptoki contributors. diff --git a/README.md b/README.md index 9490b33..2819d58 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Cryptoki is a **software HSM** — a shared library (`.so` / `.dylib`) that spea A few things worth highlighting: -* **All 92 functions.** Every `C_*` in the OASIS PKCS#11 v3.0 (2020) spec is implemented — not stubbed, not returning `CKR_FUNCTION_NOT_SUPPORTED`. +* **Complete PKCS#11 surface.** All v2.40 + v3.0 entry points are exported, with unsupported flows returning spec-appropriate codes (`CKR_FUNCTION_NOT_SUPPORTED`, etc.). * **Both dispatch tables.** We expose the v2.40 `CK_FUNCTION_LIST` (68 slots) and the v3.0 `CK_FUNCTION_LIST_3_0` (24 extra slots), plus `C_GetInterface` / `C_GetInterfaceList` for clients that do capability discovery. * **Fork-safe.** A `pthread_atfork` child handler closes inherited lock FDs and reseeds the CSPRNG — no parent/child RNG state sharing. * **Pluggable backend.** The `CryptoProvider` trait is the only interface the PKCS#11 layer talks to. It doesn't know or care what's underneath — a software library, a TPM, an HSM, whatever. The included OpenSSL implementation is a working reference, not a dependency. @@ -29,7 +29,51 @@ Stable Rust toolchain. If you're using the bundled OpenSSL reference backend, yo cargo build --release ``` -Output lands at `target/release/libcryptoki.so` (Linux) or `target/release/libcryptoki.dylib` (macOS). +### Bazel workflow + +The repository builds fully with native `rules_rust`. + +#### Build targets + +```bash +# Rust library +bazel build //src:cryptoki_lib + +# Both example binaries +bazel build //examples:pkcs11_demo +bazel build //examples:pkcs11_business_demo +``` + +#### Test + +```bash +# All 22 Rust integration tests (native rust_test, sandboxed, cached) +bazel test //tests:integration_tests + +# Single test +bazel test //tests:pkcs11_integration + +# C++ Google Test conformance suite +bazel test //tests/cpp:cpp_tests +``` + +#### Layout + +| Path | Contents | +| --- | --- | +| `MODULE.bazel` | `rules_rust` + `crate_universe` (`crate.from_cargo(...)`) | +| `src/BUILD` | `rust_library` → `//src:cryptoki_lib` | +| `examples/BUILD` | `rust_binary` per example | +| `tests/BUILD` | 22 native `rust_test` targets + `integration_tests` suite | +| `tests/rust/BUILD` | Smoke test | +| `tests/cpp/BUILD` | C++ `cc_test` via gtest | + +#### Notes + +* Dependencies resolved through `crate_universe`; `package_name = ""` refers to the workspace root crate. +* Tests that use `mod common` include `tests/common/mod.rs` explicitly in `srcs`. +* `serial_test` proc-macro (used by `always_authenticate`, `persistence_integration`, `pkcs11_v3_integration`) is pulled via `proc_macro_deps`. +* Each test target is independently cacheable — Bazel only reruns tests whose inputs changed. ### Run the tests @@ -124,7 +168,7 @@ Transfer the compiled test binaries from `target/aarch64-unknown-nto-qnx800/rele ## Architecture -The library is strictly layered. The idea is that each layer only knows about the layer below it — the FFI boundary doesn't do crypto, the crypto layer doesn't know about sessions, and so on. +The PKCS#11 layer is organized as small operation-focused modules with thin hubs: ```text ┌──────────────────────────────────────────────────────────────────────┐ @@ -133,7 +177,7 @@ The library is strictly layered. The idea is that each layer only knows about th │ C ABI (unsafe extern "C") ▼ ┌──────────────────────────────────────────────────────────────────────┐ -│ src/pkcs11/mod.rs — FFI boundary + orchestrator │ +│ src/pkcs11/ffi_api_* — FFI boundary + orchestrators │ │ │ │ GlobalState: OnceLock>> │ │ None → Some on C_Initialize, Some → None on C_Finalize │ @@ -145,8 +189,8 @@ The library is strictly layered. The idea is that each layer only knows about th │ 3. session.rs — op context lookup, auth checks │ │ 4. attribute_policy.rs — ratchets, immutability, access control │ │ 5. object_store.rs — handle → KeyObject resolution │ -│ 6. backend.rs — crypto dispatch (crypto ops only) │ -│ 7. storage.rs — persist if token object (mutating ops only) │ +│ 6. backend/ — crypto dispatch (crypto ops only) │ +│ 7. storage/ — persist if token object (mutating ops only) │ └──┬──────────┬────────────┬──────────────┬────────────┬──────────────┬┘ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ │ @@ -167,7 +211,7 @@ The library is strictly layered. The idea is that each layer only knows about th │auth │ │ counters│ │ │ │ │ │ └──────────┘ └──────────┘ └────────────┘ └──────────────────────────┘ │ │ - (mod.rs → object_store) + (ffi_api_* → object_store) │ ▼ ┌───────────────────────────────────────────────────┐ @@ -191,7 +235,7 @@ The library is strictly layered. The idea is that each layer only knows about th │ │ ▼ ▼ ┌──────────────────────────┐ ┌───────────────────────────────────┐ - │ storage.rs │ │ backend.rs │ + │ storage/ │ │ backend/ │ │ │ │ │ │ JSON persistence: │ │ Crypto dispatch (no OpenSSL): │ │ • NamedTempFile → fsync │ │ sign / verify / digest │ @@ -238,22 +282,96 @@ The library is strictly layered. The idea is that each layer only knows about th └─────────────────────────────────┘ ``` -### Layer Dependency Rules -Hard rules — nothing reaches up or sideways: +```text +src/pkcs11/ +├── mod.rs # top-level exports + function tables + shared state/helpers +├── ffi_api_core/ # hub +│ ├── lifecycle_and_slot_token.rs +│ ├── session_and_login.rs +│ └── keys_objects_attributes_find.rs +├── ffi_api_crypto/ # hub +│ ├── sign_verify.rs +│ ├── encrypt_decrypt.rs +│ ├── digest.rs +│ ├── key_wrap_derive.rs +│ ├── misc_v240.rs +│ └── helpers.rs +├── ffi_api_v3/ # hub +│ ├── session_user.rs +│ ├── message_encrypt_decrypt.rs +│ ├── message_sign_verify.rs +│ └── interface_discovery.rs +├── attribute_policy.rs # ratchets / immutability / access checks +├── backend/ # hub +│ ├── keygen.rs +│ ├── sign_verify.rs +│ ├── symmetric.rs +│ ├── message_aead.rs +│ ├── digest_random.rs +│ ├── rsa_wrap_derive.rs +│ └── attributes.rs +├── object_store.rs # object model + handles + persistence hooks +├── session.rs # session contexts and login state +├── storage/ # hub +│ ├── models.rs +│ ├── path.rs +│ ├── locks.rs +│ ├── io.rs +│ └── helpers.rs +├── token.rs # token metadata + PIN state +└── mechanisms.rs # mechanism allow/block policy +``` -| Layer | May call | Must NOT call | -| --- | --- | --- | -| `mod.rs` | All layers | — | -| `session.rs` | — | backend, storage, object_store, registry | -| `token.rs` | — | backend, storage, object_store, registry | -| `mechanisms.rs` | — | all others | -| `attribute_policy.rs` | `object_store` types only | backend, storage, registry | -| `object_store.rs` | `storage`, `registry`, `token` | `backend` | -| `backend.rs` | `registry`, `traits` | `storage`, `session`, `object_store` | -| `storage.rs` | `traits` (Provider param only) | `backend`, `session`, `registry` | -| `registry.rs` | `traits` | all PKCS#11 layers | -| `your_backend.rs` | its own crypto deps | all PKCS#11 layers | +Operational flow for most `C_*` functions: + +1. Validate init/session state in the FFI API module (`check_init`, `require_rw_session`, session lookup). +2. Enforce mechanism and attribute policy (`mechanisms.rs`, `attribute_policy.rs`). +3. Resolve object handles (`object_store.rs`). +4. Dispatch crypto to the provider (`backend/*` + `traits.rs`). +5. Persist token-object mutations (`storage/io.rs` through `object_store.rs`). + +This keeps each file focused while preserving one consistent ABI surface. + +## Contributor Map + +Use this table to decide where new code should go: + +| Concern | Primary module | +| --- | --- | +| Init/finalize, slots, token info | `ffi_api_core/lifecycle_and_slot_token.rs` | +| Session open/close, login state | `ffi_api_core/session_and_login.rs` | +| Key/object create/destroy/attributes/find | `ffi_api_core/keys_objects_attributes_find.rs` | +| Sign/verify C APIs | `ffi_api_crypto/sign_verify.rs` | +| Encrypt/decrypt C APIs | `ffi_api_crypto/encrypt_decrypt.rs` | +| Digest C APIs | `ffi_api_crypto/digest.rs` | +| Wrap/unwrap/derive C APIs | `ffi_api_crypto/key_wrap_derive.rs` | +| v2.40 misc/unsupported C APIs | `ffi_api_crypto/misc_v240.rs` | +| v3 session/user extensions | `ffi_api_v3/session_user.rs` | +| v3 message encrypt/decrypt | `ffi_api_v3/message_encrypt_decrypt.rs` | +| v3 message sign/verify | `ffi_api_v3/message_sign_verify.rs` | +| v3 interface discovery | `ffi_api_v3/interface_discovery.rs` | +| Provider key generation adapters | `backend/keygen.rs` | +| Provider sign/verify adapters | `backend/sign_verify.rs` | +| Provider symmetric cipher adapters | `backend/symmetric.rs` | +| Provider message AEAD adapters | `backend/message_aead.rs` | +| Provider digest/random adapters | `backend/digest_random.rs` | +| Provider RSA/wrap/derive adapters | `backend/rsa_wrap_derive.rs` | +| Provider attribute fallback | `backend/attributes.rs` | +| Persistent storage models | `storage/models.rs` | +| Persistent storage I/O + atomic writes | `storage/io.rs` | +| Storage lock/fork helpers | `storage/locks.rs` | +| Storage path config | `storage/path.rs` | + +## Development Guidelines + +To preserve the architecture and readability: + +1. **Size Limits:** Prefer adding a new focused module over growing an existing file past ~500-600 LOC. +2. **Thin Hubs:** Keep hub modules (`ffi_api_*`, `backend/`, `storage/`) thin. Re-export through the relevant hub module instead of importing deep internals externally. +3. **Documentation:** Add a short module-level `//!` ownership header for every new module. +4. **Dependencies:** Keep cross-module dependencies one-directional where possible. +5. **CI Checks:** Keep `cargo clippy --all-targets --all-features -- -D warnings` and full `cargo test` green for every structural change. --- @@ -263,7 +381,7 @@ Key material handling was a first-class concern from the start, not an afterthou ### 1. The PKCS#11 layer never sees key bytes -All key material is an opaque `EngineKeyRef` (`Zeroizing>`). The orchestration code in `mod.rs` passes handles around without ever interpreting them — only the Provider (`openssl_provider.rs`) knows what the bytes mean. If a backend can't digest an opaque handle, `key_value_for_digest` fails closed with `CKR_MECHANISM_INVALID`. +All key material is an opaque `EngineKeyRef` (`Zeroizing>`). The PKCS#11 FFI/API modules pass handles around without interpreting key internals — only the Provider (`openssl_provider.rs`) knows what the bytes mean. If a backend can't digest an opaque handle, `key_value_for_digest` fails closed with `CKR_MECHANISM_INVALID`. ### 2. Buffers are zeroed on drop @@ -341,7 +459,7 @@ Keys with `CKA_ALWAYS_AUTHENTICATE=TRUE` need a fresh `C_Login(CKU_CONTEXT_SPECI ## PKCS#11 v3.0 Compliance -All 92 functions are implemented. RW session enforcement is strict — anything that mutates persistent state requires a session opened with `CKF_RW_SESSION`. +All PKCS#11 v2.40 + v3.0 entry points are present. Some flows are intentionally not implemented yet and return standard PKCS#11 status codes (for example `CKR_FUNCTION_NOT_SUPPORTED`). RW session enforcement is strict — anything that mutates persistent state requires a session opened with `CKF_RW_SESSION`. A few honest deviations: @@ -404,7 +522,7 @@ impl CryptoProvider for MyProvider { } ``` -Register it in `src/pkcs11/mod.rs`: +Register it in `C_Initialize` (currently wired in `src/pkcs11/ffi_api_core/lifecycle_and_slot_token.rs`): ```rust let _ = crate::registry::register_engine(MyEngine::new()); @@ -420,7 +538,7 @@ Multiple engines coexist fine — each gets sequential global slot IDs. Every test goes through the C ABI function pointers (`fn_list()` / `fn_list_3_0()`) — the same path a real PKCS#11 consumer would take. No shortcuts. -Current Status: 197 tests — 0 failures +Current Status: 198 tests — 0 failures (last verified on April 30, 2026 via `cargo test`) ```text [████████████████████] 197 / 197 100% diff --git a/build_qnx.sh b/build_qnx.sh new file mode 100755 index 0000000..73bc3b0 --- /dev/null +++ b/build_qnx.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +# Source QNX environment +source ~/qnx800/qnxsdp-env.sh + +# OpenSSL configuration for cross-compilation +export OPENSSL_LIB_DIR="$QNX_TARGET/aarch64le/usr/lib" +export OPENSSL_INCLUDE_DIR="$QNX_TARGET/usr/include" +export OPENSSL_DIR="$QNX_TARGET" +export PKG_CONFIG_ALLOW_CROSS=1 + +# Compiler settings +export CC="qcc -Vgcc_ntoaarch64le" +export CXX="qcc -Vgcc_ntoaarch64le_cxx" + +CMD=${1:-build} +shift || true + +if [ "$CMD" = "examples" ]; then + CMD="build" + if [ -n "$1" ]; then + EXAMPLE_NAME="$1" + shift + set -- "--example" "$EXAMPLE_NAME" "$@" + else + set -- "--examples" "$@" + fi +fi + +echo "Running 'cargo $CMD' for QNX SDP 8.0 (aarch64)..." +cargo $CMD --target aarch64-unknown-nto-qnx800 --release "$@" diff --git a/cpp/BUILD b/cpp/BUILD new file mode 100644 index 0000000..616632e --- /dev/null +++ b/cpp/BUILD @@ -0,0 +1,35 @@ +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "pkcs11_headers", + hdrs = [ + "pkcs11.h", + "pkcs11_cpp.hpp", + ], + includes = ["."], +) + +cc_binary( + name = "demo", + srcs = ["demo.cpp"], + deps = [":pkcs11_headers"], + linkopts = ["-ldl"], +) + +cc_binary( + name = "pkcs11test", + srcs = [ + "test_cpp.cpp", + ], + deps = [":pkcs11_headers"], + linkopts = ["-ldl"], +) + +cc_binary( + name = "test_cpp_bin", + srcs = ["test_cpp.cpp"], + deps = [":pkcs11_headers"], + linkopts = ["-ldl"], +) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 0000000..867922f --- /dev/null +++ b/cpp/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) +project(pkcs11_cpp_frontend CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Include directories +include_directories("${CMAKE_CURRENT_SOURCE_DIR}") + +# ----- Demo executable ----- +add_executable(demo demo.cpp) +if(NOT CMAKE_SYSTEM_NAME STREQUAL "QNX") + target_link_libraries(demo PRIVATE dl) +endif() + +# ----- C++ test executable ----- +add_executable(test_cpp test_cpp.cpp) +if(NOT CMAKE_SYSTEM_NAME STREQUAL "QNX") + target_link_libraries(test_cpp PRIVATE dl) +endif() + +# Register as CTest test +enable_testing() +add_test(NAME cpp_unit_tests COMMAND test_cpp) diff --git a/cpp/demo.cpp b/cpp/demo.cpp new file mode 100644 index 0000000..d617b9b --- /dev/null +++ b/cpp/demo.cpp @@ -0,0 +1,316 @@ +/** + * @file demo.cpp + * @brief Demonstration program for the PKCS#11 C++ wrapper. + * + * Build (after `cargo build --release`): + * @code{.sh} + * cd cpp && mkdir -p build && cd build + * cmake .. && make + * ./demo + * @endcode + */ + +#include "pkcs11_cpp.hpp" + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Utility helpers +// --------------------------------------------------------------------------- + +static std::string toHexStr(const std::vector& v) { + std::ostringstream oss; + for (auto b : v) + oss << std::hex << std::setw(2) << std::setfill('0') << (int)b; + return oss.str(); +} + +static void printResult(const char* name, bool ok) { + std::cout << " [" << (ok ? "PASS" : "FAIL") << "] " << name << "\n"; +} + +// --------------------------------------------------------------------------- +// Demo sections +// --------------------------------------------------------------------------- + +void demoRsaSignVerify(pkcs11::Session& session) { + std::cout << "\n--- RSA-2048 Sign / Verify ---\n"; + auto [hPub, hPriv] = session.generateRsaKeyPair(2048); + std::cout << " Generated RSA-2048 key pair (pub=" << hPub + << ", priv=" << hPriv << ")\n"; + + std::vector msg = {'H','e','l','l','o',' ','W','o','r','l','d'}; + auto sig = session.sign(CKM_SHA256_RSA_PKCS, hPriv, msg); + std::cout << " Signature (" << sig.size() << " bytes): " + << toHexStr(sig).substr(0, 32) << "...\n"; + + bool verified = false; + try { + session.verify(CKM_SHA256_RSA_PKCS, hPub, msg, sig); + verified = true; + } catch (const pkcs11::Pkcs11Exception& e) { + std::cerr << " Verify exception: " << e.what() << "\n"; + } + printResult("RSA sign+verify", verified); + + // Tamper test + auto tampered = sig; + tampered[0] ^= 0xFF; + bool tamperRejected = false; + try { + session.verify(CKM_SHA256_RSA_PKCS, hPub, msg, tampered); + } catch (const pkcs11::Pkcs11Exception& e) { + tamperRejected = (e.rv() == CKR_SIGNATURE_INVALID); + } + printResult("Tampered signature rejected", tamperRejected); +} + +void demoRsaPssSignVerify(pkcs11::Session& session) { + std::cout << "\n--- RSA-PSS Sign / Verify ---\n"; + auto [hPub, hPriv] = session.generateRsaKeyPair(2048); + + std::vector msg = {'P','S','S',' ','t','e','s','t'}; + auto sig = session.sign(CKM_SHA256_RSA_PKCS_PSS, hPriv, msg); + std::cout << " PSS Signature (" << sig.size() << " bytes)\n"; + + bool verified = false; + try { + session.verify(CKM_SHA256_RSA_PKCS_PSS, hPub, msg, sig); + verified = true; + } catch (...) {} + printResult("RSA-PSS sign+verify", verified); +} + +void demoRsaOaep(pkcs11::Session& session) { + std::cout << "\n--- RSA-OAEP Encrypt / Decrypt ---\n"; + auto [hPub, hPriv] = session.generateRsaKeyPair(2048); + + std::vector secret = {'s','e','c','r','e','t'}; + auto ct = session.encryptRsaOaep(hPub, secret); + std::cout << " OAEP ciphertext (" << ct.size() << " bytes)\n"; + + auto pt = session.decryptRsaOaep(hPriv, ct); + bool ok = (pt == secret); + printResult("RSA-OAEP encrypt+decrypt", ok); +} + +void demoEcSignVerify(pkcs11::Session& session) { + std::cout << "\n--- EC P-256 Sign / Verify ---\n"; + auto [hPub, hPriv] = session.generateEcKeyPair(); + std::cout << " Generated EC P-256 key pair (pub=" << hPub + << ", priv=" << hPriv << ")\n"; + + std::vector msg(32, 0xAB); + auto sig = session.sign(CKM_ECDSA, hPriv, msg); + std::cout << " Signature (" << sig.size() << " bytes): " + << toHexStr(sig).substr(0, 32) << "...\n"; + + bool verified = false; + try { + session.verify(CKM_ECDSA, hPub, msg, sig); + verified = true; + } catch (...) {} + printResult("EC sign+verify", verified); +} + +void demoAesGcm(pkcs11::Session& session) { + std::cout << "\n--- AES-256-GCM Encrypt / Decrypt ---\n"; + CK_OBJECT_HANDLE hKey = session.generateAesKey(32); + std::cout << " Generated AES-256 key (handle=" << hKey << ")\n"; + + std::vector iv(12, 0x01); + std::vector plain = {'S','e','c','r','e','t',' ','d','a','t','a'}; + + auto ct = session.encryptAesGcm(hKey, iv, plain); + std::cout << " Ciphertext+tag (" << ct.size() << " bytes): " + << toHexStr(ct).substr(0, 40) << "...\n"; + + auto recovered = session.decryptAesGcm(hKey, iv, ct); + bool ok = (recovered == plain); + printResult("AES-GCM encrypt+decrypt", ok); + + // Authentication check: corrupt tag + auto corrupted = ct; + corrupted.back() ^= 0xFF; + bool authFailed = false; + try { + session.decryptAesGcm(hKey, iv, corrupted); + } catch (const pkcs11::Pkcs11Exception& e) { + authFailed = (e.rv() == CKR_ENCRYPTED_DATA_INVALID); + } + printResult("AES-GCM tampered tag rejected", authFailed); +} + +void demoAesCbc(pkcs11::Session& session) { + std::cout << "\n--- AES-256-CBC Encrypt / Decrypt ---\n"; + CK_OBJECT_HANDLE hKey = session.generateAesKey(32); + + std::vector iv(16, 0x01); + std::vector plain = {'H','e','l','l','o',' ','C','B','C','!','!','!'}; + + auto ct = session.encryptAesCbc(hKey, iv, plain); + std::cout << " Ciphertext (" << ct.size() << " bytes): " + << toHexStr(ct) << "\n"; + + auto recovered = session.decryptAesCbc(hKey, iv, ct); + bool ok = (recovered == plain); + printResult("AES-CBC encrypt+decrypt", ok); +} + +void demoDigest(pkcs11::Session& session) { + std::cout << "\n--- SHA-256 Digest ---\n"; + std::vector data = {'a','b','c'}; + auto hash = session.digest(CKM_SHA256, data); + std::cout << " SHA-256(\"abc\"): " << toHexStr(hash) << "\n"; + // First byte of SHA-256("abc") is 0xba + bool ok = (hash.size() == 32 && hash[0] == 0xba); + printResult("SHA-256 known vector", ok); +} + +void demoRandom(pkcs11::Session& session) { + std::cout << "\n--- Random Generation ---\n"; + auto rnd = session.generateRandom(32); + bool nonZero = false; + for (auto b : rnd) if (b) { nonZero = true; break; } + printResult("32 bytes random (non-zero)", nonZero); + std::cout << " " << toHexStr(rnd) << "\n"; +} + +void demoEdDsaSignVerify(pkcs11::Session& session) { + std::cout << "\n--- EdDSA (Ed25519) Sign / Verify ---\n"; + auto [hPub, hPriv] = session.generateEdKeyPair(); + std::cout << " Generated Ed25519 key pair (pub=" << hPub + << ", priv=" << hPriv << ")\n"; + + std::vector msg = {'E','d','D','S','A',' ','t','e','s','t'}; + auto sig = session.sign(CKM_EDDSA, hPriv, msg); + std::cout << " Signature (" << sig.size() << " bytes): " + << toHexStr(sig).substr(0, 32) << "...\n"; + + bool verified = false; + try { + session.verify(CKM_EDDSA, hPub, msg, sig); + verified = true; + } catch (...) {} + printResult("EdDSA sign+verify", verified); + + // Tamper test + auto tampered = sig; + tampered[0] ^= 0xFF; + bool tamperRejected = false; + try { + session.verify(CKM_EDDSA, hPub, msg, tampered); + } catch (const pkcs11::Pkcs11Exception& e) { + tamperRejected = (e.rv() == CKR_SIGNATURE_INVALID); + } + printResult("EdDSA tampered signature rejected", tamperRejected); +} + +void demoChaCha20Poly1305(pkcs11::Session& session) { + std::cout << "\n--- ChaCha20-Poly1305 Encrypt / Decrypt ---\n"; + CK_OBJECT_HANDLE hKey = session.generateChaCha20Key(); + std::cout << " Generated ChaCha20 key (handle=" << hKey << ")\n"; + + std::vector nonce(12, 0x42); + std::vector plain = {'C','h','a','C','h','a','2','0',' ','t','e','s','t'}; + + auto ct = session.encryptChaCha20Poly1305(hKey, nonce, plain); + std::cout << " Ciphertext+tag (" << ct.size() << " bytes): " + << toHexStr(ct).substr(0, 40) << "...\n"; + + auto recovered = session.decryptChaCha20Poly1305(hKey, nonce, ct); + bool ok = (recovered == plain); + printResult("ChaCha20-Poly1305 encrypt+decrypt", ok); + + // Authentication check: corrupt tag + auto corrupted = ct; + corrupted.back() ^= 0xFF; + bool authFailed = false; + try { + session.decryptChaCha20Poly1305(hKey, nonce, corrupted); + } catch (const pkcs11::Pkcs11Exception&) { + authFailed = true; + } + printResult("ChaCha20-Poly1305 tampered tag rejected", authFailed); +} + +void demoSha3Digest(pkcs11::Session& session) { + std::cout << "\n--- SHA-3 Digest ---\n"; + std::vector data = {'a','b','c'}; + + auto hash256 = session.digest(CKM_SHA3_256, data); + bool ok256 = (hash256.size() == 32); + printResult("SHA3-256 digest (32 bytes)", ok256); + std::cout << " SHA3-256(\"abc\"): " << toHexStr(hash256).substr(0, 32) << "...\n"; + + auto hash384 = session.digest(CKM_SHA384, data); + bool ok384 = (hash384.size() == 48); + printResult("SHA-384 digest (48 bytes)", ok384); + + auto hash512 = session.digest(CKM_SHA512, data); + bool ok512 = (hash512.size() == 64); + printResult("SHA-512 digest (64 bytes)", ok512); +} + +void demoFindObjects(pkcs11::Session& session) { + std::cout << "\n--- FindObjects ---\n"; + auto all = session.findAllObjects(); + std::cout << " Total objects in store: " << all.size() << "\n"; + printResult("FindObjects returned >0 results", !all.empty()); + + // Find only private keys + CK_ULONG classPriv = CKO_PRIVATE_KEY; + auto privKeys = session.findObjects(CKA_CLASS, &classPriv, sizeof(classPriv)); + std::cout << " Private key count: " << privKeys.size() << "\n"; + printResult("Private key filter works", !privKeys.empty()); +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +int main() { + std::cout << "============================\n"; + std::cout << " PKCS#11 C++ Frontend Demo \n"; + std::cout << "============================\n"; + + try { + pkcs11::Library lib; + + auto slots = lib.getSlotList(); + std::cout << "\nFound " << slots.size() << " slot(s).\n"; + assert(!slots.empty() && "Expected at least one slot"); + + pkcs11::Session session(lib, slots[0]); + session.login("1234"); + std::cout << "Logged in successfully.\n"; + + demoRsaSignVerify(session); + demoRsaPssSignVerify(session); + demoRsaOaep(session); + demoEcSignVerify(session); + demoEdDsaSignVerify(session); + demoAesGcm(session); + demoAesCbc(session); + demoChaCha20Poly1305(session); + demoDigest(session); + demoSha3Digest(session); + demoRandom(session); + demoFindObjects(session); + + session.logout(); + std::cout << "\nAll demos completed.\n"; + + } catch (const pkcs11::Pkcs11Exception& e) { + std::cerr << "\nFATAL: " << e.what() << "\n"; + return 1; + } catch (const std::exception& e) { + std::cerr << "\nFATAL: " << e.what() << "\n"; + return 1; + } + + return 0; +} diff --git a/cpp/pkcs11.h b/cpp/pkcs11.h new file mode 100644 index 0000000..d833722 --- /dev/null +++ b/cpp/pkcs11.h @@ -0,0 +1,421 @@ +/* + * pkcs11.h — PKCS#11 v3.0 type definitions for LP64 (Linux x86-64). + * + * This header matches the Rust struct layout in src/pkcs11/types.rs + * (CK_ULONG = unsigned long = 8 bytes on LP64). + */ + +#ifndef PKCS11_H +#define PKCS11_H + +#include +#include + +/* ── Primitive type aliases ────────────────────────────────────────────── */ + +typedef uint8_t CK_BYTE; +typedef uint8_t CK_CHAR; +typedef uint8_t CK_UTF8CHAR; +typedef CK_BYTE CK_BBOOL; +typedef unsigned long CK_ULONG; /* 8 bytes on LP64 */ +typedef long CK_LONG; +typedef CK_ULONG CK_FLAGS; +typedef CK_ULONG CK_SLOT_ID; +typedef CK_ULONG CK_SESSION_HANDLE; +typedef CK_ULONG CK_OBJECT_HANDLE; +typedef CK_ULONG CK_OBJECT_CLASS; +typedef CK_ULONG CK_KEY_TYPE; +typedef CK_ULONG CK_ATTRIBUTE_TYPE; +typedef CK_ULONG CK_MECHANISM_TYPE; +typedef CK_ULONG CK_RV; +typedef CK_ULONG CK_NOTIFICATION; +typedef CK_ULONG CK_USER_TYPE; +typedef CK_ULONG CK_STATE; +typedef CK_ULONG CK_PROFILE_ID; + +typedef CK_BYTE* CK_BYTE_PTR; +typedef CK_ULONG* CK_ULONG_PTR; +typedef void* CK_VOID_PTR; +typedef CK_SLOT_ID* CK_SLOT_ID_PTR; +typedef CK_OBJECT_HANDLE* CK_OBJECT_HANDLE_PTR; +typedef CK_MECHANISM_TYPE* CK_MECHANISM_TYPE_PTR; + +#define CK_TRUE 1 +#define CK_FALSE 0 +#define CK_UNAVAILABLE_INFORMATION (~(CK_ULONG)0) +#define CK_INVALID_HANDLE 0 + +/* ── Structs ───────────────────────────────────────────────────────────── */ + +typedef struct CK_VERSION { + CK_BYTE major; + CK_BYTE minor; +} CK_VERSION; + +typedef struct CK_INFO { + CK_VERSION cryptokiVersion; + CK_UTF8CHAR manufacturerID[32]; + CK_FLAGS flags; + CK_UTF8CHAR libraryDescription[32]; + CK_VERSION libraryVersion; +} CK_INFO; + +typedef CK_INFO* CK_INFO_PTR; + +typedef struct CK_SLOT_INFO { + CK_UTF8CHAR slotDescription[64]; + CK_UTF8CHAR manufacturerID[32]; + CK_FLAGS flags; + CK_VERSION hardwareVersion; + CK_VERSION firmwareVersion; +} CK_SLOT_INFO; + +typedef CK_SLOT_INFO* CK_SLOT_INFO_PTR; + +typedef struct CK_TOKEN_INFO { + CK_UTF8CHAR label[32]; + CK_UTF8CHAR manufacturerID[32]; + CK_UTF8CHAR model[16]; + CK_CHAR serialNumber[16]; + CK_FLAGS flags; + CK_ULONG ulMaxSessionCount; + CK_ULONG ulSessionCount; + CK_ULONG ulMaxRwSessionCount; + CK_ULONG ulRwSessionCount; + CK_ULONG ulMaxPinLen; + CK_ULONG ulMinPinLen; + CK_ULONG ulTotalPublicMemory; + CK_ULONG ulFreePublicMemory; + CK_ULONG ulTotalPrivateMemory; + CK_ULONG ulFreePrivateMemory; + CK_VERSION hardwareVersion; + CK_VERSION firmwareVersion; + CK_CHAR utcTime[16]; +} CK_TOKEN_INFO; + +typedef CK_TOKEN_INFO* CK_TOKEN_INFO_PTR; + +typedef struct CK_SESSION_INFO { + CK_SLOT_ID slotID; + CK_STATE state; + CK_FLAGS flags; + CK_ULONG ulDeviceError; +} CK_SESSION_INFO; + +typedef CK_SESSION_INFO* CK_SESSION_INFO_PTR; + +typedef struct CK_MECHANISM { + CK_MECHANISM_TYPE mechanism; + CK_VOID_PTR pParameter; + CK_ULONG ulParameterLen; +} CK_MECHANISM; + +typedef const CK_MECHANISM* CK_MECHANISM_PTR; + +typedef struct CK_MECHANISM_INFO { + CK_ULONG ulMinKeySize; + CK_ULONG ulMaxKeySize; + CK_FLAGS flags; +} CK_MECHANISM_INFO; + +typedef CK_MECHANISM_INFO* CK_MECHANISM_INFO_PTR; + +typedef struct CK_ATTRIBUTE { + CK_ATTRIBUTE_TYPE type; + CK_VOID_PTR pValue; + CK_ULONG ulValueLen; +} CK_ATTRIBUTE; + +typedef CK_ATTRIBUTE* CK_ATTRIBUTE_PTR; +typedef const CK_ATTRIBUTE* CK_ATTRIBUTE_CONST_PTR; + +/* ── Mechanism parameter structs ───────────────────────────────────────── */ + +typedef struct CK_AES_CTR_PARAMS { + CK_ULONG ulCounterBits; + CK_BYTE cb[16]; +} CK_AES_CTR_PARAMS; + +typedef struct CK_GCM_PARAMS { + const CK_BYTE* pIv; + CK_ULONG ulIvLen; + CK_ULONG ulIvBits; + const CK_BYTE* pAAD; + CK_ULONG ulAADLen; + CK_ULONG ulTagBits; +} CK_GCM_PARAMS; + +/* ── Callback / init args ──────────────────────────────────────────────── */ + +typedef CK_RV (*CK_NOTIFY)(CK_SESSION_HANDLE, CK_NOTIFICATION, CK_VOID_PTR); +typedef CK_RV (*CK_CREATEMUTEX)(CK_VOID_PTR*); +typedef CK_RV (*CK_DESTROYMUTEX)(CK_VOID_PTR); +typedef CK_RV (*CK_LOCKMUTEX)(CK_VOID_PTR); +typedef CK_RV (*CK_UNLOCKMUTEX)(CK_VOID_PTR); + +typedef struct CK_C_INITIALIZE_ARGS { + CK_CREATEMUTEX CreateMutex; + CK_DESTROYMUTEX DestroyMutex; + CK_LOCKMUTEX LockMutex; + CK_UNLOCKMUTEX UnlockMutex; + CK_FLAGS flags; + CK_VOID_PTR pReserved; +} CK_C_INITIALIZE_ARGS; + +typedef CK_C_INITIALIZE_ARGS* CK_C_INITIALIZE_ARGS_PTR; + +/* ── CK_FUNCTION_LIST (v2.40 compat) ──────────────────────────────────── */ + +typedef struct CK_FUNCTION_LIST CK_FUNCTION_LIST; +typedef CK_FUNCTION_LIST* CK_FUNCTION_LIST_PTR; +typedef CK_FUNCTION_LIST_PTR* CK_FUNCTION_LIST_PTR_PTR; + +struct CK_FUNCTION_LIST { + CK_VERSION version; + + CK_RV (*C_Initialize)(CK_C_INITIALIZE_ARGS_PTR); + CK_RV (*C_Finalize)(CK_VOID_PTR); + CK_RV (*C_GetInfo)(CK_INFO_PTR); + CK_RV (*C_GetFunctionList)(CK_FUNCTION_LIST_PTR_PTR); + + CK_RV (*C_GetSlotList)(CK_BBOOL, CK_SLOT_ID_PTR, CK_ULONG_PTR); + CK_RV (*C_GetSlotInfo)(CK_SLOT_ID, CK_SLOT_INFO_PTR); + CK_RV (*C_GetTokenInfo)(CK_SLOT_ID, CK_TOKEN_INFO_PTR); + CK_RV (*C_GetMechanismList)(CK_SLOT_ID, CK_MECHANISM_TYPE_PTR, CK_ULONG_PTR); + CK_RV (*C_GetMechanismInfo)(CK_SLOT_ID, CK_MECHANISM_TYPE, CK_MECHANISM_INFO_PTR); + + CK_RV (*C_InitToken)(CK_SLOT_ID, const CK_UTF8CHAR*, CK_ULONG, const CK_UTF8CHAR*); + CK_RV (*C_InitPIN)(CK_SESSION_HANDLE, const CK_UTF8CHAR*, CK_ULONG); + CK_RV (*C_SetPIN)(CK_SESSION_HANDLE, const CK_UTF8CHAR*, CK_ULONG, const CK_UTF8CHAR*, CK_ULONG); + + CK_RV (*C_OpenSession)(CK_SLOT_ID, CK_FLAGS, CK_VOID_PTR, CK_NOTIFY, CK_SESSION_HANDLE*); + CK_RV (*C_CloseSession)(CK_SESSION_HANDLE); + CK_RV (*C_CloseAllSessions)(CK_SLOT_ID); + CK_RV (*C_GetSessionInfo)(CK_SESSION_HANDLE, CK_SESSION_INFO_PTR); + CK_RV (*C_GetOperationState)(CK_SESSION_HANDLE, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_SetOperationState)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_OBJECT_HANDLE, CK_OBJECT_HANDLE); + CK_RV (*C_Login)(CK_SESSION_HANDLE, CK_USER_TYPE, const CK_UTF8CHAR*, CK_ULONG); + CK_RV (*C_Logout)(CK_SESSION_HANDLE); + + CK_RV (*C_CreateObject)(CK_SESSION_HANDLE, CK_ATTRIBUTE_CONST_PTR, CK_ULONG, CK_OBJECT_HANDLE*); + CK_RV (*C_CopyObject)(CK_SESSION_HANDLE, CK_OBJECT_HANDLE, CK_ATTRIBUTE_CONST_PTR, CK_ULONG, CK_OBJECT_HANDLE*); + CK_RV (*C_DestroyObject)(CK_SESSION_HANDLE, CK_OBJECT_HANDLE); + CK_RV (*C_GetObjectSize)(CK_SESSION_HANDLE, CK_OBJECT_HANDLE, CK_ULONG_PTR); + CK_RV (*C_GetAttributeValue)(CK_SESSION_HANDLE, CK_OBJECT_HANDLE, CK_ATTRIBUTE_PTR, CK_ULONG); + CK_RV (*C_SetAttributeValue)(CK_SESSION_HANDLE, CK_OBJECT_HANDLE, CK_ATTRIBUTE_PTR, CK_ULONG); + + CK_RV (*C_FindObjectsInit)(CK_SESSION_HANDLE, CK_ATTRIBUTE_CONST_PTR, CK_ULONG); + CK_RV (*C_FindObjects)(CK_SESSION_HANDLE, CK_OBJECT_HANDLE_PTR, CK_ULONG, CK_ULONG_PTR); + CK_RV (*C_FindObjectsFinal)(CK_SESSION_HANDLE); + + CK_RV (*C_EncryptInit)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE); + CK_RV (*C_Encrypt)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_EncryptUpdate)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_EncryptFinal)(CK_SESSION_HANDLE, CK_BYTE_PTR, CK_ULONG_PTR); + + CK_RV (*C_DecryptInit)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE); + CK_RV (*C_Decrypt)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_DecryptUpdate)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_DecryptFinal)(CK_SESSION_HANDLE, CK_BYTE_PTR, CK_ULONG_PTR); + + CK_RV (*C_DigestInit)(CK_SESSION_HANDLE, CK_MECHANISM_PTR); + CK_RV (*C_Digest)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_DigestUpdate)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG); + CK_RV (*C_DigestKey)(CK_SESSION_HANDLE, CK_OBJECT_HANDLE); + CK_RV (*C_DigestFinal)(CK_SESSION_HANDLE, CK_BYTE_PTR, CK_ULONG_PTR); + + CK_RV (*C_SignInit)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE); + CK_RV (*C_Sign)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_SignUpdate)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG); + CK_RV (*C_SignFinal)(CK_SESSION_HANDLE, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_SignRecoverInit)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE); + CK_RV (*C_SignRecover)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + + CK_RV (*C_VerifyInit)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE); + CK_RV (*C_Verify)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, const CK_BYTE*, CK_ULONG); + CK_RV (*C_VerifyUpdate)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG); + CK_RV (*C_VerifyFinal)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG); + CK_RV (*C_VerifyRecoverInit)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE); + CK_RV (*C_VerifyRecover)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + + CK_RV (*C_DigestEncryptUpdate)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_DecryptDigestUpdate)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_SignEncryptUpdate)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_DecryptVerifyUpdate)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR); + + CK_RV (*C_GenerateKey)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_ATTRIBUTE_CONST_PTR, CK_ULONG, CK_OBJECT_HANDLE*); + CK_RV (*C_GenerateKeyPair)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_ATTRIBUTE_CONST_PTR, CK_ULONG, CK_ATTRIBUTE_CONST_PTR, CK_ULONG, CK_OBJECT_HANDLE*, CK_OBJECT_HANDLE*); + CK_RV (*C_WrapKey)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE, CK_OBJECT_HANDLE, CK_BYTE_PTR, CK_ULONG_PTR); + CK_RV (*C_UnwrapKey)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE, const CK_BYTE*, CK_ULONG, CK_ATTRIBUTE_CONST_PTR, CK_ULONG, CK_OBJECT_HANDLE*); + CK_RV (*C_DeriveKey)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE, CK_ATTRIBUTE_CONST_PTR, CK_ULONG, CK_OBJECT_HANDLE*); + CK_RV (*C_SeedRandom)(CK_SESSION_HANDLE, const CK_BYTE*, CK_ULONG); + CK_RV (*C_GenerateRandom)(CK_SESSION_HANDLE, CK_BYTE_PTR, CK_ULONG); + CK_RV (*C_GetFunctionStatus)(CK_SESSION_HANDLE); + CK_RV (*C_CancelFunction)(CK_SESSION_HANDLE); + CK_RV (*C_WaitForSlotEvent)(CK_FLAGS, CK_SLOT_ID*, CK_VOID_PTR); +}; + +/* ── CK_INTERFACE (v3.0) ──────────────────────────────────────────────── */ + +typedef struct CK_INTERFACE { + const CK_CHAR* pInterfaceName; + CK_VOID_PTR pFunctionList; + CK_FLAGS flags; +} CK_INTERFACE; + +typedef CK_INTERFACE* CK_INTERFACE_PTR; + +/* ── Return codes (CKR_*) ──────────────────────────────────────────────── */ + +#define CKR_OK 0x00000000UL +#define CKR_CANCEL 0x00000001UL +#define CKR_HOST_MEMORY 0x00000002UL +#define CKR_SLOT_ID_INVALID 0x00000003UL +#define CKR_GENERAL_ERROR 0x00000005UL +#define CKR_FUNCTION_FAILED 0x00000006UL +#define CKR_ARGUMENTS_BAD 0x00000007UL +#define CKR_ATTRIBUTE_READ_ONLY 0x00000010UL +#define CKR_ATTRIBUTE_SENSITIVE 0x00000011UL +#define CKR_ATTRIBUTE_TYPE_INVALID 0x00000012UL +#define CKR_ATTRIBUTE_VALUE_INVALID 0x00000013UL +#define CKR_DATA_INVALID 0x00000020UL +#define CKR_DATA_LEN_RANGE 0x00000021UL +#define CKR_DEVICE_ERROR 0x00000030UL +#define CKR_AEAD_DECRYPT_FAILED 0x00000035UL +#define CKR_ENCRYPTED_DATA_INVALID 0x00000040UL +#define CKR_FUNCTION_NOT_SUPPORTED 0x00000054UL +#define CKR_STATE_UNSAVEABLE 0x00000180UL +#define CKR_KEY_HANDLE_INVALID 0x00000060UL +#define CKR_KEY_SIZE_RANGE 0x00000062UL +#define CKR_KEY_INDIGESTIBLE 0x00000067UL +#define CKR_MECHANISM_INVALID 0x00000070UL +#define CKR_MECHANISM_PARAM_INVALID 0x00000071UL +#define CKR_OBJECT_HANDLE_INVALID 0x00000082UL +#define CKR_OPERATION_ACTIVE 0x00000090UL +#define CKR_OPERATION_NOT_INITIALIZED 0x00000091UL +#define CKR_PIN_INCORRECT 0x000000A0UL +#define CKR_SESSION_HANDLE_INVALID 0x000000B3UL +#define CKR_SIGNATURE_INVALID 0x000000C0UL +#define CKR_TEMPLATE_INCOMPLETE 0x000000D0UL +#define CKR_TEMPLATE_INCONSISTENT 0x000000D1UL +#define CKR_BUFFER_TOO_SMALL 0x00000150UL +#define CKR_USER_ALREADY_LOGGED_IN 0x00000100UL +#define CKR_USER_NOT_LOGGED_IN 0x00000101UL +#define CKR_CRYPTOKI_NOT_INITIALIZED 0x00000190UL +#define CKR_TOKEN_WRITE_PROTECTED 0x000000E2UL +#define CKR_CRYPTOKI_ALREADY_INITIALIZED 0x00000191UL +#define CKR_FUNCTION_REJECTED 0x00000200UL +#define CKR_OPERATION_CANCEL_FAILED 0x00000202UL + +/* ── Mechanism types (CKM_*) ───────────────────────────────────────────── */ + +#define CKM_RSA_PKCS_KEY_PAIR_GEN 0x00000000UL +#define CKM_RSA_PKCS 0x00000001UL +#define CKM_RSA_PKCS_OAEP 0x00000009UL +#define CKM_SHA256_RSA_PKCS 0x00000040UL +#define CKM_SHA384_RSA_PKCS 0x00000041UL +#define CKM_SHA512_RSA_PKCS 0x00000042UL +#define CKM_SHA256_RSA_PKCS_PSS 0x00000043UL +#define CKM_SHA384_RSA_PKCS_PSS 0x00000044UL +#define CKM_SHA512_RSA_PKCS_PSS 0x00000045UL +#define CKM_MD5 0x00000210UL +#define CKM_SHA_1 0x00000220UL +#define CKM_SHA256 0x00000250UL +#define CKM_SHA384 0x00000260UL +#define CKM_SHA512 0x00000270UL +#define CKM_SHA3_256 0x000002B0UL +#define CKM_SHA3_384 0x000002C0UL +#define CKM_SHA3_512 0x000002D0UL +#define CKM_EC_KEY_PAIR_GEN 0x00001040UL +#define CKM_ECDSA 0x00001041UL +#define CKM_ECDSA_SHA256 0x00001043UL +#define CKM_ECDSA_SHA384 0x00001044UL +#define CKM_ECDSA_SHA512 0x00001045UL +#define CKM_EC_EDWARDS_KEY_PAIR_GEN 0x00001055UL +#define CKM_EDDSA 0x00001057UL +#define CKM_AES_KEY_GEN 0x00001080UL +#define CKM_AES_CBC_PAD 0x00001085UL +#define CKM_AES_CTR 0x00001086UL +#define CKM_AES_GCM 0x00001087UL +#define CKM_CHACHA20_POLY1305 0x00004021UL +#define CKM_CHACHA20_KEY_GEN 0x00004022UL +#define CKM_HKDF_DERIVE 0x0000402AUL +#define CKM_HKDF_KEY_GEN 0x0000402CUL + +/* ── Object classes (CKO_*) ────────────────────────────────────────────── */ + +#define CKO_DATA 0x00000000UL +#define CKO_CERTIFICATE 0x00000001UL +#define CKO_PUBLIC_KEY 0x00000002UL +#define CKO_PRIVATE_KEY 0x00000003UL +#define CKO_SECRET_KEY 0x00000004UL +#define CKO_PROFILE 0x00000009UL + +/* ── Key types (CKK_*) ────────────────────────────────────────────────── */ + +#define CKK_RSA 0x00000000UL +#define CKK_EC 0x00000003UL +#define CKK_AES 0x0000001FUL +#define CKK_GENERIC_SECRET 0x00000010UL +#define CKK_CHACHA20 0x00000033UL +#define CKK_EC_EDWARDS 0x00000040UL +#define CKK_EC_MONTGOMERY 0x00000041UL + +/* ── Attribute types (CKA_*) ───────────────────────────────────────────── */ + +#define CKA_CLASS 0x00000000UL +#define CKA_TOKEN 0x00000001UL +#define CKA_PRIVATE 0x00000002UL +#define CKA_LABEL 0x00000003UL +#define CKA_VALUE 0x00000011UL +#define CKA_PRIVATE_EXPONENT 0x00000123UL +#define CKA_PRIME_1 0x00000124UL +#define CKA_PRIME_2 0x00000125UL +#define CKA_EXPONENT_1 0x00000126UL +#define CKA_EXPONENT_2 0x00000127UL +#define CKA_COEFFICIENT 0x00000128UL +#define CKA_UNIQUE_ID 0x0000010AUL +#define CKA_KEY_TYPE 0x00000100UL +#define CKA_SENSITIVE 0x00000103UL +#define CKA_ENCRYPT 0x00000104UL +#define CKA_DECRYPT 0x00000105UL +#define CKA_SIGN 0x00000108UL +#define CKA_VERIFY 0x00000109UL +#define CKA_MODULUS 0x00000120UL +#define CKA_MODULUS_BITS 0x00000121UL +#define CKA_PUBLIC_EXPONENT 0x00000122UL +#define CKA_VALUE_LEN 0x00000161UL +#define CKA_COPYABLE 0x00000171UL +#define CKA_DESTROYABLE 0x00000172UL +#define CKA_EC_PARAMS 0x00000180UL +#define CKA_EC_POINT 0x00000181UL +#define CKA_PROFILE_ID 0x00000601UL + +/* ── Flags (CKF_*) ────────────────────────────────────────────────────── */ + +#define CKF_TOKEN_PRESENT 0x00000001UL +#define CKF_RNG 0x00000001UL +#define CKF_LOGIN_REQUIRED 0x00000004UL +#define CKF_RW_SESSION 0x00000002UL +#define CKF_SERIAL_SESSION 0x00000004UL + +/* ── User types (CKU_*) ───────────────────────────────────────────────── */ + +#define CKU_SO 0UL +#define CKU_USER 1UL +#define CKU_CONTEXT_SPECIFIC 2UL + +/* ── Entry points ──────────────────────────────────────────────────────── */ + +extern "C" { + CK_RV C_GetFunctionList(CK_FUNCTION_LIST_PTR_PTR ppFunctionList); + CK_RV C_GetInterfaceList(CK_INTERFACE_PTR pInterfacesList, CK_ULONG_PTR pulCount); + CK_RV C_GetInterface(const CK_UTF8CHAR* pInterfaceName, CK_VERSION* pVersion, + CK_INTERFACE_PTR* ppInterface, CK_FLAGS flags); + CK_RV C_SessionCancel(CK_SESSION_HANDLE hSession, CK_FLAGS flags); + CK_RV C_LoginUser(CK_SESSION_HANDLE hSession, CK_USER_TYPE userType, + const CK_UTF8CHAR* pPin, CK_ULONG ulPinLen, + const CK_UTF8CHAR* pUsername, CK_ULONG ulUsernameLen); +} + +#endif /* PKCS11_H */ diff --git a/cpp/pkcs11_cpp.hpp b/cpp/pkcs11_cpp.hpp new file mode 100644 index 0000000..8e3566c --- /dev/null +++ b/cpp/pkcs11_cpp.hpp @@ -0,0 +1,815 @@ +/** + * @file pkcs11_cpp.hpp + * @brief C++ RAII wrapper around the PKCS#11 v3.0 C API (dlopen-based). + * + * This header provides ergonomic, exception-safe C++ classes that wrap the + * raw C API loaded via dlopen. The Library class uses v3.0 interface + * discovery (C_GetInterface) when available, falling back to the legacy + * C_GetFunctionList for v2.40 modules. + * + * @example + * @code{.cpp} + * #include "pkcs11_cpp.hpp" + * + * int main() { + * using namespace pkcs11; + * + * Library lib; // dlopen + C_Initialize + * Session session(lib, 0); // opens R/W session on slot 0 + * session.login("1234"); + * + * // RSA + * auto [pubKey, privKey] = session.generateRsaKeyPair(2048); + * std::vector msg = {'H','e','l','l','o'}; + * auto sig = session.sign(CKM_SHA256_RSA_PKCS, privKey, msg); + * session.verify(CKM_SHA256_RSA_PKCS, pubKey, msg, sig); + * + * // EdDSA (v3.0) + * auto [edPub, edPriv] = session.generateEdKeyPair(); + * auto edSig = session.sign(CKM_EDDSA, edPriv, msg); + * session.verify(CKM_EDDSA, edPub, msg, edSig); + * + * // ChaCha20-Poly1305 (v3.0) + * auto chaKey = session.generateChaCha20Key(); + * std::vector nonce(12, 0x42); + * auto ct = session.encryptChaCha20Poly1305(chaKey, nonce, msg); + * auto pt = session.decryptChaCha20Poly1305(chaKey, nonce, ct); + * + * // C_Finalize + dlclose called automatically. + * } + * @endcode + */ + +#pragma once + +#include "pkcs11.h" + +#include +#include +#include +#include +#include +#include + +namespace pkcs11 { + +// ========================================================================== +// Exception +// ========================================================================== + +/** + * @brief Exception thrown for any non-CKR_OK return value. + * + * The `rv()` accessor returns the raw PKCS#11 return code so callers can + * inspect the error programmatically. + */ +class Pkcs11Exception : public std::runtime_error { +public: + explicit Pkcs11Exception(const std::string& func, CK_RV rv) + : std::runtime_error(func + " failed: CKR 0x" + toHex(rv)), + rv_(rv) {} + + /** The raw PKCS#11 return code. */ + CK_RV rv() const noexcept { return rv_; } + +private: + CK_RV rv_; + + static std::string toHex(CK_RV v) { + char buf[16]; + snprintf(buf, sizeof(buf), "%08lX", static_cast(v)); + return buf; + } +}; + +namespace detail { +inline void check(const char* func, CK_RV rv) { + if (rv != CKR_OK) throw Pkcs11Exception(func, rv); +} +} // namespace detail + +#define CK_CHECK(call) ::pkcs11::detail::check(#call, (call)) + +// ========================================================================== +// Library (dlopen + C_Initialize / C_Finalize + dlclose) +// ========================================================================== + +/** + * @brief Owns the lifetime of the PKCS#11 library loaded via dlopen. + * + * Construct exactly one `Library` object per process. The destructor calls + * `C_Finalize` and `dlclose`. + * + * The constructor tries v3.0 interface discovery (`C_GetInterface`) first. + * If the library doesn't export it, it falls back to `C_GetFunctionList`. + */ +class Library { +public: + /** + * @brief Load the PKCS#11 shared library and initialise it. + * + * Searches several common paths for libcryptoki.so. + * @throws std::runtime_error if the library cannot be loaded. + * @throws Pkcs11Exception if C_Initialize fails. + */ + Library() : dlHandle_(nullptr), F_(nullptr), ownsInit_(false) { + const char* paths[] = { + "libcryptoki.so", // LD_LIBRARY_PATH + "./target/release/libcryptoki.so", // from project root + "./target/debug/libcryptoki.so", // from project root + "../target/release/libcryptoki.so", // from cpp/build/ + "../target/debug/libcryptoki.so", // from cpp/build/ + "../../target/release/libcryptoki.so", // from cpp/ + "../../target/debug/libcryptoki.so", // from cpp/ + nullptr + }; + + for (int i = 0; paths[i]; i++) { + dlHandle_ = dlopen(paths[i], RTLD_NOW); + if (dlHandle_) break; + } + if (!dlHandle_) { + throw std::runtime_error( + std::string("Failed to load libcryptoki.so: ") + dlerror() + + "\nMake sure to run 'cargo build --release' first."); + } + + // Try v3.0 interface discovery first + using GetInterfaceFn = CK_RV (*)(const CK_UTF8CHAR*, CK_VERSION*, + CK_INTERFACE_PTR*, CK_FLAGS); + auto getInterface = reinterpret_cast( + dlsym(dlHandle_, "C_GetInterface")); + + if (getInterface) { + CK_INTERFACE_PTR iface = nullptr; + CK_RV rv = getInterface(nullptr, nullptr, &iface, 0); + if (rv == CKR_OK && iface && iface->pFunctionList) { + F_ = static_cast(iface->pFunctionList); + } + } + + // Fall back to legacy C_GetFunctionList + if (!F_) { + using GetFunctionListFn = CK_RV (*)(CK_FUNCTION_LIST_PTR_PTR); + auto getFunctionList = reinterpret_cast( + dlsym(dlHandle_, "C_GetFunctionList")); + if (!getFunctionList) { + dlclose(dlHandle_); + throw std::runtime_error("Neither C_GetInterface nor C_GetFunctionList found"); + } + + CK_RV rv = getFunctionList(&F_); + if (rv != CKR_OK || !F_) { + dlclose(dlHandle_); + throw Pkcs11Exception("C_GetFunctionList", rv); + } + } + + CK_RV rv = F_->C_Initialize(nullptr); + if (rv == CKR_OK) { + ownsInit_ = true; + } else if (rv != CKR_CRYPTOKI_ALREADY_INITIALIZED) { + dlclose(dlHandle_); + throw Pkcs11Exception("C_Initialize", rv); + } + } + + ~Library() noexcept { + if (F_ && ownsInit_) F_->C_Finalize(nullptr); + if (dlHandle_) dlclose(dlHandle_); + } + + // Non-copyable, non-movable + Library(const Library&) = delete; + Library& operator=(const Library&) = delete; + Library(Library&&) = delete; + Library& operator=(Library&&) = delete; + + /** @return The function list pointer (for direct C API access if needed). */ + CK_FUNCTION_LIST* functions() const noexcept { return F_; } + + /** + * @brief Enumerate available slots. + * @param tokenPresent If true, return only slots with a token present. + * @return Vector of slot IDs. + */ + std::vector getSlotList(bool tokenPresent = true) const { + CK_ULONG count = 0; + CK_CHECK(F_->C_GetSlotList(tokenPresent ? CK_TRUE : CK_FALSE, nullptr, &count)); + std::vector slots(count); + CK_CHECK(F_->C_GetSlotList(tokenPresent ? CK_TRUE : CK_FALSE, slots.data(), &count)); + slots.resize(count); + return slots; + } + + /** + * @brief Retrieve token information for a given slot. + */ + CK_TOKEN_INFO getTokenInfo(CK_SLOT_ID slotId) const { + CK_TOKEN_INFO info{}; + CK_CHECK(F_->C_GetTokenInfo(slotId, &info)); + return info; + } + + // ------------------------------------------------------------------ + // Token management (v3.0) + // ------------------------------------------------------------------ + + /** + * @brief Initialise the token in a given slot. + * @param slotId Slot containing the token to initialise. + * @param soPin Security Officer PIN. + * @param label Token label (padded/truncated to 32 bytes by the token). + */ + void initToken(CK_SLOT_ID slotId, const std::string& soPin, + const std::string& label) { + CK_CHECK(F_->C_InitToken( + slotId, + reinterpret_cast(soPin.c_str()), + static_cast(soPin.size()), + reinterpret_cast(label.c_str()))); + } + +private: + void* dlHandle_; + CK_FUNCTION_LIST* F_; + bool ownsInit_; +}; + +// ========================================================================== +// Session +// ========================================================================== + +/** + * @brief RAII wrapper for a PKCS#11 session. + * + * The session is automatically closed when this object is destroyed. + */ +class Session { +public: + /** + * @brief Open a new session on @p slotId. + * @param lib The Library object (must outlive this Session). + * @param slotId The target slot identifier. + * @param readWrite If true (default), open an R/W session. + */ + explicit Session(const Library& lib, CK_SLOT_ID slotId, bool readWrite = true) + : F_(lib.functions()), handle_(0) + { + CK_FLAGS flags = CKF_SERIAL_SESSION; + if (readWrite) flags |= CKF_RW_SESSION; + CK_CHECK(F_->C_OpenSession(slotId, flags, nullptr, nullptr, &handle_)); + } + + ~Session() noexcept { + if (handle_) F_->C_CloseSession(handle_); + } + + // Non-copyable; movable + Session(const Session&) = delete; + Session& operator=(const Session&) = delete; + + Session(Session&& other) noexcept : F_(other.F_), handle_(other.handle_) { + other.handle_ = 0; + } + Session& operator=(Session&& other) noexcept { + if (this != &other) { + if (handle_) F_->C_CloseSession(handle_); + F_ = other.F_; + handle_ = other.handle_; + other.handle_ = 0; + } + return *this; + } + + /** @return The raw session handle. */ + CK_SESSION_HANDLE handle() const noexcept { return handle_; } + + // ------------------------------------------------------------------ + // Authentication + // ------------------------------------------------------------------ + + /** + * @brief Log in as the normal user. + * @param pin User PIN string. + */ + void login(const std::string& pin) { + CK_CHECK(F_->C_Login(handle_, CKU_USER, + reinterpret_cast(pin.c_str()), + static_cast(pin.size()))); + } + + /** @brief Log out. */ + void logout() { + CK_CHECK(F_->C_Logout(handle_)); + } + + // ------------------------------------------------------------------ + // Key Generation + // ------------------------------------------------------------------ + + /** + * @brief Generate an RSA key pair. + * @param modulusBits RSA modulus size in bits (e.g. 2048, 4096). + * @return {publicKeyHandle, privateKeyHandle} + */ + std::pair + generateRsaKeyPair(CK_ULONG modulusBits = 2048) { + CK_MECHANISM mech{ CKM_RSA_PKCS_KEY_PAIR_GEN, nullptr, 0 }; + + CK_BYTE pubExp[] = { 0x01, 0x00, 0x01 }; // 65537 + CK_ATTRIBUTE pubTemplate[] = { + { CKA_MODULUS_BITS, &modulusBits, sizeof(modulusBits) }, + { CKA_PUBLIC_EXPONENT, pubExp, sizeof(pubExp) }, + }; + CK_ULONG pubCount = sizeof(pubTemplate) / sizeof(pubTemplate[0]); + + CK_OBJECT_HANDLE hPub = 0, hPriv = 0; + CK_CHECK(F_->C_GenerateKeyPair(handle_, &mech, + pubTemplate, pubCount, + nullptr, 0, + &hPub, &hPriv)); + return { hPub, hPriv }; + } + + /** + * @brief Generate an EC key pair (P-256). + * @return {publicKeyHandle, privateKeyHandle} + */ + std::pair + generateEcKeyPair() { + CK_MECHANISM mech{ CKM_EC_KEY_PAIR_GEN, nullptr, 0 }; + CK_BYTE p256_oid[] = { 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07 }; + CK_ATTRIBUTE pubTemplate[] = { + { CKA_EC_PARAMS, p256_oid, sizeof(p256_oid) }, + }; + CK_OBJECT_HANDLE hPub = 0, hPriv = 0; + CK_CHECK(F_->C_GenerateKeyPair(handle_, &mech, + pubTemplate, 1, + nullptr, 0, + &hPub, &hPriv)); + return { hPub, hPriv }; + } + + /** + * @brief Generate an AES secret key. + * @param keyLenBytes Key length in bytes: 16, 24, or 32. + */ + CK_OBJECT_HANDLE generateAesKey(CK_ULONG keyLenBytes = 32) { + CK_MECHANISM mech{ CKM_AES_KEY_GEN, nullptr, 0 }; + CK_ATTRIBUTE tmpl[] = { + { CKA_VALUE_LEN, &keyLenBytes, sizeof(keyLenBytes) }, + }; + CK_OBJECT_HANDLE hKey = 0; + CK_CHECK(F_->C_GenerateKey(handle_, &mech, tmpl, 1, &hKey)); + return hKey; + } + + // ------------------------------------------------------------------ + // Sign / Verify + // ------------------------------------------------------------------ + + /** + * @brief Sign data with a private key. + * @param mechanismType e.g. CKM_SHA256_RSA_PKCS or CKM_ECDSA. + * @param hPrivKey Private key handle. + * @param data Data to sign. + * @return Raw signature bytes. + */ + std::vector sign(CK_MECHANISM_TYPE mechanismType, + CK_OBJECT_HANDLE hPrivKey, + const std::vector& data) { + CK_MECHANISM mech{ mechanismType, nullptr, 0 }; + CK_CHECK(F_->C_SignInit(handle_, &mech, hPrivKey)); + + // Allocate a generous buffer (512 bytes covers RSA-4096 and ECDSA). + // C_Sign consumes the context even on a length query, so we avoid the + // two-pass pattern which can fail for variable-length signatures (ECDSA). + CK_ULONG sigLen = 512; + std::vector sig(sigLen); + CK_CHECK(F_->C_Sign(handle_, + data.data(), static_cast(data.size()), + sig.data(), &sigLen)); + sig.resize(sigLen); + return sig; + } + + /** + * @brief Verify a signature. + * @param mechanismType Matching mechanism used for signing. + * @param hPubKey Public key handle. + * @param data Original data. + * @param signature Signature bytes. + * @throws Pkcs11Exception with CKR_SIGNATURE_INVALID if verification fails. + */ + void verify(CK_MECHANISM_TYPE mechanismType, + CK_OBJECT_HANDLE hPubKey, + const std::vector& data, + const std::vector& signature) { + CK_MECHANISM mech{ mechanismType, nullptr, 0 }; + CK_CHECK(F_->C_VerifyInit(handle_, &mech, hPubKey)); + CK_CHECK(F_->C_Verify(handle_, + data.data(), static_cast(data.size()), + signature.data(), static_cast(signature.size()))); + } + + // ------------------------------------------------------------------ + // Encrypt / Decrypt (AES-GCM) + // ------------------------------------------------------------------ + + /** + * @brief Encrypt data with AES-GCM. + * @param hKey AES key handle. + * @param iv Initialisation vector (12 bytes recommended). + * @param plaintext Data to encrypt. + * @param tagBits Authentication tag length in bits (default 128). + * @return Ciphertext || authentication tag. + */ + std::vector encryptAesGcm(CK_OBJECT_HANDLE hKey, + const std::vector& iv, + const std::vector& plaintext, + CK_ULONG tagBits = 128) { + auto gcmParams = makeGcmParams(iv, tagBits); + CK_MECHANISM mech{ CKM_AES_GCM, &gcmParams, + static_cast(sizeof(gcmParams)) }; + + CK_CHECK(F_->C_EncryptInit(handle_, &mech, hKey)); + + CK_ULONG ctLen = static_cast(plaintext.size()) + tagBits / 8; + std::vector ct(ctLen); + CK_CHECK(F_->C_Encrypt(handle_, + plaintext.data(), static_cast(plaintext.size()), + ct.data(), &ctLen)); + ct.resize(ctLen); + return ct; + } + + /** + * @brief Decrypt data with AES-GCM. + * @param hKey AES key handle. + * @param iv Same IV used during encryption. + * @param ciphertext Ciphertext || tag. + * @param tagBits Authentication tag length in bits (must match encryption). + * @return Decrypted plaintext. + * @throws Pkcs11Exception if authentication fails. + */ + std::vector decryptAesGcm(CK_OBJECT_HANDLE hKey, + const std::vector& iv, + const std::vector& ciphertext, + CK_ULONG tagBits = 128) { + auto gcmParams = makeGcmParams(iv, tagBits); + CK_MECHANISM mech{ CKM_AES_GCM, &gcmParams, + static_cast(sizeof(gcmParams)) }; + + CK_CHECK(F_->C_DecryptInit(handle_, &mech, hKey)); + + CK_ULONG ptLen = static_cast(ciphertext.size() + tagBits / 8); + std::vector pt(ptLen); + CK_CHECK(F_->C_Decrypt(handle_, + ciphertext.data(), static_cast(ciphertext.size()), + pt.data(), &ptLen)); + pt.resize(ptLen); + return pt; + } + + // ------------------------------------------------------------------ + // Encrypt / Decrypt (AES-CBC-PAD) + // ------------------------------------------------------------------ + + /** + * @brief Encrypt data with AES-CBC-PAD. + * @param hKey AES key handle. + * @param iv Initialisation vector (16 bytes). + * @param plaintext Data to encrypt. + * @return Ciphertext with PKCS#7 padding. + */ + std::vector encryptAesCbc(CK_OBJECT_HANDLE hKey, + const std::vector& iv, + const std::vector& plaintext) { + CK_MECHANISM mech{ CKM_AES_CBC_PAD, + const_cast(iv.data()), + static_cast(iv.size()) }; + + CK_CHECK(F_->C_EncryptInit(handle_, &mech, hKey)); + + CK_ULONG ctLen = static_cast(plaintext.size()) + 16; + std::vector ct(ctLen); + CK_CHECK(F_->C_Encrypt(handle_, + plaintext.data(), static_cast(plaintext.size()), + ct.data(), &ctLen)); + ct.resize(ctLen); + return ct; + } + + /** + * @brief Decrypt data with AES-CBC-PAD. + * @param hKey AES key handle. + * @param iv Same IV used during encryption (16 bytes). + * @param ciphertext Ciphertext to decrypt. + * @return Decrypted plaintext (padding removed). + */ + std::vector decryptAesCbc(CK_OBJECT_HANDLE hKey, + const std::vector& iv, + const std::vector& ciphertext) { + CK_MECHANISM mech{ CKM_AES_CBC_PAD, + const_cast(iv.data()), + static_cast(iv.size()) }; + + CK_CHECK(F_->C_DecryptInit(handle_, &mech, hKey)); + + CK_ULONG ptLen = static_cast(ciphertext.size()); + std::vector pt(ptLen); + CK_CHECK(F_->C_Decrypt(handle_, + ciphertext.data(), static_cast(ciphertext.size()), + pt.data(), &ptLen)); + pt.resize(ptLen); + return pt; + } + + // ------------------------------------------------------------------ + // Encrypt / Decrypt (RSA-OAEP) + // ------------------------------------------------------------------ + + /** + * @brief Encrypt data with RSA-OAEP. + * @param hPubKey RSA public key handle. + * @param plaintext Data to encrypt. + * @return Ciphertext. + */ + std::vector encryptRsaOaep(CK_OBJECT_HANDLE hPubKey, + const std::vector& plaintext) { + CK_MECHANISM mech{ CKM_RSA_PKCS_OAEP, nullptr, 0 }; + CK_CHECK(F_->C_EncryptInit(handle_, &mech, hPubKey)); + + std::vector ct(512); + CK_ULONG ctLen = static_cast(ct.size()); + CK_CHECK(F_->C_Encrypt(handle_, + plaintext.data(), static_cast(plaintext.size()), + ct.data(), &ctLen)); + ct.resize(ctLen); + return ct; + } + + /** + * @brief Decrypt data with RSA-OAEP. + * @param hPrivKey RSA private key handle. + * @param ciphertext Data to decrypt. + * @return Decrypted plaintext. + */ + std::vector decryptRsaOaep(CK_OBJECT_HANDLE hPrivKey, + const std::vector& ciphertext) { + CK_MECHANISM mech{ CKM_RSA_PKCS_OAEP, nullptr, 0 }; + CK_CHECK(F_->C_DecryptInit(handle_, &mech, hPrivKey)); + + std::vector pt(512); + CK_ULONG ptLen = static_cast(pt.size()); + CK_CHECK(F_->C_Decrypt(handle_, + ciphertext.data(), static_cast(ciphertext.size()), + pt.data(), &ptLen)); + pt.resize(ptLen); + return pt; + } + + // ------------------------------------------------------------------ + // Digest + // ------------------------------------------------------------------ + + /** + * @brief Compute a hash of `data`. + * @param mechanismType e.g. CKM_SHA256, CKM_SHA_1, CKM_MD5. + * @param data Input data. + * @return Hash bytes. + */ + std::vector digest(CK_MECHANISM_TYPE mechanismType, + const std::vector& data) { + CK_MECHANISM mech{ mechanismType, nullptr, 0 }; + CK_CHECK(F_->C_DigestInit(handle_, &mech)); + + // 64 bytes covers SHA-512; no need for a two-pass size query. + CK_ULONG hashLen = 64; + std::vector hash(hashLen); + CK_CHECK(F_->C_Digest(handle_, + data.data(), static_cast(data.size()), + hash.data(), &hashLen)); + hash.resize(hashLen); + return hash; + } + + // ------------------------------------------------------------------ + // Random + // ------------------------------------------------------------------ + + /** + * @brief Generate `length` cryptographically strong random bytes. + */ + std::vector generateRandom(size_t length) { + std::vector buf(length); + CK_CHECK(F_->C_GenerateRandom(handle_, buf.data(), + static_cast(length))); + return buf; + } + + // ------------------------------------------------------------------ + // EdDSA key generation (v3.0) + // ------------------------------------------------------------------ + + /** + * @brief Generate an EdDSA key pair (Ed25519 by default). + * @param curve OID bytes for the curve. Defaults to Ed25519 + * ({0x06, 0x03, 0x2b, 0x65, 0x70}). + * @return {publicKeyHandle, privateKeyHandle} + */ + std::pair + generateEdKeyPair(const std::vector& curve = {0x06, 0x03, 0x2b, 0x65, 0x70}) { + CK_MECHANISM mech{ CKM_EC_EDWARDS_KEY_PAIR_GEN, nullptr, 0 }; + CK_ATTRIBUTE pubTemplate[] = { + { CKA_EC_PARAMS, const_cast(curve.data()), + static_cast(curve.size()) }, + }; + CK_OBJECT_HANDLE hPub = 0, hPriv = 0; + CK_CHECK(F_->C_GenerateKeyPair(handle_, &mech, + pubTemplate, 1, + nullptr, 0, + &hPub, &hPriv)); + return { hPub, hPriv }; + } + + // ------------------------------------------------------------------ + // ChaCha20 key generation (v3.0) + // ------------------------------------------------------------------ + + /** + * @brief Generate a 256-bit ChaCha20 secret key. + * @return Key object handle. + */ + CK_OBJECT_HANDLE generateChaCha20Key() { + CK_MECHANISM mech{ CKM_CHACHA20_KEY_GEN, nullptr, 0 }; + CK_OBJECT_HANDLE hKey = 0; + CK_CHECK(F_->C_GenerateKey(handle_, &mech, nullptr, 0, &hKey)); + return hKey; + } + + // ------------------------------------------------------------------ + // Encrypt / Decrypt (ChaCha20-Poly1305, v3.0) + // ------------------------------------------------------------------ + + /** + * @brief Encrypt data with ChaCha20-Poly1305 AEAD. + * @param hKey ChaCha20 key handle. + * @param nonce 12-byte nonce. + * @param plaintext Data to encrypt. + * @param aad Additional authenticated data (optional). + * @return Ciphertext || 16-byte authentication tag. + */ + std::vector encryptChaCha20Poly1305( + CK_OBJECT_HANDLE hKey, + const std::vector& nonce, + const std::vector& plaintext, + const std::vector& aad = {}) { + auto params = makeGcmParams(nonce, 128, aad); + CK_MECHANISM mech{ CKM_CHACHA20_POLY1305, ¶ms, + static_cast(sizeof(params)) }; + + CK_CHECK(F_->C_EncryptInit(handle_, &mech, hKey)); + + CK_ULONG ctLen = static_cast(plaintext.size()) + 16; + std::vector ct(ctLen); + CK_CHECK(F_->C_Encrypt(handle_, + plaintext.data(), static_cast(plaintext.size()), + ct.data(), &ctLen)); + ct.resize(ctLen); + return ct; + } + + /** + * @brief Decrypt data with ChaCha20-Poly1305 AEAD. + * @param hKey ChaCha20 key handle. + * @param nonce Same 12-byte nonce used during encryption. + * @param ciphertext Ciphertext || tag. + * @param aad Additional authenticated data (must match encryption). + * @return Decrypted plaintext. + * @throws Pkcs11Exception if authentication fails. + */ + std::vector decryptChaCha20Poly1305( + CK_OBJECT_HANDLE hKey, + const std::vector& nonce, + const std::vector& ciphertext, + const std::vector& aad = {}) { + auto params = makeGcmParams(nonce, 128, aad); + CK_MECHANISM mech{ CKM_CHACHA20_POLY1305, ¶ms, + static_cast(sizeof(params)) }; + + CK_CHECK(F_->C_DecryptInit(handle_, &mech, hKey)); + + CK_ULONG ptLen = static_cast(ciphertext.size()); + std::vector pt(ptLen); + CK_CHECK(F_->C_Decrypt(handle_, + ciphertext.data(), static_cast(ciphertext.size()), + pt.data(), &ptLen)); + pt.resize(ptLen); + return pt; + } + + // ------------------------------------------------------------------ + // PIN management (v3.0) + // ------------------------------------------------------------------ + + /** + * @brief Initialise the user PIN (requires SO login). + * @param pin The new user PIN. + */ + void initPin(const std::string& pin) { + CK_CHECK(F_->C_InitPIN(handle_, + reinterpret_cast(pin.c_str()), + static_cast(pin.size()))); + } + + /** + * @brief Change the PIN of the currently logged-in user. + * @param oldPin Current PIN. + * @param newPin Desired new PIN. + */ + void setPin(const std::string& oldPin, const std::string& newPin) { + CK_CHECK(F_->C_SetPIN(handle_, + reinterpret_cast(oldPin.c_str()), + static_cast(oldPin.size()), + reinterpret_cast(newPin.c_str()), + static_cast(newPin.size()))); + } + + // ------------------------------------------------------------------ + // Session cancel (v3.0) + // ------------------------------------------------------------------ + + /** + * @brief Cancel all active operations on this session. + */ + void sessionCancel() { + CK_CHECK(::C_SessionCancel(handle_, 0)); + } + + // ------------------------------------------------------------------ + // Object management + // ------------------------------------------------------------------ + + /** + * @brief Find objects matching a template. + * @param attrType Attribute type to match (pass CKA_CLASS, etc.). + * @param attrValue Pointer to the attribute value. + * @param attrLen Length of the attribute value. + * @return List of matching object handles. + */ + std::vector + findObjects(CK_ATTRIBUTE_TYPE attrType, void* attrValue, CK_ULONG attrLen) { + CK_ATTRIBUTE tmpl{ attrType, attrValue, attrLen }; + return findObjectsImpl(&tmpl, 1); + } + + /** + * @brief Find all objects (empty template). + */ + std::vector findAllObjects() { + return findObjectsImpl(nullptr, 0); + } + + /** + * @brief Destroy an object. + */ + void destroyObject(CK_OBJECT_HANDLE hObject) { + CK_CHECK(F_->C_DestroyObject(handle_, hObject)); + } + +private: + CK_FUNCTION_LIST* F_; + CK_SESSION_HANDLE handle_; + + std::vector + findObjectsImpl(CK_ATTRIBUTE* pTemplate, CK_ULONG count) { + CK_CHECK(F_->C_FindObjectsInit(handle_, pTemplate, count)); + std::vector results; + CK_OBJECT_HANDLE batch[32]; + CK_ULONG found = 0; + for (;;) { + CK_CHECK(F_->C_FindObjects(handle_, batch, 32, &found)); + if (found == 0) break; + results.insert(results.end(), batch, batch + found); + } + CK_CHECK(F_->C_FindObjectsFinal(handle_)); + return results; + } + + static CK_GCM_PARAMS makeGcmParams(const std::vector& iv, + CK_ULONG tagBits, + const std::vector& aad = {}) { + CK_GCM_PARAMS p{}; + p.pIv = iv.data(); + p.ulIvLen = static_cast(iv.size()); + p.ulIvBits = static_cast(iv.size() * 8); + p.pAAD = aad.empty() ? nullptr : aad.data(); + p.ulAADLen = static_cast(aad.size()); + p.ulTagBits = tagBits; + return p; + } +}; + +} // namespace pkcs11 diff --git a/cpp/test_cpp.cpp b/cpp/test_cpp.cpp new file mode 100644 index 0000000..0e93405 --- /dev/null +++ b/cpp/test_cpp.cpp @@ -0,0 +1,307 @@ +/** + * @file test_cpp.cpp + * @brief C++ unit tests for the PKCS#11 wrapper. + * + * Deliberately uses no external framework so it can be built with just + * g++ and a linker path to the Rust shared library. + * + * Build: + * @code{.sh} + * cd cpp && mkdir -p build && cd build + * cmake .. && make + * ./test_cpp + * @endcode + */ + +#include "pkcs11_cpp.hpp" + +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Tiny test runner +// --------------------------------------------------------------------------- + +struct TestCase { + std::string name; + std::function fn; +}; + +static int g_passed = 0; +static int g_failed = 0; + +static void run(const TestCase& tc) { + try { + tc.fn(); + std::cout << "[PASS] " << tc.name << "\n"; + ++g_passed; + } catch (const std::exception& e) { + std::cout << "[FAIL] " << tc.name << " => " << e.what() << "\n"; + ++g_failed; + } +} + +#define ASSERT_EQ(a, b) \ + do { if ((a) != (b)) throw std::runtime_error(std::string("ASSERT_EQ failed at line ") + std::to_string(__LINE__)); } while(0) +#define ASSERT_TRUE(x) \ + do { if (!(x)) throw std::runtime_error(std::string("ASSERT_TRUE failed at line ") + std::to_string(__LINE__)); } while(0) +#define ASSERT_THROW(expr, rvCode) \ + do { \ + bool caught_ = false; \ + try { expr; } \ + catch (const pkcs11::Pkcs11Exception& _e) { \ + if (_e.rv() != (rvCode)) \ + throw std::runtime_error(std::string("Expected CKR 0x") + std::to_string(rvCode) + " got 0x" + std::to_string(_e.rv())); \ + caught_ = true; \ + } \ + if (!caught_) throw std::runtime_error("Expected exception not thrown"); \ + } while(0) + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +static pkcs11::Library* g_lib = nullptr; + +static pkcs11::Session makeSession() { + return pkcs11::Session(*g_lib, 0, true); +} + +static pkcs11::Session makeLoggedInSession() { + auto s = makeSession(); + s.login("1234"); + return s; +} + +// --------------------------------------------------------------------------- +// Tests: Library +// --------------------------------------------------------------------------- + +void test_library_init() { + pkcs11::Library lib2; // double-init accepted silently +} + +void test_slot_list_non_empty() { + auto slots = g_lib->getSlotList(); + ASSERT_TRUE(!slots.empty()); + ASSERT_EQ(slots[0], (CK_SLOT_ID)0); +} + +// --------------------------------------------------------------------------- +// Tests: Session +// --------------------------------------------------------------------------- + +void test_open_close_session() { + auto s = makeSession(); +} + +void test_login_logout() { + auto s = makeLoggedInSession(); + s.logout(); +} + +void test_wrong_pin() { + auto s = makeSession(); + ASSERT_THROW(s.login("wrong"), CKR_PIN_INCORRECT); +} + +void test_double_login() { + auto s = makeLoggedInSession(); + ASSERT_THROW(s.login("1234"), CKR_USER_ALREADY_LOGGED_IN); +} + +// --------------------------------------------------------------------------- +// Tests: RSA +// --------------------------------------------------------------------------- + +void test_rsa_keygen() { + auto s = makeLoggedInSession(); + auto [hPub, hPriv] = s.generateRsaKeyPair(2048); + ASSERT_TRUE(hPub != 0); + ASSERT_TRUE(hPriv != 0); + ASSERT_TRUE(hPub != hPriv); +} + +void test_rsa_sign_verify() { + auto s = makeLoggedInSession(); + auto [hPub, hPriv] = s.generateRsaKeyPair(2048); + std::vector msg = {1,2,3,4,5}; + auto sig = s.sign(CKM_SHA256_RSA_PKCS, hPriv, msg); + ASSERT_TRUE(!sig.empty()); + s.verify(CKM_SHA256_RSA_PKCS, hPub, msg, sig); +} + +void test_rsa_verify_bad_sig() { + auto s = makeLoggedInSession(); + auto [hPub, hPriv] = s.generateRsaKeyPair(2048); + std::vector msg = {1,2,3}; + auto sig = s.sign(CKM_SHA256_RSA_PKCS, hPriv, msg); + sig[0] ^= 0xFF; + ASSERT_THROW(s.verify(CKM_SHA256_RSA_PKCS, hPub, msg, sig), CKR_SIGNATURE_INVALID); +} + +void test_rsa_pss_sign_verify() { + auto s = makeLoggedInSession(); + auto [hPub, hPriv] = s.generateRsaKeyPair(2048); + std::vector msg = {1,2,3,4,5,6,7,8}; + auto sig = s.sign(CKM_SHA256_RSA_PKCS_PSS, hPriv, msg); + ASSERT_TRUE(!sig.empty()); + s.verify(CKM_SHA256_RSA_PKCS_PSS, hPub, msg, sig); +} + +void test_rsa_oaep_round_trip() { + auto s = makeLoggedInSession(); + auto [hPub, hPriv] = s.generateRsaKeyPair(2048); + std::vector plain = {'s','e','c','r','e','t'}; + auto ct = s.encryptRsaOaep(hPub, plain); + auto pt = s.decryptRsaOaep(hPriv, ct); + ASSERT_EQ(pt, plain); +} + +// --------------------------------------------------------------------------- +// Tests: EC +// --------------------------------------------------------------------------- + +void test_ec_keygen() { + auto s = makeLoggedInSession(); + auto [hPub, hPriv] = s.generateEcKeyPair(); + ASSERT_TRUE(hPub != 0); + ASSERT_TRUE(hPriv != 0); +} + +void test_ec_sign_verify() { + auto s = makeLoggedInSession(); + auto [hPub, hPriv] = s.generateEcKeyPair(); + std::vector msg(20, 0xCC); + auto sig = s.sign(CKM_ECDSA, hPriv, msg); + s.verify(CKM_ECDSA, hPub, msg, sig); +} + +// --------------------------------------------------------------------------- +// Tests: AES-GCM +// --------------------------------------------------------------------------- + +void test_aes_keygen() { + auto s = makeLoggedInSession(); + auto hKey = s.generateAesKey(32); + ASSERT_TRUE(hKey != 0); +} + +void test_aes_gcm_round_trip() { + auto s = makeLoggedInSession(); + auto hKey = s.generateAesKey(32); + std::vector iv(12, 0x42); + std::vector plain = {'t','e','s','t',' ','d','a','t','a'}; + auto ct = s.encryptAesGcm(hKey, iv, plain); + auto pt = s.decryptAesGcm(hKey, iv, ct); + ASSERT_EQ(pt, plain); +} + +void test_aes_gcm_tamper() { + auto s = makeLoggedInSession(); + auto hKey = s.generateAesKey(32); + std::vector iv(12, 0x00); + std::vector plain = {1,2,3,4,5,6,7,8}; + auto ct = s.encryptAesGcm(hKey, iv, plain); + ct.back() ^= 0x01; + ASSERT_THROW(s.decryptAesGcm(hKey, iv, ct), CKR_ENCRYPTED_DATA_INVALID); +} + +// --------------------------------------------------------------------------- +// Tests: AES-CBC +// --------------------------------------------------------------------------- + +void test_aes_cbc_round_trip() { + auto s = makeLoggedInSession(); + auto hKey = s.generateAesKey(32); + std::vector iv(16, 0x01); + std::vector plain = {'H','e','l','l','o',' ','C','B','C'}; + auto ct = s.encryptAesCbc(hKey, iv, plain); + auto pt = s.decryptAesCbc(hKey, iv, ct); + ASSERT_EQ(pt, plain); +} + +// --------------------------------------------------------------------------- +// Tests: Digest +// --------------------------------------------------------------------------- + +void test_sha256() { + auto s = makeSession(); + std::vector data = {'a','b','c'}; + auto hash = s.digest(CKM_SHA256, data); + ASSERT_EQ(hash.size(), (size_t)32); + ASSERT_EQ(hash[0], (uint8_t)0xba); +} + +// --------------------------------------------------------------------------- +// Tests: Random +// --------------------------------------------------------------------------- + +void test_random_nonzero() { + auto s = makeSession(); + auto rnd = s.generateRandom(64); + ASSERT_EQ(rnd.size(), (size_t)64); + bool hasNonZero = false; + for (auto b : rnd) if (b) { hasNonZero = true; break; } + ASSERT_TRUE(hasNonZero); +} + +// --------------------------------------------------------------------------- +// Tests: FindObjects / DestroyObject +// --------------------------------------------------------------------------- + +void test_find_and_destroy() { + auto s = makeLoggedInSession(); + auto hKey = s.generateAesKey(16); + auto all = s.findAllObjects(); + ASSERT_TRUE(!all.empty()); + s.destroyObject(hKey); + ASSERT_THROW(s.destroyObject(hKey), CKR_OBJECT_HANDLE_INVALID); +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +int main() { + std::cout << "==============================\n"; + std::cout << " PKCS#11 C++ Unit Tests\n"; + std::cout << "==============================\n\n"; + + pkcs11::Library lib; + g_lib = &lib; + + std::vector tests = { + { "library: double init", test_library_init }, + { "library: slot list non-empty", test_slot_list_non_empty }, + { "session: open/close", test_open_close_session }, + { "session: login/logout", test_login_logout }, + { "session: wrong PIN", test_wrong_pin }, + { "session: double login", test_double_login }, + { "rsa: keygen", test_rsa_keygen }, + { "rsa: sign+verify", test_rsa_sign_verify }, + { "rsa: verify bad sig", test_rsa_verify_bad_sig }, + { "rsa: PSS sign+verify", test_rsa_pss_sign_verify }, + { "rsa: OAEP round trip", test_rsa_oaep_round_trip }, + { "ec: keygen", test_ec_keygen }, + { "ec: sign+verify", test_ec_sign_verify }, + { "aes: keygen", test_aes_keygen }, + { "aes-gcm: round trip", test_aes_gcm_round_trip }, + { "aes-gcm: tamper rejection", test_aes_gcm_tamper }, + { "aes-cbc: round trip", test_aes_cbc_round_trip }, + { "digest: SHA-256", test_sha256 }, + { "random: non-zero", test_random_nonzero }, + { "objects: find+destroy", test_find_and_destroy }, + }; + + for (auto& tc : tests) run(tc); + + std::cout << "\n------------------------------\n"; + std::cout << "Results: " << g_passed << " passed, " << g_failed << " failed\n"; + + return g_failed == 0 ? 0 : 1; +} diff --git a/docs/BUILD b/docs/BUILD new file mode 100644 index 0000000..049f193 --- /dev/null +++ b/docs/BUILD @@ -0,0 +1,11 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "conf.py", + "index.rst", +]) + +filegroup( + name = "docs", + srcs = ["conf.py", "index.rst"], +) diff --git a/docs/conf.py b/docs/conf.py index cf13475..2e918f9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,56 +1,4 @@ -# ******************************************************************************* -# Copyright (c) 2024 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = "Module Template Project" -project_url = "https://eclipse-score.github.io/module_template/" -project_prefix = "MODULE_TEMPLATE_" -author = "S-CORE" -version = "0.1" - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - - -extensions = [ - "sphinx_design", - "sphinx_needs", - "sphinxcontrib.plantuml", - "score_plantuml", - "score_metamodel", - "score_draw_uml_funcs", - "score_source_code_linker", - "score_layout", -] - -exclude_patterns = [ - # The following entries are not required when building the documentation via 'bazel - # build //docs:docs', as that command runs in a sandboxed environment. However, when - # building the documentation via 'bazel run //docs:incremental' or esbonio, these - # entries are required to prevent the build from failing. - "bazel-*", - ".venv_docs", -] - -templates_path = ["templates"] - -# Enable numref -numfig = True +project = "Cryptoki" +author = "Cryptoki Contributors" +extensions = [] +master_doc = "index" diff --git a/docs/index.rst b/docs/index.rst index f8e53da..1f42c35 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,82 +1,6 @@ -.. - # ******************************************************************************* - # Copyright (c) 2024 Contributors to the Eclipse Foundation - # - # See the NOTICE file(s) distributed with this work for additional - # information regarding copyright ownership. - # - # This program and the accompanying materials are made available under the - # terms of the Apache License Version 2.0 which is available at - # https://www.apache.org/licenses/LICENSE-2.0 - # - # SPDX-License-Identifier: Apache-2.0 - # ******************************************************************************* +Cryptoki +======== -Module Template Documentation -============================= +Welcome to the Bazel-oriented docs scaffold for Cryptoki. -This documentation describes the structure, usage and configuration of the Bazel-based C++/Rust module template. - -.. contents:: Table of Contents - :depth: 2 - :local: - -Overview --------- - -This repository provides a standardized setup for projects using **C++** or **Rust** and **Bazel** as a build system. -It integrates best practices for build, test, CI/CD and documentation. - -Requirements ------------- - -.. stkh_req:: Example Functional Requirement - :id: stkh_req__docgen_enabled__example - :status: valid - :safety: QM - :security: YES - :reqtype: Functional - :rationale: Ensure documentation builds are possible for all modules - - -Project Layout --------------- - -The module template includes the following top-level structure: - -- `src/`: Main C++/Rust sources -- `tests/`: Unit and integration tests -- `examples/`: Usage examples -- `docs/`: Documentation using `docs-as-code` -- `.github/workflows/`: CI/CD pipelines - -Quick Start ------------ - -To build the module: - -.. code-block:: bash - - bazel build //src/... - -To run tests: - -.. code-block:: bash - - bazel test //tests/... - -Configuration -------------- - -The `project_config.bzl` file defines metadata used by Bazel macros. - -Example: - -.. code-block:: python - - PROJECT_CONFIG = { - "asil_level": "QM", - "source_code": ["cpp", "rust"] - } - -This enables conditional behavior (e.g., choosing `clang-tidy` for C++ or `clippy` for Rust). +Use the repository README as the primary developer guide. diff --git a/examples/BUILD b/examples/BUILD index 012dd54..ea115e8 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -1,8 +1,27 @@ -# Needed for Dash tool to check python dependency licenses. +load("@crates//:defs.bzl", "aliases", "all_crate_deps") +load("@rules_rust//rust:defs.bzl", "rust_binary") + +package(default_visibility = ["//visibility:public"]) + filegroup( - name = "cargo_lock", - srcs = [ - "Cargo.lock", - ], - visibility = ["//visibility:public"], + name = "examples", + srcs = glob(["**/*.rs"]), +) + +rust_binary( + name = "pkcs11_demo", + crate_root = "pkcs11_demo.rs", + srcs = ["pkcs11_demo.rs"], + aliases = aliases(package_name = ""), + deps = ["//src:cryptoki_lib"] + all_crate_deps(normal = True, package_name = ""), + proc_macro_deps = all_crate_deps(proc_macro = True, package_name = ""), +) + +rust_binary( + name = "pkcs11_business_demo", + crate_root = "pkcs11_business_demo.rs", + srcs = ["pkcs11_business_demo.rs"], + aliases = aliases(package_name = ""), + deps = ["//src:cryptoki_lib"] + all_crate_deps(normal = True, package_name = ""), + proc_macro_deps = all_crate_deps(proc_macro = True, package_name = ""), ) diff --git a/examples/pkcs11_business_demo.rs b/examples/pkcs11_business_demo.rs new file mode 100644 index 0000000..f556536 --- /dev/null +++ b/examples/pkcs11_business_demo.rs @@ -0,0 +1,289 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Real-world PKCS#11 Business Logic Showcase. +//! +//! This example demonstrates how to orchestrate PKCS#11 cryptographic primitives +//! to solve actual business use cases, rather than just testing algorithms in a vacuum. +//! +//! Scenarios covered: +//! 1. Secure Randomness (Session/Nonce generation) +//! 2. Symmetric Encrypted Storage (AES-GCM for database records) +//! 3. Document Signing PKI (ECDSA for digital signatures) +//! 4. Envelope Encryption (AES Key Wrap for secure key exchange/storage) +//! 5. Secure Hashing (SHA-256 for file integrity) + +use std::ffi::c_void; +use std::ptr; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::C_GetFunctionList; + +/// Dispatch a call through a PKCS#11 function-list table. +macro_rules! p11 { + ($fl:expr, $func:ident $(, $arg:expr)* $(,)?) => { + ($fl.$func.unwrap())($($arg),*) + } +} + +#[repr(C)] +#[allow(non_snake_case)] +struct GcmParams { + pIv: *const u8, + ulIvLen: u64, + ulIvBits: u64, + pAAD: *const u8, + ulAADLen: u64, + ulTagBits: u64, +} + +const PIN: &[u8] = b"1234"; + +fn ck_ok(rv: CK_RV, label: &str) { + if rv != CKR_OK { + eprintln!(" FAIL {label}: {rv:#010x}"); + std::process::exit(1); + } +} + +fn section(title: &str) { + println!("\n══ {title} ══"); +} + +fn ok(label: &str) { + println!(" ok {label}"); +} + +fn info(label: &str, detail: &str) { + println!(" info {label}: {detail}"); +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect::>().join("") +} + +fn main() { + println!("Cryptoki — Business Logic Showcase"); + println!("=========================================="); + + unsafe { run() } + + println!("\n=========================================="); + println!("All business scenarios completed successfully."); +} + +unsafe fn run() { + // 1. Bootstrap the token and establish an authenticated session + let (fl_ptr, h_session) = bootstrap_and_login(); + let fl = &*fl_ptr; + + // 2. Execute isolated, real-world business scenarios + demo_secure_randomness(fl, h_session); + demo_symmetric_aead(fl, h_session); + demo_document_signing_pki(fl, h_session); + demo_envelope_encryption(fl, h_session); + demo_secure_hashing(fl, h_session); + + // 3. Teardown + cleanup_and_logout(fl, h_session); +} + +unsafe fn bootstrap_and_login() -> (*const CK_FUNCTION_LIST, CK_SESSION_HANDLE) { + section("Bootstrap: Obtain Dispatch Table & Login"); + let mut fl_ptr: *const CK_FUNCTION_LIST = ptr::null(); + ck_ok(C_GetFunctionList(&mut fl_ptr), "C_GetFunctionList"); + let fl = &*fl_ptr; + info("PKCS#11 Version", &format!("{}.{}", fl.version.major, fl.version.minor)); + + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, "C_Initialize failed"); + + let mut slot_count: CK_ULONG = 0; + ck_ok(p11!(fl, C_GetSlotList, CK_TRUE, ptr::null_mut(), &mut slot_count), "C_GetSlotList"); + let mut slots = vec![0u64; slot_count as usize]; + ck_ok(p11!(fl, C_GetSlotList, CK_TRUE, slots.as_mut_ptr(), &mut slot_count), "C_GetSlotList"); + + let mut h_session: CK_SESSION_HANDLE = 0; + ck_ok(p11!(fl, C_OpenSession, slots[0], CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h_session), "C_OpenSession"); + ck_ok(p11!(fl, C_Login, h_session, CKU_USER, PIN.as_ptr(), PIN.len() as CK_ULONG), "C_Login"); + + ok("Successfully established an authenticated R/W session"); + (fl_ptr, h_session) +} + +unsafe fn cleanup_and_logout(fl: &CK_FUNCTION_LIST, h_session: CK_SESSION_HANDLE) { + section("Teardown: Logout & Finalize"); + ck_ok(p11!(fl, C_Logout, h_session), "C_Logout"); + ck_ok(p11!(fl, C_CloseSession, h_session), "C_CloseSession"); + ck_ok(p11!(fl, C_Finalize, ptr::null_mut()), "C_Finalize"); + ok("Session closed, library finalised securely."); +} + +unsafe fn demo_secure_randomness(fl: &CK_FUNCTION_LIST, h_session: CK_SESSION_HANDLE) { + section("Scenario 1: Secure Randomness (Session/Nonce Generation)"); + let mut rand_buf = [0u8; 32]; + ck_ok(p11!(fl, C_GenerateRandom, h_session, rand_buf.as_mut_ptr(), 32), "C_GenerateRandom"); + info("Generated Secure Session ID", &hex(&rand_buf)); + ok("Randomness generated directly from HSM/Provider entropy pool."); +} + +unsafe fn demo_symmetric_aead(fl: &CK_FUNCTION_LIST, h_session: CK_SESSION_HANDLE) { + section("Scenario 2: Symmetric Encrypted Storage (AES-GCM)"); + + let key_len: CK_ULONG = 32; + let mut aes_attrs = [CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: &key_len as *const CK_ULONG as *mut c_void, + ulValueLen: 8, + }]; + let mech_aes_gen = CK_MECHANISM { mechanism: CKM_AES_KEY_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut h_aes: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_GenerateKey, h_session, &mech_aes_gen, aes_attrs.as_mut_ptr(), 1, &mut h_aes), "Generate AES-256 DEK"); + + let pii_record = b"{\"ssn\": \"123-45-6789\", \"dob\": \"1980-01-01\"}"; + let db_row_id = b"row_id=987654"; + let iv = [0x12u8; 12]; + + let gcm_params = GcmParams { + pIv: iv.as_ptr(), ulIvLen: iv.len() as u64, ulIvBits: (iv.len() * 8) as u64, + pAAD: db_row_id.as_ptr(), ulAADLen: db_row_id.len() as u64, ulTagBits: 128, + }; + let mech_gcm = CK_MECHANISM { + mechanism: CKM_AES_GCM, + pParameter: &gcm_params as *const GcmParams as *mut c_void, + ulParameterLen: std::mem::size_of::() as CK_ULONG, + }; + + ck_ok(p11!(fl, C_EncryptInit, h_session, &mech_gcm, h_aes), "Initialize AES-GCM Encryption"); + let mut ciphertext = vec![0u8; pii_record.len() + 16]; + let mut ct_len = ciphertext.len() as CK_ULONG; + ck_ok(p11!(fl, C_Encrypt, h_session, pii_record.as_ptr(), pii_record.len() as CK_ULONG, ciphertext.as_mut_ptr(), &mut ct_len), "Encrypt PII Record"); + ciphertext.truncate(ct_len as usize); + info("Encrypted Database Payload (CT + Tag)", &hex(&ciphertext)); + + ck_ok(p11!(fl, C_DecryptInit, h_session, &mech_gcm, h_aes), "Initialize AES-GCM Decryption"); + let mut plaintext = vec![0u8; ciphertext.len()]; + let mut pt_len = plaintext.len() as CK_ULONG; + ck_ok(p11!(fl, C_Decrypt, h_session, ciphertext.as_ptr(), ct_len, plaintext.as_mut_ptr(), &mut pt_len), "Decrypt PII Record"); + plaintext.truncate(pt_len as usize); + + assert_eq!(plaintext, pii_record, "Decrypted record does not match!"); + ok("Successfully encrypted and authenticated database record."); + ck_ok(p11!(fl, C_DestroyObject, h_session, h_aes), "Destroy AES DEK"); +} + +unsafe fn demo_document_signing_pki(fl: &CK_FUNCTION_LIST, h_session: CK_SESSION_HANDLE) { + section("Scenario 3: Document Signing PKI (ECDSA P-256)"); + + let p256_oid = [0x06u8, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; + let mut pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_EC_PARAMS, + pValue: p256_oid.as_ptr() as *mut c_void, + ulValueLen: p256_oid.len() as CK_ULONG, + }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech_gen = CK_MECHANISM { mechanism: CKM_EC_KEY_PAIR_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + + let mut h_pub: CK_OBJECT_HANDLE = 0; + let mut h_priv: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_GenerateKeyPair, h_session, &mech_gen, pub_attrs.as_mut_ptr(), 1, priv_attrs.as_mut_ptr(), 0, &mut h_pub, &mut h_priv), "Generate ECDSA Keypair"); + + let document_hash = b"8e35c2cd3bf6641bdb0e2050b76932cbb2e6034a0ddfad1d928b0488"; // Hash to sign + let mech_ecdsa = CK_MECHANISM { mechanism: CKM_ECDSA, pParameter: ptr::null(), ulParameterLen: 0 }; + + ck_ok(p11!(fl, C_SignInit, h_session, &mech_ecdsa, h_priv), "Initialize ECDSA Signature"); + let mut signature = vec![0u8; 72]; + let mut sig_len = 72 as CK_ULONG; + ck_ok(p11!(fl, C_Sign, h_session, document_hash.as_ptr(), document_hash.len() as CK_ULONG, signature.as_mut_ptr(), &mut sig_len), "Sign Document Hash"); + signature.truncate(sig_len as usize); + info("Document Signature (DER)", &hex(&signature)); + + ck_ok(p11!(fl, C_VerifyInit, h_session, &mech_ecdsa, h_pub), "Initialize ECDSA Verification"); + ck_ok(p11!(fl, C_Verify, h_session, document_hash.as_ptr(), document_hash.len() as CK_ULONG, signature.as_ptr(), sig_len), "Verify Document Signature"); + ok("Document signature verified successfully."); + + ck_ok(p11!(fl, C_DestroyObject, h_session, h_priv), "Destroy ECDSA Private Key"); + ck_ok(p11!(fl, C_DestroyObject, h_session, h_pub), "Destroy ECDSA Public Key"); +} + +unsafe fn demo_envelope_encryption(fl: &CK_FUNCTION_LIST, h_session: CK_SESSION_HANDLE) { + section("Scenario 4: Envelope Encryption (AES Key Wrap)"); + + let val_true = [CK_TRUE]; + + // 1. Generate the Master KEK (Key Encrypting Key) + let key_len: CK_ULONG = 32; + let mut kek_attrs = [ + CK_ATTRIBUTE { r#type: CKA_VALUE_LEN, pValue: &key_len as *const CK_ULONG as *mut c_void, ulValueLen: 8 }, + CK_ATTRIBUTE { r#type: CKA_WRAP, pValue: val_true.as_ptr() as *mut c_void, ulValueLen: 1 }, + CK_ATTRIBUTE { r#type: CKA_UNWRAP, pValue: val_true.as_ptr() as *mut c_void, ulValueLen: 1 }, + ]; + let mech_aes_gen = CK_MECHANISM { mechanism: CKM_AES_KEY_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut h_kek: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_GenerateKey, h_session, &mech_aes_gen, kek_attrs.as_mut_ptr(), 3, &mut h_kek), "Generate Master KEK"); + + // 2. Generate the ephemeral DEK (Data Encrypting Key) + let mut dek_attrs = [ + CK_ATTRIBUTE { r#type: CKA_VALUE_LEN, pValue: &key_len as *const CK_ULONG as *mut c_void, ulValueLen: 8 }, + CK_ATTRIBUTE { r#type: CKA_EXTRACTABLE, pValue: val_true.as_ptr() as *mut c_void, ulValueLen: 1 }, + ]; + let mut h_dek: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_GenerateKey, h_session, &mech_aes_gen, dek_attrs.as_mut_ptr(), 2, &mut h_dek), "Generate Ephemeral DEK"); + + // 3. Wrap DEK with KEK + let mech_wrap = CK_MECHANISM { mechanism: CKM_AES_KEY_WRAP, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut wrapped_key = vec![0u8; 64]; + let mut wrapped_len = 64 as CK_ULONG; + ck_ok(p11!(fl, C_WrapKey, h_session, &mech_wrap, h_kek, h_dek, wrapped_key.as_mut_ptr(), &mut wrapped_len), "Wrap DEK with Master KEK"); + wrapped_key.truncate(wrapped_len as usize); + info("Wrapped DEK Blob (RFC 3394)", &hex(&wrapped_key)); + + // 4. Unwrap to a new handle + let mut unwrap_attrs = [ + CK_ATTRIBUTE { r#type: CKA_CLASS, pValue: &(CKO_SECRET_KEY as CK_ULONG) as *const CK_ULONG as *mut c_void, ulValueLen: 8 }, + CK_ATTRIBUTE { r#type: CKA_KEY_TYPE, pValue: &(CKK_AES as CK_ULONG) as *const CK_ULONG as *mut c_void, ulValueLen: 8 }, + ]; + let mut h_unwrapped_dek: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_UnwrapKey, h_session, &mech_wrap, h_kek, wrapped_key.as_ptr(), wrapped_len, unwrap_attrs.as_mut_ptr(), 2, &mut h_unwrapped_dek), "Unwrap Blob to New DEK Handle"); + ok("Successfully executed Envelope Encryption (Wrap/Unwrap)."); + + ck_ok(p11!(fl, C_DestroyObject, h_session, h_kek), "Destroy Master KEK"); + ck_ok(p11!(fl, C_DestroyObject, h_session, h_dek), "Destroy Original DEK"); + ck_ok(p11!(fl, C_DestroyObject, h_session, h_unwrapped_dek), "Destroy Unwrapped DEK"); +} + +unsafe fn demo_secure_hashing(fl: &CK_FUNCTION_LIST, h_session: CK_SESSION_HANDLE) { + section("Scenario 5: Secure Hashing (SHA-256)"); + + let mech_sha256 = CK_MECHANISM { mechanism: CKM_SHA256, pParameter: ptr::null(), ulParameterLen: 0 }; + ck_ok(p11!(fl, C_DigestInit, h_session, &mech_sha256), "Initialize SHA-256 Digest"); + + let file_chunks: &[&[u8]] = &[ + b"Business Contract Version 1.0\n", + b"Section 1: Confidentiality...\n", + b"Section 2: Terms and Conditions...\n", + ]; + + for (i, chunk) in file_chunks.iter().enumerate() { + ck_ok(p11!(fl, C_DigestUpdate, h_session, chunk.as_ptr(), chunk.len() as CK_ULONG), &format!("DigestUpdate (Chunk {})", i + 1)); + } + + let mut digest_buf = [0u8; 32]; + let mut digest_len: CK_ULONG = 32; + ck_ok(p11!(fl, C_DigestFinal, h_session, digest_buf.as_mut_ptr(), &mut digest_len), "Finalize Digest"); + + info("Document SHA-256 Hash", &hex(&digest_buf)); + ok("File integrity hash computed successfully using streaming chunks."); +} diff --git a/examples/pkcs11_demo.rs b/examples/pkcs11_demo.rs new file mode 100644 index 0000000..cce8acf --- /dev/null +++ b/examples/pkcs11_demo.rs @@ -0,0 +1,739 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Full PKCS#11 scenario demo — function-list dispatch pattern. +//! +//! Demonstrates the idiomatic way a real application consumes a PKCS#11 +//! shared library: only **two** symbols are resolved via dlsym — +//! `C_GetFunctionList` (v2.40) and `C_GetInterface` (v3.0). Every +//! subsequent call is dispatched through the `CK_FUNCTION_LIST` (or +//! `CK_FUNCTION_LIST_3_0`) table returned by those bootstrap functions. +//! +//! The `p11!` macro used throughout this file unwraps an `Option` +//! slot from the function list and calls it, keeping the call-sites concise. +//! +//! Scenario: +//! 0. C_GetFunctionList — obtain dispatch table (bootstrap) +//! 1. C_Initialize — start the library, register OpenSslEngine +//! 2. C_GetInfo — inspect library version +//! 3. C_GetSlotList — enumerate slots +//! 4. C_GetMechanismList — list supported mechanisms +//! 5. C_OpenSession — open an R/W session +//! 6. C_Login — authenticate as CKU_USER +//! 7. C_GenerateRandom — fill a 32-byte random buffer +//! 8. C_GenerateKey — generate AES-256 key +//! C_EncryptInit+C_Encrypt (AES-CBC-PAD) +//! C_DecryptInit+C_Decrypt (AES-CBC-PAD) +//! C_EncryptInit+C_Encrypt (AES-GCM) +//! C_DecryptInit+C_Decrypt (AES-GCM) +//! 9. C_GenerateKeyPair — generate RSA-2048 key pair +//! C_GetAttributeValue — read CKA_MODULUS_BITS, CKA_MODULUS +//! C_GetAttributeValue — CKA_VALUE on private key → CKR_ATTRIBUTE_SENSITIVE +//! C_SignInit+C_Sign (CKM_SHA256_RSA_PKCS) +//! C_VerifyInit+C_Verify (CKM_SHA256_RSA_PKCS) +//! C_SignInit+C_Sign (CKM_SHA256_RSA_PKCS_PSS) +//! C_VerifyInit+C_Verify (CKM_SHA256_RSA_PKCS_PSS) +//! C_EncryptInit+C_Encrypt (CKM_RSA_PKCS_OAEP) +//! C_DecryptInit+C_Decrypt (CKM_RSA_PKCS_OAEP) +//! 10. C_GenerateKeyPair — generate EC P-256 key pair +//! C_GetAttributeValue — CKA_EC_POINT from private key (engine fallback) +//! C_SignInit+C_Sign (CKM_ECDSA) +//! C_VerifyInit+C_Verify (CKM_ECDSA) +//! 11. C_GenerateKeyPair — generate Ed25519 key pair (v3.0) +//! C_SignInit+C_Sign (CKM_EDDSA) +//! C_VerifyInit+C_Verify (CKM_EDDSA) +//! 12. C_GenerateKey — generate ChaCha20 key (v3.0) +//! C_EncryptInit+C_Encrypt (CKM_CHACHA20_POLY1305) +//! C_DecryptInit+C_Decrypt (CKM_CHACHA20_POLY1305) +//! 13. SHA-3 / SHA-384 / SHA-512 digests (v3.0) +//! 14. C_DigestInit+C_DigestUpdate+C_DigestFinal — multi-part SHA-256 +//! 15. C_GetInterfaceList + C_GetInterface (v3.0 interface discovery) +//! 16. C_FindObjectsInit+C_FindObjects+C_FindObjectsFinal — enumerate keys +//! 17. C_DestroyObject — delete the AES key +//! 18. C_Logout + C_CloseSession + C_Finalize + +use std::ffi::c_void; +use std::ptr; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::{C_GetFunctionList, C_GetInterface}; + +/// Dispatch a call through a PKCS#11 function-list table. +/// +/// Unwraps the `Option` slot and invokes it with the given arguments. +macro_rules! p11 { + ($fl:expr, $func:ident $(, $arg:expr)* $(,)?) => { + ($fl.$func.unwrap())($($arg),*) + } +} + +// ── GCM parameter struct (mirrors CK_GCM_PARAMS) ───────────────────────── + +#[repr(C)] +#[allow(non_snake_case)] +struct GcmParams { + pIv: *const u8, + ulIvLen: u64, + ulIvBits: u64, + pAAD: *const u8, + ulAADLen: u64, + ulTagBits: u64, +} + +// ── Helpers ─────────────────────────────────────────────────────────────── + +const PIN: &[u8] = b"1234"; + +fn ck_ok(rv: CK_RV, label: &str) { + if rv != CKR_OK { + eprintln!(" FAIL {label}: {rv:#010x}"); + std::process::exit(1); + } +} + +fn section(title: &str) { + println!("\n══ {title} ══"); +} + +fn ok(label: &str) { + println!(" ok {label}"); +} + +fn info(label: &str, detail: &str) { + println!(" info {label}: {detail}"); +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect::>().join("") +} + +// ── Main ────────────────────────────────────────────────────────────────── + +fn main() { + println!("Cryptoki demo — full PKCS#11 scenario"); + println!("=========================================="); + + unsafe { run() } + + println!("\n=========================================="); + println!("All scenarios completed successfully."); +} + +unsafe fn run() { + // ── 0. C_GetFunctionList — bootstrap ──────────────────────────────── + // + // In a real application this is the ONLY symbol obtained via dlsym. + // Every subsequent call goes through the returned function pointer table. + section("0. C_GetFunctionList — obtain dispatch table"); + let mut fl_ptr: *const CK_FUNCTION_LIST = ptr::null(); + let rv = C_GetFunctionList(&mut fl_ptr); + assert_eq!(rv, CKR_OK, "C_GetFunctionList failed: {rv:#010x}"); + assert!(!fl_ptr.is_null()); + let fl = &*fl_ptr; + info("version", &format!("{}.{}", fl.version.major, fl.version.minor)); + ok("function list obtained — all subsequent calls go through this table"); + + // ── 1. C_Initialize ─────────────────────────────────────────────────── + + section("1. C_Initialize"); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + ok("library initialised, OpenSslEngine registered"); + + // ── 2. C_GetInfo ────────────────────────────────────────────────────── + + section("2. C_GetInfo"); + let mut ck_info: CK_INFO = std::mem::zeroed(); + ck_ok(p11!(fl, C_GetInfo, &mut ck_info), "C_GetInfo"); + info("cryptokiVersion", + &format!("{}.{}", ck_info.cryptokiVersion.major, ck_info.cryptokiVersion.minor)); + let mfr = std::str::from_utf8(&ck_info.manufacturerID) + .unwrap_or("?").trim_end(); + info("manufacturerID", mfr); + let desc = std::str::from_utf8(&ck_info.libraryDescription) + .unwrap_or("?").trim_end(); + info("libraryDescription", desc); + + // ── 3. C_GetSlotList ────────────────────────────────────────────────── + + section("3. C_GetSlotList"); + let mut slot_count: CK_ULONG = 0; + ck_ok(p11!(fl, C_GetSlotList, CK_TRUE, ptr::null_mut(), &mut slot_count), "C_GetSlotList (count)"); + let mut slots = vec![0u64; slot_count as usize]; + ck_ok(p11!(fl, C_GetSlotList, CK_TRUE, slots.as_mut_ptr(), &mut slot_count), "C_GetSlotList"); + info("slot count", &slot_count.to_string()); + info("slot IDs", &format!("{slots:?}")); + + // ── 4. C_GetMechanismList ───────────────────────────────────────────── + + section("4. C_GetMechanismList"); + let mut mech_count: CK_ULONG = 0; + ck_ok(p11!(fl, C_GetMechanismList, 0, ptr::null_mut(), &mut mech_count), "C_GetMechanismList (count)"); + let mut mechs = vec![0u64; mech_count as usize]; + ck_ok(p11!(fl, C_GetMechanismList, 0, mechs.as_mut_ptr(), &mut mech_count), "C_GetMechanismList"); + info("mechanism count", &mech_count.to_string()); + + // ── 5+6. C_OpenSession + C_Login ───────────────────────────────────── + + section("5+6. C_OpenSession + C_Login"); + let mut h_session: CK_SESSION_HANDLE = 0; + ck_ok(p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h_session), "C_OpenSession"); + ck_ok(p11!(fl, C_Login, h_session, CKU_USER, PIN.as_ptr(), PIN.len() as CK_ULONG), "C_Login"); + info("session handle", &h_session.to_string()); + ok("logged in as CKU_USER"); + + // ── 7. C_GenerateRandom ─────────────────────────────────────────────── + + section("7. C_GenerateRandom"); + let mut rand_buf = [0u8; 32]; + ck_ok(p11!(fl, C_GenerateRandom, h_session, rand_buf.as_mut_ptr(), 32), "C_GenerateRandom"); + info("32 random bytes", &hex(&rand_buf)); + + // ── 8. AES ─────────────────────────────────────────────────────────── + + section("8. AES-256 — key generation + CBC + GCM"); + + // Generate AES-256 key + let key_len: CK_ULONG = 32; + let mut aes_attrs = [CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: &key_len as *const CK_ULONG as *mut c_void, + ulValueLen: 8, + }]; + let mech_aes_gen = CK_MECHANISM { mechanism: CKM_AES_KEY_GEN, + pParameter: ptr::null(), ulParameterLen: 0 }; + let mut h_aes: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_GenerateKey, h_session, &mech_aes_gen, + aes_attrs.as_mut_ptr(), 1, &mut h_aes), "C_GenerateKey(AES-256)"); + info("AES-256 handle", &h_aes.to_string()); + + // AES-CBC-PAD round-trip + let plaintext = b"Hello, PKCS#11 AES-CBC world!!!"; // 32 bytes — one block + let iv_cbc = [0x01u8; 16]; + let mech_cbc = CK_MECHANISM { mechanism: CKM_AES_CBC_PAD, + pParameter: iv_cbc.as_ptr() as *mut c_void, ulParameterLen: 16 }; + + ck_ok(p11!(fl, C_EncryptInit, h_session, &mech_cbc, h_aes), "C_EncryptInit(AES-CBC)"); + let mut ct_cbc = vec![0u8; 64]; + let mut ct_cbc_len: CK_ULONG = 64; + ck_ok(p11!(fl, C_Encrypt, h_session, plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct_cbc.as_mut_ptr(), &mut ct_cbc_len), "C_Encrypt(AES-CBC)"); + ct_cbc.truncate(ct_cbc_len as usize); + info("AES-CBC ciphertext", &hex(&ct_cbc)); + + ck_ok(p11!(fl, C_DecryptInit, h_session, &mech_cbc, h_aes), "C_DecryptInit(AES-CBC)"); + let mut pt_cbc = vec![0u8; 64]; + let mut pt_cbc_len: CK_ULONG = 64; + ck_ok(p11!(fl, C_Decrypt, h_session, ct_cbc.as_ptr(), ct_cbc_len, + pt_cbc.as_mut_ptr(), &mut pt_cbc_len), "C_Decrypt(AES-CBC)"); + pt_cbc.truncate(pt_cbc_len as usize); + assert_eq!(pt_cbc, plaintext, "AES-CBC round-trip mismatch"); + ok("AES-CBC-PAD encrypt/decrypt round-trip verified"); + + // AES-GCM round-trip + let iv_gcm = [0x02u8; 12]; + let aad = b"additional authenticated data"; + let gcm_params = GcmParams { + pIv: iv_gcm.as_ptr(), + ulIvLen: iv_gcm.len() as u64, + ulIvBits: (iv_gcm.len() * 8) as u64, + pAAD: aad.as_ptr(), + ulAADLen: aad.len() as u64, + ulTagBits: 128, + }; + let mech_gcm = CK_MECHANISM { + mechanism: CKM_AES_GCM, + pParameter: &gcm_params as *const GcmParams as *mut c_void, + ulParameterLen: std::mem::size_of::() as CK_ULONG, + }; + + ck_ok(p11!(fl, C_EncryptInit, h_session, &mech_gcm, h_aes), "C_EncryptInit(AES-GCM)"); + let mut ct_gcm = vec![0u8; plaintext.len() + 16]; + let mut ct_gcm_len = ct_gcm.len() as CK_ULONG; + ck_ok(p11!(fl, C_Encrypt, h_session, plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct_gcm.as_mut_ptr(), &mut ct_gcm_len), "C_Encrypt(AES-GCM)"); + ct_gcm.truncate(ct_gcm_len as usize); + info("AES-GCM ciphertext+tag", &hex(&ct_gcm)); + + ck_ok(p11!(fl, C_DecryptInit, h_session, &mech_gcm, h_aes), "C_DecryptInit(AES-GCM)"); + let mut pt_gcm = vec![0u8; ct_gcm.len()]; + let mut pt_gcm_len = pt_gcm.len() as CK_ULONG; + ck_ok(p11!(fl, C_Decrypt, h_session, ct_gcm.as_ptr(), ct_gcm_len, + pt_gcm.as_mut_ptr(), &mut pt_gcm_len), "C_Decrypt(AES-GCM)"); + pt_gcm.truncate(pt_gcm_len as usize); + assert_eq!(pt_gcm, plaintext, "AES-GCM round-trip mismatch"); + ok("AES-GCM encrypt/decrypt round-trip verified (with AAD)"); + + // ── 9. RSA-2048 ────────────────────────────────────────────────────── + + section("9. RSA-2048 — key generation + attributes + sign/verify + OAEP"); + + let modulus_bits: CK_ULONG = 2048; + let pub_exp: [u8; 3] = [0x01, 0x00, 0x01]; // 65537 + let mut rsa_pub_attrs = [ + CK_ATTRIBUTE { r#type: CKA_MODULUS_BITS, pValue: &modulus_bits as *const CK_ULONG as *mut c_void, ulValueLen: 8 }, + CK_ATTRIBUTE { r#type: CKA_PUBLIC_EXPONENT, pValue: pub_exp.as_ptr() as *mut c_void, ulValueLen: 3 }, + ]; + let mut rsa_priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech_rsa_gen = CK_MECHANISM { mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, + pParameter: ptr::null(), ulParameterLen: 0 }; + let mut h_rsa_pub: CK_OBJECT_HANDLE = 0; + let mut h_rsa_priv: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_GenerateKeyPair, h_session, &mech_rsa_gen, + rsa_pub_attrs.as_mut_ptr(), 2, + rsa_priv_attrs.as_mut_ptr(), 0, + &mut h_rsa_pub, &mut h_rsa_priv), "C_GenerateKeyPair(RSA-2048)"); + info("RSA-2048 pub handle", &h_rsa_pub.to_string()); + info("RSA-2048 priv handle", &h_rsa_priv.to_string()); + + // Read CKA_MODULUS_BITS from public key (HashMap path) + let mut bits: u64 = 0; + let mut attr_bits = CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: &mut bits as *mut u64 as *mut c_void, + ulValueLen: 8, + }; + ck_ok(p11!(fl, C_GetAttributeValue, h_session, h_rsa_pub, &mut attr_bits, 1), + "C_GetAttributeValue(CKA_MODULUS_BITS)"); + info("CKA_MODULUS_BITS", &bits.to_string()); + assert_eq!(bits, 2048); + + // Read CKA_MODULUS from public key (HashMap path) + let mut attr_mod = CK_ATTRIBUTE { r#type: CKA_MODULUS, pValue: ptr::null_mut(), ulValueLen: 0 }; + ck_ok(p11!(fl, C_GetAttributeValue, h_session, h_rsa_pub, &mut attr_mod, 1), + "C_GetAttributeValue(CKA_MODULUS, length)"); + let mod_len = attr_mod.ulValueLen; + let mut modulus_buf = vec![0u8; mod_len as usize]; + attr_mod.pValue = modulus_buf.as_mut_ptr() as *mut c_void; + attr_mod.ulValueLen = mod_len; + ck_ok(p11!(fl, C_GetAttributeValue, h_session, h_rsa_pub, &mut attr_mod, 1), + "C_GetAttributeValue(CKA_MODULUS, value)"); + info("CKA_MODULUS (first 8 bytes)", &hex(&modulus_buf[..8])); + + // CKA_VALUE on private key → must be sensitive (engine fallback path) + let mut dummy = [0u8; 512]; + let mut attr_val = CK_ATTRIBUTE { + r#type: CKA_VALUE, + pValue: dummy.as_mut_ptr() as *mut c_void, + ulValueLen: 512, + }; + let rv_sensitive = p11!(fl, C_GetAttributeValue, h_session, h_rsa_priv, &mut attr_val, 1); + assert_eq!(rv_sensitive, CKR_ATTRIBUTE_SENSITIVE, + "CKA_VALUE on RSA private key must be CKR_ATTRIBUTE_SENSITIVE"); + ok("CKA_VALUE on RSA private key → CKR_ATTRIBUTE_SENSITIVE (correct)"); + + // RSA PKCS#1 v1.5 sign / verify + let message = b"The quick brown fox jumps over the lazy dog"; + let mech_rsa_pkcs = CK_MECHANISM { mechanism: CKM_SHA256_RSA_PKCS, + pParameter: ptr::null(), ulParameterLen: 0 }; + + ck_ok(p11!(fl, C_SignInit, h_session, &mech_rsa_pkcs, h_rsa_priv), "C_SignInit(SHA256_RSA_PKCS)"); + let mut sig_len: CK_ULONG = 0; + ck_ok(p11!(fl, C_Sign, h_session, message.as_ptr(), message.len() as CK_ULONG, + ptr::null_mut(), &mut sig_len), "C_Sign (length)"); + let mut signature = vec![0u8; sig_len as usize]; + ck_ok(p11!(fl, C_Sign, h_session, message.as_ptr(), message.len() as CK_ULONG, + signature.as_mut_ptr(), &mut sig_len), "C_Sign"); + signature.truncate(sig_len as usize); + info("RSA-PKCS1 signature (first 8 bytes)", &hex(&signature[..8])); + + ck_ok(p11!(fl, C_VerifyInit, h_session, &mech_rsa_pkcs, h_rsa_pub), "C_VerifyInit(SHA256_RSA_PKCS)"); + ck_ok(p11!(fl, C_Verify, h_session, message.as_ptr(), message.len() as CK_ULONG, + signature.as_ptr(), sig_len), "C_Verify(SHA256_RSA_PKCS)"); + ok("RSA PKCS#1 v1.5 SHA-256 sign/verify passed"); + + // RSA-PSS sign / verify + let mech_pss = CK_MECHANISM { mechanism: CKM_SHA256_RSA_PKCS_PSS, + pParameter: ptr::null(), ulParameterLen: 0 }; + + ck_ok(p11!(fl, C_SignInit, h_session, &mech_pss, h_rsa_priv), "C_SignInit(PSS)"); + let mut pss_sig_len = sig_len; + let mut pss_sig = vec![0u8; pss_sig_len as usize]; + ck_ok(p11!(fl, C_Sign, h_session, message.as_ptr(), message.len() as CK_ULONG, + pss_sig.as_mut_ptr(), &mut pss_sig_len), "C_Sign(PSS)"); + pss_sig.truncate(pss_sig_len as usize); + + ck_ok(p11!(fl, C_VerifyInit, h_session, &mech_pss, h_rsa_pub), "C_VerifyInit(PSS)"); + ck_ok(p11!(fl, C_Verify, h_session, message.as_ptr(), message.len() as CK_ULONG, + pss_sig.as_ptr(), pss_sig_len), "C_Verify(PSS)"); + ok("RSA-PSS SHA-256 sign/verify passed"); + + // RSA-OAEP encrypt / decrypt + let secret_msg = b"secret payload"; + let mech_oaep = CK_MECHANISM { mechanism: CKM_RSA_PKCS_OAEP, + pParameter: ptr::null(), ulParameterLen: 0 }; + + ck_ok(p11!(fl, C_EncryptInit, h_session, &mech_oaep, h_rsa_pub), "C_EncryptInit(OAEP)"); + let mut oaep_ct = vec![0u8; 512]; + let mut oaep_ct_len = oaep_ct.len() as CK_ULONG; + ck_ok(p11!(fl, C_Encrypt, h_session, secret_msg.as_ptr(), secret_msg.len() as CK_ULONG, + oaep_ct.as_mut_ptr(), &mut oaep_ct_len), "C_Encrypt(OAEP)"); + oaep_ct.truncate(oaep_ct_len as usize); + + ck_ok(p11!(fl, C_DecryptInit, h_session, &mech_oaep, h_rsa_priv), "C_DecryptInit(OAEP)"); + let mut oaep_pt = vec![0u8; 512]; + let mut oaep_pt_len = oaep_pt.len() as CK_ULONG; + ck_ok(p11!(fl, C_Decrypt, h_session, oaep_ct.as_ptr(), oaep_ct_len, + oaep_pt.as_mut_ptr(), &mut oaep_pt_len), "C_Decrypt(OAEP)"); + oaep_pt.truncate(oaep_pt_len as usize); + assert_eq!(oaep_pt, secret_msg, "RSA-OAEP round-trip mismatch"); + ok("RSA-OAEP encrypt/decrypt round-trip verified"); + + // ── 10. EC P-256 ───────────────────────────────────────────────────── + + section("10. EC P-256 — key generation + attributes + ECDSA"); + + let p256_oid = [0x06u8, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; + let mut ec_pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_EC_PARAMS, + pValue: p256_oid.as_ptr() as *mut c_void, + ulValueLen: p256_oid.len() as CK_ULONG, + }]; + let mut ec_priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech_ec_gen = CK_MECHANISM { mechanism: CKM_EC_KEY_PAIR_GEN, + pParameter: ptr::null(), ulParameterLen: 0 }; + let mut h_ec_pub: CK_OBJECT_HANDLE = 0; + let mut h_ec_priv: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_GenerateKeyPair, h_session, &mech_ec_gen, + ec_pub_attrs.as_mut_ptr(), 1, + ec_priv_attrs.as_mut_ptr(), 0, + &mut h_ec_pub, &mut h_ec_priv), "C_GenerateKeyPair(EC P-256)"); + info("EC P-256 pub handle", &h_ec_pub.to_string()); + info("EC P-256 priv handle", &h_ec_priv.to_string()); + + // CKA_EC_POINT from private key via engine fallback (not in HashMap) + let mut ec_point_attr = CK_ATTRIBUTE { r#type: CKA_EC_POINT, + pValue: ptr::null_mut(), ulValueLen: 0 }; + ck_ok(p11!(fl, C_GetAttributeValue, h_session, h_ec_priv, &mut ec_point_attr, 1), + "C_GetAttributeValue(CKA_EC_POINT, length) from private key"); + let point_len = ec_point_attr.ulValueLen as usize; + let mut point_buf = vec![0u8; point_len]; + ec_point_attr.pValue = point_buf.as_mut_ptr() as *mut c_void; + ec_point_attr.ulValueLen = point_len as CK_ULONG; + ck_ok(p11!(fl, C_GetAttributeValue, h_session, h_ec_priv, &mut ec_point_attr, 1), + "C_GetAttributeValue(CKA_EC_POINT, value) from private key"); + info("CKA_EC_POINT derived from private key (first 8 bytes)", &hex(&point_buf[..8])); + ok("CKA_EC_POINT retrieved from EC private key via engine fallback"); + + // ECDSA sign / verify + let ec_message = b"sign this with ECDSA P-256"; + let mech_ecdsa = CK_MECHANISM { mechanism: CKM_ECDSA, + pParameter: ptr::null(), ulParameterLen: 0 }; + + // P-256 DER-encoded ECDSA signature is at most 72 bytes. + // ECDSA is randomised, so the two-call (null→length, then data) pattern + // is unsafe for variable-length DER — just pre-allocate the max. + let mut ec_sig = vec![0u8; 72]; + let mut ec_sig_len: CK_ULONG = 72; + ck_ok(p11!(fl, C_SignInit, h_session, &mech_ecdsa, h_ec_priv), "C_SignInit(ECDSA)"); + ck_ok(p11!(fl, C_Sign, h_session, ec_message.as_ptr(), ec_message.len() as CK_ULONG, + ec_sig.as_mut_ptr(), &mut ec_sig_len), "C_Sign(ECDSA)"); + ec_sig.truncate(ec_sig_len as usize); + info("ECDSA signature (DER, first 8 bytes)", &hex(&ec_sig[..8])); + + ck_ok(p11!(fl, C_VerifyInit, h_session, &mech_ecdsa, h_ec_pub), "C_VerifyInit(ECDSA)"); + ck_ok(p11!(fl, C_Verify, h_session, ec_message.as_ptr(), ec_message.len() as CK_ULONG, + ec_sig.as_ptr(), ec_sig_len), "C_Verify(ECDSA)"); + ok("ECDSA P-256 sign/verify passed"); + + // ── 11. EdDSA (Ed25519) — v3.0 ─────────────────────────────────────── + + section("11. EdDSA (Ed25519) — key generation + sign/verify (v3.0)"); + + let ed25519_oid = [0x06u8, 0x03, 0x2b, 0x65, 0x70]; // OID 1.3.101.112 + let mut ed_pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_EC_PARAMS, + pValue: ed25519_oid.as_ptr() as *mut c_void, + ulValueLen: ed25519_oid.len() as CK_ULONG, + }]; + let mut ed_priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech_ed_gen = CK_MECHANISM { mechanism: CKM_EC_EDWARDS_KEY_PAIR_GEN, + pParameter: ptr::null(), ulParameterLen: 0 }; + let mut h_ed_pub: CK_OBJECT_HANDLE = 0; + let mut h_ed_priv: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_GenerateKeyPair, h_session, &mech_ed_gen, + ed_pub_attrs.as_mut_ptr(), 1, + ed_priv_attrs.as_mut_ptr(), 0, + &mut h_ed_pub, &mut h_ed_priv), "C_GenerateKeyPair(Ed25519)"); + info("Ed25519 pub handle", &h_ed_pub.to_string()); + info("Ed25519 priv handle", &h_ed_priv.to_string()); + + // EdDSA sign / verify + let ed_message = b"EdDSA sign/verify via PKCS#11 v3.0"; + let mech_eddsa = CK_MECHANISM { mechanism: CKM_EDDSA, + pParameter: ptr::null(), ulParameterLen: 0 }; + + ck_ok(p11!(fl, C_SignInit, h_session, &mech_eddsa, h_ed_priv), "C_SignInit(EdDSA)"); + let mut ed_sig = vec![0u8; 128]; + let mut ed_sig_len: CK_ULONG = 128; + ck_ok(p11!(fl, C_Sign, h_session, ed_message.as_ptr(), ed_message.len() as CK_ULONG, + ed_sig.as_mut_ptr(), &mut ed_sig_len), "C_Sign(EdDSA)"); + ed_sig.truncate(ed_sig_len as usize); + info("EdDSA signature", &format!("{} bytes", ed_sig_len)); + assert_eq!(ed_sig_len, 64, "Ed25519 signature must be 64 bytes"); + + ck_ok(p11!(fl, C_VerifyInit, h_session, &mech_eddsa, h_ed_pub), "C_VerifyInit(EdDSA)"); + ck_ok(p11!(fl, C_Verify, h_session, ed_message.as_ptr(), ed_message.len() as CK_ULONG, + ed_sig.as_ptr(), ed_sig_len), "C_Verify(EdDSA)"); + ok("EdDSA Ed25519 sign/verify passed"); + + // Tamper test + ed_sig[0] ^= 0xFF; + ck_ok(p11!(fl, C_VerifyInit, h_session, &mech_eddsa, h_ed_pub), "C_VerifyInit(EdDSA, tamper)"); + let tamper_rv = p11!(fl, C_Verify, h_session, ed_message.as_ptr(), ed_message.len() as CK_ULONG, + ed_sig.as_ptr(), ed_sig_len); + assert_eq!(tamper_rv, CKR_SIGNATURE_INVALID, "tampered EdDSA signature must fail"); + ok("EdDSA tampered signature correctly rejected"); + + // ── 12. ChaCha20-Poly1305 — v3.0 ──────────────────────────────────── + + section("12. ChaCha20-Poly1305 — key generation + AEAD (v3.0)"); + + let mech_chacha_gen = CK_MECHANISM { mechanism: CKM_CHACHA20_KEY_GEN, + pParameter: ptr::null(), ulParameterLen: 0 }; + let mut h_chacha: CK_OBJECT_HANDLE = 0; + ck_ok(p11!(fl, C_GenerateKey, h_session, &mech_chacha_gen, + ptr::null(), 0, &mut h_chacha), "C_GenerateKey(ChaCha20)"); + info("ChaCha20 key handle", &h_chacha.to_string()); + + let nonce_chacha = [0x42u8; 12]; + let aad_chacha = b"additional data"; + let chacha_params = GcmParams { + pIv: nonce_chacha.as_ptr(), + ulIvLen: 12, + ulIvBits: 96, + pAAD: aad_chacha.as_ptr(), + ulAADLen: aad_chacha.len() as u64, + ulTagBits: 128, + }; + let mech_chacha = CK_MECHANISM { + mechanism: CKM_CHACHA20_POLY1305, + pParameter: &chacha_params as *const GcmParams as *mut c_void, + ulParameterLen: std::mem::size_of::() as CK_ULONG, + }; + let chacha_plain = b"ChaCha20-Poly1305 AEAD demo"; + + ck_ok(p11!(fl, C_EncryptInit, h_session, &mech_chacha, h_chacha), "C_EncryptInit(ChaCha20-Poly1305)"); + let mut chacha_ct = vec![0u8; chacha_plain.len() + 16]; + let mut chacha_ct_len = chacha_ct.len() as CK_ULONG; + ck_ok(p11!(fl, C_Encrypt, h_session, chacha_plain.as_ptr(), chacha_plain.len() as CK_ULONG, + chacha_ct.as_mut_ptr(), &mut chacha_ct_len), "C_Encrypt(ChaCha20-Poly1305)"); + chacha_ct.truncate(chacha_ct_len as usize); + info("ChaCha20-Poly1305 ciphertext+tag", &format!("{} bytes", chacha_ct_len)); + + ck_ok(p11!(fl, C_DecryptInit, h_session, &mech_chacha, h_chacha), "C_DecryptInit(ChaCha20-Poly1305)"); + let mut chacha_pt = vec![0u8; chacha_ct.len()]; + let mut chacha_pt_len = chacha_pt.len() as CK_ULONG; + ck_ok(p11!(fl, C_Decrypt, h_session, chacha_ct.as_ptr(), chacha_ct_len, + chacha_pt.as_mut_ptr(), &mut chacha_pt_len), "C_Decrypt(ChaCha20-Poly1305)"); + chacha_pt.truncate(chacha_pt_len as usize); + assert_eq!(chacha_pt, chacha_plain, "ChaCha20-Poly1305 round-trip mismatch"); + ok("ChaCha20-Poly1305 AEAD encrypt/decrypt round-trip verified"); + + // Tamper test — corrupt ciphertext + chacha_ct[0] ^= 0xFF; + ck_ok(p11!(fl, C_DecryptInit, h_session, &mech_chacha, h_chacha), "C_DecryptInit(ChaCha20 tamper)"); + let mut bad_len: CK_ULONG = 128; + let mut bad_buf = vec![0u8; 128]; + let tamper_chacha_rv = p11!(fl, C_Decrypt, h_session, chacha_ct.as_ptr(), chacha_ct.len() as CK_ULONG, + bad_buf.as_mut_ptr(), &mut bad_len); + assert_ne!(tamper_chacha_rv, CKR_OK, "tampered ChaCha20-Poly1305 must fail"); + ok("ChaCha20-Poly1305 tampered ciphertext correctly rejected"); + + // ── 13. SHA-3 / SHA-384 / SHA-512 digests — v3.0 ──────────────────── + + section("13. SHA-3 / SHA-384 / SHA-512 digests (v3.0)"); + + let digest_data = b"abc"; + + // SHA3-256 + let mech_sha3_256 = CK_MECHANISM { mechanism: CKM_SHA3_256, + pParameter: ptr::null(), ulParameterLen: 0 }; + ck_ok(p11!(fl, C_DigestInit, h_session, &mech_sha3_256), "C_DigestInit(SHA3-256)"); + let mut sha3_buf = [0u8; 32]; + let mut sha3_len: CK_ULONG = 32; + ck_ok(p11!(fl, C_Digest, h_session, digest_data.as_ptr(), digest_data.len() as CK_ULONG, + sha3_buf.as_mut_ptr(), &mut sha3_len), "C_Digest(SHA3-256)"); + info("SHA3-256(\"abc\")", &hex(&sha3_buf)); + ok("SHA3-256 digest (32 bytes)"); + + // SHA-384 + let mech_sha384 = CK_MECHANISM { mechanism: CKM_SHA384, + pParameter: ptr::null(), ulParameterLen: 0 }; + ck_ok(p11!(fl, C_DigestInit, h_session, &mech_sha384), "C_DigestInit(SHA-384)"); + let mut sha384_buf = [0u8; 48]; + let mut sha384_len: CK_ULONG = 48; + ck_ok(p11!(fl, C_Digest, h_session, digest_data.as_ptr(), digest_data.len() as CK_ULONG, + sha384_buf.as_mut_ptr(), &mut sha384_len), "C_Digest(SHA-384)"); + info("SHA-384(\"abc\") first 16 bytes", &hex(&sha384_buf[..16])); + ok("SHA-384 digest (48 bytes)"); + + // SHA-512 + let mech_sha512 = CK_MECHANISM { mechanism: CKM_SHA512, + pParameter: ptr::null(), ulParameterLen: 0 }; + ck_ok(p11!(fl, C_DigestInit, h_session, &mech_sha512), "C_DigestInit(SHA-512)"); + let mut sha512_buf = [0u8; 64]; + let mut sha512_len: CK_ULONG = 64; + ck_ok(p11!(fl, C_Digest, h_session, digest_data.as_ptr(), digest_data.len() as CK_ULONG, + sha512_buf.as_mut_ptr(), &mut sha512_len), "C_Digest(SHA-512)"); + info("SHA-512(\"abc\") first 16 bytes", &hex(&sha512_buf[..16])); + ok("SHA-512 digest (64 bytes)"); + + // SHA3-384 + let mech_sha3_384 = CK_MECHANISM { mechanism: CKM_SHA3_384, + pParameter: ptr::null(), ulParameterLen: 0 }; + ck_ok(p11!(fl, C_DigestInit, h_session, &mech_sha3_384), "C_DigestInit(SHA3-384)"); + let mut sha3_384_buf = [0u8; 48]; + let mut sha3_384_len: CK_ULONG = 48; + ck_ok(p11!(fl, C_Digest, h_session, digest_data.as_ptr(), digest_data.len() as CK_ULONG, + sha3_384_buf.as_mut_ptr(), &mut sha3_384_len), "C_Digest(SHA3-384)"); + ok("SHA3-384 digest (48 bytes)"); + + // SHA3-512 + let mech_sha3_512 = CK_MECHANISM { mechanism: CKM_SHA3_512, + pParameter: ptr::null(), ulParameterLen: 0 }; + ck_ok(p11!(fl, C_DigestInit, h_session, &mech_sha3_512), "C_DigestInit(SHA3-512)"); + let mut sha3_512_buf = [0u8; 64]; + let mut sha3_512_len: CK_ULONG = 64; + ck_ok(p11!(fl, C_Digest, h_session, digest_data.as_ptr(), digest_data.len() as CK_ULONG, + sha3_512_buf.as_mut_ptr(), &mut sha3_512_len), "C_Digest(SHA3-512)"); + ok("SHA3-512 digest (64 bytes)"); + + // ── 14. Multi-part SHA-256 digest ───────────────────────────────────── + + section("14. Multi-part SHA-256 digest"); + + let mech_sha256 = CK_MECHANISM { mechanism: CKM_SHA256, + pParameter: ptr::null(), ulParameterLen: 0 }; + ck_ok(p11!(fl, C_DigestInit, h_session, &mech_sha256), "C_DigestInit(SHA-256)"); + + let chunks: &[&[u8]] = &[b"The ", b"quick ", b"brown ", b"fox"]; + for chunk in chunks { + ck_ok(p11!(fl, C_DigestUpdate, h_session, chunk.as_ptr(), chunk.len() as CK_ULONG), + "C_DigestUpdate"); + } + + let mut digest_buf = [0u8; 32]; + let mut digest_len: CK_ULONG = 32; + ck_ok(p11!(fl, C_DigestFinal, h_session, digest_buf.as_mut_ptr(), &mut digest_len), + "C_DigestFinal"); + info("SHA-256(\"The quick brown fox\")", &hex(&digest_buf)); + ok("multi-part SHA-256 digest produced"); + + // ── 15. C_GetInterfaceList + C_GetInterface (v3.0) ─────────────────── + // + // C_GetInterface is the v3.0 bootstrap symbol — a real consumer would + // dlsym it alongside C_GetFunctionList. We call it as a bare symbol + // here (imported at the top), then cast the returned pFunctionList to + // the v3.0 function list to prove we can access the extended table. + + section("15. C_GetInterfaceList + C_GetInterface (v3.0)"); + + let mut iface_count: CK_ULONG = 0; + // Use the v2.40 function list for C_GetInterfaceList — it's not on CK_FUNCTION_LIST, + // so we call C_GetInterface (bare symbol) to get the v3.0 table first. + // Actually, C_GetInterfaceList is only on CK_FUNCTION_LIST_3_0, so we call + // C_GetInterface directly as a bare symbol to bootstrap the v3.0 path. + let mut iface_ptr: *const CK_INTERFACE = ptr::null(); + let name = b"PKCS 11\0"; + ck_ok(C_GetInterface(name.as_ptr(), ptr::null_mut(), &mut iface_ptr, 0), + "C_GetInterface(\"PKCS 11\")"); + assert!(!iface_ptr.is_null(), "interface pointer must be non-null"); + assert!(!(*iface_ptr).pFunctionList.is_null(), "function list must be non-null"); + + // Cast to CK_FUNCTION_LIST_3_0 to access v3.0 extensions + let fl3 = &*((*iface_ptr).pFunctionList as *const CK_FUNCTION_LIST_3_0); + info("v3.0 function list version", &format!("{}.{}", fl3.version.major, fl3.version.minor)); + + let iface_name = std::ffi::CStr::from_ptr((*iface_ptr).pInterfaceName as *const libc::c_char); + info("interface name", iface_name.to_str().unwrap_or("?")); + + // Now use the v3.0 function list to call C_GetInterfaceList + ck_ok(p11!(fl3, C_GetInterfaceList, ptr::null_mut(), &mut iface_count), + "C_GetInterfaceList (count)"); + info("interface count", &iface_count.to_string()); + assert!(iface_count >= 1, "expected at least 1 interface"); + ok("v3.0 interface discovery works"); + + // ── 16. C_FindObjects ───────────────────────────────────────────────── + + section("16. C_FindObjects — enumerate all objects"); + + let mut find_attrs: [CK_ATTRIBUTE; 0] = []; + ck_ok(p11!(fl, C_FindObjectsInit, h_session, find_attrs.as_mut_ptr(), 0), + "C_FindObjectsInit (no filter)"); + let mut handles = vec![0u64; 32]; + let mut found: CK_ULONG = 0; + ck_ok(p11!(fl, C_FindObjects, h_session, handles.as_mut_ptr(), 32, &mut found), + "C_FindObjects"); + ck_ok(p11!(fl, C_FindObjectsFinal, h_session), "C_FindObjectsFinal"); + info("total objects in store", &found.to_string()); + // We generated: 1 AES + 2 RSA + 2 EC + 2 Ed25519 + 1 ChaCha20 = 8 + assert!(found >= 8, "expected at least 8 objects, got {found}"); + ok("found all generated keys"); + + // Find only private keys + let class_priv: CK_ULONG = CKO_PRIVATE_KEY; + let mut priv_filter = [CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: &class_priv as *const CK_ULONG as *mut c_void, + ulValueLen: 8, + }]; + ck_ok(p11!(fl, C_FindObjectsInit, h_session, priv_filter.as_mut_ptr(), 1), + "C_FindObjectsInit (CKO_PRIVATE_KEY)"); + let mut priv_handles = vec![0u64; 64]; + let mut priv_found: CK_ULONG = 0; + ck_ok(p11!(fl, C_FindObjects, h_session, priv_handles.as_mut_ptr(), 64, &mut priv_found), + "C_FindObjects (private keys)"); + ck_ok(p11!(fl, C_FindObjectsFinal, h_session), "C_FindObjectsFinal"); + info("private key count", &priv_found.to_string()); + let priv_slice = &priv_handles[..priv_found as usize]; + assert!(priv_slice.contains(&h_rsa_priv), "RSA private key must be found"); + assert!(priv_slice.contains(&h_ec_priv), "EC private key must be found"); + assert!(priv_slice.contains(&h_ed_priv), "Ed25519 private key must be found"); + ok("private key filter returned our RSA + EC + Ed25519 private keys"); + + // ── 17. C_DestroyObject ─────────────────────────────────────────────── + + section("17. C_DestroyObject — delete AES key"); + ck_ok(p11!(fl, C_DestroyObject, h_session, h_aes), "C_DestroyObject(AES)"); + ok("AES-256 key destroyed"); + + // Confirm it's gone via FindObjects + let class_secret: CK_ULONG = CKO_SECRET_KEY; + let mut secret_filter = [CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: &class_secret as *const CK_ULONG as *mut c_void, + ulValueLen: 8, + }]; + ck_ok(p11!(fl, C_FindObjectsInit, h_session, secret_filter.as_mut_ptr(), 1), + "C_FindObjectsInit (CKO_SECRET_KEY, post-delete)"); + let mut secret_handles = vec![0u64; 64]; + let mut secret_found: CK_ULONG = 0; + ck_ok(p11!(fl, C_FindObjects, h_session, secret_handles.as_mut_ptr(), 64, &mut secret_found), + "C_FindObjects (secret keys, post-delete)"); + ck_ok(p11!(fl, C_FindObjectsFinal, h_session), "C_FindObjectsFinal"); + assert!(!secret_handles[..secret_found as usize].contains(&h_aes), "AES key should no longer be found"); + ok("confirmed: AES key destroyed, no longer returned by FindObjects"); + + // ── 18. Cleanup ─────────────────────────────────────────────────────── + + section("18. C_Logout + C_CloseSession + C_Finalize"); + ck_ok(p11!(fl, C_Logout, h_session), "C_Logout"); + ck_ok(p11!(fl, C_CloseSession, h_session), "C_CloseSession"); + ck_ok(p11!(fl, C_Finalize, ptr::null_mut()), "C_Finalize"); + ok("session closed, library finalised"); +} diff --git a/extensions.bzl b/extensions.bzl new file mode 100644 index 0000000..8cf5c6f --- /dev/null +++ b/extensions.bzl @@ -0,0 +1,35 @@ +load("//toolchains:rules.bzl", "qcc_toolchain") + +def _local_sdp_impl(rctx): + rctx.file("WORKSPACE.bazel", "") + + rctx.template("BUILD.bazel", rctx.attr.build_file, {}) + + qnx_path = rctx.path(rctx.attr.path) + for item in qnx_path.readdir(): + if item.basename not in ["BUILD", "BUILD.bazel", "WORKSPACE", "WORKSPACE.bazel", "REPO.bazel"]: + rctx.symlink(item, item.basename) + +local_sdp = repository_rule( + implementation = _local_sdp_impl, + attrs = { + "path": attr.string(mandatory = True), + "build_file": attr.label(mandatory = True), + }, +) + +def _toolchains_qnx_impl(mctx): + local_sdp( + name = "toolchains_qnx_sdp", + path = "/home/oeweda/qnx800", + build_file = "//toolchains:sdp.BUILD", + ) + + qcc_toolchain( + name = "toolchains_qnx_qcc", + sdp_repo = "toolchains_qnx_sdp", + sdp_version = "8.0.0", + qcc_version = "12.2.0", + ) + +toolchains_qnx = module_extension(implementation = _toolchains_qnx_impl) diff --git a/project_config.bzl b/project_config.bzl index f764a1d..9ab6006 100644 --- a/project_config.bzl +++ b/project_config.bzl @@ -1,4 +1,8 @@ -# project_config.bzl +"""Project-wide Bazel config constants.""" + +PROJECT_NAME = "cryptoki" +RUST_EDITION = "2021" + PROJECT_CONFIG = { "asil_level": "QM", "source_code": ["rust"], diff --git a/pyproject.toml b/pyproject.toml index 5819ca5..3b23679 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,13 @@ [tool.basedpyright] -extends = "bazel-bin/ide_support.runfiles/score_tooling+/python_basics/pyproject.toml" verboseOutput = true +include = [ + "docs", +] + exclude = [ "**/__pycache__", "**/.*", "**/bazel-*", + "cpp/pkcs11test", ] diff --git a/src/BUILD b/src/BUILD index e69de29..2278e57 100644 --- a/src/BUILD +++ b/src/BUILD @@ -0,0 +1,29 @@ +load("@crates//:defs.bzl", "aliases", "all_crate_deps") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_shared_library") + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "src", + srcs = glob(["**/*.rs"]), +) + +rust_library( + name = "cryptoki_lib", + crate_name = "cryptoki", + crate_root = "lib.rs", + srcs = glob(["**/*.rs"]), + aliases = aliases(package_name = ""), + deps = all_crate_deps(normal = True, package_name = ""), + proc_macro_deps = all_crate_deps(proc_macro = True, package_name = ""), +) + +rust_shared_library( + name = "cryptoki_cdylib", + crate_name = "cryptoki", + crate_root = "lib.rs", + srcs = glob(["**/*.rs"]), + aliases = aliases(package_name = ""), + deps = all_crate_deps(normal = True, package_name = ""), + proc_macro_deps = all_crate_deps(proc_macro = True, package_name = ""), +) diff --git a/src/attributes.rs b/src/attributes.rs new file mode 100644 index 0000000..0b071db --- /dev/null +++ b/src/attributes.rs @@ -0,0 +1,146 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +/// PKCS#11 attribute types — CKA_* constants. +/// +/// The discriminant values match the PKCS#11 specification so the PKCS#11 +/// layer can convert directly: `attr_type as u32` == `CKA_*` value. +/// +/// Usage in a PKCS#11 implementation: +/// +/// // C_GetAttributeValue: map CK_ATTRIBUTE_TYPE → AttributeType, call engine, +/// // then write AttributeValue bytes into the caller's pValue buffer. +/// //let val = engine()?.rsa_attribute(&key_der, is_private, AttributeType::Modulus)?; +/// +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AttributeType { + Class = 0x00000000, // CKA_CLASS + Token = 0x00000001, // CKA_TOKEN + Private = 0x00000002, // CKA_PRIVATE + Label = 0x00000003, // CKA_LABEL + Value = 0x00000011, // CKA_VALUE + KeyType = 0x00000100, // CKA_KEY_TYPE + Id = 0x00000102, // CKA_ID + Sensitive = 0x00000103, // CKA_SENSITIVE + Encrypt = 0x00000104, // CKA_ENCRYPT + Decrypt = 0x00000105, // CKA_DECRYPT + Wrap = 0x00000106, // CKA_WRAP + Unwrap = 0x00000107, // CKA_UNWRAP + Sign = 0x00000108, // CKA_SIGN + Verify = 0x00000109, // CKA_VERIFY + Derive = 0x0000010C, // CKA_DERIVE + Modulus = 0x00000120, // CKA_MODULUS (RSA) + ModulusBits = 0x00000121, // CKA_MODULUS_BITS (RSA) + PublicExponent = 0x00000122, // CKA_PUBLIC_EXPONENT (RSA) + ValueLen = 0x00000161, // CKA_VALUE_LEN (AES) + Extractable = 0x00000162, // CKA_EXTRACTABLE + Local = 0x00000163, // CKA_LOCAL + NeverExtractable = 0x00000164, // CKA_NEVER_EXTRACTABLE + AlwaysSensitive = 0x00000165, // CKA_ALWAYS_SENSITIVE + Modifiable = 0x00000170, // CKA_MODIFIABLE + EcParams = 0x00000180, // CKA_EC_PARAMS (EC) + EcPoint = 0x00000181, // CKA_EC_POINT (EC) + AlwaysAuthenticate = 0x00000202, // CKA_ALWAYS_AUTHENTICATE + Destroyable = 0x00000172, // CKA_DESTROYABLE (v3.0) + Copyable = 0x00000171, // CKA_COPYABLE (v3.0) + UniqueId = 0x0000010A, // CKA_UNIQUE_ID (v3.0) + ProfileId = 0x00000601, // CKA_PROFILE_ID (v3.0) + VendorDefined = 0x80000000, // CKA_VENDOR_DEFINED +} + +impl AttributeType { + /// Construct from a raw CKA_* u32 value. + /// Returns `None` for unknown values (use `CKR_ATTRIBUTE_TYPE_INVALID`). + pub fn from_u32(v: u32) -> Option { + match v { + 0x00000000 => Some(Self::Class), + 0x00000001 => Some(Self::Token), + 0x00000002 => Some(Self::Private), + 0x00000003 => Some(Self::Label), + 0x00000011 => Some(Self::Value), + 0x00000100 => Some(Self::KeyType), + 0x00000102 => Some(Self::Id), + 0x00000103 => Some(Self::Sensitive), + 0x00000104 => Some(Self::Encrypt), + 0x00000105 => Some(Self::Decrypt), + 0x00000106 => Some(Self::Wrap), + 0x00000107 => Some(Self::Unwrap), + 0x00000108 => Some(Self::Sign), + 0x00000109 => Some(Self::Verify), + 0x0000010C => Some(Self::Derive), + 0x00000120 => Some(Self::Modulus), + 0x00000121 => Some(Self::ModulusBits), + 0x00000122 => Some(Self::PublicExponent), + 0x00000161 => Some(Self::ValueLen), + 0x00000162 => Some(Self::Extractable), + 0x00000163 => Some(Self::Local), + 0x00000164 => Some(Self::NeverExtractable), + 0x00000165 => Some(Self::AlwaysSensitive), + 0x00000170 => Some(Self::Modifiable), + 0x00000180 => Some(Self::EcParams), + 0x00000181 => Some(Self::EcPoint), + 0x00000202 => Some(Self::AlwaysAuthenticate), + 0x00000172 => Some(Self::Destroyable), + 0x00000171 => Some(Self::Copyable), + 0x0000010A => Some(Self::UniqueId), + 0x00000601 => Some(Self::ProfileId), + 0x80000000 => Some(Self::VendorDefined), + _ => None, + } + } +} + +/// Typed attribute value for a CKA_* query result. +/// +/// The PKCS#11 layer serialises this into the caller's `pValue` buffer: +/// - `Bool(b)` → `CK_BBOOL` (1 byte: 0x00 or 0x01) +/// - `Ulong(n)` → `CK_ULONG` (4 or 8 bytes, platform-dependent in PKCS#11) +/// - `Bytes(vec)` → raw bytes copied into pValue +#[derive(Debug, Clone)] +pub enum AttributeValue { + /// Boolean attribute (CK_BBOOL). + Bool(bool), + /// Unsigned integer attribute (CK_ULONG). + Ulong(u64), + /// Byte-array attribute (CK_BYTE[]). + Bytes(Vec), +} + +impl AttributeValue { + /// Serialise to raw bytes in PKCS#11 wire format. + /// + /// - `Bool` → 1 byte (0x00 = false, 0x01 = true) + /// - `Ulong` → 8 bytes little-endian (matches CK_ULONG on 64-bit platforms) + /// - `Bytes` → the bytes as-is + pub fn to_bytes(&self) -> Vec { + match self { + AttributeValue::Bool(b) => vec![if *b { 0x01 } else { 0x00 }], + AttributeValue::Ulong(n) => n.to_le_bytes().to_vec(), + AttributeValue::Bytes(v) => v.clone(), + } + } + + /// Byte length of the serialised value (for CK_ATTRIBUTE.ulValueLen). + pub fn len(&self) -> usize { + match self { + AttributeValue::Bool(_) => 1, + AttributeValue::Ulong(_) => 8, + AttributeValue::Bytes(v) => v.len(), + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..66790d7 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,123 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +/// Unified error type for all crypto engine operations. +/// Every variant maps to a PKCS#11 CKR_* return code via `ckr_code()`. +#[derive(Debug)] +pub enum CryptoError { + /// CKR_GENERAL_ERROR + KeyGenFailed { message: String }, + /// CKR_KEY_HANDLE_INVALID + InvalidKeyData { message: String }, + /// CKR_KEY_SIZE_RANGE + InvalidKeySize { message: String }, + /// CKR_DATA_INVALID + DataInvalid { message: String }, + /// CKR_DATA_LEN_RANGE + DataLenRange { message: String }, + /// CKR_GENERAL_ERROR + EncryptFailed { message: String }, + /// CKR_ENCRYPTED_DATA_INVALID + DecryptFailed { message: String }, + /// CKR_GENERAL_ERROR + SignFailed { message: String }, + /// CKR_SIGNATURE_INVALID + VerifyFailed { message: String }, + /// CKR_SIGNATURE_LEN_RANGE + SignatureLenRange { message: String }, + /// CKR_GENERAL_ERROR + HashFailed { message: String }, + /// CKR_RANDOM_NO_RNG + RandomFailed { message: String }, + /// CKR_BUFFER_TOO_SMALL + BufferTooSmall { needed: usize }, + /// CKR_MECHANISM_INVALID + MechanismInvalid { name: &'static str }, + /// CKR_MECHANISM_PARAM_INVALID + MechanismParamInvalid { message: String }, + /// CKR_ATTRIBUTE_TYPE_INVALID + AttributeTypeInvalid, + /// CKR_ATTRIBUTE_SENSITIVE + AttributeSensitive, + /// CKR_ATTRIBUTE_VALUE_INVALID + AttributeValueInvalid, + /// CKR_CRYPTOKI_NOT_INITIALIZED + NotInitialized, + /// CKR_CRYPTOKI_ALREADY_INITIALIZED + AlreadyInitialized, + /// CKR_GENERAL_ERROR + GeneralError { message: String }, + /// CKR_SLOT_ID_INVALID + SlotIdInvalid, +} + +impl CryptoError { + /// Returns the PKCS#11 CKR_* return code for this error. + pub fn ckr_code(&self) -> u32 { + match self { + Self::KeyGenFailed { .. } => 0x00000005, + Self::InvalidKeyData { .. } => 0x00000060, + Self::InvalidKeySize { .. } => 0x00000062, + Self::DataInvalid { .. } => 0x00000020, + Self::DataLenRange { .. } => 0x00000021, + Self::EncryptFailed { .. } => 0x00000005, + Self::DecryptFailed { .. } => 0x00000040, + Self::SignFailed { .. } => 0x00000005, + Self::VerifyFailed { .. } => 0x000000C0, + Self::SignatureLenRange { .. } => 0x000000C1, + Self::HashFailed { .. } => 0x00000005, + Self::RandomFailed { .. } => 0x00000121, + Self::BufferTooSmall { .. } => 0x00000150, + Self::MechanismInvalid { .. } => 0x00000070, + Self::MechanismParamInvalid { .. } => 0x00000071, + Self::AttributeTypeInvalid => 0x00000012, + Self::AttributeSensitive => 0x00000011, + Self::AttributeValueInvalid => 0x00000013, + Self::NotInitialized => 0x00000190, + Self::AlreadyInitialized => 0x00000191, + Self::GeneralError { .. } => 0x00000005, + Self::SlotIdInvalid => 0x00000003, + } + } +} + +impl std::fmt::Display for CryptoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::KeyGenFailed { message } => write!(f, "key generation failed: {message}"), + Self::InvalidKeyData { message } => write!(f, "invalid key data: {message}"), + Self::InvalidKeySize { message } => write!(f, "invalid key size: {message}"), + Self::DataInvalid { message } => write!(f, "invalid data: {message}"), + Self::DataLenRange { message } => write!(f, "data length out of range: {message}"), + Self::EncryptFailed { message } => write!(f, "encryption failed: {message}"), + Self::DecryptFailed { message } => write!(f, "decryption failed: {message}"), + Self::SignFailed { message } => write!(f, "signing failed: {message}"), + Self::VerifyFailed { message } => write!(f, "verification failed: {message}"), + Self::SignatureLenRange { message } => write!(f, "signature length out of range: {message}"), + Self::HashFailed { message } => write!(f, "hash failed: {message}"), + Self::RandomFailed { message } => write!(f, "random generation failed: {message}"), + Self::BufferTooSmall { needed } => write!(f, "buffer too small: need {needed} bytes"), + Self::MechanismInvalid { name } => write!(f, "mechanism not supported: {name}"), + Self::MechanismParamInvalid { message }=> write!(f, "invalid mechanism parameter: {message}"), + Self::AttributeTypeInvalid => write!(f, "attribute type invalid for this object"), + Self::AttributeSensitive => write!(f, "attribute is sensitive and cannot be read"), + Self::AttributeValueInvalid => write!(f, "attribute value is invalid"), + Self::NotInitialized => write!(f, "crypto engine not initialized"), + Self::AlreadyInitialized => write!(f, "crypto engine already initialized"), + Self::GeneralError { message } => write!(f, "general error: {message}"), + Self::SlotIdInvalid => write!(f, "slot ID is invalid"), + } + } +} + +impl std::error::Error for CryptoError {} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..30e7f3c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,61 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! # cryptoki +//! +//! PKCS#11 v3.0 software token with a multi-engine registry and pluggable +//! crypto backends. +//! +//! ## Usage +//! +//! +//! use cryptoki::{register_engine, engine_for_slot, OpenSslEngine, HashAlgorithm}; +//! +//! // C_Initialize — register one or more engines. +//! // Each engine gets one or more global slot IDs assigned automatically. +//! let slots = register_engine(OpenSslEngine).unwrap(); // e.g. [0] +//! +//! // Retrieve the engine for a given global slot ID. +//! let (eng, _internal_id) = engine_for_slot(slots[0]).unwrap(); +//! +//! // C_GenerateKey(CKM_AES_KEY_GEN) — generate a 128-bit AES key. +//! let key = eng.generate_aes_key(16).unwrap(); +//! +//! // C_DigestInit(CKM_SHA256) + C_Digest — one-shot hash. +//! let digest = eng.hash(HashAlgorithm::Sha256, b"hello").unwrap(); +//! +//! +//! ## Implementing a new engine +//! +//! Implement [`traits::CryptoProvider`] and [`traits::StreamHasher`] for your +//! crypto library, then call [`registry::register_engine`] with an instance. +//! The PKCS#11 layer never needs to change. Each engine declares how many +//! slots it provides via [`CryptoProvider::slot_count`](traits::CryptoProvider::slot_count). + +pub mod attributes; +pub mod error; +pub mod openssl_provider; +pub mod pkcs11; +pub mod registry; +pub mod traits; +pub mod types; + +// ── Convenience re-exports ──────────────────────────────────────────────────── + +pub use attributes::{AttributeType, AttributeValue}; +pub use error::CryptoError; +pub use openssl_provider::OpenSslEngine; +pub use registry::{engine, engine_for_slot, register_engine, try_engine, + is_valid_slot, slot_ids, slot_count, reset_registry}; +pub use traits::{CryptoProvider, EngineMechanismInfo, EngineKeyRef, StreamHasher}; +pub use types::{EcCurve, EcKeyPair, EdKeyPair, EdwardsCurve, HashAlgorithm, RsaKeyPair}; diff --git a/src/openssl_engine.rs b/src/openssl_engine.rs new file mode 100755 index 0000000..9536e64 --- /dev/null +++ b/src/openssl_engine.rs @@ -0,0 +1,860 @@ +use openssl::bn::BigNumContext; +use openssl::ec::{EcGroup, EcKey, PointConversionForm}; +use openssl::ecdsa::EcdsaSig; +use openssl::hash::{hash, Hasher, MessageDigest}; +use openssl::nid::Nid; +use openssl::pkey::{PKey, Private, Public}; +use openssl::rand::rand_bytes; +use openssl::rsa::{Padding, Rsa}; +use openssl::sign::{RsaPssSaltlen, Signer, Verifier}; +use openssl::symm::{decrypt_aead, encrypt_aead, Cipher, Crypter, Mode}; + +use crate::attributes::{AttributeType, AttributeValue}; +use crate::error::CryptoError; +use crate::traits::{CryptoEngine, StreamHasher}; +use crate::types::{EcCurve, EcKeyPair, EdKeyPair, EdwardsCurve, HashAlgorithm, RsaKeyPair}; + +// ── Error conversion helpers ────────────────────────────────────────────────── + +fn key_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::KeyGenFailed { message: e.to_string() } +} + +fn invalid_key_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::InvalidKeyData { message: e.to_string() } +} + +fn encrypt_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::EncryptFailed { message: e.to_string() } +} + +fn decrypt_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::DecryptFailed { message: e.to_string() } +} + +fn sign_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::SignFailed { message: e.to_string() } +} + +fn verify_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::VerifyFailed { message: e.to_string() } +} + +fn hash_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::HashFailed { message: e.to_string() } +} + +fn random_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::RandomFailed { message: e.to_string() } +} + +// ── AES cipher selection ────────────────────────────────────────────────────── + +fn aes_cbc_cipher(key_len: usize) -> Result { + match key_len { + 16 => Ok(Cipher::aes_128_cbc()), + 24 => Ok(Cipher::aes_192_cbc()), + 32 => Ok(Cipher::aes_256_cbc()), + n => Err(CryptoError::InvalidKeySize { + message: format!("AES-CBC key must be 16, 24, or 32 bytes; got {n}"), + }), + } +} + +fn aes_ctr_cipher(key_len: usize) -> Result { + match key_len { + 16 => Ok(Cipher::aes_128_ctr()), + 24 => Ok(Cipher::aes_192_ctr()), + 32 => Ok(Cipher::aes_256_ctr()), + n => Err(CryptoError::InvalidKeySize { + message: format!("AES-CTR key must be 16, 24, or 32 bytes; got {n}"), + }), + } +} + +fn aes_gcm_cipher(key_len: usize) -> Result { + match key_len { + 16 => Ok(Cipher::aes_128_gcm()), + 24 => Ok(Cipher::aes_192_gcm()), + 32 => Ok(Cipher::aes_256_gcm()), + n => Err(CryptoError::InvalidKeySize { + message: format!("AES-GCM key must be 16, 24, or 32 bytes; got {n}"), + }), + } +} + +// ── MessageDigest mapping ───────────────────────────────────────────────────── + +fn message_digest(algorithm: HashAlgorithm) -> Result { + match algorithm { + HashAlgorithm::Md5 => Ok(MessageDigest::md5()), + HashAlgorithm::Sha1 => Ok(MessageDigest::sha1()), + HashAlgorithm::Sha256 => Ok(MessageDigest::sha256()), + HashAlgorithm::Sha384 => Ok(MessageDigest::sha384()), + HashAlgorithm::Sha512 => Ok(MessageDigest::sha512()), + HashAlgorithm::Sha3_256 => Ok(MessageDigest::sha3_256()), + HashAlgorithm::Sha3_384 => Ok(MessageDigest::sha3_384()), + HashAlgorithm::Sha3_512 => Ok(MessageDigest::sha3_512()), + #[allow(unreachable_patterns)] + _ => Err(CryptoError::MechanismInvalid { name: "unknown HashAlgorithm variant" }), + } +} + +// ── DER OCTET STRING wrapper (for CKA_EC_POINT) ─────────────────────────────── + +/// Wrap raw bytes in a DER OCTET STRING (tag 0x04 + length + data). +/// Used to produce the CKA_EC_POINT encoding that PKCS#11 expects. +fn der_octet_string(bytes: &[u8]) -> Vec { + let len = bytes.len(); + let mut out = Vec::with_capacity(4 + len); + out.push(0x04); // OCTET STRING tag + if len < 0x80 { + out.push(len as u8); + } else if len < 0x100 { + out.push(0x81); + out.push(len as u8); + } else { + out.push(0x82); + out.push((len >> 8) as u8); + out.push((len & 0xFF) as u8); + } + out.extend_from_slice(bytes); + out +} + +// ── Streaming hasher wrapper ────────────────────────────────────────────────── + +struct OpenSslStreamHasher { + inner: Hasher, +} + +impl StreamHasher for OpenSslStreamHasher { + fn update(&mut self, data: &[u8]) -> Result<(), CryptoError> { + self.inner.update(data).map_err(hash_err) + } + + fn finish(mut self: Box) -> Result, CryptoError> { + self.inner.finish().map(|d| d.to_vec()).map_err(hash_err) + } +} + +// ── OpenSslEngine ───────────────────────────────────────────────────────────── + +/// OpenSSL-backed crypto engine. +/// +/// A zero-size struct — all state lives in the OpenSSL library itself. +/// Thread-safety is guaranteed by OpenSSL's internal locking. +pub struct OpenSslEngine; + +impl CryptoEngine for OpenSslEngine { + + // ── Key generation ──────────────────────────────────────────────────────── + + fn generate_rsa_key_pair(&self, bits: u32) -> Result { + let exponent = openssl::bn::BigNum::from_u32(65537).map_err(key_err)?; + let rsa = Rsa::generate_with_e(bits, &exponent).map_err(key_err)?; + + // Pre-extract attribute values before moving rsa into PKey. + let modulus = rsa.n().to_vec(); + let public_exponent = rsa.e().to_vec(); + + let pkey = PKey::from_rsa(rsa).map_err(key_err)?; + let private_der = pkey.private_key_to_pkcs8().map_err(key_err)?; + let public_der = pkey.public_key_to_der().map_err(key_err)?; + + Ok(RsaKeyPair { private_der, public_der, bits, modulus, public_exponent }) + } + + fn generate_ec_key_pair(&self, curve: EcCurve) -> Result { + let nid = match curve { + EcCurve::P256 => Nid::X9_62_PRIME256V1, + EcCurve::P384 => Nid::SECP384R1, + EcCurve::P521 => Nid::SECP521R1, + #[allow(unreachable_patterns)] + _ => return Err(CryptoError::MechanismInvalid { name: "unsupported EC curve" }), + }; + + let ec_params_der = match curve { + EcCurve::P256 => vec![0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07], + EcCurve::P384 => vec![0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22], + EcCurve::P521 => vec![0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23], + #[allow(unreachable_patterns)] + _ => return Err(CryptoError::MechanismInvalid { name: "unsupported EC curve" }), + }; + + let group = EcGroup::from_curve_name(nid).map_err(key_err)?; + let ec_key = EcKey::generate(&group).map_err(key_err)?; + + // Extract CKA_EC_POINT: DER OCTET STRING wrapping the uncompressed point. + let mut ctx = BigNumContext::new().map_err(key_err)?; + let point_bytes = ec_key + .public_key() + .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx) + .map_err(key_err)?; + let ec_point_uncompressed = der_octet_string(&point_bytes); + + let pkey = PKey::from_ec_key(ec_key).map_err(key_err)?; + let private_der = pkey.private_key_to_pkcs8().map_err(key_err)?; + let public_der = pkey.public_key_to_der().map_err(key_err)?; + + Ok(EcKeyPair { private_der, public_der, curve, ec_params_der, ec_point_uncompressed }) + } + + fn generate_aes_key(&self, len: usize) -> Result, CryptoError> { + if !matches!(len, 16 | 24 | 32) { + return Err(CryptoError::InvalidKeySize { + message: format!("AES key must be 16, 24, or 32 bytes; got {len}"), + }); + } + let mut key = vec![0u8; len]; + rand_bytes(&mut key).map_err(key_err)?; + Ok(key) + } + + // ── Random ──────────────────────────────────────────────────────────────── + + fn generate_random(&self, buf: &mut [u8]) -> Result<(), CryptoError> { + rand_bytes(buf).map_err(random_err) + } + + // ── AES-CBC ─────────────────────────────────────────────────────────────── + + fn aes_cbc_encrypt( + &self, + key: &[u8], + iv: &[u8], + plaintext: &[u8], + ) -> Result, CryptoError> { + let cipher = aes_cbc_cipher(key.len())?; + let mut c = Crypter::new(cipher, Mode::Encrypt, key, Some(iv)).map_err(encrypt_err)?; + c.pad(true); + let mut out = vec![0u8; plaintext.len() + cipher.block_size()]; + let mut n = c.update(plaintext, &mut out).map_err(encrypt_err)?; + n += c.finalize(&mut out[n..]).map_err(encrypt_err)?; + out.truncate(n); + Ok(out) + } + + fn aes_cbc_decrypt( + &self, + key: &[u8], + iv: &[u8], + ciphertext: &[u8], + ) -> Result, CryptoError> { + let cipher = aes_cbc_cipher(key.len())?; + let mut c = Crypter::new(cipher, Mode::Decrypt, key, Some(iv)).map_err(decrypt_err)?; + c.pad(true); + let mut out = vec![0u8; ciphertext.len() + cipher.block_size()]; + let mut n = c.update(ciphertext, &mut out).map_err(decrypt_err)?; + n += c.finalize(&mut out[n..]).map_err(decrypt_err)?; + out.truncate(n); + Ok(out) + } + + // ── AES-CTR ─────────────────────────────────────────────────────────────── + + fn aes_ctr_crypt( + &self, + key: &[u8], + iv: &[u8], + input: &[u8], + ) -> Result, CryptoError> { + let cipher = aes_ctr_cipher(key.len())?; + // CTR is a stream cipher — no padding, encrypt == decrypt. + let mut c = Crypter::new(cipher, Mode::Encrypt, key, Some(iv)).map_err(encrypt_err)?; + c.pad(false); + let mut out = vec![0u8; input.len() + cipher.block_size()]; + let mut n = c.update(input, &mut out).map_err(encrypt_err)?; + n += c.finalize(&mut out[n..]).map_err(encrypt_err)?; + out.truncate(n); + Ok(out) + } + + // ── AES-GCM ─────────────────────────────────────────────────────────────── + + fn aes_gcm_encrypt( + &self, + key: &[u8], + iv: &[u8], + aad: &[u8], + plaintext: &[u8], + ) -> Result<(Vec, Vec), CryptoError> { + let cipher = aes_gcm_cipher(key.len())?; + let mut tag = vec![0u8; 16]; + let ciphertext = encrypt_aead(cipher, key, Some(iv), aad, plaintext, &mut tag) + .map_err(encrypt_err)?; + Ok((ciphertext, tag)) + } + + fn aes_gcm_decrypt( + &self, + key: &[u8], + iv: &[u8], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8], + ) -> Result, CryptoError> { + let cipher = aes_gcm_cipher(key.len())?; + decrypt_aead(cipher, key, Some(iv), aad, ciphertext, tag).map_err(decrypt_err) + } + + // ── RSA PKCS#1 v1.5 encryption ──────────────────────────────────────────── + + fn rsa_pkcs1_encrypt( + &self, + public_der: &[u8], + plaintext: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::::public_key_from_der(public_der).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + let mut out = vec![0u8; rsa.size() as usize]; + let n = rsa.public_encrypt(plaintext, &mut out, Padding::PKCS1).map_err(encrypt_err)?; + out.truncate(n); + Ok(out) + } + + fn rsa_pkcs1_decrypt( + &self, + private_der: &[u8], + ciphertext: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::::private_key_from_pkcs8(private_der).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + let mut out = vec![0u8; rsa.size() as usize]; + let n = rsa.private_decrypt(ciphertext, &mut out, Padding::PKCS1).map_err(decrypt_err)?; + out.truncate(n); + Ok(out) + } + + // ── RSA-OAEP encryption ─────────────────────────────────────────────────── + + fn rsa_oaep_encrypt( + &self, + public_der: &[u8], + plaintext: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::::public_key_from_der(public_der).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + let mut out = vec![0u8; rsa.size() as usize]; + let n = rsa.public_encrypt(plaintext, &mut out, Padding::PKCS1_OAEP).map_err(encrypt_err)?; + out.truncate(n); + Ok(out) + } + + fn rsa_oaep_decrypt( + &self, + private_der: &[u8], + ciphertext: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::::private_key_from_pkcs8(private_der).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + let mut out = vec![0u8; rsa.size() as usize]; + let n = rsa.private_decrypt(ciphertext, &mut out, Padding::PKCS1_OAEP).map_err(decrypt_err)?; + out.truncate(n); + Ok(out) + } + + // ── RSA PKCS#1 v1.5 signing ─────────────────────────────────────────────── + + fn rsa_pkcs1_sign( + &self, + private_der: &[u8], + message: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::private_key_from_pkcs8(private_der).map_err(invalid_key_err)?; + let mut signer = Signer::new(MessageDigest::sha256(), &pkey).map_err(sign_err)?; + // Default RSA padding for Signer is PKCS#1 v1.5 — no explicit set needed. + signer.update(message).map_err(sign_err)?; + signer.sign_to_vec().map_err(sign_err) + } + + fn rsa_pkcs1_verify( + &self, + public_der: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result { + let pkey = PKey::public_key_from_der(public_der).map_err(invalid_key_err)?; + let mut verifier = Verifier::new(MessageDigest::sha256(), &pkey).map_err(verify_err)?; + verifier.update(message).map_err(verify_err)?; + verifier.verify(signature).map_err(verify_err) + } + + // ── RSA-PSS signing ─────────────────────────────────────────────────────── + + fn rsa_pss_sign( + &self, + private_der: &[u8], + message: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::private_key_from_pkcs8(private_der).map_err(invalid_key_err)?; + let mut signer = Signer::new(MessageDigest::sha256(), &pkey).map_err(sign_err)?; + signer.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS).map_err(sign_err)?; + signer.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH).map_err(sign_err)?; + signer.update(message).map_err(sign_err)?; + signer.sign_to_vec().map_err(sign_err) + } + + fn rsa_pss_verify( + &self, + public_der: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result { + let pkey = PKey::public_key_from_der(public_der).map_err(invalid_key_err)?; + let mut verifier = Verifier::new(MessageDigest::sha256(), &pkey).map_err(verify_err)?; + verifier.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS).map_err(verify_err)?; + verifier.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH).map_err(verify_err)?; + verifier.update(message).map_err(verify_err)?; + verifier.verify(signature).map_err(verify_err) + } + + // ── ECDSA signing ───────────────────────────────────────────────────────── + + fn ecdsa_sign( + &self, + private_der: &[u8], + message: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::::private_key_from_pkcs8(private_der).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + // Hash the message first (CKM_ECDSA requires a pre-hashed digest). + let digest = hash(MessageDigest::sha256(), message).map_err(hash_err)?; + let sig = EcdsaSig::sign(digest.as_ref(), &ec_key).map_err(sign_err)?; + sig.to_der().map_err(sign_err) + } + + fn ecdsa_verify( + &self, + public_der: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result { + let pkey = PKey::::public_key_from_der(public_der).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let digest = hash(MessageDigest::sha256(), message).map_err(hash_err)?; + let sig = EcdsaSig::from_der(signature) + .map_err(|e| CryptoError::VerifyFailed { message: e.to_string() })?; + sig.verify(digest.as_ref(), &ec_key).map_err(verify_err) + } + + // ── Hash-parameterized RSA/ECDSA signing (SHA-384/512 etc.) ────────────── + + fn rsa_pkcs1_sign_hash( + &self, + private_der: &[u8], + message: &[u8], + hash_algo: HashAlgorithm, + ) -> Result, CryptoError> { + let md = message_digest(hash_algo)?; + let pkey = PKey::private_key_from_pkcs8(private_der).map_err(invalid_key_err)?; + let mut signer = Signer::new(md, &pkey).map_err(sign_err)?; + signer.update(message).map_err(sign_err)?; + signer.sign_to_vec().map_err(sign_err) + } + + fn rsa_pkcs1_verify_hash( + &self, + public_der: &[u8], + message: &[u8], + signature: &[u8], + hash_algo: HashAlgorithm, + ) -> Result { + let md = message_digest(hash_algo)?; + let pkey = PKey::public_key_from_der(public_der).map_err(invalid_key_err)?; + let mut verifier = Verifier::new(md, &pkey).map_err(verify_err)?; + verifier.update(message).map_err(verify_err)?; + verifier.verify(signature).map_err(verify_err) + } + + fn rsa_pss_sign_hash( + &self, + private_der: &[u8], + message: &[u8], + hash_algo: HashAlgorithm, + ) -> Result, CryptoError> { + let md = message_digest(hash_algo)?; + let pkey = PKey::private_key_from_pkcs8(private_der).map_err(invalid_key_err)?; + let mut signer = Signer::new(md, &pkey).map_err(sign_err)?; + signer.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS).map_err(sign_err)?; + signer.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH).map_err(sign_err)?; + signer.update(message).map_err(sign_err)?; + signer.sign_to_vec().map_err(sign_err) + } + + fn rsa_pss_verify_hash( + &self, + public_der: &[u8], + message: &[u8], + signature: &[u8], + hash_algo: HashAlgorithm, + ) -> Result { + let md = message_digest(hash_algo)?; + let pkey = PKey::public_key_from_der(public_der).map_err(invalid_key_err)?; + let mut verifier = Verifier::new(md, &pkey).map_err(verify_err)?; + verifier.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS).map_err(verify_err)?; + verifier.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH).map_err(verify_err)?; + verifier.update(message).map_err(verify_err)?; + verifier.verify(signature).map_err(verify_err) + } + + fn ecdsa_sign_hash( + &self, + private_der: &[u8], + message: &[u8], + hash_algo: HashAlgorithm, + ) -> Result, CryptoError> { + let md = message_digest(hash_algo)?; + let pkey = PKey::::private_key_from_pkcs8(private_der).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let digest = hash(md, message).map_err(hash_err)?; + let sig = EcdsaSig::sign(digest.as_ref(), &ec_key).map_err(sign_err)?; + sig.to_der().map_err(sign_err) + } + + fn ecdsa_verify_hash( + &self, + public_der: &[u8], + message: &[u8], + signature: &[u8], + hash_algo: HashAlgorithm, + ) -> Result { + let md = message_digest(hash_algo)?; + let pkey = PKey::::public_key_from_der(public_der).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let digest = hash(md, message).map_err(hash_err)?; + let sig = EcdsaSig::from_der(signature) + .map_err(|e| CryptoError::VerifyFailed { message: e.to_string() })?; + sig.verify(digest.as_ref(), &ec_key).map_err(verify_err) + } + + // ── AES Key Wrap (RFC 3394) ───────────────────────────────────────────── + + fn aes_key_wrap( + &self, + kek: &[u8], + plaintext_key: &[u8], + ) -> Result, CryptoError> { + use openssl::aes::{AesKey, wrap_key}; + let aes_key = AesKey::new_encrypt(kek) + .map_err(|_| CryptoError::EncryptFailed { message: "AES key wrap: invalid KEK".into() })?; + let mut out = vec![0u8; plaintext_key.len() + 8]; // wrap adds 8-byte IV + let n = wrap_key(&aes_key, None, &mut out, plaintext_key) + .map_err(|_| CryptoError::EncryptFailed { message: "AES key wrap failed".into() })?; + out.truncate(n); + Ok(out) + } + + fn aes_key_unwrap( + &self, + kek: &[u8], + wrapped_key: &[u8], + ) -> Result, CryptoError> { + use openssl::aes::{AesKey, unwrap_key}; + let aes_key = AesKey::new_decrypt(kek) + .map_err(|_| CryptoError::DecryptFailed { message: "AES key unwrap: invalid KEK".into() })?; + let mut out = vec![0u8; wrapped_key.len()]; // unwrap result is shorter + let n = unwrap_key(&aes_key, None, &mut out, wrapped_key) + .map_err(|_| CryptoError::DecryptFailed { message: "AES key unwrap failed".into() })?; + out.truncate(n); + Ok(out) + } + + // ── Hashing ─────────────────────────────────────────────────────────────── + + fn hash( + &self, + algorithm: HashAlgorithm, + data: &[u8], + ) -> Result, CryptoError> { + let md = message_digest(algorithm)?; + hash(md, data).map(|d| d.to_vec()).map_err(hash_err) + } + + fn new_stream_hasher( + &self, + algorithm: HashAlgorithm, + ) -> Result, CryptoError> { + let md = message_digest(algorithm)?; + let inner = Hasher::new(md).map_err(hash_err)?; + Ok(Box::new(OpenSslStreamHasher { inner })) + } + + // ── Attribute access ────────────────────────────────────────────────────── + + fn rsa_attribute( + &self, + key_der: &[u8], + is_private: bool, + attr: AttributeType, + ) -> Result { + // Sensitive attributes on the private-key object. + if is_private && matches!(attr, AttributeType::Value) { + return Err(CryptoError::AttributeSensitive); + } + + // Parse key — extract n, e, and size from whichever half we have. + let (n_bytes, e_bytes, size_bytes) = if is_private { + let pkey = PKey::::private_key_from_pkcs8(key_der).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + (rsa.n().to_vec(), rsa.e().to_vec(), rsa.size() as u64) + } else { + let pkey = PKey::::public_key_from_der(key_der).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + (rsa.n().to_vec(), rsa.e().to_vec(), rsa.size() as u64) + }; + + match attr { + AttributeType::Modulus => Ok(AttributeValue::Bytes(n_bytes)), + AttributeType::ModulusBits => Ok(AttributeValue::Ulong(size_bytes * 8)), + AttributeType::PublicExponent => Ok(AttributeValue::Bytes(e_bytes)), + _ => Err(CryptoError::AttributeTypeInvalid), + } + } + + fn ec_attribute( + &self, + key_der: &[u8], + is_private: bool, + attr: AttributeType, + ) -> Result { + if is_private && matches!(attr, AttributeType::Value) { + return Err(CryptoError::AttributeSensitive); + } + + // Parse key to extract group + public point. + let (ec_params_der, ec_point_der) = if is_private { + let pkey = PKey::::private_key_from_pkcs8(key_der).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let params = ec_params_for_group(ec_key.group())?; + let point = ec_point_for_key(&ec_key)?; + (params, point) + } else { + let pkey = PKey::::public_key_from_der(key_der).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let params = ec_params_for_group(ec_key.group())?; + let point = ec_point_for_key(&ec_key)?; + (params, point) + }; + + match attr { + AttributeType::EcParams => Ok(AttributeValue::Bytes(ec_params_der)), + AttributeType::EcPoint => Ok(AttributeValue::Bytes(ec_point_der)), + _ => Err(CryptoError::AttributeTypeInvalid), + } + } + + fn aes_attribute( + &self, + key: &[u8], + attr: AttributeType, + ) -> Result { + match attr { + AttributeType::ValueLen => Ok(AttributeValue::Ulong(key.len() as u64)), + // Value (raw key bytes) — the engine returns it; the PKCS#11 layer + // applies CKA_SENSITIVE / CKA_EXTRACTABLE policy before calling this. + AttributeType::Value => Ok(AttributeValue::Bytes(key.to_vec())), + _ => Err(CryptoError::AttributeTypeInvalid), + } + } + + // ── EdDSA (v3.0) ──────────────────────────────────────────────────────── + + fn generate_ed_key_pair(&self, curve: EdwardsCurve) -> Result { + let pkey = match curve { + EdwardsCurve::Ed25519 => PKey::generate_ed25519().map_err(key_err)?, + EdwardsCurve::Ed448 => PKey::generate_ed448().map_err(key_err)?, + }; + let private_der = pkey.private_key_to_pkcs8().map_err(key_err)?; + let public_der = pkey.public_key_to_der().map_err(key_err)?; + let raw_pub = pkey.raw_public_key().map_err(key_err)?; + + // EdDSA OIDs for CKA_EC_PARAMS + let ec_params_der = match curve { + // Ed25519: 1.3.101.112 → 06 03 2b 65 70 + EdwardsCurve::Ed25519 => vec![0x06, 0x03, 0x2b, 0x65, 0x70], + // Ed448: 1.3.101.113 → 06 03 2b 65 71 + EdwardsCurve::Ed448 => vec![0x06, 0x03, 0x2b, 0x65, 0x71], + }; + + Ok(EdKeyPair { + private_der, + public_der, + curve, + ec_params_der, + ec_point: raw_pub, + }) + } + + fn eddsa_sign( + &self, + private_der: &[u8], + message: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::private_key_from_pkcs8(private_der).map_err(invalid_key_err)?; + // EdDSA uses None for digest — the sign is done over the raw message + let mut signer = Signer::new_without_digest(&pkey).map_err(sign_err)?; + signer.sign_oneshot_to_vec(message).map_err(sign_err) + } + + fn eddsa_verify( + &self, + public_der: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result { + let pkey = PKey::public_key_from_der(public_der).map_err(invalid_key_err)?; + let mut verifier = Verifier::new_without_digest(&pkey).map_err(verify_err)?; + verifier.verify_oneshot(signature, message).map_err(verify_err) + } + + fn ed_attribute( + &self, + key_der: &[u8], + is_private: bool, + attr: AttributeType, + ) -> Result { + if is_private && matches!(attr, AttributeType::Value) { + return Err(CryptoError::AttributeSensitive); + } + + // Parse the key to determine which Edwards curve and get raw public key + let (key_id, raw_pub) = if is_private { + let pkey = PKey::::private_key_from_pkcs8(key_der).map_err(invalid_key_err)?; + let id = pkey.id(); + let raw = pkey.raw_public_key().map_err(invalid_key_err)?; + (id, raw) + } else { + let pkey = PKey::::public_key_from_der(key_der).map_err(invalid_key_err)?; + let id = pkey.id(); + let raw = pkey.raw_public_key().map_err(invalid_key_err)?; + (id, raw) + }; + + let ec_params_der = match key_id { + openssl::pkey::Id::ED25519 => vec![0x06, 0x03, 0x2b, 0x65, 0x70], + openssl::pkey::Id::ED448 => vec![0x06, 0x03, 0x2b, 0x65, 0x71], + _ => return Err(CryptoError::AttributeTypeInvalid), + }; + + match attr { + AttributeType::EcParams => Ok(AttributeValue::Bytes(ec_params_der)), + AttributeType::EcPoint => Ok(AttributeValue::Bytes(der_octet_string(&raw_pub))), + _ => Err(CryptoError::AttributeTypeInvalid), + } + } + + // ── ChaCha20-Poly1305 (v3.0) ──────────────────────────────────────────── + + fn generate_chacha20_key(&self) -> Result, CryptoError> { + let mut key = vec![0u8; 32]; // ChaCha20 always uses 256-bit keys + rand_bytes(&mut key).map_err(key_err)?; + Ok(key) + } + + fn chacha20_poly1305_encrypt( + &self, + key: &[u8], + nonce: &[u8], + aad: &[u8], + plaintext: &[u8], + ) -> Result<(Vec, Vec), CryptoError> { + if key.len() != 32 { + return Err(CryptoError::InvalidKeySize { + message: format!("ChaCha20-Poly1305 key must be 32 bytes; got {}", key.len()), + }); + } + if nonce.len() != 12 { + return Err(CryptoError::MechanismParamInvalid { + message: format!("ChaCha20-Poly1305 nonce must be 12 bytes; got {}", nonce.len()), + }); + } + let cipher = Cipher::chacha20_poly1305(); + let mut tag = vec![0u8; 16]; + let ciphertext = encrypt_aead(cipher, key, Some(nonce), aad, plaintext, &mut tag) + .map_err(encrypt_err)?; + Ok((ciphertext, tag)) + } + + fn chacha20_poly1305_decrypt( + &self, + key: &[u8], + nonce: &[u8], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8], + ) -> Result, CryptoError> { + if key.len() != 32 { + return Err(CryptoError::InvalidKeySize { + message: format!("ChaCha20-Poly1305 key must be 32 bytes; got {}", key.len()), + }); + } + let cipher = Cipher::chacha20_poly1305(); + decrypt_aead(cipher, key, Some(nonce), aad, ciphertext, tag).map_err(decrypt_err) + } + + // ── HKDF (v3.0) ───────────────────────────────────────────────────────── + + fn hkdf_derive( + &self, + hash_algo: HashAlgorithm, + ikm: &[u8], + salt: &[u8], + info: &[u8], + okm_len: usize, + ) -> Result, CryptoError> { + use openssl::md::Md; + use openssl::pkey_ctx::PkeyCtx; + + let md = match hash_algo { + HashAlgorithm::Sha256 => Md::sha256(), + HashAlgorithm::Sha384 => Md::sha384(), + HashAlgorithm::Sha512 => Md::sha512(), + HashAlgorithm::Sha1 => Md::sha1(), + _ => return Err(CryptoError::MechanismInvalid { name: "HKDF: unsupported hash" }), + }; + let gen_err = |e: openssl::error::ErrorStack| CryptoError::GeneralError { message: e.to_string() }; + let mut ctx = PkeyCtx::new_id(openssl::pkey::Id::HKDF).map_err(gen_err)?; + ctx.derive_init().map_err(gen_err)?; + ctx.set_hkdf_md(md).map_err(gen_err)?; + ctx.set_hkdf_key(ikm).map_err(gen_err)?; + ctx.set_hkdf_salt(salt).map_err(gen_err)?; + ctx.add_hkdf_info(info).map_err(gen_err)?; + let mut okm = vec![0u8; okm_len]; + ctx.derive(Some(&mut okm)).map_err(gen_err)?; + Ok(okm) + } +} + +// ── Private EC helpers ──────────────────────────────────────────────────────── + +/// Return the DER-encoded OID for a named curve (CKA_EC_PARAMS). +fn ec_params_for_group(group: &openssl::ec::EcGroupRef) -> Result, CryptoError> { + let nid = group.curve_name().ok_or(CryptoError::GeneralError { + message: "EC group has no named curve NID".into(), + })?; + match nid { + Nid::X9_62_PRIME256V1 => + Ok(vec![0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]), + Nid::SECP384R1 => + Ok(vec![0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22]), + Nid::SECP521R1 => + Ok(vec![0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23]), + _other => Err(CryptoError::MechanismInvalid { + name: "unsupported EC curve", + }), + } +} + +/// Return a DER OCTET STRING wrapping the uncompressed EC public key point (CKA_EC_POINT). +fn ec_point_for_key( + ec_key: &EcKey, +) -> Result, CryptoError> { + let mut ctx = BigNumContext::new() + .map_err(|e| CryptoError::GeneralError { message: e.to_string() })?; + let bytes = ec_key + .public_key() + .to_bytes(ec_key.group(), PointConversionForm::UNCOMPRESSED, &mut ctx) + .map_err(|e| CryptoError::GeneralError { message: e.to_string() })?; + Ok(der_octet_string(&bytes)) +} diff --git a/src/openssl_provider.rs b/src/openssl_provider.rs new file mode 100644 index 0000000..13a82c0 --- /dev/null +++ b/src/openssl_provider.rs @@ -0,0 +1,1073 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use openssl::bn::BigNumContext; +use openssl::ec::{EcGroup, EcKey, PointConversionForm}; +use openssl::ecdsa::EcdsaSig; +use openssl::hash::{hash, Hasher, MessageDigest}; +use openssl::nid::Nid; +use openssl::pkey::{PKey, Private, Public}; +use openssl::rand::rand_bytes; +use openssl::rsa::{Padding, Rsa}; +use openssl::sign::{RsaPssSaltlen, Signer, Verifier}; +use openssl::symm::{decrypt_aead, encrypt_aead, Cipher, Crypter, Mode}; + +use zeroize::Zeroizing; + +use crate::attributes::{AttributeType, AttributeValue}; +use crate::error::CryptoError; +use crate::traits::{CryptoProvider, EngineMechanismInfo, EngineKeyRef, StreamHasher}; +use crate::types::{EcCurve, EcKeyPair, EdKeyPair, EdwardsCurve, HashAlgorithm, RsaKeyPair}; + +// ── Error conversion helpers ────────────────────────────────────────────────── + +fn key_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::KeyGenFailed { message: e.to_string() } +} + +fn invalid_key_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::InvalidKeyData { message: e.to_string() } +} + +fn encrypt_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::EncryptFailed { message: e.to_string() } +} + +fn decrypt_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::DecryptFailed { message: e.to_string() } +} + +fn sign_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::SignFailed { message: e.to_string() } +} + +fn verify_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::VerifyFailed { message: e.to_string() } +} + +fn hash_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::HashFailed { message: e.to_string() } +} + +fn random_err(e: openssl::error::ErrorStack) -> CryptoError { + CryptoError::RandomFailed { message: e.to_string() } +} + +// ── AES cipher selection ────────────────────────────────────────────────────── + +fn aes_cbc_cipher(key_len: usize) -> Result { + match key_len { + 16 => Ok(Cipher::aes_128_cbc()), + 24 => Ok(Cipher::aes_192_cbc()), + 32 => Ok(Cipher::aes_256_cbc()), + n => Err(CryptoError::InvalidKeySize { + message: format!("AES-CBC key must be 16, 24, or 32 bytes; got {n}"), + }), + } +} + +fn aes_ctr_cipher(key_len: usize) -> Result { + match key_len { + 16 => Ok(Cipher::aes_128_ctr()), + 24 => Ok(Cipher::aes_192_ctr()), + 32 => Ok(Cipher::aes_256_ctr()), + n => Err(CryptoError::InvalidKeySize { + message: format!("AES-CTR key must be 16, 24, or 32 bytes; got {n}"), + }), + } +} + +fn aes_gcm_cipher(key_len: usize) -> Result { + match key_len { + 16 => Ok(Cipher::aes_128_gcm()), + 24 => Ok(Cipher::aes_192_gcm()), + 32 => Ok(Cipher::aes_256_gcm()), + n => Err(CryptoError::InvalidKeySize { + message: format!("AES-GCM key must be 16, 24, or 32 bytes; got {n}"), + }), + } +} + +// ── MessageDigest mapping ───────────────────────────────────────────────────── + +fn message_digest(algorithm: HashAlgorithm) -> Result { + match algorithm { + HashAlgorithm::Md5 => Ok(MessageDigest::md5()), + HashAlgorithm::Sha1 => Ok(MessageDigest::sha1()), + HashAlgorithm::Sha256 => Ok(MessageDigest::sha256()), + HashAlgorithm::Sha384 => Ok(MessageDigest::sha384()), + HashAlgorithm::Sha512 => Ok(MessageDigest::sha512()), + HashAlgorithm::Sha3_256 => Ok(MessageDigest::sha3_256()), + HashAlgorithm::Sha3_384 => Ok(MessageDigest::sha3_384()), + HashAlgorithm::Sha3_512 => Ok(MessageDigest::sha3_512()), + #[allow(unreachable_patterns)] + _ => Err(CryptoError::MechanismInvalid { name: "unknown HashAlgorithm variant" }), + } +} + +// ── DER OCTET STRING wrapper (for CKA_EC_POINT) ─────────────────────────────── + +/// Wrap raw bytes in a DER OCTET STRING (tag 0x04 + length + data). +/// Used to produce the CKA_EC_POINT encoding that PKCS#11 expects. +fn der_octet_string(bytes: &[u8]) -> Vec { + let len = bytes.len(); + let mut out = Vec::with_capacity(4 + len); + out.push(0x04); // OCTET STRING tag + if len < 0x80 { + out.push(len as u8); + } else if len < 0x100 { + out.push(0x81); + out.push(len as u8); + } else { + out.push(0x82); + out.push((len >> 8) as u8); + out.push((len & 0xFF) as u8); + } + out.extend_from_slice(bytes); + out +} + +// ── Streaming hasher wrapper ────────────────────────────────────────────────── + +struct OpenSslStreamHasher { + inner: Hasher, +} + +impl StreamHasher for OpenSslStreamHasher { + fn update(&mut self, data: &[u8]) -> Result<(), CryptoError> { + self.inner.update(data).map_err(hash_err) + } + + fn finish(mut self: Box) -> Result, CryptoError> { + self.inner.finish().map(|d| d.to_vec()).map_err(hash_err) + } +} + +// ── OpenSslEngine ───────────────────────────────────────────────────────────── + +/// OpenSSL-backed crypto engine. +/// +/// A zero-size struct — all state lives in the OpenSSL library itself. +/// Thread-safety is guaranteed by OpenSSL's internal locking. +pub struct OpenSslEngine; + +impl CryptoProvider for OpenSslEngine { + + // ── Key generation ──────────────────────────────────────────────────────── + + fn generate_rsa_key_pair(&self, bits: u32) -> Result { + let exponent = openssl::bn::BigNum::from_u32(65537).map_err(key_err)?; + let rsa = Rsa::generate_with_e(bits, &exponent).map_err(key_err)?; + + // Pre-extract attribute values before moving rsa into PKey. + let modulus = rsa.n().to_vec(); + let public_exponent = rsa.e().to_vec(); + + let pkey = PKey::from_rsa(rsa).map_err(key_err)?; + let private_der = Zeroizing::new(pkey.private_key_to_pkcs8().map_err(key_err)?); + let public_der = pkey.public_key_to_der().map_err(key_err)?; + + Ok(RsaKeyPair { private_der, public_der, bits, modulus, public_exponent }) + } + + fn generate_ec_key_pair(&self, curve: EcCurve) -> Result { + let nid = match curve { + EcCurve::P256 => Nid::X9_62_PRIME256V1, + EcCurve::P384 => Nid::SECP384R1, + EcCurve::P521 => Nid::SECP521R1, + #[allow(unreachable_patterns)] + _ => return Err(CryptoError::MechanismInvalid { name: "unsupported EC curve" }), + }; + + let ec_params_der = match curve { + EcCurve::P256 => vec![0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07], + EcCurve::P384 => vec![0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22], + EcCurve::P521 => vec![0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23], + #[allow(unreachable_patterns)] + _ => return Err(CryptoError::MechanismInvalid { name: "unsupported EC curve" }), + }; + + let group = EcGroup::from_curve_name(nid).map_err(key_err)?; + let ec_key = EcKey::generate(&group).map_err(key_err)?; + + // Extract CKA_EC_POINT: DER OCTET STRING wrapping the uncompressed point. + let mut ctx = BigNumContext::new().map_err(key_err)?; + let point_bytes = ec_key + .public_key() + .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx) + .map_err(key_err)?; + let ec_point_uncompressed = der_octet_string(&point_bytes); + + let pkey = PKey::from_ec_key(ec_key).map_err(key_err)?; + let private_der = Zeroizing::new(pkey.private_key_to_pkcs8().map_err(key_err)?); + let public_der = pkey.public_key_to_der().map_err(key_err)?; + + Ok(EcKeyPair { private_der, public_der, curve, ec_params_der, ec_point_uncompressed }) + } + + fn generate_aes_key(&self, len: usize) -> Result { + if !matches!(len, 16 | 24 | 32) { + return Err(CryptoError::InvalidKeySize { + message: format!("AES key must be 16, 24, or 32 bytes; got {len}"), + }); + } + let mut buf = vec![0u8; len]; + rand_bytes(&mut buf).map_err(key_err)?; + Ok(EngineKeyRef::from_bytes(buf)) + } + + // ── Random ──────────────────────────────────────────────────────────────── + + fn generate_random(&self, buf: &mut [u8]) -> Result<(), CryptoError> { + rand_bytes(buf).map_err(random_err) + } + + // ── AES-CBC ─────────────────────────────────────────────────────────────── + + fn aes_cbc_encrypt( + &self, + key: &EngineKeyRef, + iv: &[u8], + plaintext: &[u8], + ) -> Result, CryptoError> { + let key = key.as_bytes(); + let cipher = aes_cbc_cipher(key.len())?; + let mut c = Crypter::new(cipher, Mode::Encrypt, key, Some(iv)).map_err(encrypt_err)?; + c.pad(true); + let mut out = vec![0u8; plaintext.len() + cipher.block_size()]; + let mut n = c.update(plaintext, &mut out).map_err(encrypt_err)?; + n += c.finalize(&mut out[n..]).map_err(encrypt_err)?; + out.truncate(n); + Ok(out) + } + + fn aes_cbc_decrypt( + &self, + key: &EngineKeyRef, + iv: &[u8], + ciphertext: &[u8], + ) -> Result>, CryptoError> { + let key = key.as_bytes(); + let cipher = aes_cbc_cipher(key.len())?; + let mut c = Crypter::new(cipher, Mode::Decrypt, key, Some(iv)).map_err(decrypt_err)?; + c.pad(true); + let mut out = vec![0u8; ciphertext.len() + cipher.block_size()]; + let mut n = c.update(ciphertext, &mut out).map_err(decrypt_err)?; + n += c.finalize(&mut out[n..]).map_err(decrypt_err)?; + out.truncate(n); + Ok(Zeroizing::new(out)) + } + + // ── AES-CTR ─────────────────────────────────────────────────────────────── + + fn aes_ctr_crypt( + &self, + key: &EngineKeyRef, + iv: &[u8], + input: &[u8], + ) -> Result, CryptoError> { + let key = key.as_bytes(); + let cipher = aes_ctr_cipher(key.len())?; + // CTR is a stream cipher — no padding, encrypt == decrypt. + let mut c = Crypter::new(cipher, Mode::Encrypt, key, Some(iv)).map_err(encrypt_err)?; + c.pad(false); + let mut out = vec![0u8; input.len() + cipher.block_size()]; + let mut n = c.update(input, &mut out).map_err(encrypt_err)?; + n += c.finalize(&mut out[n..]).map_err(encrypt_err)?; + out.truncate(n); + Ok(out) + } + + // ── AES-GCM ─────────────────────────────────────────────────────────────── + + fn aes_gcm_encrypt( + &self, + key: &EngineKeyRef, + iv: &[u8], + aad: &[u8], + plaintext: &[u8], + ) -> Result<(Vec, Vec), CryptoError> { + let key = key.as_bytes(); + let cipher = aes_gcm_cipher(key.len())?; + let mut tag = vec![0u8; 16]; + let ciphertext = encrypt_aead(cipher, key, Some(iv), aad, plaintext, &mut tag) + .map_err(encrypt_err)?; + Ok((ciphertext, tag)) + } + + fn aes_gcm_decrypt( + &self, + key: &EngineKeyRef, + iv: &[u8], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8], + ) -> Result>, CryptoError> { + let key = key.as_bytes(); + let cipher = aes_gcm_cipher(key.len())?; + decrypt_aead(cipher, key, Some(iv), aad, ciphertext, tag) + .map(Zeroizing::new) + .map_err(decrypt_err) + } + + // ── RSA PKCS#1 v1.5 encryption ──────────────────────────────────────────── + + fn rsa_pkcs1_encrypt( + &self, + key: &EngineKeyRef, + plaintext: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + let mut out = vec![0u8; rsa.size() as usize]; + let n = rsa.public_encrypt(plaintext, &mut out, Padding::PKCS1).map_err(encrypt_err)?; + out.truncate(n); + Ok(out) + } + + fn rsa_pkcs1_decrypt( + &self, + key: &EngineKeyRef, + ciphertext: &[u8], + ) -> Result>, CryptoError> { + let pkey = PKey::::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + let mut out = Zeroizing::new(vec![0u8; rsa.size() as usize]); + let n = rsa.private_decrypt(ciphertext, &mut out, Padding::PKCS1).map_err(decrypt_err)?; + out.truncate(n); + Ok(out) + } + + // ── RSA-OAEP encryption ─────────────────────────────────────────────────── + + fn rsa_oaep_encrypt( + &self, + key: &EngineKeyRef, + plaintext: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + let mut out = vec![0u8; rsa.size() as usize]; + let n = rsa.public_encrypt(plaintext, &mut out, Padding::PKCS1_OAEP).map_err(encrypt_err)?; + out.truncate(n); + Ok(out) + } + + fn rsa_oaep_decrypt( + &self, + key: &EngineKeyRef, + ciphertext: &[u8], + ) -> Result>, CryptoError> { + let pkey = PKey::::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + let mut out = Zeroizing::new(vec![0u8; rsa.size() as usize]); + let n = rsa.private_decrypt(ciphertext, &mut out, Padding::PKCS1_OAEP).map_err(decrypt_err)?; + out.truncate(n); + Ok(out) + } + + // ── RSA PKCS#1 v1.5 signing ─────────────────────────────────────────────── + + fn rsa_pkcs1_sign( + &self, + key: &EngineKeyRef, + message: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + let mut signer = Signer::new(MessageDigest::sha256(), &pkey).map_err(sign_err)?; + // Default RSA padding for Signer is PKCS#1 v1.5 — no explicit set needed. + signer.update(message).map_err(sign_err)?; + signer.sign_to_vec().map_err(sign_err) + } + + fn rsa_pkcs1_verify( + &self, + key: &EngineKeyRef, + message: &[u8], + signature: &[u8], + ) -> Result { + let pkey = PKey::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let mut verifier = Verifier::new(MessageDigest::sha256(), &pkey).map_err(verify_err)?; + verifier.update(message).map_err(verify_err)?; + verifier.verify(signature).map_err(verify_err) + } + + // ── RSA-PSS signing ─────────────────────────────────────────────────────── + + fn rsa_pss_sign( + &self, + key: &EngineKeyRef, + message: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + let mut signer = Signer::new(MessageDigest::sha256(), &pkey).map_err(sign_err)?; + signer.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS).map_err(sign_err)?; + signer.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH).map_err(sign_err)?; + signer.update(message).map_err(sign_err)?; + signer.sign_to_vec().map_err(sign_err) + } + + fn rsa_pss_verify( + &self, + key: &EngineKeyRef, + message: &[u8], + signature: &[u8], + ) -> Result { + let pkey = PKey::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let mut verifier = Verifier::new(MessageDigest::sha256(), &pkey).map_err(verify_err)?; + verifier.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS).map_err(verify_err)?; + verifier.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH).map_err(verify_err)?; + verifier.update(message).map_err(verify_err)?; + verifier.verify(signature).map_err(verify_err) + } + + // ── ECDSA signing ───────────────────────────────────────────────────────── + + fn ecdsa_sign( + &self, + key: &EngineKeyRef, + message: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + // Hash the message first (CKM_ECDSA requires a pre-hashed digest). + let digest = hash(MessageDigest::sha256(), message).map_err(hash_err)?; + let sig = EcdsaSig::sign(digest.as_ref(), &ec_key).map_err(sign_err)?; + sig.to_der().map_err(sign_err) + } + + fn ecdsa_verify( + &self, + key: &EngineKeyRef, + message: &[u8], + signature: &[u8], + ) -> Result { + let pkey = PKey::::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let digest = hash(MessageDigest::sha256(), message).map_err(hash_err)?; + let sig = EcdsaSig::from_der(signature) + .map_err(|e| CryptoError::VerifyFailed { message: e.to_string() })?; + sig.verify(digest.as_ref(), &ec_key).map_err(verify_err) + } + + /// Sign a **pre-computed** digest with ECDSA. + /// + /// The caller is responsible for hashing the message with the appropriate + /// algorithm before calling this method. `EcdsaSig::sign` takes raw + /// digest bytes directly — no internal hashing is performed here. + fn ecdsa_sign_prehashed( + &self, + key: &EngineKeyRef, + digest: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let sig = EcdsaSig::sign(digest, &ec_key).map_err(sign_err)?; + sig.to_der().map_err(sign_err) + } + + fn ecdsa_verify_prehashed( + &self, + key: &EngineKeyRef, + digest: &[u8], + signature: &[u8], + ) -> Result { + let pkey = PKey::::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let sig = EcdsaSig::from_der(signature) + .map_err(|e| CryptoError::VerifyFailed { message: e.to_string() })?; + sig.verify(digest, &ec_key).map_err(verify_err) + } + + // ── Hash-parameterized RSA/ECDSA signing (SHA-384/512 etc.) ────────────── + + fn rsa_pkcs1_sign_hash( + &self, + key: &EngineKeyRef, + message: &[u8], + hash_algo: HashAlgorithm, + ) -> Result, CryptoError> { + let md = message_digest(hash_algo)?; + let pkey = PKey::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + let mut signer = Signer::new(md, &pkey).map_err(sign_err)?; + signer.update(message).map_err(sign_err)?; + signer.sign_to_vec().map_err(sign_err) + } + + fn rsa_pkcs1_verify_hash( + &self, + key: &EngineKeyRef, + message: &[u8], + signature: &[u8], + hash_algo: HashAlgorithm, + ) -> Result { + let md = message_digest(hash_algo)?; + let pkey = PKey::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let mut verifier = Verifier::new(md, &pkey).map_err(verify_err)?; + verifier.update(message).map_err(verify_err)?; + verifier.verify(signature).map_err(verify_err) + } + + fn rsa_pss_sign_hash( + &self, + key: &EngineKeyRef, + message: &[u8], + hash_algo: HashAlgorithm, + ) -> Result, CryptoError> { + let md = message_digest(hash_algo)?; + let pkey = PKey::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + let mut signer = Signer::new(md, &pkey).map_err(sign_err)?; + signer.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS).map_err(sign_err)?; + signer.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH).map_err(sign_err)?; + signer.update(message).map_err(sign_err)?; + signer.sign_to_vec().map_err(sign_err) + } + + fn rsa_pss_verify_hash( + &self, + key: &EngineKeyRef, + message: &[u8], + signature: &[u8], + hash_algo: HashAlgorithm, + ) -> Result { + let md = message_digest(hash_algo)?; + let pkey = PKey::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let mut verifier = Verifier::new(md, &pkey).map_err(verify_err)?; + verifier.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS).map_err(verify_err)?; + verifier.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH).map_err(verify_err)?; + verifier.update(message).map_err(verify_err)?; + verifier.verify(signature).map_err(verify_err) + } + + fn ecdsa_sign_hash( + &self, + key: &EngineKeyRef, + message: &[u8], + hash_algo: HashAlgorithm, + ) -> Result, CryptoError> { + let md = message_digest(hash_algo)?; + let pkey = PKey::::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let digest = hash(md, message).map_err(hash_err)?; + let sig = EcdsaSig::sign(digest.as_ref(), &ec_key).map_err(sign_err)?; + sig.to_der().map_err(sign_err) + } + + fn ecdsa_verify_hash( + &self, + key: &EngineKeyRef, + message: &[u8], + signature: &[u8], + hash_algo: HashAlgorithm, + ) -> Result { + let md = message_digest(hash_algo)?; + let pkey = PKey::::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let digest = hash(md, message).map_err(hash_err)?; + let sig = EcdsaSig::from_der(signature) + .map_err(|e| CryptoError::VerifyFailed { message: e.to_string() })?; + sig.verify(digest.as_ref(), &ec_key).map_err(verify_err) + } + + // ── AES Key Wrap (RFC 3394) ───────────────────────────────────────────── + + fn aes_key_wrap( + &self, + kek: &EngineKeyRef, + plaintext_key: &EngineKeyRef, + ) -> Result, CryptoError> { + use openssl::aes::{AesKey, wrap_key}; + let plaintext_key = plaintext_key.as_bytes(); + let aes_key = AesKey::new_encrypt(kek.as_bytes()) + .map_err(|_| CryptoError::EncryptFailed { message: "AES key wrap: invalid KEK".into() })?; + let mut out = vec![0u8; plaintext_key.len() + 8]; // wrap adds 8-byte IV + let n = wrap_key(&aes_key, None, &mut out, plaintext_key) + .map_err(|_| CryptoError::EncryptFailed { message: "AES key wrap failed".into() })?; + out.truncate(n); + Ok(out) + } + + fn aes_key_unwrap( + &self, + kek: &EngineKeyRef, + wrapped_key: &[u8], + ) -> Result>, CryptoError> { + use openssl::aes::{AesKey, unwrap_key}; + let aes_key = AesKey::new_decrypt(kek.as_bytes()) + .map_err(|_| CryptoError::DecryptFailed { message: "AES key unwrap: invalid KEK".into() })?; + // AES Key Wrap (RFC 3394) adds an 8-byte integrity check value, so the + // plaintext is always 8 bytes shorter than the wrapped ciphertext. + let pt_len = wrapped_key.len().checked_sub(8) + .ok_or_else(|| CryptoError::DecryptFailed { message: "wrapped key too short".into() })?; + let mut out = Zeroizing::new(vec![0u8; pt_len]); + let n = unwrap_key(&aes_key, None, &mut out, wrapped_key) + .map_err(|_| CryptoError::DecryptFailed { message: "AES key unwrap failed".into() })?; + out.truncate(n); + Ok(out) + } + + fn key_value_for_digest(&self, key_ref: &EngineKeyRef) -> Result, CryptoError> { + // For the software engine the key ref IS the raw key bytes, so return them directly. + // This is the only permitted call to as_bytes() for semantic purposes — all other + // callers in the PKCS#11 layer pass key refs through opaquely. + Ok(key_ref.as_bytes().to_vec()) + } + + // ── Hashing ─────────────────────────────────────────────────────────────── + + fn hash( + &self, + algorithm: HashAlgorithm, + data: &[u8], + ) -> Result, CryptoError> { + let md = message_digest(algorithm)?; + hash(md, data).map(|d| d.to_vec()).map_err(hash_err) + } + + fn new_stream_hasher( + &self, + algorithm: HashAlgorithm, + ) -> Result, CryptoError> { + let md = message_digest(algorithm)?; + let inner = Hasher::new(md).map_err(hash_err)?; + Ok(Box::new(OpenSslStreamHasher { inner })) + } + + // ── Attribute access ────────────────────────────────────────────────────── + + fn rsa_attribute( + &self, + key: &EngineKeyRef, + is_private: bool, + attr: AttributeType, + ) -> Result { + if is_private && matches!(attr, AttributeType::Value) { + return Err(CryptoError::AttributeSensitive); + } + + let key_der = key.as_bytes(); + let (n_bytes, e_bytes, size_bytes) = if is_private { + let pkey = PKey::::private_key_from_pkcs8(key_der).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + (rsa.n().to_vec(), rsa.e().to_vec(), rsa.size() as u64) + } else { + let pkey = PKey::::public_key_from_der(key_der).map_err(invalid_key_err)?; + let rsa = pkey.rsa().map_err(invalid_key_err)?; + (rsa.n().to_vec(), rsa.e().to_vec(), rsa.size() as u64) + }; + + match attr { + AttributeType::Modulus => Ok(AttributeValue::Bytes(n_bytes)), + AttributeType::ModulusBits => Ok(AttributeValue::Ulong(size_bytes * 8)), + AttributeType::PublicExponent => Ok(AttributeValue::Bytes(e_bytes)), + _ => Err(CryptoError::AttributeTypeInvalid), + } + } + + fn ec_attribute( + &self, + key: &EngineKeyRef, + is_private: bool, + attr: AttributeType, + ) -> Result { + if is_private && matches!(attr, AttributeType::Value) { + return Err(CryptoError::AttributeSensitive); + } + + let key_der = key.as_bytes(); + let (ec_params_der, ec_point_der) = if is_private { + let pkey = PKey::::private_key_from_pkcs8(key_der).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let params = ec_params_for_group(ec_key.group())?; + let point = ec_point_for_key(&ec_key)?; + (params, point) + } else { + let pkey = PKey::::public_key_from_der(key_der).map_err(invalid_key_err)?; + let ec_key = pkey.ec_key().map_err(invalid_key_err)?; + let params = ec_params_for_group(ec_key.group())?; + let point = ec_point_for_key(&ec_key)?; + (params, point) + }; + + match attr { + AttributeType::EcParams => Ok(AttributeValue::Bytes(ec_params_der)), + AttributeType::EcPoint => Ok(AttributeValue::Bytes(ec_point_der)), + _ => Err(CryptoError::AttributeTypeInvalid), + } + } + + fn aes_attribute( + &self, + key: &EngineKeyRef, + attr: AttributeType, + ) -> Result { + let raw = key.as_bytes(); + match attr { + AttributeType::ValueLen => Ok(AttributeValue::Ulong(raw.len() as u64)), + AttributeType::Value => Ok(AttributeValue::Bytes(raw.to_vec())), + _ => Err(CryptoError::AttributeTypeInvalid), + } + } + + // ── EdDSA (v3.0) ──────────────────────────────────────────────────────── + + fn generate_ed_key_pair(&self, curve: EdwardsCurve) -> Result { + let pkey = match curve { + EdwardsCurve::Ed25519 => PKey::generate_ed25519().map_err(key_err)?, + EdwardsCurve::Ed448 => PKey::generate_ed448().map_err(key_err)?, + }; + let private_der = Zeroizing::new(pkey.private_key_to_pkcs8().map_err(key_err)?); + let public_der = pkey.public_key_to_der().map_err(key_err)?; + let raw_pub = pkey.raw_public_key().map_err(key_err)?; + + // EdDSA OIDs for CKA_EC_PARAMS + let ec_params_der = match curve { + // Ed25519: 1.3.101.112 → 06 03 2b 65 70 + EdwardsCurve::Ed25519 => vec![0x06, 0x03, 0x2b, 0x65, 0x70], + // Ed448: 1.3.101.113 → 06 03 2b 65 71 + EdwardsCurve::Ed448 => vec![0x06, 0x03, 0x2b, 0x65, 0x71], + }; + + Ok(EdKeyPair { + private_der, + public_der, + curve, + ec_params_der, + ec_point: raw_pub, + }) + } + + fn eddsa_sign( + &self, + key: &EngineKeyRef, + message: &[u8], + ) -> Result, CryptoError> { + let pkey = PKey::private_key_from_pkcs8(key.as_bytes()).map_err(invalid_key_err)?; + // EdDSA uses None for digest — the sign is done over the raw message + let mut signer = Signer::new_without_digest(&pkey).map_err(sign_err)?; + signer.sign_oneshot_to_vec(message).map_err(sign_err) + } + + fn eddsa_verify( + &self, + key: &EngineKeyRef, + message: &[u8], + signature: &[u8], + ) -> Result { + let pkey = PKey::public_key_from_der(key.as_bytes()).map_err(invalid_key_err)?; + let mut verifier = Verifier::new_without_digest(&pkey).map_err(verify_err)?; + verifier.verify_oneshot(signature, message).map_err(verify_err) + } + + fn ed_attribute( + &self, + key: &EngineKeyRef, + is_private: bool, + attr: AttributeType, + ) -> Result { + if is_private && matches!(attr, AttributeType::Value) { + return Err(CryptoError::AttributeSensitive); + } + + let key_der = key.as_bytes(); + let (key_id, raw_pub) = if is_private { + let pkey = PKey::::private_key_from_pkcs8(key_der).map_err(invalid_key_err)?; + let id = pkey.id(); + let raw = pkey.raw_public_key().map_err(invalid_key_err)?; + (id, raw) + } else { + let pkey = PKey::::public_key_from_der(key_der).map_err(invalid_key_err)?; + let id = pkey.id(); + let raw = pkey.raw_public_key().map_err(invalid_key_err)?; + (id, raw) + }; + + let ec_params_der = match key_id { + openssl::pkey::Id::ED25519 => vec![0x06, 0x03, 0x2b, 0x65, 0x70], + openssl::pkey::Id::ED448 => vec![0x06, 0x03, 0x2b, 0x65, 0x71], + _ => return Err(CryptoError::AttributeTypeInvalid), + }; + + match attr { + AttributeType::EcParams => Ok(AttributeValue::Bytes(ec_params_der)), + AttributeType::EcPoint => Ok(AttributeValue::Bytes(der_octet_string(&raw_pub))), + _ => Err(CryptoError::AttributeTypeInvalid), + } + } + + // ── ChaCha20-Poly1305 (v3.0) ──────────────────────────────────────────── + + fn generate_chacha20_key(&self) -> Result { + let mut buf = vec![0u8; 32]; // ChaCha20 always uses 256-bit keys + rand_bytes(&mut buf).map_err(key_err)?; + Ok(EngineKeyRef::from_bytes(buf)) + } + + fn chacha20_poly1305_encrypt( + &self, + key: &EngineKeyRef, + nonce: &[u8], + aad: &[u8], + plaintext: &[u8], + ) -> Result<(Vec, Vec), CryptoError> { + let key = key.as_bytes(); + if key.len() != 32 { + return Err(CryptoError::InvalidKeySize { + message: format!("ChaCha20-Poly1305 key must be 32 bytes; got {}", key.len()), + }); + } + if nonce.len() != 12 { + return Err(CryptoError::MechanismParamInvalid { + message: format!("ChaCha20-Poly1305 nonce must be 12 bytes; got {}", nonce.len()), + }); + } + let cipher = Cipher::chacha20_poly1305(); + let mut tag = vec![0u8; 16]; + let ciphertext = encrypt_aead(cipher, key, Some(nonce), aad, plaintext, &mut tag) + .map_err(encrypt_err)?; + Ok((ciphertext, tag)) + } + + fn chacha20_poly1305_decrypt( + &self, + key: &EngineKeyRef, + nonce: &[u8], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8], + ) -> Result>, CryptoError> { + let key = key.as_bytes(); + if key.len() != 32 { + return Err(CryptoError::InvalidKeySize { + message: format!("ChaCha20-Poly1305 key must be 32 bytes; got {}", key.len()), + }); + } + let cipher = Cipher::chacha20_poly1305(); + decrypt_aead(cipher, key, Some(nonce), aad, ciphertext, tag) + .map(Zeroizing::new) + .map_err(decrypt_err) + } + + // ── HKDF (v3.0) ───────────────────────────────────────────────────────── + + fn hkdf_derive( + &self, + hash_algo: HashAlgorithm, + ikm: &EngineKeyRef, + salt: &[u8], + info: &[u8], + okm_len: usize, + ) -> Result>, CryptoError> { + let ikm = ikm.as_bytes(); + use openssl::md::Md; + use openssl::pkey_ctx::PkeyCtx; + + let md = match hash_algo { + HashAlgorithm::Sha256 => Md::sha256(), + HashAlgorithm::Sha384 => Md::sha384(), + HashAlgorithm::Sha512 => Md::sha512(), + HashAlgorithm::Sha1 => Md::sha1(), + _ => return Err(CryptoError::MechanismInvalid { name: "HKDF: unsupported hash" }), + }; + let gen_err = |e: openssl::error::ErrorStack| CryptoError::GeneralError { message: e.to_string() }; + let mut ctx = PkeyCtx::new_id(openssl::pkey::Id::HKDF).map_err(gen_err)?; + ctx.derive_init().map_err(gen_err)?; + ctx.set_hkdf_md(md).map_err(gen_err)?; + ctx.set_hkdf_key(ikm).map_err(gen_err)?; + ctx.set_hkdf_salt(salt).map_err(gen_err)?; + ctx.add_hkdf_info(info).map_err(gen_err)?; + let mut okm = Zeroizing::new(vec![0u8; okm_len]); + ctx.derive(Some(&mut okm)).map_err(gen_err)?; + Ok(okm) + } + + // ── Key persistence ────────────────────────────────────────────────── + + fn serialize_key(&self, key: &EngineKeyRef) -> Result, CryptoError> { + Ok(key.as_bytes().to_vec()) + } + + fn deserialize_key(&self, data: &[u8]) -> Result { + Ok(EngineKeyRef::from_bytes(data.to_vec())) + } + + // ── Mechanism capability reporting ──────────────────────────────────────── + + /// Return actual OpenSSL capabilities for a given `CKM_*` mechanism. + /// + /// `min_key_size` / `max_key_size` follow the PKCS#11 convention: + /// - RSA / EC / EdDSA: key size **in bits** + /// - AES / ChaCha20: key size **in bytes** + /// - Hash / HKDF: 0 (no key) + /// + /// The `flags` field uses `CKF_*` bit values from PKCS#11. + /// The PKCS#11 layer may clamp `min_key_size` upward (e.g. RSA ≥ 2048) + /// per the mechanism-tier policy in `mechanisms.rs`. + fn mechanism_info(&self, _slot: usize, mechanism: u64) -> Option { + const ENCRYPT: u32 = 0x0000_0100; + const DECRYPT: u32 = 0x0000_0200; + const DIGEST: u32 = 0x0000_0400; + const SIGN: u32 = 0x0000_0800; + const VERIFY: u32 = 0x0000_2000; + const GENERATE: u32 = 0x0000_8000; + const GENERATE_KEY_PAIR: u32 = 0x0001_0000; + const WRAP: u32 = 0x0002_0000; + const UNWRAP: u32 = 0x0004_0000; + const DERIVE: u32 = 0x0008_0000; + + Some(match mechanism { + // ── RSA ────────────────────────────────────────────────────────── + // CKM_RSA_PKCS_KEY_PAIR_GEN (0x0000) + 0x0000_0000 => EngineMechanismInfo { + min_key_size: 1024, + max_key_size: 16384, + flags: GENERATE_KEY_PAIR, + }, + // CKM_RSA_PKCS (0x0001) + 0x0000_0001 => EngineMechanismInfo { + min_key_size: 1024, + max_key_size: 16384, + flags: ENCRYPT | DECRYPT | SIGN | VERIFY, + }, + // CKM_RSA_PKCS_OAEP (0x0009) + 0x0000_0009 => EngineMechanismInfo { + min_key_size: 1024, + max_key_size: 16384, + flags: ENCRYPT | DECRYPT | WRAP | UNWRAP, + }, + // All RSA-PSS and SHA-RSA Mechanisms + 0x0000_000D | 0x0000_0006 | 0x0000_000E + | 0x0000_0040 | 0x0000_0041 | 0x0000_0042 + | 0x0000_0043 | 0x0000_0044 | 0x0000_0045 => EngineMechanismInfo { + min_key_size: 1024, + max_key_size: 16384, + flags: SIGN | VERIFY, + }, + + // ── EC (Weierstrass) ───────────────────────────────────────────── + 0x0000_1040 => EngineMechanismInfo { + min_key_size: 256, max_key_size: 521, + flags: GENERATE_KEY_PAIR, + }, + 0x0000_1041..=0x0000_1045 => EngineMechanismInfo { + min_key_size: 256, max_key_size: 521, + flags: SIGN | VERIFY, + }, + 0x0000_1050 => EngineMechanismInfo { + min_key_size: 256, max_key_size: 521, + flags: DERIVE, + }, + + // ── EdDSA ──────────────────────────────────────────────────────── + 0x0000_1055 => EngineMechanismInfo { + min_key_size: 255, max_key_size: 448, + flags: GENERATE_KEY_PAIR, + }, + 0x0000_1057 => EngineMechanismInfo { + min_key_size: 255, max_key_size: 448, + flags: SIGN | VERIFY, + }, + + // ── AES ──────────────────────────────────────────────────────── + 0x0000_1080 => EngineMechanismInfo { + min_key_size: 16, max_key_size: 32, + flags: GENERATE, + }, + 0x0000_0120 | 0x0000_0131 => EngineMechanismInfo { + min_key_size: 8, max_key_size: 24, + flags: GENERATE, + }, + 0x0000_1082 | 0x0000_1085 | 0x0000_1086 | 0x0000_1087 => EngineMechanismInfo { + min_key_size: 16, max_key_size: 32, + flags: ENCRYPT | DECRYPT, + }, + 0x0000_0121 | 0x0000_0122 | 0x0000_0132 | 0x0000_0133 => EngineMechanismInfo { + min_key_size: 8, max_key_size: 24, + flags: ENCRYPT | DECRYPT, + }, + 0x0000_1090 => EngineMechanismInfo { + min_key_size: 16, max_key_size: 32, + flags: WRAP | UNWRAP, + }, + + // ── ChaCha20 ──────────────────────────────────────────────────────── + 0x0000_4021 => EngineMechanismInfo { + min_key_size: 32, max_key_size: 32, + flags: ENCRYPT | DECRYPT, + }, + 0x0000_4022 => EngineMechanismInfo { + min_key_size: 32, max_key_size: 32, + flags: GENERATE, + }, + + // ── Hashing ──────────────────────────────────────────────────────── + 0x0000_0210 | 0x0000_0220 + | 0x0000_0250 | 0x0000_0260 | 0x0000_0270 + | 0x0000_02B0 | 0x0000_02C0 | 0x0000_02D0 => EngineMechanismInfo { + min_key_size: 0, max_key_size: 0, + flags: DIGEST, + }, + + // ── HKDF ──────────────────────────────────────────────────────── + 0x0000_402A => EngineMechanismInfo { + min_key_size: 0, max_key_size: 0, + flags: DERIVE, + }, + 0x0000_402C => EngineMechanismInfo { + min_key_size: 0, max_key_size: 0, + flags: GENERATE, + }, + // CKM_GENERIC_SECRET_KEY_GEN (0x0350) + 0x0000_0350 => EngineMechanismInfo { + min_key_size: 1, + max_key_size: 4096, + flags: GENERATE, + }, + + _ => return None, + }) +} +} + +// ── Private EC helpers ──────────────────────────────────────────────────────── + +/// Return the DER-encoded OID for a named curve (CKA_EC_PARAMS). +fn ec_params_for_group(group: &openssl::ec::EcGroupRef) -> Result, CryptoError> { + let nid = group.curve_name().ok_or(CryptoError::GeneralError { + message: "EC group has no named curve NID".into(), + })?; + match nid { + Nid::X9_62_PRIME256V1 => + Ok(vec![0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]), + Nid::SECP384R1 => + Ok(vec![0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22]), + Nid::SECP521R1 => + Ok(vec![0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23]), + _other => Err(CryptoError::MechanismInvalid { + name: "unsupported EC curve", + }), + } +} + +/// Return a DER OCTET STRING wrapping the uncompressed EC public key point (CKA_EC_POINT). +fn ec_point_for_key( + ec_key: &EcKey, +) -> Result, CryptoError> { + let mut ctx = BigNumContext::new() + .map_err(|e| CryptoError::GeneralError { message: e.to_string() })?; + let bytes = ec_key + .public_key() + .to_bytes(ec_key.group(), PointConversionForm::UNCOMPRESSED, &mut ctx) + .map_err(|e| CryptoError::GeneralError { message: e.to_string() })?; + Ok(der_octet_string(&bytes)) +} diff --git a/src/pkcs11/attribute_policy.rs b/src/pkcs11/attribute_policy.rs new file mode 100644 index 0000000..bd5a017 --- /dev/null +++ b/src/pkcs11/attribute_policy.rs @@ -0,0 +1,176 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Attribute policy enforcement — one-way ratchets, immutability, access control. +//! +//! Three entry points called from `mod.rs`: +//! +//! * [`validate_attribute_change`] — called by `C_SetAttributeValue` before +//! each attribute is written. Enforces: +//! - One-way ratchets (`CKA_SENSITIVE`, `CKA_EXTRACTABLE`, `CKA_WRAP_WITH_TRUSTED`) +//! - Immutable-after-creation attributes (`CKA_CLASS`, `CKA_KEY_TYPE`, …) +//! +//! * [`check_attribute_access`] — called by `C_GetAttributeValue` before each +//! attribute is returned. Blocks `CKA_VALUE` for sensitive or non-extractable +//! keys. +//! +//! * [`update_derived_attributes`] — called by `C_SetAttributeValue` after each +//! attribute is written. Keeps `always_sensitive` / `never_extractable` in +//! sync with the key's attribute history. + +use super::constants::*; +use super::error::{Pkcs11Error, Result}; +use super::object_store::KeyObject; +use super::types::*; + +// ── validate_attribute_change ──────────────────────────────────────────────── + +/// Validate a proposed attribute write against PKCS#11 ratchet rules. +/// +/// Call this **before** writing `new_val` into `obj.attributes`. +/// +/// * `old_val` — current bytes for this attribute, or `None` if absent. +/// * `new_val` — proposed replacement bytes. +/// +/// Returns `Err(AttributeReadOnly)` if the change violates a ratchet or an +/// immutability rule, `Ok(())` if the change is permitted. +pub fn validate_attribute_change( + attr: CK_ATTRIBUTE_TYPE, + old_val: Option<&[u8]>, + new_val: &[u8], +) -> Result<()> { + match attr { + // ── One-way: FALSE → TRUE only ─────────────────────────────────────── + CKA_SENSITIVE | CKA_WRAP_WITH_TRUSTED => { + let was_true = old_val.is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + let going_false = !new_val.is_empty() && new_val[0] == CK_FALSE; + if was_true && going_false { + return Err(Pkcs11Error::AttributeReadOnly); + } + } + // ── One-way: TRUE → FALSE only ─────────────────────────────────────── + CKA_EXTRACTABLE => { + let was_false = old_val.is_some_and(|v| v.is_empty() || v[0] == CK_FALSE); + let going_true = !new_val.is_empty() && new_val[0] == CK_TRUE; + if was_false && going_true { + return Err(Pkcs11Error::AttributeReadOnly); + } + } + // ── Immutable after creation ───────────────────────────────────────── + CKA_CLASS + | CKA_KEY_TYPE + | CKA_MODULUS + | CKA_EC_PARAMS + | CKA_MODULUS_BITS + | CKA_VALUE_LEN => { + return Err(Pkcs11Error::AttributeReadOnly); + } + _ => {} + } + Ok(()) +} + +// ── check_attribute_access ─────────────────────────────────────────────────── + +/// Gate `CKA_VALUE` reads for sensitive or non-extractable keys. +/// +/// Per PKCS#11: +/// - Sensitive keys (`CKA_SENSITIVE = TRUE`) must not expose `CKA_VALUE`. +/// - Non-extractable keys (`CKA_EXTRACTABLE = FALSE`) must not expose `CKA_VALUE`. +/// +/// All other attribute types are returned unconditionally. +pub fn check_attribute_access(attr: CK_ATTRIBUTE_TYPE, obj: &KeyObject) -> Result<()> { + // Check if the requested attribute is a secret payload + let is_secret_attribute = matches!(attr, + CKA_VALUE | + CKA_PRIVATE_EXPONENT | + CKA_PRIME_1 | + CKA_PRIME_2 | + CKA_EXPONENT_1 | + CKA_EXPONENT_2 | + CKA_COEFFICIENT + ); + if !is_secret_attribute { + return Ok(()); + } + + let class = obj.attributes + .get(&CKA_CLASS) + .map(|v| { + let mut arr = [0u8; 8]; + let n = v.len().min(8); + arr[..n].copy_from_slice(&v[..n]); + CK_ULONG::from_le_bytes(arr) + }); + if matches!(class, Some(CKO_DATA | CKO_PUBLIC_KEY)) { + return Ok(()); + } + + // Evaluate SENSITIVE (Defaults to FALSE if missing) + let is_sensitive = obj.attributes + .get(&CKA_SENSITIVE) + .is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + if is_sensitive { + return Err(Pkcs11Error::AttributeSensitive); + } + + // Evaluate EXTRACTABLE (FAIL-CLOSED: Defaults to FALSE if missing) + let is_extractable = obj.attributes + .get(&CKA_EXTRACTABLE) + .is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + + if !is_extractable { + return Err(Pkcs11Error::AttributeSensitive); + } + Ok(()) +} + +// ── update_derived_attributes ──────────────────────────────────────────────── + +/// Sync `always_sensitive` and `never_extractable` after an attribute mutation. +/// +/// Call this **after** writing the new value into `obj.attributes` so that +/// the struct fields reflect the key's full attribute history. +/// +/// | Change | Derived effect | +/// |------------------------|-------------------------------------------| +/// | `CKA_SENSITIVE=FALSE` | `always_sensitive = false` | +/// | `CKA_EXTRACTABLE=TRUE` | `never_extractable = false` | +/// | anything else | no effect | +/// +/// Because the ratchets in [`validate_attribute_change`] prevent the reverse +/// transitions (`SENSITIVE TRUE→FALSE`, `EXTRACTABLE FALSE→TRUE`), these +/// updates only fire for the allowed ratchet directions. +pub fn update_derived_attributes(obj: &mut KeyObject, changed_attr: CK_ATTRIBUTE_TYPE) { + match changed_attr { + CKA_SENSITIVE => { + // If CKA_SENSITIVE is now FALSE, the key was not always sensitive. + let now_false = obj.attributes + .get(&CKA_SENSITIVE) + .is_some_and(|v| !v.is_empty() && v[0] == CK_FALSE); + if now_false { + obj.always_sensitive = false; + } + } + CKA_EXTRACTABLE => { + // If CKA_EXTRACTABLE is now TRUE, the key was not never-extractable. + let now_true = obj.attributes + .get(&CKA_EXTRACTABLE) + .is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + if now_true { + obj.never_extractable = false; + } + } + _ => {} + } +} diff --git a/src/pkcs11/backend.rs b/src/pkcs11/backend.rs new file mode 100644 index 0000000..69607ab --- /dev/null +++ b/src/pkcs11/backend.rs @@ -0,0 +1,71 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Crypto backend — all operations delegate to the registered `CryptoProvider`. +//! +//! This module has no direct OpenSSL dependency. Key material is identified by +//! `KeyType` and passed to the engine as opaque `EngineKeyRef` values. +//! +//! Every public function takes a `slot_id` parameter to look up the correct +//! engine from the multi-engine registry. + +use std::collections::HashMap; + +use crate::traits::EngineKeyRef; + +use super::constants::*; +use super::error::{Pkcs11Error, Result}; +use super::object_store::{KeyObject, KeyType}; +use super::types::*; + +mod attributes; +mod digest_random; +mod keygen; +mod message_aead; +mod rsa_wrap_derive; +mod sign_verify; +mod symmetric; + +pub use attributes::*; +pub use digest_random::*; +pub use keygen::*; +pub use message_aead::*; +pub use rsa_wrap_derive::*; +pub use sign_verify::*; +pub use symmetric::*; + +/// Key material and attributes produced by a keygen operation. +/// +/// `backend` does not allocate handles or touch the object store. +/// The caller assigns a handle, constructs the `KeyObject`, stamps policy +/// fields, and calls `object_store::store_object()`. +pub struct GeneratedKey { + pub key_type: KeyType, + pub key_ref: EngineKeyRef, + pub attrs: HashMap>, + pub key_gen_mechanism: CK_MECHANISM_TYPE, +} + +fn eng(slot_id: CK_SLOT_ID) -> Result> { + let (engine, _internal_slot_id) = crate::registry::engine_for_slot(slot_id).map_err(Pkcs11Error::from)?; + Ok(engine) +} + +pub fn ulong_bytes(v: CK_ULONG) -> Vec { v.to_le_bytes().to_vec() } + +pub fn bytes_to_ulong(b: &[u8]) -> CK_ULONG { + let mut arr = [0u8; 8]; + let n = b.len().min(8); + arr[..n].copy_from_slice(&b[..n]); + CK_ULONG::from_le_bytes(arr) +} diff --git a/src/pkcs11/backend/attributes.rs b/src/pkcs11/backend/attributes.rs new file mode 100644 index 0000000..c49d9ef --- /dev/null +++ b/src/pkcs11/backend/attributes.rs @@ -0,0 +1,34 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +pub fn get_attribute(slot_id: CK_SLOT_ID, key: &KeyObject, attr_type: CK_ATTRIBUTE_TYPE) -> Result> { + use crate::attributes::AttributeType; + let e = eng(slot_id)?; + let at = AttributeType::from_u32(attr_type as u32).ok_or(Pkcs11Error::InvalidAttributeType)?; + let val = match key.key_type { + KeyType::RsaPrivate => e.rsa_attribute(&key.key_ref, true, at), + KeyType::RsaPublic => e.rsa_attribute(&key.key_ref, false, at), + KeyType::EcPrivate => e.ec_attribute(&key.key_ref, true, at), + KeyType::EcPublic => e.ec_attribute(&key.key_ref, false, at), + KeyType::AesSecret => e.aes_attribute(&key.key_ref, at), + KeyType::EdPrivate => e.ed_attribute(&key.key_ref, true, at), + KeyType::EdPublic => e.ed_attribute(&key.key_ref, false, at), + KeyType::ChaCha20Secret => e.aes_attribute(&key.key_ref, at), + KeyType::GenericSecret => e.aes_attribute(&key.key_ref, at), + KeyType::Profile => return Err(Pkcs11Error::InvalidAttributeType), + } + .map_err(Pkcs11Error::from)?; + + Ok(val.to_bytes()) +} diff --git a/src/pkcs11/backend/digest_random.rs b/src/pkcs11/backend/digest_random.rs new file mode 100644 index 0000000..a17b653 --- /dev/null +++ b/src/pkcs11/backend/digest_random.rs @@ -0,0 +1,43 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +pub fn key_value_for_digest(slot_id: CK_SLOT_ID, key: &KeyObject) -> Result> { + eng(slot_id)?.key_value_for_digest(&key.key_ref).map_err(Pkcs11Error::from) +} + +pub fn digest(slot_id: CK_SLOT_ID, mechanism: CK_MECHANISM_TYPE, data: &[u8]) -> Result> { + let e = eng(slot_id)?; + let alg = mechanism_to_hash_algorithm(mechanism)?; + e.hash(alg, data).map_err(Pkcs11Error::from) +} + +pub fn generate_random(slot_id: CK_SLOT_ID, buf: &mut [u8]) -> Result<()> { + let e = eng(slot_id)?; + e.generate_random(buf).map_err(Pkcs11Error::from) +} + +fn mechanism_to_hash_algorithm(mechanism: CK_MECHANISM_TYPE) -> Result { + use crate::types::HashAlgorithm; + match mechanism { + CKM_MD5 => Ok(HashAlgorithm::Md5), + CKM_SHA_1 => Ok(HashAlgorithm::Sha1), + CKM_SHA256 => Ok(HashAlgorithm::Sha256), + CKM_SHA384 => Ok(HashAlgorithm::Sha384), + CKM_SHA512 => Ok(HashAlgorithm::Sha512), + CKM_SHA3_256 => Ok(HashAlgorithm::Sha3_256), + CKM_SHA3_384 => Ok(HashAlgorithm::Sha3_384), + CKM_SHA3_512 => Ok(HashAlgorithm::Sha3_512), + _ => Err(Pkcs11Error::InvalidMechanism), + } +} diff --git a/src/pkcs11/backend/keygen.rs b/src/pkcs11/backend/keygen.rs new file mode 100644 index 0000000..7a6db17 --- /dev/null +++ b/src/pkcs11/backend/keygen.rs @@ -0,0 +1,198 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; +use crate::types::{EcCurve, EdwardsCurve}; + +pub fn generate_rsa_key_pair( + slot_id: CK_SLOT_ID, + modulus_bits: u32, + _pub_exponent: u32, + pub_template: HashMap>, + priv_template: HashMap>, +) -> Result<(GeneratedKey, GeneratedKey)> { + if !(1024..=16384).contains(&modulus_bits) { + return Err(Pkcs11Error::KeySizeRange); + } + let e = eng(slot_id)?; + let pair = e.generate_rsa_key_pair(modulus_bits).map_err(Pkcs11Error::from)?; + + let mut pub_attrs = pub_template; + pub_attrs.insert(CKA_CLASS, ulong_bytes(CKO_PUBLIC_KEY)); + pub_attrs.insert(CKA_KEY_TYPE, ulong_bytes(CKK_RSA)); + pub_attrs.insert(CKA_MODULUS, pair.modulus.clone()); + pub_attrs.insert(CKA_PUBLIC_EXPONENT, pair.public_exponent.clone()); + pub_attrs.insert(CKA_MODULUS_BITS, ulong_bytes(pair.bits as CK_ULONG)); + + let mut priv_attrs = priv_template; + priv_attrs.insert(CKA_CLASS, ulong_bytes(CKO_PRIVATE_KEY)); + priv_attrs.insert(CKA_KEY_TYPE, ulong_bytes(CKK_RSA)); + priv_attrs.insert(CKA_MODULUS, pair.modulus); + priv_attrs.insert(CKA_PUBLIC_EXPONENT, pair.public_exponent); + priv_attrs.insert(CKA_MODULUS_BITS, ulong_bytes(pair.bits as CK_ULONG)); + + Ok(( + GeneratedKey { key_type: KeyType::RsaPrivate, key_ref: EngineKeyRef::from_bytes(pair.private_der.to_vec()), attrs: priv_attrs, key_gen_mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN }, + GeneratedKey { key_type: KeyType::RsaPublic, key_ref: EngineKeyRef::from_bytes(pair.public_der), attrs: pub_attrs, key_gen_mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN }, + )) +} + +pub fn generate_ec_key_pair( + slot_id: CK_SLOT_ID, + curve: EcCurve, + pub_template: HashMap>, + priv_template: HashMap>, +) -> Result<(GeneratedKey, GeneratedKey)> { + let e = eng(slot_id)?; + let pair = e.generate_ec_key_pair(curve).map_err(Pkcs11Error::from)?; + + let mut pub_attrs = pub_template; + pub_attrs.insert(CKA_CLASS, ulong_bytes(CKO_PUBLIC_KEY)); + pub_attrs.insert(CKA_KEY_TYPE, ulong_bytes(CKK_EC)); + pub_attrs.insert(CKA_EC_PARAMS, pair.ec_params_der.clone()); + pub_attrs.insert(CKA_EC_POINT, pair.ec_point_uncompressed); + + let mut priv_attrs = priv_template; + priv_attrs.insert(CKA_CLASS, ulong_bytes(CKO_PRIVATE_KEY)); + priv_attrs.insert(CKA_KEY_TYPE, ulong_bytes(CKK_EC)); + priv_attrs.insert(CKA_EC_PARAMS, pair.ec_params_der); + + Ok(( + GeneratedKey { key_type: KeyType::EcPrivate, key_ref: EngineKeyRef::from_bytes(pair.private_der.to_vec()), attrs: priv_attrs, key_gen_mechanism: CKM_EC_KEY_PAIR_GEN }, + GeneratedKey { key_type: KeyType::EcPublic, key_ref: EngineKeyRef::from_bytes(pair.public_der), attrs: pub_attrs, key_gen_mechanism: CKM_EC_KEY_PAIR_GEN }, + )) +} + +pub fn generate_ed_key_pair( + slot_id: CK_SLOT_ID, + curve: EdwardsCurve, + pub_template: HashMap>, + priv_template: HashMap>, +) -> Result<(GeneratedKey, GeneratedKey)> { + let e = eng(slot_id)?; + let pair = e.generate_ed_key_pair(curve).map_err(Pkcs11Error::from)?; + + let mut pub_attrs = pub_template; + pub_attrs.insert(CKA_CLASS, ulong_bytes(CKO_PUBLIC_KEY)); + pub_attrs.insert(CKA_KEY_TYPE, ulong_bytes(CKK_EC_EDWARDS)); + pub_attrs.insert(CKA_EC_PARAMS, pair.ec_params_der.clone()); + pub_attrs.insert(CKA_EC_POINT, pair.ec_point.clone()); + + let mut priv_attrs = priv_template; + priv_attrs.insert(CKA_CLASS, ulong_bytes(CKO_PRIVATE_KEY)); + priv_attrs.insert(CKA_KEY_TYPE, ulong_bytes(CKK_EC_EDWARDS)); + priv_attrs.insert(CKA_EC_PARAMS, pair.ec_params_der); + + Ok(( + GeneratedKey { key_type: KeyType::EdPrivate, key_ref: EngineKeyRef::from_bytes(pair.private_der.to_vec()), attrs: priv_attrs, key_gen_mechanism: CKM_EC_EDWARDS_KEY_PAIR_GEN }, + GeneratedKey { key_type: KeyType::EdPublic, key_ref: EngineKeyRef::from_bytes(pair.public_der), attrs: pub_attrs, key_gen_mechanism: CKM_EC_EDWARDS_KEY_PAIR_GEN }, + )) +} + +pub fn generate_aes_key( + slot_id: CK_SLOT_ID, + key_len_bytes: usize, + template: HashMap>, +) -> Result { + if !matches!(key_len_bytes, 16 | 24 | 32) { + return Err(Pkcs11Error::KeySizeRange); + } + + let e = eng(slot_id)?; + let key_ref = e.generate_aes_key(key_len_bytes).map_err(Pkcs11Error::from)?; + + let mut unique_id = vec![0u8; 16]; + e.generate_random(&mut unique_id).map_err(Pkcs11Error::from)?; + + let mut attrs = template; + attrs.insert(CKA_CLASS, ulong_bytes(CKO_SECRET_KEY)); + attrs.insert(CKA_KEY_TYPE, ulong_bytes(CKK_AES)); + attrs.insert(CKA_VALUE_LEN, ulong_bytes(key_len_bytes as CK_ULONG)); + attrs.insert(CKA_UNIQUE_ID, unique_id); + + Ok(GeneratedKey { + key_type: KeyType::AesSecret, + key_ref, + attrs, + key_gen_mechanism: CKM_AES_KEY_GEN, + }) +} + +pub fn generate_chacha20_key( + slot_id: CK_SLOT_ID, + template: HashMap>, +) -> Result { + let e = eng(slot_id)?; + let key_ref = e.generate_chacha20_key().map_err(Pkcs11Error::from)?; + + let mut attrs = template; + attrs.insert(CKA_CLASS, ulong_bytes(CKO_SECRET_KEY)); + attrs.insert(CKA_KEY_TYPE, ulong_bytes(CKK_CHACHA20)); + attrs.insert(CKA_VALUE_LEN, ulong_bytes(32)); + + Ok(GeneratedKey { key_type: KeyType::ChaCha20Secret, key_ref, attrs, key_gen_mechanism: CKM_CHACHA20_KEY_GEN }) +} + +pub fn generate_generic_secret_key( + slot_id: CK_SLOT_ID, + key_len_bytes: usize, + template: HashMap>, +) -> Result { + if key_len_bytes == 0 || key_len_bytes > 4096 { + return Err(Pkcs11Error::KeySizeRange); + } + + let e = eng(slot_id)?; + let mut key_bytes = vec![0u8; key_len_bytes]; + e.generate_random(&mut key_bytes).map_err(Pkcs11Error::from)?; + let key_ref = EngineKeyRef::from_bytes(key_bytes); + + let mut unique_id = vec![0u8; 16]; + e.generate_random(&mut unique_id).map_err(Pkcs11Error::from)?; + + let mut attrs = template; + attrs.insert(CKA_CLASS, ulong_bytes(CKO_SECRET_KEY)); + attrs.insert(CKA_KEY_TYPE, ulong_bytes(CKK_GENERIC_SECRET)); + attrs.insert(CKA_VALUE_LEN, ulong_bytes(key_len_bytes as CK_ULONG)); + attrs.insert(CKA_UNIQUE_ID, unique_id); + + Ok(GeneratedKey { + key_type: KeyType::GenericSecret, + key_ref, + attrs, + key_gen_mechanism: CKM_GENERIC_SECRET_KEY_GEN, + }) +} + +pub fn generate_legacy_secret_key( + slot_id: CK_SLOT_ID, + mechanism: CK_MECHANISM_TYPE, + key_len_bytes: usize, + key_type: CK_KEY_TYPE, + template: HashMap>, +) -> Result { + let e = eng(slot_id)?; + let mut key_bytes = vec![0u8; key_len_bytes]; + e.generate_random(&mut key_bytes).map_err(Pkcs11Error::from)?; + + let mut attrs = template; + attrs.insert(CKA_CLASS, ulong_bytes(CKO_SECRET_KEY)); + attrs.insert(CKA_KEY_TYPE, ulong_bytes(key_type)); + attrs.insert(CKA_VALUE_LEN, ulong_bytes(key_len_bytes as CK_ULONG)); + + Ok(GeneratedKey { + key_type: KeyType::GenericSecret, + key_ref: EngineKeyRef::from_bytes(key_bytes), + attrs, + key_gen_mechanism: mechanism, + }) +} diff --git a/src/pkcs11/backend/message_aead.rs b/src/pkcs11/backend/message_aead.rs new file mode 100644 index 0000000..1e0f4d1 --- /dev/null +++ b/src/pkcs11/backend/message_aead.rs @@ -0,0 +1,48 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use zeroize::Zeroizing; + +use super::*; + +pub fn encrypt_message( + slot_id: CK_SLOT_ID, + mechanism: CK_MECHANISM_TYPE, + key: &KeyObject, + iv: &[u8], + aad: &[u8], + plaintext: &[u8], +) -> Result<(Vec, Vec)> { + let e = eng(slot_id)?; + match mechanism { + CKM_AES_GCM => e.aes_gcm_encrypt(&key.key_ref, iv, aad, plaintext).map_err(Pkcs11Error::from), + CKM_CHACHA20_POLY1305 => e.chacha20_poly1305_encrypt(&key.key_ref, iv, aad, plaintext).map_err(Pkcs11Error::from), + _ => Err(Pkcs11Error::MechanismUnsupported), + } +} + +pub fn decrypt_message( + slot_id: CK_SLOT_ID, + mechanism: CK_MECHANISM_TYPE, + key: &KeyObject, + iv: &[u8], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8], +) -> Result>> { + let e = eng(slot_id)?; + match mechanism { + CKM_AES_GCM => e.aes_gcm_decrypt(&key.key_ref, iv, aad, ciphertext, tag).map_err(Pkcs11Error::from), + CKM_CHACHA20_POLY1305 => e.chacha20_poly1305_decrypt(&key.key_ref, iv, aad, ciphertext, tag).map_err(Pkcs11Error::from), + _ => Err(Pkcs11Error::MechanismUnsupported), + } +} diff --git a/src/pkcs11/backend/rsa_wrap_derive.rs b/src/pkcs11/backend/rsa_wrap_derive.rs new file mode 100644 index 0000000..79d17b3 --- /dev/null +++ b/src/pkcs11/backend/rsa_wrap_derive.rs @@ -0,0 +1,68 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use zeroize::Zeroizing; + +use super::*; + +pub fn rsa_encrypt(slot_id: CK_SLOT_ID, mechanism: CK_MECHANISM_TYPE, key: &KeyObject, plaintext: &[u8]) -> Result> { + let e = eng(slot_id)?; + match mechanism { + CKM_RSA_PKCS => e.rsa_pkcs1_encrypt(&key.key_ref, plaintext).map_err(Pkcs11Error::from), + CKM_RSA_PKCS_OAEP => e.rsa_oaep_encrypt(&key.key_ref, plaintext).map_err(Pkcs11Error::from), + _ => Err(Pkcs11Error::InvalidMechanism), + } +} + +pub fn rsa_decrypt(slot_id: CK_SLOT_ID, mechanism: CK_MECHANISM_TYPE, key: &KeyObject, ciphertext: &[u8]) -> Result>> { + let e = eng(slot_id)?; + match mechanism { + CKM_RSA_PKCS => e.rsa_pkcs1_decrypt(&key.key_ref, ciphertext).map_err(Pkcs11Error::from), + CKM_RSA_PKCS_OAEP => e.rsa_oaep_decrypt(&key.key_ref, ciphertext).map_err(Pkcs11Error::from), + _ => Err(Pkcs11Error::InvalidMechanism), + } +} + +pub fn aes_wrap_key(slot_id: CK_SLOT_ID, wrapping_key: &KeyObject, target_key: &KeyObject) -> Result> { + let e = eng(slot_id)?; + e.aes_key_wrap(&wrapping_key.key_ref, &target_key.key_ref).map_err(Pkcs11Error::from) +} + +pub fn aes_wrap_key_refs( + slot_id: CK_SLOT_ID, + wrapping_ref: &crate::traits::EngineKeyRef, + target_ref: &crate::traits::EngineKeyRef, +) -> Result> { + let e = eng(slot_id)?; + e.aes_key_wrap(wrapping_ref, target_ref).map_err(Pkcs11Error::from) +} + +pub fn aes_unwrap_key( + slot_id: CK_SLOT_ID, + unwrapping_key: &KeyObject, + wrapped_key: &[u8], +) -> Result>> { + let e = eng(slot_id)?; + e.aes_key_unwrap(&unwrapping_key.key_ref, wrapped_key).map_err(Pkcs11Error::from) +} + +pub fn hkdf_derive( + slot_id: CK_SLOT_ID, + base_key: &KeyObject, + hash: crate::types::HashAlgorithm, + salt: &[u8], + info: &[u8], + okm_len: usize, +) -> Result>> { + let e = eng(slot_id)?; + e.hkdf_derive(hash, &base_key.key_ref, salt, info, okm_len).map_err(Pkcs11Error::from) +} diff --git a/src/pkcs11/backend/sign_verify.rs b/src/pkcs11/backend/sign_verify.rs new file mode 100644 index 0000000..d5e7686 --- /dev/null +++ b/src/pkcs11/backend/sign_verify.rs @@ -0,0 +1,99 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +pub fn sign(slot_id: CK_SLOT_ID, mechanism: CK_MECHANISM_TYPE, key: &KeyObject, data: &[u8]) -> Result> { + let e = eng(slot_id)?; + match key.key_type { + KeyType::RsaPrivate => match mechanism { + CKM_RSA_PKCS => e.rsa_pkcs1_sign(&key.key_ref, data).map_err(Pkcs11Error::from), + CKM_MD5_RSA_PKCS => e.rsa_pkcs1_sign_hash(&key.key_ref, data, crate::types::HashAlgorithm::Md5).map_err(Pkcs11Error::from), + CKM_SHA1_RSA_PKCS => e.rsa_pkcs1_sign_hash(&key.key_ref, data, crate::types::HashAlgorithm::Sha1).map_err(Pkcs11Error::from), + CKM_SHA256_RSA_PKCS => e.rsa_pkcs1_sign(&key.key_ref, data).map_err(Pkcs11Error::from), + CKM_SHA384_RSA_PKCS => e.rsa_pkcs1_sign_hash(&key.key_ref, data, crate::types::HashAlgorithm::Sha384).map_err(Pkcs11Error::from), + CKM_SHA512_RSA_PKCS => e.rsa_pkcs1_sign_hash(&key.key_ref, data, crate::types::HashAlgorithm::Sha512).map_err(Pkcs11Error::from), + CKM_SHA256_RSA_PKCS_PSS => e.rsa_pss_sign(&key.key_ref, data).map_err(Pkcs11Error::from), + CKM_SHA384_RSA_PKCS_PSS => e.rsa_pss_sign_hash(&key.key_ref, data, crate::types::HashAlgorithm::Sha384).map_err(Pkcs11Error::from), + CKM_SHA512_RSA_PKCS_PSS => e.rsa_pss_sign_hash(&key.key_ref, data, crate::types::HashAlgorithm::Sha512).map_err(Pkcs11Error::from), + _ => Err(Pkcs11Error::InvalidMechanism), + }, + KeyType::EcPrivate => match mechanism { + CKM_ECDSA => e.ecdsa_sign_prehashed(&key.key_ref, data).map_err(Pkcs11Error::from), + CKM_ECDSA_SHA256 => { + let digest = e.hash(crate::types::HashAlgorithm::Sha256, data).map_err(Pkcs11Error::from)?; + e.ecdsa_sign_prehashed(&key.key_ref, &digest).map_err(Pkcs11Error::from) + } + CKM_ECDSA_SHA384 => { + let digest = e.hash(crate::types::HashAlgorithm::Sha384, data).map_err(Pkcs11Error::from)?; + e.ecdsa_sign_prehashed(&key.key_ref, &digest).map_err(Pkcs11Error::from) + } + CKM_ECDSA_SHA512 => { + let digest = e.hash(crate::types::HashAlgorithm::Sha512, data).map_err(Pkcs11Error::from)?; + e.ecdsa_sign_prehashed(&key.key_ref, &digest).map_err(Pkcs11Error::from) + } + _ => Err(Pkcs11Error::InvalidMechanism), + }, + KeyType::EdPrivate => match mechanism { + CKM_EDDSA => e.eddsa_sign(&key.key_ref, data).map_err(Pkcs11Error::from), + _ => Err(Pkcs11Error::InvalidMechanism), + }, + _ => Err(Pkcs11Error::KeyTypeInconsistent), + } +} + +pub fn verify( + slot_id: CK_SLOT_ID, + mechanism: CK_MECHANISM_TYPE, + key: &KeyObject, + data: &[u8], + signature: &[u8], +) -> Result<()> { + let e = eng(slot_id)?; + let ok = match key.key_type { + KeyType::RsaPublic => match mechanism { + CKM_RSA_PKCS => e.rsa_pkcs1_verify(&key.key_ref, data, signature).map_err(Pkcs11Error::from)?, + CKM_MD5_RSA_PKCS => e.rsa_pkcs1_verify_hash(&key.key_ref, data, signature, crate::types::HashAlgorithm::Md5).map_err(Pkcs11Error::from)?, + CKM_SHA1_RSA_PKCS => e.rsa_pkcs1_verify_hash(&key.key_ref, data, signature, crate::types::HashAlgorithm::Sha1).map_err(Pkcs11Error::from)?, + CKM_SHA256_RSA_PKCS => e.rsa_pkcs1_verify(&key.key_ref, data, signature).map_err(Pkcs11Error::from)?, + CKM_SHA384_RSA_PKCS => e.rsa_pkcs1_verify_hash(&key.key_ref, data, signature, crate::types::HashAlgorithm::Sha384).map_err(Pkcs11Error::from)?, + CKM_SHA512_RSA_PKCS => e.rsa_pkcs1_verify_hash(&key.key_ref, data, signature, crate::types::HashAlgorithm::Sha512).map_err(Pkcs11Error::from)?, + CKM_SHA256_RSA_PKCS_PSS => e.rsa_pss_verify(&key.key_ref, data, signature).map_err(Pkcs11Error::from)?, + CKM_SHA384_RSA_PKCS_PSS => e.rsa_pss_verify_hash(&key.key_ref, data, signature, crate::types::HashAlgorithm::Sha384).map_err(Pkcs11Error::from)?, + CKM_SHA512_RSA_PKCS_PSS => e.rsa_pss_verify_hash(&key.key_ref, data, signature, crate::types::HashAlgorithm::Sha512).map_err(Pkcs11Error::from)?, + _ => return Err(Pkcs11Error::InvalidMechanism), + }, + KeyType::EcPublic => match mechanism { + CKM_ECDSA => e.ecdsa_verify_prehashed(&key.key_ref, data, signature).map_err(Pkcs11Error::from)?, + CKM_ECDSA_SHA256 => { + let digest = e.hash(crate::types::HashAlgorithm::Sha256, data).map_err(Pkcs11Error::from)?; + e.ecdsa_verify_prehashed(&key.key_ref, &digest, signature).map_err(Pkcs11Error::from)? + } + CKM_ECDSA_SHA384 => { + let digest = e.hash(crate::types::HashAlgorithm::Sha384, data).map_err(Pkcs11Error::from)?; + e.ecdsa_verify_prehashed(&key.key_ref, &digest, signature).map_err(Pkcs11Error::from)? + } + CKM_ECDSA_SHA512 => { + let digest = e.hash(crate::types::HashAlgorithm::Sha512, data).map_err(Pkcs11Error::from)?; + e.ecdsa_verify_prehashed(&key.key_ref, &digest, signature).map_err(Pkcs11Error::from)? + } + _ => return Err(Pkcs11Error::InvalidMechanism), + }, + KeyType::EdPublic => match mechanism { + CKM_EDDSA => e.eddsa_verify(&key.key_ref, data, signature).map_err(Pkcs11Error::from)?, + _ => return Err(Pkcs11Error::InvalidMechanism), + }, + _ => return Err(Pkcs11Error::KeyTypeInconsistent), + }; + + if ok { Ok(()) } else { Err(Pkcs11Error::SignatureInvalid) } +} diff --git a/src/pkcs11/backend/symmetric.rs b/src/pkcs11/backend/symmetric.rs new file mode 100644 index 0000000..a56c29b --- /dev/null +++ b/src/pkcs11/backend/symmetric.rs @@ -0,0 +1,149 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use std::sync::OnceLock; + +use openssl::provider::Provider; +use openssl::symm::{Cipher, Crypter, Mode}; +use zeroize::Zeroizing; + +use super::*; + +static OPENSSL_PROVIDERS: OnceLock> = OnceLock::new(); + +pub fn encrypt_symmetric( + slot_id: CK_SLOT_ID, + mechanism: CK_MECHANISM_TYPE, + key: &KeyObject, + iv: &[u8], + aad: Option<&[u8]>, + plaintext: &[u8], +) -> Result> { + let e = eng(slot_id)?; + match mechanism { + CKM_DES_ECB | CKM_DES_CBC | CKM_DES3_ECB | CKM_DES3_CBC | CKM_AES_ECB | CKM_AES_CBC => { + block_cipher_crypt(mechanism, key.key_ref.as_bytes(), iv, plaintext, Mode::Encrypt, false) + } + CKM_AES_CBC_PAD => e.aes_cbc_encrypt(&key.key_ref, iv, plaintext).map_err(Pkcs11Error::from), + CKM_AES_GCM => { + let (mut ct, tag) = e.aes_gcm_encrypt(&key.key_ref, iv, aad.unwrap_or(&[]), plaintext).map_err(Pkcs11Error::from)?; + ct.extend_from_slice(&tag); + Ok(ct) + } + CKM_CHACHA20_POLY1305 => { + let (mut ct, tag) = e.chacha20_poly1305_encrypt(&key.key_ref, iv, aad.unwrap_or(&[]), plaintext).map_err(Pkcs11Error::from)?; + ct.extend_from_slice(&tag); + Ok(ct) + } + _ => Err(Pkcs11Error::MechanismUnsupported), + } +} + +pub fn decrypt_symmetric( + slot_id: CK_SLOT_ID, + mechanism: CK_MECHANISM_TYPE, + key: &KeyObject, + iv: &[u8], + aad: Option<&[u8]>, + ciphertext: &[u8], + tag_len: usize, +) -> Result>> { + let e = eng(slot_id)?; + match mechanism { + CKM_DES_ECB | CKM_DES_CBC | CKM_DES3_ECB | CKM_DES3_CBC | CKM_AES_ECB | CKM_AES_CBC => { + block_cipher_crypt(mechanism, key.key_ref.as_bytes(), iv, ciphertext, Mode::Decrypt, false).map(Zeroizing::new) + } + CKM_AES_CBC_PAD => e.aes_cbc_decrypt(&key.key_ref, iv, ciphertext).map_err(Pkcs11Error::from), + CKM_AES_GCM => { + if ciphertext.len() < tag_len { + return Err(Pkcs11Error::EncryptedDataInvalid); + } + let (ct, tag) = ciphertext.split_at(ciphertext.len() - tag_len); + e.aes_gcm_decrypt(&key.key_ref, iv, aad.unwrap_or(&[]), ct, tag).map_err(Pkcs11Error::from) + } + CKM_CHACHA20_POLY1305 => { + if ciphertext.len() < tag_len { + return Err(Pkcs11Error::EncryptedDataInvalid); + } + let (ct, tag) = ciphertext.split_at(ciphertext.len() - tag_len); + e.chacha20_poly1305_decrypt(&key.key_ref, iv, aad.unwrap_or(&[]), ct, tag).map_err(Pkcs11Error::from) + } + _ => Err(Pkcs11Error::MechanismUnsupported), + } +} + +pub fn is_rsa_enc_mechanism(mechanism: CK_MECHANISM_TYPE) -> bool { + matches!(mechanism, CKM_RSA_PKCS | CKM_RSA_PKCS_OAEP) +} + +fn block_cipher_crypt( + mechanism: CK_MECHANISM_TYPE, + key: &[u8], + iv: &[u8], + input: &[u8], + mode: Mode, + padding: bool, +) -> Result> { + load_openssl_legacy_provider(); + let cipher = match mechanism { + CKM_DES_ECB => Cipher::des_ecb(), + CKM_DES_CBC => Cipher::des_cbc(), + CKM_DES3_ECB => Cipher::des_ede3(), + CKM_DES3_CBC => Cipher::des_ede3_cbc(), + CKM_AES_ECB => aes_ecb_cipher(key.len())?, + CKM_AES_CBC => aes_cbc_cipher(key.len())?, + _ => return Err(Pkcs11Error::InvalidMechanism), + }; + let iv_arg = if matches!(mechanism, CKM_DES_CBC | CKM_DES3_CBC | CKM_AES_CBC) { + Some(iv) + } else { + None + }; + let mut crypter = Crypter::new(cipher, mode, key, iv_arg).map_err(Pkcs11Error::from)?; + crypter.pad(padding); + let mut out = vec![0u8; input.len() + cipher.block_size()]; + let mut n = crypter.update(input, &mut out).map_err(Pkcs11Error::from)?; + n += crypter.finalize(&mut out[n..]).map_err(Pkcs11Error::from)?; + out.truncate(n); + Ok(out) +} + +fn load_openssl_legacy_provider() { + let _ = OPENSSL_PROVIDERS.get_or_init(|| { + let mut providers = Vec::new(); + if let Ok(provider) = Provider::try_load(None, "default", true) { + providers.push(provider); + } + if let Ok(provider) = Provider::try_load(None, "legacy", true) { + providers.push(provider); + } + providers + }); +} + +fn aes_ecb_cipher(key_len: usize) -> Result { + match key_len { + 16 => Ok(Cipher::aes_128_ecb()), + 24 => Ok(Cipher::aes_192_ecb()), + 32 => Ok(Cipher::aes_256_ecb()), + _ => Err(Pkcs11Error::KeySizeRange), + } +} + +fn aes_cbc_cipher(key_len: usize) -> Result { + match key_len { + 16 => Ok(Cipher::aes_128_cbc()), + 24 => Ok(Cipher::aes_192_cbc()), + 32 => Ok(Cipher::aes_256_cbc()), + _ => Err(Pkcs11Error::KeySizeRange), + } +} diff --git a/src/pkcs11/constants.rs b/src/pkcs11/constants.rs new file mode 100644 index 0000000..7081831 --- /dev/null +++ b/src/pkcs11/constants.rs @@ -0,0 +1,460 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! PKCS#11 v3.0 constants — CKR_*, CKM_*, CKO_*, CKK_*, CKA_*, CKF_*, CKS_*, CKU_*. + +use super::types::*; + +// ── CKR_* — Return codes ─────────────────────────────────────────────────── +pub const CKR_OK: CK_RV = 0x00000000; +pub const CKR_CANCEL: CK_RV = 0x00000001; +pub const CKR_HOST_MEMORY: CK_RV = 0x00000002; +pub const CKR_SLOT_ID_INVALID: CK_RV = 0x00000003; +pub const CKR_GENERAL_ERROR: CK_RV = 0x00000005; +pub const CKR_FUNCTION_FAILED: CK_RV = 0x00000006; +pub const CKR_ARGUMENTS_BAD: CK_RV = 0x00000007; +pub const CKR_CANT_LOCK: CK_RV = 0x0000000A; +pub const CKR_NEED_TO_CREATE_THREADS: CK_RV = 0x00000009; +pub const CKR_ATTRIBUTE_READ_ONLY: CK_RV = 0x00000010; +pub const CKR_ATTRIBUTE_SENSITIVE: CK_RV = 0x00000011; +pub const CKR_ATTRIBUTE_TYPE_INVALID: CK_RV = 0x00000012; +pub const CKR_ATTRIBUTE_VALUE_INVALID: CK_RV = 0x00000013; +pub const CKR_DATA_INVALID: CK_RV = 0x00000020; +pub const CKR_DATA_LEN_RANGE: CK_RV = 0x00000021; +pub const CKR_DEVICE_ERROR: CK_RV = 0x00000030; +pub const CKR_ENCRYPTED_DATA_INVALID: CK_RV = 0x00000040; +pub const CKR_ENCRYPTED_DATA_LEN_RANGE: CK_RV = 0x00000041; +pub const CKR_FUNCTION_CANCELED: CK_RV = 0x00000050; +pub const CKR_FUNCTION_NOT_SUPPORTED: CK_RV = 0x00000054; +pub const CKR_STATE_UNSAVEABLE: CK_RV = 0x00000180; +pub const CKR_KEY_HANDLE_INVALID: CK_RV = 0x00000060; +pub const CKR_KEY_SIZE_RANGE: CK_RV = 0x00000062; +pub const CKR_KEY_TYPE_INCONSISTENT: CK_RV = 0x00000063; +pub const CKR_KEY_INDIGESTIBLE: CK_RV = 0x00000067; +pub const CKR_KEY_FUNCTION_NOT_PERMITTED: CK_RV = 0x00000068; +pub const CKR_KEY_NOT_WRAPPABLE: CK_RV = 0x00000069; +pub const CKR_KEY_UNEXTRACTABLE: CK_RV = 0x0000006A; +pub const CKR_UNWRAPPING_KEY_HANDLE_INVALID: CK_RV = 0x000000F0; +pub const CKR_WRAPPING_KEY_HANDLE_INVALID: CK_RV = 0x00000113; +pub const CKR_MECHANISM_INVALID: CK_RV = 0x00000070; +pub const CKR_MECHANISM_PARAM_INVALID: CK_RV = 0x00000071; +pub const CKR_OBJECT_HANDLE_INVALID: CK_RV = 0x00000082; +pub const CKR_OPERATION_ACTIVE: CK_RV = 0x00000090; +pub const CKR_OPERATION_NOT_INITIALIZED: CK_RV = 0x00000091; +pub const CKR_PIN_INCORRECT: CK_RV = 0x000000A0; +pub const CKR_PIN_LEN_RANGE: CK_RV = 0x000000A1; +pub const CKR_PIN_LOCKED: CK_RV = 0x000000A4; +pub const CKR_SESSION_CLOSED: CK_RV = 0x000000B0; +pub const CKR_SESSION_HANDLE_INVALID: CK_RV = 0x000000B3; +pub const CKR_SESSION_READ_ONLY: CK_RV = 0x000000B5; +pub const CKR_SIGNATURE_INVALID: CK_RV = 0x000000C0; +pub const CKR_SIGNATURE_LEN_RANGE: CK_RV = 0x000000C1; +pub const CKR_TEMPLATE_INCOMPLETE: CK_RV = 0x000000D0; +pub const CKR_TEMPLATE_INCONSISTENT: CK_RV = 0x000000D1; +pub const CKR_TOKEN_NOT_PRESENT: CK_RV = 0x000000E0; +pub const CKR_USER_ALREADY_LOGGED_IN: CK_RV = 0x00000100; +pub const CKR_USER_NOT_LOGGED_IN: CK_RV = 0x00000101; +pub const CKR_USER_PIN_NOT_INITIALIZED: CK_RV = 0x00000102; +pub const CKR_USER_TYPE_INVALID: CK_RV = 0x00000103; +pub const CKR_USER_ANOTHER_ALREADY_LOGGED_IN: CK_RV = 0x00000104; +pub const CKR_RANDOM_SEED_NOT_SUPPORTED: CK_RV = 0x00000120; +pub const CKR_RANDOM_NO_RNG: CK_RV = 0x00000121; +pub const CKR_BUFFER_TOO_SMALL: CK_RV = 0x00000150; +pub const CKR_CRYPTOKI_NOT_INITIALIZED: CK_RV = 0x00000190; +pub const CKR_CRYPTOKI_ALREADY_INITIALIZED: CK_RV = 0x00000191; +pub const CKR_FUNCTION_REJECTED: CK_RV = 0x00000200; +pub const CKR_TOKEN_RESOURCE_EXCEEDED: CK_RV = 0x00000201; +pub const CKR_OPERATION_CANCEL_FAILED: CK_RV = 0x00000202; + +// v3.0 additional return codes +pub const CKR_NEW_PIN_MODE: CK_RV = 0x000001B0; +pub const CKR_NEXT_OTP: CK_RV = 0x000001B1; +pub const CKR_EXCEEDED_MAX_ITERATIONS: CK_RV = 0x000001B5; +pub const CKR_FIPS_SELF_TEST_FAILED: CK_RV = 0x000001B6; +pub const CKR_LIBRARY_LOAD_FAILED: CK_RV = 0x000001B7; +pub const CKR_PIN_TOO_WEAK: CK_RV = 0x000001B8; +pub const CKR_PUBLIC_KEY_INVALID: CK_RV = 0x000001B9; +pub const CKR_AEAD_DECRYPT_FAILED: CK_RV = 0x00000035; + +// ── CKM_* — Mechanism types ──────────────────────────────────────────────── +pub const CKM_RSA_PKCS_KEY_PAIR_GEN: CK_MECHANISM_TYPE = 0x00000000; +pub const CKM_GENERIC_SECRET_KEY_GEN:CK_MECHANISM_TYPE = 0x00000350; +pub const CKM_RSA_PKCS: CK_MECHANISM_TYPE = 0x00000001; +pub const CKM_RSA_PKCS_OAEP: CK_MECHANISM_TYPE = 0x00000009; +pub const CKM_RSA_PKCS_PSS: CK_MECHANISM_TYPE = 0x0000000D; +pub const CKM_MD5_RSA_PKCS: CK_MECHANISM_TYPE = 0x00000005; +pub const CKM_SHA1_RSA_PKCS: CK_MECHANISM_TYPE = 0x00000006; +pub const CKM_SHA1_RSA_PKCS_PSS: CK_MECHANISM_TYPE = 0x0000000E; +pub const CKM_SHA256_RSA_PKCS: CK_MECHANISM_TYPE = 0x00000040; +pub const CKM_SHA256_RSA_PKCS_PSS: CK_MECHANISM_TYPE = 0x00000043; +pub const CKM_MD5: CK_MECHANISM_TYPE = 0x00000210; +pub const CKM_SHA_1: CK_MECHANISM_TYPE = 0x00000220; +pub const CKM_SHA256: CK_MECHANISM_TYPE = 0x00000250; +pub const CKM_DES_KEY_GEN: CK_MECHANISM_TYPE = 0x00000120; +pub const CKM_DES_ECB: CK_MECHANISM_TYPE = 0x00000121; +pub const CKM_DES_CBC: CK_MECHANISM_TYPE = 0x00000122; +pub const CKM_DES3_KEY_GEN: CK_MECHANISM_TYPE = 0x00000131; +pub const CKM_DES3_ECB: CK_MECHANISM_TYPE = 0x00000132; +pub const CKM_DES3_CBC: CK_MECHANISM_TYPE = 0x00000133; +pub const CKM_EC_KEY_PAIR_GEN: CK_MECHANISM_TYPE = 0x00001040; +pub const CKM_ECDSA: CK_MECHANISM_TYPE = 0x00001041; +pub const CKM_ECDSA_SHA1: CK_MECHANISM_TYPE = 0x00001042; +pub const CKM_ECDSA_SHA256: CK_MECHANISM_TYPE = 0x00001043; +pub const CKM_AES_KEY_GEN: CK_MECHANISM_TYPE = 0x00001080; +pub const CKM_AES_CBC: CK_MECHANISM_TYPE = 0x00001082; +pub const CKM_AES_ECB: CK_MECHANISM_TYPE = 0x00001081; +pub const CKM_AES_CBC_PAD: CK_MECHANISM_TYPE = 0x00001085; +pub const CKM_AES_CTR: CK_MECHANISM_TYPE = 0x00001086; +pub const CKM_AES_GCM: CK_MECHANISM_TYPE = 0x00001087; +pub const CKM_AES_KEY_WRAP: CK_MECHANISM_TYPE = 0x00001090; +pub const CKM_ECDH1_DERIVE: CK_MECHANISM_TYPE = 0x00001050; + +// v3.0 hash mechanisms +pub const CKM_SHA384: CK_MECHANISM_TYPE = 0x00000260; +pub const CKM_SHA512: CK_MECHANISM_TYPE = 0x00000270; +pub const CKM_SHA3_256: CK_MECHANISM_TYPE = 0x000002B0; +pub const CKM_SHA3_384: CK_MECHANISM_TYPE = 0x000002C0; +pub const CKM_SHA3_512: CK_MECHANISM_TYPE = 0x000002D0; + +// v3.0 EdDSA mechanisms +pub const CKM_EC_EDWARDS_KEY_PAIR_GEN: CK_MECHANISM_TYPE = 0x00001055; +pub const CKM_EDDSA: CK_MECHANISM_TYPE = 0x00001057; + +// v3.0 Montgomery key exchange +pub const CKM_EC_MONTGOMERY_KEY_PAIR_GEN: CK_MECHANISM_TYPE = 0x00001056; +pub const CKM_XEDDSA: CK_MECHANISM_TYPE = 0x00001058; // X25519/X448 signing + +// v3.0 HKDF +pub const CKM_HKDF_DERIVE: CK_MECHANISM_TYPE = 0x0000402A; +pub const CKM_HKDF_DATA: CK_MECHANISM_TYPE = 0x0000402B; +pub const CKM_HKDF_KEY_GEN: CK_MECHANISM_TYPE = 0x0000402C; + +// v3.0 ChaCha20-Poly1305 +pub const CKM_CHACHA20_POLY1305: CK_MECHANISM_TYPE = 0x00004021; +pub const CKM_CHACHA20_KEY_GEN: CK_MECHANISM_TYPE = 0x00004022; +pub const CKM_CHACHA20: CK_MECHANISM_TYPE = 0x00004023; +pub const CKM_POLY1305_KEY_GEN: CK_MECHANISM_TYPE = 0x00004024; +pub const CKM_POLY1305: CK_MECHANISM_TYPE = 0x00004025; + +// v3.0 additional RSA-PSS with SHA-384/512 +pub const CKM_SHA384_RSA_PKCS: CK_MECHANISM_TYPE = 0x00000041; +pub const CKM_SHA512_RSA_PKCS: CK_MECHANISM_TYPE = 0x00000042; +pub const CKM_SHA384_RSA_PKCS_PSS: CK_MECHANISM_TYPE = 0x00000044; +pub const CKM_SHA512_RSA_PKCS_PSS: CK_MECHANISM_TYPE = 0x00000045; + +// v3.0 ECDSA with SHA-384/512 +pub const CKM_ECDSA_SHA384: CK_MECHANISM_TYPE = 0x00001044; +pub const CKM_ECDSA_SHA512: CK_MECHANISM_TYPE = 0x00001045; + +// v3.0 SP 800-108 KDF +pub const CKM_SP800_108_COUNTER_KDF: CK_MECHANISM_TYPE = 0x000003AC; +pub const CKM_SP800_108_FEEDBACK_KDF: CK_MECHANISM_TYPE = 0x000003AD; + +/// KDF type: raw shared secret, no derivation. +pub const CKD_NULL: CK_ULONG = 0x00000001; + +// ── CKR_* — Legacy compat ───────────────────────────────────────────── +pub const CKR_FUNCTION_NOT_PARALLEL: CK_RV = 0x00000051; +pub const CKR_SESSION_PARALLEL_NOT_SUPPORTED: CK_RV = 0x000000B4; +pub const CKR_SESSION_EXISTS: CK_RV = 0x000000B6; +pub const CKR_SESSION_READ_ONLY_EXISTS: CK_RV = 0x000000B7; +pub const CKR_SESSION_READ_WRITE_SO_EXISTS: CK_RV = 0x000000B8; +pub const CKR_TOKEN_WRITE_PROTECTED: CK_RV = 0x000000E2; + +// ── HKDF salt types (v3.0) ─────────────────────────────────────────── +pub const CKF_HKDF_SALT_NULL: CK_ULONG = 0x00000001; +pub const CKF_HKDF_SALT_DATA: CK_ULONG = 0x00000002; +pub const CKF_HKDF_SALT_KEY: CK_ULONG = 0x00000004; + +/// All mechanisms this token supports (used by C_GetMechanismList). +pub const SUPPORTED_MECHANISMS: &[CK_MECHANISM_TYPE] = &[ + // RSA + CKM_RSA_PKCS_KEY_PAIR_GEN, + CKM_RSA_PKCS, + CKM_RSA_PKCS_OAEP, + CKM_SHA1_RSA_PKCS, // legacy — filtered unless CRYPTOKI_LEGACY=1 + CKM_SHA1_RSA_PKCS_PSS, // legacy — filtered unless CRYPTOKI_LEGACY=1 + CKM_SHA256_RSA_PKCS, + CKM_SHA256_RSA_PKCS_PSS, + CKM_SHA384_RSA_PKCS, + CKM_SHA512_RSA_PKCS, + CKM_SHA384_RSA_PKCS_PSS, + CKM_SHA512_RSA_PKCS_PSS, + // EC (Weierstrass) + CKM_EC_KEY_PAIR_GEN, + CKM_ECDSA, + CKM_ECDSA_SHA256, + CKM_ECDSA_SHA384, + CKM_ECDSA_SHA512, + CKM_ECDH1_DERIVE, + // EdDSA (v3.0) + CKM_EC_EDWARDS_KEY_PAIR_GEN, + CKM_EDDSA, + // AES + CKM_AES_KEY_GEN, + CKM_AES_ECB, + CKM_AES_CBC, + CKM_AES_CBC_PAD, + CKM_AES_CTR, + CKM_AES_GCM, + // Legacy DES/3DES key generation is needed for PKCS#11 v2.40 consumers. + CKM_DES_KEY_GEN, + CKM_DES3_KEY_GEN, + CKM_DES_ECB, + CKM_DES_CBC, + CKM_DES3_ECB, + CKM_DES3_CBC, + // ChaCha20 (v3.0) + CKM_CHACHA20_KEY_GEN, + CKM_CHACHA20_POLY1305, + // Hashing + CKM_MD5, + CKM_SHA_1, + CKM_SHA256, + CKM_SHA384, + CKM_SHA512, + CKM_SHA3_256, + CKM_SHA3_384, + CKM_SHA3_512, + // HKDF (v3.0) + CKM_HKDF_DERIVE, + CKM_HKDF_KEY_GEN, +]; + +// ── CKO_* — Object classes ───────────────────────────────────────────────── +pub const CKO_DATA: CK_OBJECT_CLASS = 0x00000000; +pub const CKO_CERTIFICATE: CK_OBJECT_CLASS = 0x00000001; +pub const CKO_PUBLIC_KEY: CK_OBJECT_CLASS = 0x00000002; +pub const CKO_PRIVATE_KEY: CK_OBJECT_CLASS = 0x00000003; +pub const CKO_SECRET_KEY: CK_OBJECT_CLASS = 0x00000004; +pub const CKO_PROFILE: CK_OBJECT_CLASS = 0x00000009; // v3.0 + +// ── CKP_* — Profile IDs (v3.0) ────────────────────────────────────────── +pub const CKP_INVALID_ID: CK_ULONG = 0x00000000; +pub const CKP_BASELINE_PROVIDER: CK_ULONG = 0x00000001; +pub const CKP_EXTENDED_PROVIDER: CK_ULONG = 0x00000002; +pub const CKP_AUTHENTICATION_TOKEN: CK_ULONG = 0x00000003; +pub const CKP_PUBLIC_CERTIFICATES_TOKEN: CK_ULONG = 0x00000004; + +// ── CKK_* — Key types ────────────────────────────────────────────────────── +pub const CKK_RSA: CK_KEY_TYPE = 0x00000000; +pub const CKK_DES: CK_KEY_TYPE = 0x00000013; +pub const CKK_DES3: CK_KEY_TYPE = 0x00000015; +pub const CKK_EC: CK_KEY_TYPE = 0x00000003; +pub const CKK_AES: CK_KEY_TYPE = 0x0000001F; +pub const CKK_GENERIC_SECRET: CK_KEY_TYPE = 0x00000010; +pub const CKK_CHACHA20: CK_KEY_TYPE = 0x00000033; // v3.0 +pub const CKK_POLY1305: CK_KEY_TYPE = 0x00000034; // v3.0 +pub const CKK_EC_EDWARDS: CK_KEY_TYPE = 0x00000040; // v3.0 — Ed25519, Ed448 +pub const CKK_EC_MONTGOMERY: CK_KEY_TYPE = 0x00000041; // v3.0 — X25519, X448 +pub const CKK_HKDF: CK_KEY_TYPE = 0x00000042; // v3.0 + +// ── CKA_* — Attribute types ──────────────────────────────────────────────── +pub const CKA_CLASS: CK_ATTRIBUTE_TYPE = 0x00000000; +pub const CKA_TOKEN: CK_ATTRIBUTE_TYPE = 0x00000001; +pub const CKA_PRIVATE: CK_ATTRIBUTE_TYPE = 0x00000002; +pub const CKA_LABEL: CK_ATTRIBUTE_TYPE = 0x00000003; +pub const CKA_VALUE: CK_ATTRIBUTE_TYPE = 0x00000011; +pub const CKA_PRIVATE_EXPONENT: CK_ATTRIBUTE_TYPE = 0x00000123; +pub const CKA_PRIME_1: CK_ATTRIBUTE_TYPE = 0x00000124; +pub const CKA_PRIME_2: CK_ATTRIBUTE_TYPE = 0x00000125; +pub const CKA_EXPONENT_1: CK_ATTRIBUTE_TYPE = 0x00000126; +pub const CKA_EXPONENT_2: CK_ATTRIBUTE_TYPE = 0x00000127; +pub const CKA_COEFFICIENT: CK_ATTRIBUTE_TYPE = 0x00000128; +pub const CKA_KEY_TYPE: CK_ATTRIBUTE_TYPE = 0x00000100; +pub const CKA_ID: CK_ATTRIBUTE_TYPE = 0x00000102; +pub const CKA_SENSITIVE: CK_ATTRIBUTE_TYPE = 0x00000103; +pub const CKA_ENCRYPT: CK_ATTRIBUTE_TYPE = 0x00000104; +pub const CKA_DECRYPT: CK_ATTRIBUTE_TYPE = 0x00000105; +pub const CKA_TRUSTED: CK_ATTRIBUTE_TYPE = 0x00000086; +pub const CKA_WRAP: CK_ATTRIBUTE_TYPE = 0x00000106; +pub const CKA_UNWRAP: CK_ATTRIBUTE_TYPE = 0x00000107; +pub const CKA_SIGN: CK_ATTRIBUTE_TYPE = 0x00000108; +pub const CKA_VERIFY: CK_ATTRIBUTE_TYPE = 0x00000109; +pub const CKA_DERIVE: CK_ATTRIBUTE_TYPE = 0x0000010C; +pub const CKA_MODULUS: CK_ATTRIBUTE_TYPE = 0x00000120; +pub const CKA_MODULUS_BITS: CK_ATTRIBUTE_TYPE = 0x00000121; +pub const CKA_PUBLIC_EXPONENT: CK_ATTRIBUTE_TYPE = 0x00000122; +pub const CKA_VALUE_LEN: CK_ATTRIBUTE_TYPE = 0x00000161; +pub const CKA_EXTRACTABLE: CK_ATTRIBUTE_TYPE = 0x00000162; +pub const CKA_LOCAL: CK_ATTRIBUTE_TYPE = 0x00000163; +pub const CKA_NEVER_EXTRACTABLE:CK_ATTRIBUTE_TYPE = 0x00000164; +pub const CKA_ALWAYS_SENSITIVE: CK_ATTRIBUTE_TYPE = 0x00000165; +pub const CKA_KEY_GEN_MECHANISM: CK_ATTRIBUTE_TYPE = 0x00000166; +pub const CKA_ALWAYS_AUTHENTICATE: CK_ATTRIBUTE_TYPE = 0x00000202; +pub const CKA_WRAP_WITH_TRUSTED: CK_ATTRIBUTE_TYPE = 0x00000210; +pub const CKA_COPYABLE: CK_ATTRIBUTE_TYPE = 0x00000171; // v3.0 +pub const CKA_DESTROYABLE: CK_ATTRIBUTE_TYPE = 0x00000172; // v3.0 +pub const CKA_EC_PARAMS: CK_ATTRIBUTE_TYPE = 0x00000180; +pub const CKA_EC_POINT: CK_ATTRIBUTE_TYPE = 0x00000181; +pub const CKA_UNIQUE_ID: CK_ATTRIBUTE_TYPE = 0x0000010A; // v3.0 +pub const CKA_PROFILE_ID: CK_ATTRIBUTE_TYPE = 0x00000601; // v3.0 + +// ── CKF_* — Flags ────────────────────────────────────────────────────────── + +// Slot flags +pub const CKF_TOKEN_PRESENT: CK_FLAGS = 0x00000001; +pub const CKF_REMOVABLE_DEVICE: CK_FLAGS = 0x00000002; + +// Token flags +pub const CKF_RNG: CK_FLAGS = 0x00000001; +pub const CKF_WRITE_PROTECTED: CK_FLAGS = 0x00000002; +pub const CKF_LOGIN_REQUIRED: CK_FLAGS = 0x00000004; +pub const CKF_USER_PIN_INITIALIZED: CK_FLAGS = 0x00000008; +pub const CKF_TOKEN_INITIALIZED: CK_FLAGS = 0x00000400; +pub const CKF_USER_PIN_COUNT_LOW: CK_FLAGS = 0x00010000; +pub const CKF_USER_PIN_FINAL_TRY: CK_FLAGS = 0x00020000; +pub const CKF_USER_PIN_LOCKED: CK_FLAGS = 0x00040000; +pub const CKF_SO_PIN_COUNT_LOW: CK_FLAGS = 0x01000000; +pub const CKF_SO_PIN_FINAL_TRY: CK_FLAGS = 0x02000000; +pub const CKF_SO_PIN_LOCKED: CK_FLAGS = 0x00400000; + +// C_Initialize flags +pub const CKF_OS_LOCKING_OK: CK_FLAGS = 0x00000002; + +// Session flags +pub const CKF_RW_SESSION: CK_FLAGS = 0x00000002; +pub const CKF_SERIAL_SESSION: CK_FLAGS = 0x00000004; + +// Mechanism flags +pub const CKF_HW: CK_FLAGS = 0x00000001; +pub const CKF_ENCRYPT: CK_FLAGS = 0x00000100; +pub const CKF_DECRYPT: CK_FLAGS = 0x00000200; +pub const CKF_DIGEST: CK_FLAGS = 0x00000400; +pub const CKF_SIGN: CK_FLAGS = 0x00000800; +pub const CKF_VERIFY: CK_FLAGS = 0x00002000; +pub const CKF_GENERATE: CK_FLAGS = 0x00008000; +pub const CKF_GENERATE_KEY_PAIR: CK_FLAGS = 0x00010000; + +// Mechanism flags (additional) +pub const CKF_WRAP: CK_FLAGS = 0x00020000; +pub const CKF_UNWRAP: CK_FLAGS = 0x00040000; +pub const CKF_DERIVE: CK_FLAGS = 0x00080000; + +// ── CKS_* — Session states ───────────────────────────────────────────────── +pub const CKS_RO_PUBLIC_SESSION: CK_STATE = 0; +pub const CKS_RO_USER_FUNCTIONS: CK_STATE = 1; +pub const CKS_RW_PUBLIC_SESSION: CK_STATE = 2; +pub const CKS_RW_USER_FUNCTIONS: CK_STATE = 3; +pub const CKS_RW_SO_FUNCTIONS: CK_STATE = 4; + +// ── CKU_* — User types ──────────────────────────────────────────────────── +pub const CKU_SO: CK_USER_TYPE = 0; +pub const CKU_USER: CK_USER_TYPE = 1; +pub const CKU_CONTEXT_SPECIFIC: CK_USER_TYPE = 2; // v3.0 + +// ── CKF_* — Interface flags (v3.0) ────────────────────────────────────── +pub const CKF_INTERFACE_FORK_SAFE: CK_FLAGS = 0x00000001; + +// ── Interface name (v3.0) ──────────────────────────────────────────────── +/// The standard interface name for PKCS#11. +pub const PKCS11_INTERFACE_NAME: &[u8] = b"PKCS 11\0"; + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + #[test] + fn test_no_duplicate_constants() { + // Read this very source file at compile time + let source = include_str!("constants.rs"); + + // Domain Prefix -> (Value -> Variable Name) + let mut domains: HashMap<&str, HashMap> = HashMap::new(); + + for line in source.lines() { + let line = line.trim(); + if !line.starts_with("pub const ") { + continue; + } + + let parts: Vec<&str> = line.splitn(2, '=').collect(); + if parts.len() != 2 { continue; } + + let decl = parts[0].trim(); + let val_str = parts[1].split_whitespace().next().unwrap_or("").trim_end_matches(';'); + + let decl_parts: Vec<&str> = decl.split(':').collect(); + if decl_parts.len() != 2 { continue; } + + let name = decl_parts[0].replace("pub const ", "").trim().to_string(); + + // Group by domains that must be strictly unique. + // (We skip flags like CKF_ or CKS_ because bitmasks naturally share values across different structs) + let prefix = if name.starts_with("CKR_") { "CKR_" } + else if name.starts_with("CKA_") { "CKA_" } + else if name.starts_with("CKM_") { "CKM_" } + else if name.starts_with("CKO_") { "CKO_" } + else if name.starts_with("CKK_") { "CKK_" } + else { continue }; + + let domain = domains.entry(prefix).or_default(); + if let Some(existing) = domain.get(val_str) { + panic!( + "Collision detected! Both `{}` and `{}` share the value `{}` in the {} domain.", + existing, name, val_str, prefix + ); + } + domain.insert(val_str.to_string(), name); + } + } + + #[test] + fn test_cpp_header_sync() { + let rs_source = include_str!("constants.rs"); + let cpp_source = include_str!("../../cpp/pkcs11.h"); + + let mut rs_constants = HashMap::new(); + for line in rs_source.lines() { + let line = line.trim(); + if line.starts_with("pub const ") { + let parts: Vec<&str> = line.splitn(2, '=').collect(); + if parts.len() == 2 { + let decl = parts[0].trim(); + let val_str = parts[1].split_whitespace().next().unwrap_or("").trim_end_matches(';'); + let decl_parts: Vec<&str> = decl.split(':').collect(); + if decl_parts.len() == 2 { + let name = decl_parts[0].replace("pub const ", "").trim().to_string(); + rs_constants.insert(name, val_str.to_string()); + } + } + } + } + + // Verify every CKR, CKM, CKA, CKK, CKO, CKU in the C++ header matches the Rust constants + for line in cpp_source.lines() { + let line = line.trim(); + if line.starts_with("#define ") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 3 { + let name = parts[1].to_string(); + if name.starts_with("CKR_") || name.starts_with("CKM_") || name.starts_with("CKA_") + || name.starts_with("CKK_") || name.starts_with("CKO_") || name.starts_with("CKU_") { + + let cpp_val = parts[2].trim_end_matches("UL"); + if let Some(rs_val) = rs_constants.get(&name) { + assert_eq!( + cpp_val.to_lowercase(), + rs_val.to_lowercase(), + "Constant mismatch for {}! C++ has {}, Rust has {}", + name, cpp_val, rs_val + ); + } else { + panic!("Constant {} is defined in C++ header but missing in Rust constants.rs", name); + } + } + } + } + } + } +} diff --git a/src/pkcs11/error.rs b/src/pkcs11/error.rs new file mode 100644 index 0000000..fc3b75a --- /dev/null +++ b/src/pkcs11/error.rs @@ -0,0 +1,151 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! PKCS#11 error type with CK_RV conversion. +use super::constants::*; +use super::types::CK_RV; + +/// Unified error type for the PKCS#11 layer. +/// Every variant maps to a specific `CKR_*` return code via [`Pkcs11Error::to_ckr`]. +#[derive(Debug)] +pub enum Pkcs11Error { + NotInitialised, + AlreadyInitialised, + InvalidSlotId, + InvalidSessionHandle, + InvalidObjectHandle, + InvalidMechanism, + MechanismUnsupported, + MechanismParamInvalid, + InvalidAttributeType, + AttributeReadOnly, + AttributeValueInvalid, + AttributeSensitive, + TemplateIncomplete, + TemplateInconsistent, + ArgumentsBad, + BufferTooSmall(usize), + UserNotLoggedIn, + UserAlreadyLoggedIn, + UserAnotherAlreadyLoggedIn, + UserPinNotInitialized, + UserTypeInvalid, + PinIncorrect, + PinLenRange, + PinLocked, + OperationActive, + OperationNotInitialised, + SessionReadOnly, + SessionParallelNotSupported, + SessionReadWriteSoExists, + TokenWriteProtected, + KeyTypeInconsistent, + KeyHandleInvalid, + KeySizeRange, + KeyFunctionNotPermitted, + KeyNotExtractable, + KeyNotWrappable, + SignatureInvalid, + DataInvalid, + DataLenRange, + EncryptedDataInvalid, + FunctionNotSupported, + GeneralError, + OpenSsl(openssl::error::ErrorStack), +} + +impl Pkcs11Error { + pub fn to_ckr(&self) -> CK_RV { + match self { + Self::NotInitialised => CKR_CRYPTOKI_NOT_INITIALIZED, + Self::AlreadyInitialised => CKR_CRYPTOKI_ALREADY_INITIALIZED, + Self::InvalidSlotId => CKR_SLOT_ID_INVALID, + Self::InvalidSessionHandle => CKR_SESSION_HANDLE_INVALID, + Self::InvalidObjectHandle => CKR_OBJECT_HANDLE_INVALID, + Self::InvalidMechanism + | Self::MechanismUnsupported => CKR_MECHANISM_INVALID, + Self::MechanismParamInvalid => CKR_MECHANISM_PARAM_INVALID, + Self::InvalidAttributeType => CKR_ATTRIBUTE_TYPE_INVALID, + Self::AttributeReadOnly => CKR_ATTRIBUTE_READ_ONLY, + Self::AttributeValueInvalid => CKR_ATTRIBUTE_VALUE_INVALID, + Self::AttributeSensitive => CKR_ATTRIBUTE_SENSITIVE, + Self::TemplateIncomplete => CKR_TEMPLATE_INCOMPLETE, + Self::TemplateInconsistent => CKR_TEMPLATE_INCONSISTENT, + Self::ArgumentsBad => CKR_ARGUMENTS_BAD, + Self::BufferTooSmall(_) => CKR_BUFFER_TOO_SMALL, + Self::UserNotLoggedIn => CKR_USER_NOT_LOGGED_IN, + Self::UserAlreadyLoggedIn => CKR_USER_ALREADY_LOGGED_IN, + Self::UserAnotherAlreadyLoggedIn => CKR_USER_ANOTHER_ALREADY_LOGGED_IN, + Self::UserPinNotInitialized => CKR_USER_PIN_NOT_INITIALIZED, + Self::UserTypeInvalid => CKR_USER_TYPE_INVALID, + Self::PinIncorrect => CKR_PIN_INCORRECT, + Self::PinLenRange => CKR_PIN_LEN_RANGE, + Self::PinLocked => CKR_PIN_LOCKED, + Self::OperationActive => CKR_OPERATION_ACTIVE, + Self::OperationNotInitialised => CKR_OPERATION_NOT_INITIALIZED, + Self::SessionReadOnly => CKR_SESSION_READ_ONLY, + Self::SessionParallelNotSupported => CKR_SESSION_PARALLEL_NOT_SUPPORTED, + Self::SessionReadWriteSoExists => CKR_SESSION_READ_WRITE_SO_EXISTS, + Self::TokenWriteProtected => CKR_TOKEN_WRITE_PROTECTED, + Self::KeyTypeInconsistent => CKR_KEY_TYPE_INCONSISTENT, + Self::KeyHandleInvalid => CKR_KEY_HANDLE_INVALID, + Self::KeySizeRange => CKR_KEY_SIZE_RANGE, + Self::KeyFunctionNotPermitted => CKR_KEY_FUNCTION_NOT_PERMITTED, + Self::KeyNotExtractable => CKR_KEY_UNEXTRACTABLE, + Self::KeyNotWrappable => CKR_KEY_NOT_WRAPPABLE, + Self::SignatureInvalid => CKR_SIGNATURE_INVALID, + Self::DataInvalid => CKR_DATA_INVALID, + Self::DataLenRange => CKR_DATA_LEN_RANGE, + Self::EncryptedDataInvalid => CKR_ENCRYPTED_DATA_INVALID, + Self::FunctionNotSupported => CKR_FUNCTION_NOT_SUPPORTED, + Self::GeneralError + | Self::OpenSsl(_) => CKR_GENERAL_ERROR, + } + } +} + +impl From for Pkcs11Error { + fn from(e: openssl::error::ErrorStack) -> Self { Pkcs11Error::OpenSsl(e) } +} + +impl From for Pkcs11Error { + fn from(e: crate::error::CryptoError) -> Self { + use crate::error::CryptoError::*; + match e { + KeyGenFailed { .. } => Pkcs11Error::GeneralError, + InvalidKeyData { .. } => Pkcs11Error::KeyHandleInvalid, + InvalidKeySize { .. } => Pkcs11Error::KeySizeRange, + DataInvalid { .. } => Pkcs11Error::DataInvalid, + DataLenRange { .. } => Pkcs11Error::DataLenRange, + EncryptFailed { .. } => Pkcs11Error::GeneralError, + DecryptFailed { .. } => Pkcs11Error::EncryptedDataInvalid, + SignFailed { .. } => Pkcs11Error::GeneralError, + VerifyFailed { .. } => Pkcs11Error::SignatureInvalid, + SignatureLenRange { .. } => Pkcs11Error::SignatureInvalid, + HashFailed { .. } => Pkcs11Error::GeneralError, + RandomFailed { .. } => Pkcs11Error::GeneralError, + BufferTooSmall { needed } => Pkcs11Error::BufferTooSmall(needed), + MechanismInvalid { .. } => Pkcs11Error::InvalidMechanism, + MechanismParamInvalid { .. } => Pkcs11Error::MechanismParamInvalid, + AttributeTypeInvalid => Pkcs11Error::InvalidAttributeType, + AttributeSensitive => Pkcs11Error::AttributeSensitive, + AttributeValueInvalid => Pkcs11Error::AttributeValueInvalid, + NotInitialized => Pkcs11Error::NotInitialised, + AlreadyInitialized => Pkcs11Error::AlreadyInitialised, + GeneralError { .. } => Pkcs11Error::GeneralError, + SlotIdInvalid => Pkcs11Error::InvalidSlotId, + } + } +} + +pub type Result = std::result::Result; diff --git a/src/pkcs11/ffi_api_core.rs b/src/pkcs11/ffi_api_core.rs new file mode 100644 index 0000000..b588a07 --- /dev/null +++ b/src/pkcs11/ffi_api_core.rs @@ -0,0 +1,12 @@ +//! Hub for core PKCS#11 APIs. +//! +//! Owns lifecycle, slot/token/session/login, and object/attribute/find entry points. +use super::*; + +mod lifecycle_and_slot_token; +mod session_and_login; +mod keys_objects_attributes_find; + +pub use lifecycle_and_slot_token::*; +pub use session_and_login::*; +pub use keys_objects_attributes_find::*; diff --git a/src/pkcs11/ffi_api_core/keys_objects_attributes_find.rs b/src/pkcs11/ffi_api_core/keys_objects_attributes_find.rs new file mode 100644 index 0000000..b494862 --- /dev/null +++ b/src/pkcs11/ffi_api_core/keys_objects_attributes_find.rs @@ -0,0 +1,503 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── Key generation ──────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_GenerateKeyPair( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + p_pub_template: *const CK_ATTRIBUTE, + ul_pub_count: CK_ULONG, + p_priv_template: *const CK_ATTRIBUTE, + ul_priv_count: CK_ULONG, + ph_pub_key: *mut CK_OBJECT_HANDLE, + ph_priv_key: *mut CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() || ph_pub_key.is_null() || ph_priv_key.is_null() { + return CKR_ARGUMENTS_BAD; + } + if (ul_pub_count > 0 && p_pub_template.is_null()) || (ul_priv_count > 0 && p_priv_template.is_null()) { + return CKR_ARGUMENTS_BAD; + } + let slot_id = ck_try!(session_slot(h_session)); + let mech = &*p_mechanism; + let pub_attrs = ffi_api_crypto::collect_template(p_pub_template, ul_pub_count); + let mut priv_attrs = ffi_api_crypto::collect_template(p_priv_template, ul_priv_count); + + let pub_is_token = pub_attrs.get(&CKA_TOKEN).is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + let priv_is_token = priv_attrs.get(&CKA_TOKEN).is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + if pub_is_token || priv_is_token { + ck_try!(require_rw_session(h_session)); + } + + // Inject security defaults for the private key. + priv_attrs.entry(CKA_SENSITIVE).or_insert_with(|| vec![CK_TRUE]); + priv_attrs.entry(CKA_EXTRACTABLE).or_insert_with(|| vec![CK_FALSE]); + let priv_always_sensitive = priv_attrs.get(&CKA_SENSITIVE) + .is_none_or(|v| !v.is_empty() && v[0] == CK_TRUE); + let priv_never_extractable = priv_attrs.get(&CKA_EXTRACTABLE) + .is_none_or(|v| v.is_empty() || v[0] == CK_FALSE); + // CKA_ALWAYS_AUTHENTICATE is a struct field, not stored in the HashMap. + let priv_always_authenticate = priv_attrs.remove(&CKA_ALWAYS_AUTHENTICATE) + .is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + + // Helper: stamp a GeneratedKey into the object store, returning its handle. + let store_pair = |gen: backend::GeneratedKey, + always_sensitive: bool, + never_extractable: bool, + always_authenticate: bool| { + let h = object_store::next_handle(); + let mut obj = object_store::KeyObject::new(h, slot_id, gen.key_type, gen.key_ref, gen.attrs); + obj.local = true; + obj.key_gen_mechanism = gen.key_gen_mechanism; + obj.always_sensitive = always_sensitive; + obj.never_extractable = never_extractable; + obj.always_authenticate = always_authenticate; + object_store::store_object(obj, Some(h_session)); + h + }; + let store_pub = |gen: backend::GeneratedKey| { + let h = object_store::next_handle(); + let mut obj = object_store::KeyObject::new(h, slot_id, gen.key_type, gen.key_ref, gen.attrs); + obj.local = true; + obj.key_gen_mechanism = gen.key_gen_mechanism; + object_store::store_object(obj, Some(h_session)); + h + }; + + match mech.mechanism { + CKM_RSA_PKCS_KEY_PAIR_GEN => { + let bits = pub_attrs + .get(&CKA_MODULUS_BITS) + .map(|b| backend::bytes_to_ulong(b) as u32) + .unwrap_or(2048); + if bits < 1024 { return CKR_KEY_SIZE_RANGE; } + let (priv_gen, pub_gen) = ck_try!(backend::generate_rsa_key_pair( + slot_id, bits, 65537, pub_attrs, priv_attrs, + )); + *ph_pub_key = store_pub(pub_gen); + *ph_priv_key = store_pair(priv_gen, priv_always_sensitive, priv_never_extractable, priv_always_authenticate); + } + CKM_EC_KEY_PAIR_GEN => { + let (priv_gen, pub_gen) = ck_try!(backend::generate_ec_key_pair( + slot_id, crate::types::EcCurve::P256, pub_attrs, priv_attrs, + )); + *ph_pub_key = store_pub(pub_gen); + *ph_priv_key = store_pair(priv_gen, priv_always_sensitive, priv_never_extractable, priv_always_authenticate); + } + CKM_EC_EDWARDS_KEY_PAIR_GEN => { + // Default to Ed25519; could inspect CKA_EC_PARAMS to choose Ed448 + let curve = crate::types::EdwardsCurve::Ed25519; + let (priv_gen, pub_gen) = ck_try!(backend::generate_ed_key_pair( + slot_id, curve, pub_attrs, priv_attrs, + )); + *ph_pub_key = store_pub(pub_gen); + *ph_priv_key = store_pair(priv_gen, priv_always_sensitive, priv_never_extractable, priv_always_authenticate); + } + _ => return CKR_MECHANISM_INVALID, + } + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GenerateKey( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + p_template: *const CK_ATTRIBUTE, + ul_count: CK_ULONG, + ph_key: *mut CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + + if p_mechanism.is_null() || ph_key.is_null() { return CKR_ARGUMENTS_BAD; } + if ul_count > 0 && p_template.is_null() { return CKR_ARGUMENTS_BAD; } + + let slot_id = ck_try!(session_slot(h_session)); + let mech = &*p_mechanism; + let mut attrs = ffi_api_crypto::collect_template(p_template, ul_count); + + // Evaluate Token Status for Session R/W requirements + let is_token = attrs.get(&CKA_TOKEN).is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + if is_token { + ck_try!(require_rw_session(h_session)); + } + + // Template Consistency Checks (Class & Key Type) + if let Some(class_val) = attrs.get(&CKA_CLASS) { + if backend::bytes_to_ulong(class_val) != CKO_SECRET_KEY { + return CKR_TEMPLATE_INCONSISTENT; + } + } + + let expected_key_type = match mech.mechanism { + CKM_AES_KEY_GEN => CKK_AES, + CKM_DES_KEY_GEN => CKK_DES, + CKM_DES3_KEY_GEN => CKK_DES3, + CKM_CHACHA20_KEY_GEN => CKK_CHACHA20, + CKM_GENERIC_SECRET_KEY_GEN => CKK_GENERIC_SECRET, + _ => return CKR_MECHANISM_INVALID, + }; + + if let Some(type_val) = attrs.get(&CKA_KEY_TYPE) { + if backend::bytes_to_ulong(type_val) != expected_key_type { + return CKR_TEMPLATE_INCONSISTENT; + } + } + + attrs.entry(CKA_SENSITIVE).or_insert_with(|| vec![CK_TRUE]); + attrs.entry(CKA_EXTRACTABLE).or_insert_with(|| vec![CK_FALSE]); + let always_sensitive = attrs.get(&CKA_SENSITIVE) + .is_none_or(|v| !v.is_empty() && v[0] == CK_TRUE); + let never_extractable = attrs.get(&CKA_EXTRACTABLE) + .is_none_or(|v| v.is_empty() || v[0] == CK_FALSE); + + let gen = match mech.mechanism { + CKM_AES_KEY_GEN => { + let key_len = attrs.get(&CKA_VALUE_LEN).map(|b| backend::bytes_to_ulong(b) as usize).unwrap_or(32); + ck_try!(backend::generate_aes_key(slot_id, key_len, attrs)) + } + CKM_DES_KEY_GEN => { + ck_try!(backend::generate_legacy_secret_key(slot_id, mech.mechanism, 8, CKK_DES, attrs)) + } + CKM_DES3_KEY_GEN => { + ck_try!(backend::generate_legacy_secret_key(slot_id, mech.mechanism, 24, CKK_DES3, attrs)) + } + CKM_CHACHA20_KEY_GEN => { + ck_try!(backend::generate_chacha20_key(slot_id, attrs)) + } + // Generate the generic key + CKM_GENERIC_SECRET_KEY_GEN => { + let key_len = attrs.get(&CKA_VALUE_LEN).map(|b| backend::bytes_to_ulong(b) as usize).unwrap_or(32); + ck_try!(backend::generate_generic_secret_key(slot_id, key_len, attrs)) // <--- Make sure this exists in your backend! + } + _ => return CKR_MECHANISM_INVALID, + }; + + let handle = object_store::next_handle(); + let mut obj = object_store::KeyObject::new(handle, slot_id, gen.key_type, gen.key_ref, gen.attrs); + obj.local = true; + obj.key_gen_mechanism = gen.key_gen_mechanism; + obj.always_sensitive = always_sensitive; + obj.never_extractable = never_extractable; + object_store::store_object(obj, Some(h_session)); + *ph_key = handle; + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GenerateRandom( + h_session: CK_SESSION_HANDLE, + p_random: *mut CK_BYTE, + ul_random_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_random.is_null() { return CKR_ARGUMENTS_BAD; } + let slot_id = ck_try!(session_slot(h_session)); + let buf = std::slice::from_raw_parts_mut(p_random, ul_random_len as usize); + ck_try!(backend::generate_random(slot_id, buf)); + CKR_OK +} + +// ── C_CreateObject ──────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_CreateObject( + h_session: CK_SESSION_HANDLE, + p_template: *const CK_ATTRIBUTE, + ul_count: CK_ULONG, + ph_object: *mut CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if ph_object.is_null() { return CKR_ARGUMENTS_BAD; } + if p_template.is_null() || ul_count == 0 { return CKR_TEMPLATE_INCOMPLETE; } + let slot_id = ck_try!(session_slot(h_session)); + let attrs = ffi_api_crypto::collect_template(p_template, ul_count); + let is_token = attrs.get(&CKA_TOKEN).is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + if is_token { + ck_try!(require_rw_session(h_session)); + } + // We only support importing AES secret key values via CKA_VALUE + let class = match attrs.get(&CKA_CLASS).map(|b| backend::bytes_to_ulong(b)) { + Some(CKO_SECRET_KEY | CKO_PUBLIC_KEY | CKO_PRIVATE_KEY | CKO_DATA) => { + attrs.get(&CKA_CLASS).map(|b| backend::bytes_to_ulong(b)).unwrap() + } + None => return CKR_TEMPLATE_INCOMPLETE, + _ => return CKR_TEMPLATE_INCONSISTENT, + }; + let mut key_bytes = Vec::new(); + + if class == CKO_SECRET_KEY || class == CKO_DATA { + if let Some(v) = attrs.get(&CKA_VALUE) { + key_bytes = v.clone(); + } else { + return CKR_TEMPLATE_INCOMPLETE; + } + // Optional: Length checks for AES + } + + let handle = object_store::next_handle(); + let key_type = match attrs.get(&CKA_KEY_TYPE).map(|b| backend::bytes_to_ulong(b)) { + Some(CKK_RSA) if class == CKO_PUBLIC_KEY => object_store::KeyType::RsaPublic, + Some(CKK_RSA) if class == CKO_PRIVATE_KEY => object_store::KeyType::RsaPrivate, + Some(CKK_AES) => object_store::KeyType::AesSecret, + Some(CKK_DES | CKK_DES3 | CKK_GENERIC_SECRET) | None => object_store::KeyType::GenericSecret, + _ => object_store::KeyType::GenericSecret, + }; + let obj = object_store::KeyObject::new( + handle, + slot_id, + key_type, + crate::traits::EngineKeyRef::from_bytes(key_bytes), + attrs, + ); + object_store::store_object(obj, Some(h_session)); + *ph_object = handle; + CKR_OK +} + +// ── C_DestroyObject ─────────────────────────────────────────────────────── + +#[no_mangle] +pub extern "C" fn C_DestroyObject( + h_session: CK_SESSION_HANDLE, + h_object: CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + let (slot_id, logged_in) = ck_try!(session::with_session(h_session, |s| { + Ok((s.slot_id, s.login_state != LoginState::NotLoggedIn)) + })); + // Find the object and check its token status + let is_token_object = ck_try!(object_store::with_object(h_object, |obj| { + Ok(object_store::is_token_object(obj)) + })); + + // ONLY require RW session if the object is a Token Object + if is_token_object { + ck_try!(require_rw_session(h_session)); + } + // Validate object exists and belongs to this slot, check access rules. + ck_try!(object_store::with_object_for_slot(h_object, slot_id, |obj| { + // Private objects require login. + if object_store::is_private_object(obj) && !logged_in { + return Err(Pkcs11Error::UserNotLoggedIn); + } + Ok(()) + })); + ck_try!(object_store::destroy_object(h_object)); + CKR_OK +} + +// ── Struct-field attribute synthesis ───────────────────────────────────────── + +/// Return a synthesised attribute value for attributes that live as struct +/// fields on `KeyObject` rather than in the attributes HashMap. Returns +/// `None` for attributes not managed here (fall through to the HashMap). +fn key_object_field_attr(obj: &object_store::KeyObject, attr_type: CK_ATTRIBUTE_TYPE) -> Option> { + let bool_byte = |b: bool| vec![if b { CK_TRUE } else { CK_FALSE }]; + match attr_type { + CKA_LOCAL => Some(bool_byte(obj.local)), + CKA_ALWAYS_SENSITIVE => Some(bool_byte(obj.always_sensitive)), + CKA_NEVER_EXTRACTABLE => Some(bool_byte(obj.never_extractable)), + CKA_ALWAYS_AUTHENTICATE => Some(bool_byte(obj.always_authenticate)), + CKA_KEY_GEN_MECHANISM => Some(obj.key_gen_mechanism.to_le_bytes().to_vec()), + _ => None, + } +} + +// ── C_GetAttributeValue ─────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_GetAttributeValue( + h_session: CK_SESSION_HANDLE, + h_object: CK_OBJECT_HANDLE, + p_template: *mut CK_ATTRIBUTE, + ul_count: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_template.is_null() { return CKR_ARGUMENTS_BAD; } + let slot_id = ck_try!(session_slot(h_session)); + let attrs = std::slice::from_raw_parts_mut(p_template, ul_count as usize); + let mut any_too_small = false; + let mut any_sensitive = false; + let mut any_unavailable = false; + + ck_try!(object_store::with_object_for_slot(h_object, slot_id, |obj| { + for attr in attrs.iter_mut() { + // Access control: block CKA_VALUE for sensitive/non-extractable keys. + if let Err(Pkcs11Error::AttributeSensitive) = + attribute_policy::check_attribute_access(attr.r#type, obj) + { + attr.ulValueLen = CK_UNAVAILABLE_INFORMATION; + any_sensitive = true; + continue; + } + // Struct-field attributes — synthesised from KeyObject fields, not stored + // in the attributes HashMap. + let val_bytes: Vec = if let Some(v) = key_object_field_attr(obj, attr.r#type) { + v + } else if let Some(cached) = obj.attributes.get(&attr.r#type) { + // Primary path: pre-cached HashMap (no DER parsing). + cached.clone() + } else { + let is_private_key_object = obj.key_type == object_store::KeyType::RsaPrivate + || obj.attributes.get(&CKA_CLASS) + .map(|v| backend::bytes_to_ulong(v) == CKO_PRIVATE_KEY) + .unwrap_or(false); + if is_private_key_object && is_private_component_attr(attr.r#type) { + attr.ulValueLen = CK_UNAVAILABLE_INFORMATION; + any_sensitive = true; + continue; + } + // Fallback: ask the engine (parses key_der on demand). + match backend::get_attribute(obj.slot_id, obj, attr.r#type) { + Ok(bytes) => bytes, + Err(Pkcs11Error::AttributeSensitive) => { + attr.ulValueLen = CK_UNAVAILABLE_INFORMATION; + any_sensitive = true; + continue; + } + Err(Pkcs11Error::InvalidAttributeType) => { + let is_private_key_object = obj.key_type == object_store::KeyType::RsaPrivate + || obj.attributes.get(&CKA_CLASS) + .map(|v| backend::bytes_to_ulong(v) == CKO_PRIVATE_KEY) + .unwrap_or(false); + if is_private_key_object && is_private_component_attr(attr.r#type) { + attr.ulValueLen = CK_UNAVAILABLE_INFORMATION; + any_sensitive = true; + continue; + } + attr.ulValueLen = CK_UNAVAILABLE_INFORMATION; + any_unavailable = true; + continue; + } + Err(e) => return Err(e), + } + }; + + if attr.pValue.is_null() { + attr.ulValueLen = val_bytes.len() as CK_ULONG; + } else if (attr.ulValueLen as usize) < val_bytes.len() { + attr.ulValueLen = CK_UNAVAILABLE_INFORMATION; + any_too_small = true; + } else { + std::ptr::copy_nonoverlapping( + val_bytes.as_ptr(), attr.pValue as *mut u8, val_bytes.len(), + ); + attr.ulValueLen = val_bytes.len() as CK_ULONG; + } + } + Ok(()) + })); + + // PKCS#11 priority: sensitive > unavailable > buffer-too-small > ok + if any_sensitive { CKR_ATTRIBUTE_SENSITIVE } + else if any_unavailable { CKR_ATTRIBUTE_TYPE_INVALID } + else if any_too_small { CKR_BUFFER_TOO_SMALL } + else { CKR_OK } +} + +// ── C_SetAttributeValue ─────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_SetAttributeValue( + h_session: CK_SESSION_HANDLE, + h_object: CK_OBJECT_HANDLE, + p_template: *mut CK_ATTRIBUTE, + ul_count: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(require_rw_session(h_session)); + if p_template.is_null() { return CKR_ARGUMENTS_BAD; } + let attrs = std::slice::from_raw_parts(p_template, ul_count as usize); + let mut mutated_token_object = false; + ck_try!(object_store::with_object_mut(h_object, |obj| { + for attr in attrs { + if attr.pValue.is_null() { continue; } + let bytes = std::slice::from_raw_parts( + attr.pValue as *const u8, attr.ulValueLen as usize, + ); + // Enforce one-way ratchets and immutability rules. + let old_val = obj.attributes.get(&attr.r#type).map(|v| v.as_slice()); + attribute_policy::validate_attribute_change(attr.r#type, old_val, bytes)?; + // Apply the change. + obj.attributes.insert(attr.r#type, bytes.to_vec()); + // Keep always_sensitive / never_extractable in sync. + attribute_policy::update_derived_attributes(obj, attr.r#type); + } + // Gate persistence: only token objects are saved to disk. + mutated_token_object = !object_store::is_session_object(obj); + Ok(()) + })); + if mutated_token_object { + object_store::persist_to_disk(); + } + CKR_OK +} + +// ── Find objects ────────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_FindObjectsInit( + h_session: CK_SESSION_HANDLE, + p_template: *const CK_ATTRIBUTE, + ul_count: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + let (slot_id, logged_in) = ck_try!(session::with_session(h_session, |s| { + Ok((s.slot_id, s.login_state != LoginState::NotLoggedIn)) + })); + let template = ffi_api_crypto::collect_template_vec(p_template, ul_count); + let results = object_store::find_objects(slot_id, &template, logged_in); + ck_try!(session::with_session_mut(h_session, |s| { + if s.find_ctx.is_some() { return Err(Pkcs11Error::OperationActive); } + s.find_ctx = Some(FindContext { results, index: 0 }); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_FindObjects( + h_session: CK_SESSION_HANDLE, + ph_object: *mut CK_OBJECT_HANDLE, + ul_max_count: CK_ULONG, + pul_count: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if ph_object.is_null() || pul_count.is_null() { return CKR_ARGUMENTS_BAD; } + ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.find_ctx.as_mut().ok_or(Pkcs11Error::OperationNotInitialised)?; + let avail = ctx.results.len().saturating_sub(ctx.index); + let n = avail.min(ul_max_count as usize); + for i in 0..n { + *ph_object.add(i) = ctx.results[ctx.index + i]; + } + ctx.index += n; + *pul_count = n as CK_ULONG; + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub extern "C" fn C_FindObjectsFinal(h_session: CK_SESSION_HANDLE) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session_mut(h_session, |s| { + s.find_ctx = None; + Ok(()) + })); + CKR_OK +} diff --git a/src/pkcs11/ffi_api_core/lifecycle_and_slot_token.rs b/src/pkcs11/ffi_api_core/lifecycle_and_slot_token.rs new file mode 100644 index 0000000..23f47da --- /dev/null +++ b/src/pkcs11/ffi_api_core/lifecycle_and_slot_token.rs @@ -0,0 +1,407 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── C_Initialize / C_Finalize ───────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_Initialize(p_init_args: *mut CK_C_INITIALIZE_ARGS) -> CK_RV { + // Parse and validate the threading model. + // + // | CKF_OS_LOCKING_OK | Mutex callbacks | Action | + // |-------------------|-----------------|---------------------------------| + // | No | NULL | Single-threaded; accept. | + // | Yes | NULL | OS locking (parking_lot); accept.| + // | No | Non-NULL | App mutexes only; CKR_CANT_LOCK.| + // | Yes | Non-NULL | Prefer OS locking; ignore cbs. | + if !p_init_args.is_null() { + let args = &*p_init_args; + if !args.pReserved.is_null() { + return CKR_ARGUMENTS_BAD; + } + let has_callbacks = args.CreateMutex.is_some() + || args.DestroyMutex.is_some() + || args.LockMutex.is_some() + || args.UnlockMutex.is_some(); + let os_locking_ok = args.flags & CKF_OS_LOCKING_OK != 0; + if has_callbacks && !os_locking_ok { + // App-supplied mutexes without OS locking — we cannot use them (v1). + return CKR_CANT_LOCK; + } + // All other cases: we use parking_lot (OS-level locking) regardless. + } + + // Guard against double-initialization. + let mut guard = global().write().unwrap_or_else(|e| e.into_inner()); + if guard.is_some() { + return CKR_CRYPTOKI_ALREADY_INITIALIZED; + } + + // Register the OpenSSL engine and ensure a token exists for each slot. + let slot_ids = match crate::registry::register_engine(crate::openssl_provider::OpenSslEngine) { + Ok(ids) => ids, + Err(_) => crate::registry::slot_ids(), // already registered on re-init + }; + for &sid in &slot_ids { + token::ensure_token(sid); + } + + // Restore persisted token objects from disk. + object_store::load_persisted_objects(); + + // Ensure every slot advertises at least one profile. + // Profile objects are not persisted, so they must be re-created on each init. + for &sid in &slot_ids { + object_store::ensure_baseline_profile(sid); + } + + *guard = Some(GlobalState); + + // Register the post-fork child handler exactly once for the process lifetime. + // Must happen after `*guard = Some(...)` so the child handler sees an + // initialized library if fork occurs immediately after. + ATFORK_REGISTERED.call_once(|| { + unsafe { libc::pthread_atfork(None, None, Some(child_after_fork)); } + }); + + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_Finalize(p_reserved: *mut c_void) -> CK_RV { + // PKCS#11 pReserved must be NULL. + if !p_reserved.is_null() { + return CKR_ARGUMENTS_BAD; + } + let mut guard = global().write().unwrap_or_else(|e| e.into_inner()); + let state = match guard.as_mut() { + Some(s) => s, + None => return CKR_CRYPTOKI_NOT_INITIALIZED, + }; + state.shutdown(); + *guard = None; + CKR_OK +} + +// ── C_GetInfo ───────────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_GetInfo(p_info: *mut CK_INFO) -> CK_RV { + ck_try!(check_init()); + if p_info.is_null() { return CKR_ARGUMENTS_BAD; } + let info = &mut *p_info; + info.cryptokiVersion = CK_VERSION { major: 3, minor: 0 }; + fill_padded(&mut info.manufacturerID, b"Cryptoki"); + info.flags = 0; + fill_padded(&mut info.libraryDescription, b"Cryptoki v3.0"); + info.libraryVersion = CK_VERSION { major: 1, minor: 0 }; + CKR_OK +} + +// ── C_GetFunctionList ───────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_GetFunctionList( + ppFunctionList: *mut *const CK_FUNCTION_LIST, +) -> CK_RV { + if ppFunctionList.is_null() { return CKR_ARGUMENTS_BAD; } + *ppFunctionList = &FUNCTION_LIST; + CKR_OK +} + +// ── Slot / Token ────────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_GetSlotList( + _token_present: CK_BBOOL, + p_slot_list: *mut CK_SLOT_ID, + pul_count: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if pul_count.is_null() { return CKR_ARGUMENTS_BAD; } + let ids = crate::registry::slot_ids(); + let n = ids.len() as CK_ULONG; + if p_slot_list.is_null() { + *pul_count = n; + return CKR_OK; + } + if *pul_count < n { + *pul_count = n; + return CKR_BUFFER_TOO_SMALL; + } + for (i, &sid) in ids.iter().enumerate() { + *p_slot_list.add(i) = sid; + } + *pul_count = n; + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GetSlotInfo(slot_id: CK_SLOT_ID, p_info: *mut CK_SLOT_INFO) -> CK_RV { + ck_try!(check_init()); + let (engine, internal_id) = match crate::registry::engine_for_slot(slot_id) { + Ok(pair) => pair, + Err(_) => return CKR_SLOT_ID_INVALID, + }; + if p_info.is_null() { return CKR_ARGUMENTS_BAD; } + let info = &mut *p_info; + fill_padded(&mut info.slotDescription, engine.slot_description(internal_id).as_bytes()); + fill_padded(&mut info.manufacturerID, b"Cryptoki"); + info.flags = CKF_TOKEN_PRESENT; + info.hardwareVersion = CK_VERSION { major: 1, minor: 0 }; + info.firmwareVersion = CK_VERSION { major: 1, minor: 0 }; + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GetTokenInfo(slot_id: CK_SLOT_ID, p_info: *mut CK_TOKEN_INFO) -> CK_RV { + ck_try!(check_init()); + let (engine, internal_id) = match crate::registry::engine_for_slot(slot_id) { + Ok(pair) => pair, + Err(_) => return CKR_SLOT_ID_INVALID, + }; + if p_info.is_null() { return CKR_ARGUMENTS_BAD; } + let info = &mut *p_info; + token::with_token(slot_id, |tok| { + info.label = tok.label; + fill_padded(&mut info.manufacturerID, b"Cryptoki"); + fill_padded(&mut info.model, engine.token_model(internal_id).as_bytes()); + info.serialNumber = tok.serial_number; + info.flags = tok.token_flags(); + info.ulMaxSessionCount = CK_EFFECTIVELY_INFINITE; + info.ulSessionCount = session::session_count_for_slot(slot_id) as CK_ULONG; + info.ulMaxRwSessionCount = CK_EFFECTIVELY_INFINITE; + info.ulRwSessionCount = session::rw_session_count_for_slot(slot_id) as CK_ULONG; + info.ulMaxPinLen = tok.max_pin_len as CK_ULONG; + info.ulMinPinLen = tok.min_pin_len as CK_ULONG; + info.ulTotalPublicMemory = CK_ULONG::MAX; + info.ulFreePublicMemory = CK_ULONG::MAX; + info.ulTotalPrivateMemory = CK_ULONG::MAX; + info.ulFreePrivateMemory = CK_ULONG::MAX; + info.hardwareVersion = CK_VERSION { major: 1, minor: 0 }; + info.firmwareVersion = CK_VERSION { major: 1, minor: 0 }; + fill_padded(&mut info.utcTime, b"0000000000000000"); + }); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GetMechanismList( + slot_id: CK_SLOT_ID, + p_list: *mut CK_MECHANISM_TYPE, + pul_count: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + let (engine, internal_id) = match crate::registry::engine_for_slot(slot_id) { + Ok(pair) => pair, + Err(_) => return CKR_SLOT_ID_INVALID, + }; + if pul_count.is_null() { return CKR_ARGUMENTS_BAD; } + // Ask the engine first; fall back to global list for backward compat. + let engine_mechs = engine.supported_mechanisms(internal_id); + let base: &[CK_MECHANISM_TYPE] = if engine_mechs.is_empty() { SUPPORTED_MECHANISMS } else { engine_mechs }; + // Filter out Legacy (unless CRYPTOKI_LEGACY=1) and Forbidden mechanisms. + let mechs: Vec = base.iter() + .copied() + .filter(|&m| mechanisms::is_mechanism_allowed(m, None)) + .collect(); + if p_list.is_null() { + *pul_count = mechs.len() as CK_ULONG; + return CKR_OK; + } + if (*pul_count as usize) < mechs.len() { + *pul_count = mechs.len() as CK_ULONG; + return CKR_BUFFER_TOO_SMALL; + } + for (i, m) in mechs.iter().enumerate() { + *p_list.add(i) = *m; + } + *pul_count = mechs.len() as CK_ULONG; + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GetMechanismInfo( + slot_id: CK_SLOT_ID, + mech_type: CK_MECHANISM_TYPE, + p_info: *mut CK_MECHANISM_INFO, +) -> CK_RV { + ck_try!(check_init()); + let (engine, internal_id) = match crate::registry::engine_for_slot(slot_id) { + Ok(pair) => pair, + Err(_) => return CKR_SLOT_ID_INVALID, + }; + if p_info.is_null() { return CKR_ARGUMENTS_BAD; } + let engine_mechs = engine.supported_mechanisms(internal_id); + let mechs: &[CK_MECHANISM_TYPE] = if engine_mechs.is_empty() { SUPPORTED_MECHANISMS } else { engine_mechs }; + if !mechs.contains(&mech_type) { return CKR_MECHANISM_INVALID; } + // Reject legacy/forbidden mechanisms unless the env var opts in. + if !mechanisms::is_mechanism_allowed(mech_type, None) { return CKR_MECHANISM_INVALID; } + let info = &mut *p_info; + + // Try engine-sourced info first; fall back to hardcoded table for engines + // that do not implement mechanism_info(). + if let Some(eng_info) = engine.mechanism_info(internal_id as usize, mech_type) { + info.ulMinKeySize = eng_info.min_key_size as CK_ULONG; + info.ulMaxKeySize = eng_info.max_key_size as CK_ULONG; + info.flags = eng_info.flags as CK_FLAGS; + } else { + // Fallback hardcoded table — used when the engine returns None. + get_mechanism_info_fallback(mech_type, info); + } + CKR_OK +} + +/// True for mechanisms whose key operand is an RSA key, so that the RSA +/// minimum-key-size policy (≥ 2048 bits) is applied to engine-reported +/// values. +fn is_rsa_key_mechanism(mech: CK_MECHANISM_TYPE) -> bool { + matches!( + mech, + CKM_RSA_PKCS_KEY_PAIR_GEN + | CKM_RSA_PKCS + | CKM_RSA_PKCS_OAEP + | CKM_SHA1_RSA_PKCS + | CKM_SHA1_RSA_PKCS_PSS + | CKM_SHA256_RSA_PKCS + | CKM_SHA384_RSA_PKCS + | CKM_SHA512_RSA_PKCS + | CKM_SHA256_RSA_PKCS_PSS + | CKM_SHA384_RSA_PKCS_PSS + | CKM_SHA512_RSA_PKCS_PSS + ) +} + +/// Hardcoded mechanism info table — fallback for engines that do not implement +/// `CryptoProvider::mechanism_info()`. +fn get_mechanism_info_fallback(mech_type: CK_MECHANISM_TYPE, info: &mut CK_MECHANISM_INFO) { + match mech_type { + CKM_RSA_PKCS_KEY_PAIR_GEN => { + info.ulMinKeySize = 1024; + info.ulMaxKeySize = 16384; + info.flags = CKF_GENERATE_KEY_PAIR; + } + CKM_RSA_PKCS => { + info.ulMinKeySize = 1024; + info.ulMaxKeySize = 16384; + info.flags = CKF_ENCRYPT | CKF_DECRYPT | CKF_SIGN | CKF_VERIFY; + } + CKM_RSA_PKCS_OAEP => { + info.ulMinKeySize = 1024; + info.ulMaxKeySize = 16384; + info.flags = CKF_ENCRYPT | CKF_DECRYPT | CKF_WRAP | CKF_UNWRAP; + } + CKM_SHA1_RSA_PKCS | CKM_SHA1_RSA_PKCS_PSS + | CKM_SHA256_RSA_PKCS | CKM_SHA384_RSA_PKCS | CKM_SHA512_RSA_PKCS + | CKM_SHA256_RSA_PKCS_PSS | CKM_SHA384_RSA_PKCS_PSS | CKM_SHA512_RSA_PKCS_PSS => { + info.ulMinKeySize = 1024; + info.ulMaxKeySize = 16384; + info.flags = CKF_SIGN | CKF_VERIFY; + } + CKM_EC_KEY_PAIR_GEN => { + info.ulMinKeySize = 256; info.ulMaxKeySize = 521; + info.flags = CKF_GENERATE_KEY_PAIR; + } + CKM_ECDSA | CKM_ECDSA_SHA256 | CKM_ECDSA_SHA384 | CKM_ECDSA_SHA512 => { + info.ulMinKeySize = 256; info.ulMaxKeySize = 521; + info.flags = CKF_SIGN | CKF_VERIFY; + } + CKM_ECDH1_DERIVE => { + info.ulMinKeySize = 256; info.ulMaxKeySize = 521; + info.flags = CKF_DERIVE; + } + CKM_EC_EDWARDS_KEY_PAIR_GEN => { + info.ulMinKeySize = 255; info.ulMaxKeySize = 448; + info.flags = CKF_GENERATE_KEY_PAIR; + } + CKM_EDDSA => { + info.ulMinKeySize = 255; info.ulMaxKeySize = 448; + info.flags = CKF_SIGN | CKF_VERIFY; + } + CKM_AES_KEY_GEN | CKM_DES_KEY_GEN | CKM_DES3_KEY_GEN => { + info.ulMinKeySize = 16; info.ulMaxKeySize = 32; + info.flags = CKF_GENERATE; + } + CKM_AES_ECB | CKM_AES_CBC | CKM_AES_CBC_PAD | CKM_DES_ECB | CKM_DES_CBC | CKM_DES3_ECB | CKM_DES3_CBC => { + info.ulMinKeySize = 16; info.ulMaxKeySize = 32; + info.flags = CKF_ENCRYPT | CKF_DECRYPT; + } + CKM_AES_CTR | CKM_AES_GCM => { + info.ulMinKeySize = 16; info.ulMaxKeySize = 32; + info.flags = CKF_ENCRYPT | CKF_DECRYPT; + } + CKM_AES_KEY_WRAP => { + info.ulMinKeySize = 16; info.ulMaxKeySize = 32; + info.flags = CKF_WRAP | CKF_UNWRAP; + } + CKM_CHACHA20_KEY_GEN => { + info.ulMinKeySize = 32; info.ulMaxKeySize = 32; + info.flags = CKF_GENERATE; + } + CKM_CHACHA20_POLY1305 => { + info.ulMinKeySize = 32; info.ulMaxKeySize = 32; + info.flags = CKF_ENCRYPT | CKF_DECRYPT; + } + CKM_MD5 | CKM_SHA_1 | CKM_SHA256 | CKM_SHA384 | CKM_SHA512 + | CKM_SHA3_256 | CKM_SHA3_384 | CKM_SHA3_512 => { + info.ulMinKeySize = 0; info.ulMaxKeySize = 0; + info.flags = CKF_DIGEST; + } + CKM_HKDF_DERIVE => { + info.ulMinKeySize = 0; info.ulMaxKeySize = 0; + info.flags = CKF_DERIVE; + } + CKM_HKDF_KEY_GEN => { + info.ulMinKeySize = 0; info.ulMaxKeySize = 0; + info.flags = CKF_GENERATE; + } + _ => { + info.ulMinKeySize = 0; info.ulMaxKeySize = 0; + info.flags = 0; + } + } +} + +// ── C_InitToken ────────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_InitToken( + slot_id: CK_SLOT_ID, + p_pin: *const CK_UTF8CHAR, + ul_pin_len: CK_ULONG, + p_label: *const CK_UTF8CHAR, +) -> CK_RV { + ck_try!(check_init()); + if !crate::registry::is_valid_slot(slot_id) { return CKR_SLOT_ID_INVALID; } + if p_pin.is_null() || p_label.is_null() { return CKR_ARGUMENTS_BAD; } + if session::has_open_sessions(slot_id) { + return CKR_SESSION_EXISTS; + } + let pin = std::slice::from_raw_parts(p_pin, ul_pin_len as usize); + let label: [CK_UTF8CHAR; 32] = { + let mut buf = [b' '; 32]; + let src = std::slice::from_raw_parts(p_label, 32); + buf.copy_from_slice(src); + buf + }; + + // Verify SO PIN (if already init) and update SO PIN in RAM + ck_try!(token::with_token_mut(slot_id, |tok| tok.init_token(pin, &label))); + + // Wipe objects in RAM + object_store::clear_objects_for_slot(slot_id); + object_store::ensure_baseline_profile(slot_id); + + object_store::persist_to_disk(); + CKR_OK +} diff --git a/src/pkcs11/ffi_api_core/session_and_login.rs b/src/pkcs11/ffi_api_core/session_and_login.rs new file mode 100644 index 0000000..6ab1321 --- /dev/null +++ b/src/pkcs11/ffi_api_core/session_and_login.rs @@ -0,0 +1,249 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── C_InitPIN / C_SetPIN ───────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_InitPIN( + h_session: CK_SESSION_HANDLE, + p_pin: *const CK_UTF8CHAR, + ul_pin_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(require_rw_session(h_session)); + // Caller must be SO logged in. + let slot_id = ck_try!(session::with_session(h_session, |s| { + if s.login_state != session::LoginState::SoLoggedIn { + return Err(Pkcs11Error::UserNotLoggedIn); + } + Ok(s.slot_id) + })); + let pin = if p_pin.is_null() || ul_pin_len == 0 { + &[] + } else { + std::slice::from_raw_parts(p_pin, ul_pin_len as usize) + }; + ck_try!(token::with_token_mut(slot_id, |tok| tok.init_pin(pin))); + object_store::persist_to_disk(); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_SetPIN( + h_session: CK_SESSION_HANDLE, + p_old_pin: *const CK_UTF8CHAR, + ul_old_pin_len: CK_ULONG, + p_new_pin: *const CK_UTF8CHAR, + ul_new_pin_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(require_rw_session(h_session)); + let old_pin = if p_old_pin.is_null() { &[] as &[u8] } + else { std::slice::from_raw_parts(p_old_pin, ul_old_pin_len as usize) }; + let new_pin = if p_new_pin.is_null() { &[] as &[u8] } + else { std::slice::from_raw_parts(p_new_pin, ul_new_pin_len as usize) }; + let (user_type, slot_id) = ck_try!(session::with_session(h_session, |s| { + match s.login_state { + session::LoginState::SoLoggedIn => Ok((CKU_SO, s.slot_id)), + session::LoginState::UserLoggedIn => Ok((CKU_USER, s.slot_id)), + // If not logged in, C_SetPIN defaults to changing the User PIN + session::LoginState::NotLoggedIn => Ok((CKU_USER, s.slot_id)), } + })); + // Verify old PIN with failure counting, then set the new PIN. + ck_try!(token::with_token_mut(slot_id, |tok| { + if let Err(err) = tok.verify_pin(user_type, old_pin) { + if user_type != CKU_USER || tok.verify_pin(user_type, new_pin).is_err() { + return Err(err); + } + } + tok.set_pin(user_type, new_pin) + })); + object_store::persist_to_disk(); + + ck_try!(session::with_session_mut(h_session, |s| { + s.context_specific_authed = false; + Ok(()) + })); + + CKR_OK +} + +// ── Session management ──────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_OpenSession( + slot_id: CK_SLOT_ID, + flags: CK_FLAGS, + _p_application: *mut c_void, + _notify: CK_NOTIFY, + ph_session: *mut CK_SESSION_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if !crate::registry::is_valid_slot(slot_id) { return CKR_SLOT_ID_INVALID; } + if ph_session.is_null() { return CKR_ARGUMENTS_BAD; } + *ph_session = ck_try!(session::open_session(slot_id, flags)); + CKR_OK +} + +#[no_mangle] +pub extern "C" fn C_CloseSession(h_session: CK_SESSION_HANDLE) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::close_session(h_session)); + // Destroy session objects owned by the closing session. + object_store::destroy_objects_for_session(h_session); + CKR_OK +} + +#[no_mangle] +pub extern "C" fn C_CloseAllSessions(slot_id: CK_SLOT_ID) -> CK_RV { + ck_try!(check_init()); + if !crate::registry::is_valid_slot(slot_id) { + return CKR_SLOT_ID_INVALID; + } + session::close_all_sessions(slot_id); + // All sessions gone — destroy all session objects on this slot. + object_store::destroy_session_objects_for_slot(slot_id); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GetSessionInfo( + h_session: CK_SESSION_HANDLE, + p_info: *mut CK_SESSION_INFO, +) -> CK_RV { + ck_try!(check_init()); + if p_info.is_null() { return CKR_ARGUMENTS_BAD; } + *p_info = ck_try!(session::get_session_info(h_session)); + CKR_OK +} + +// ── Login / Logout ──────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_Login( + h_session: CK_SESSION_HANDLE, + user_type: CK_USER_TYPE, + p_pin: *const CK_UTF8CHAR, + ul_pin_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + + // FFI validation to prevent silent logic bugs + if p_pin.is_null() && ul_pin_len != 0 { + return CKR_ARGUMENTS_BAD; + } + let pin: &[u8] = if p_pin.is_null() || ul_pin_len == 0 { + &[] + } else { + std::slice::from_raw_parts(p_pin, ul_pin_len as usize) + }; + let slot_id = ck_try!(session::with_session(h_session, |s| Ok(s.slot_id))); + + // CKU_CONTEXT_SPECIFIC: per-operation re-authentication for CKA_ALWAYS_AUTHENTICATE keys. + if user_type == CKU_CONTEXT_SPECIFIC { + // Must already be logged in as user. + let current = session::login_state_for_slot(slot_id); + if current != LoginState::UserLoggedIn { + return CKR_USER_NOT_LOGGED_IN; + } + + // Verify PIN without touching lockout counters — context-specific auth + // is per-operation re-auth; wrong attempts must not lock the user login. + ck_try!(token::with_token(slot_id, |tok| tok.verify_user_pin_no_lockout(pin))); + ck_try!(session::with_session_mut(h_session, |s| { + // Check if any operation requiring a private/secret key is actually active + let is_op_active = s.sign_ctx.is_some() + || s.msg_sign_ctx.is_some() + || s.decrypt_ctx.is_some() + || s.msg_decrypt_ctx.is_some() + || s.encrypt_ctx.is_some() + || s.msg_encrypt_ctx.is_some(); + + if !is_op_active { + return Err(Pkcs11Error::OperationNotInitialised); + } + s.context_specific_authed = true; + Ok(()) + })); + return CKR_OK; + } + + // Validate user type early. + let new_state = match user_type { + CKU_USER => LoginState::UserLoggedIn, + CKU_SO => LoginState::SoLoggedIn, + _ => return CKR_USER_TYPE_INVALID, + }; + + // Check if already logged in on this token (any session). + let current = session::login_state_for_slot(slot_id); + if current != LoginState::NotLoggedIn { + // Distinguish same-user vs different-user (PKCS#11). + if (current == LoginState::UserLoggedIn && user_type == CKU_USER) + || (current == LoginState::SoLoggedIn && user_type == CKU_SO) + { + return CKR_USER_ALREADY_LOGGED_IN; + } + return CKR_USER_ANOTHER_ALREADY_LOGGED_IN; + } + + // SO login requires no RO sessions exist on this token. + if user_type == CKU_SO && session::has_ro_sessions_on_slot(slot_id) { + return CKR_SESSION_READ_ONLY_EXISTS; + } + + // User login requires user PIN to have been initialized. + if user_type == CKU_USER { + let pin_init = token::with_token(slot_id, |tok| tok.user_pin.is_some()); + if !pin_init { + return CKR_USER_PIN_NOT_INITIALIZED; + } + } + + // Verify PIN against the token (mutable for failure counter tracking). + ck_try!(token::with_token_mut(slot_id, |tok| tok.verify_pin(user_type, pin))); + + ck_try!(session::with_session_mut(h_session, |s| { + s.context_specific_authed = false; + Ok(()) + })); + + // Propagate login state to ALL sessions on this token. + session::login_all_sessions_on_slot(slot_id, new_state); + CKR_OK +} + +#[no_mangle] +pub extern "C" fn C_Logout(h_session: CK_SESSION_HANDLE) -> CK_RV { + ck_try!(check_init()); + let slot_id = ck_try!(session::with_session(h_session, |s| Ok(s.slot_id))); + + // Check that we are actually logged in on this token. + let current = session::login_state_for_slot(slot_id); + if current == LoginState::NotLoggedIn { + return CKR_USER_NOT_LOGGED_IN; + } + + // Release active find-object contexts on every session for this slot, + // since object visibility changes after logout. Crypto operations are left + // intact — they will fail naturally if they depend on login state. + session::release_find_contexts_on_slot(slot_id); + + // Destroy private session objects on this slot. + object_store::destroy_private_session_objects(slot_id); + + // Reset login state for ALL sessions on this token. + session::login_all_sessions_on_slot(slot_id, LoginState::NotLoggedIn); + CKR_OK +} diff --git a/src/pkcs11/ffi_api_crypto.rs b/src/pkcs11/ffi_api_crypto.rs new file mode 100644 index 0000000..0fd5d40 --- /dev/null +++ b/src/pkcs11/ffi_api_crypto.rs @@ -0,0 +1,18 @@ +//! Hub for PKCS#11 crypto operation APIs. +//! +//! Owns sign/verify, cipher, digest, wrap/derive, and legacy v2.40 operation handlers. +use super::*; + +mod sign_verify; +mod encrypt_decrypt; +mod digest; +mod key_wrap_derive; +mod helpers; +mod misc_v240; + +pub use sign_verify::*; +pub use encrypt_decrypt::*; +pub use digest::*; +pub use key_wrap_derive::*; +pub(crate) use helpers::{collect_template, collect_template_vec, extract_cipher_params}; +pub use misc_v240::*; diff --git a/src/pkcs11/ffi_api_crypto/digest.rs b/src/pkcs11/ffi_api_crypto/digest.rs new file mode 100644 index 0000000..3a40857 --- /dev/null +++ b/src/pkcs11/ffi_api_crypto/digest.rs @@ -0,0 +1,148 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── Digest ──────────────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_DigestInit( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() { return CKR_ARGUMENTS_BAD; } + let mech_type = (*p_mechanism).mechanism; + match mech_type { + CKM_MD5 | CKM_SHA_1 | CKM_SHA256 | CKM_SHA384 | CKM_SHA512 | + CKM_SHA3_256 | CKM_SHA3_384 | CKM_SHA3_512 => { + /* valid */ + }, + _ => return CKR_MECHANISM_INVALID, + } + ck_try!(session::with_session_mut(h_session, |s| { + if s.digest_ctx.is_some() { return Err(Pkcs11Error::OperationActive); } + s.digest_ctx = Some(DigestContext { + mechanism: mech_type, + data: Vec::new(), + is_single_part: false, + is_multi_part: false, + }); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_Digest( + h_session: CK_SESSION_HANDLE, + p_data: *const CK_BYTE, + ul_data_len: CK_ULONG, + p_digest: *mut CK_BYTE, + pul_digest_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if pul_digest_len.is_null() { + let _ = session::with_session_mut(h_session, |s| { + s.digest_ctx.take(); + Ok(()) + }); + return CKR_ARGUMENTS_BAD; + } + if p_data.is_null() && ul_data_len > 0 { return CKR_ARGUMENTS_BAD; } + + // 1. Peek at context without destroying it + let (mech, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.digest_ctx.as_mut().ok_or(Pkcs11Error::OperationNotInitialised)?; + if ctx.is_multi_part { + s.digest_ctx.take(); + return Err(Pkcs11Error::OperationActive); + } + ctx.is_single_part = true; + Ok((ctx.mechanism, s.slot_id)) + })); + + let data = if ul_data_len > 0 { std::slice::from_raw_parts(p_data, ul_data_len as usize) } else { &[] }; + let hash = ck_try!(backend::digest(slot_id, mech, data)); + let rv = write_to_output(p_digest, pul_digest_len, &hash); + + // 2. ONLY consume context if the operation succeeded AND the buffer was written to + if rv == CKR_OK && !p_digest.is_null() { + let _ = session::with_session_mut(h_session, |s| { + s.digest_ctx.take(); + Ok(()) + }); + } + rv +} + +#[no_mangle] +pub unsafe extern "C" fn C_DigestUpdate( + h_session: CK_SESSION_HANDLE, + p_part: *const CK_BYTE, + ul_part_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_part.is_null() && ul_part_len > 0 { return CKR_ARGUMENTS_BAD; } + + ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.digest_ctx.as_mut().ok_or(Pkcs11Error::OperationNotInitialised)?; + if ctx.is_single_part { return Err(Pkcs11Error::OperationActive); } + ctx.is_multi_part = true; + + if ul_part_len > 0 { + let part = std::slice::from_raw_parts(p_part, ul_part_len as usize); + ctx.data.extend_from_slice(part); + } + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_DigestFinal( + h_session: CK_SESSION_HANDLE, + p_digest: *mut CK_BYTE, + pul_digest_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if pul_digest_len.is_null() { + let _ = session::with_session_mut(h_session, |s| { + s.digest_ctx.take(); + Ok(()) + }); + return CKR_ARGUMENTS_BAD; + } + + // 1. Peek at context without destroying it + let (mech, data, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.digest_ctx.as_mut().ok_or(Pkcs11Error::OperationNotInitialised)?; + if ctx.is_single_part { + s.digest_ctx.take(); + return Err(Pkcs11Error::OperationActive); + } + ctx.is_multi_part = true; + Ok((ctx.mechanism, ctx.data.clone(), s.slot_id)) + })); + + let hash = ck_try!(backend::digest(slot_id, mech, &data)); + let rv = write_to_output(p_digest, pul_digest_len, &hash); + + // 2. ONLY consume context if the operation succeeded AND the buffer was written to + if rv == CKR_OK && !p_digest.is_null() { + let _ = session::with_session_mut(h_session, |s| { + s.digest_ctx.take(); + Ok(()) + }); + } + rv +} diff --git a/src/pkcs11/ffi_api_crypto/encrypt_decrypt.rs b/src/pkcs11/ffi_api_crypto/encrypt_decrypt.rs new file mode 100644 index 0000000..d81688c --- /dev/null +++ b/src/pkcs11/ffi_api_crypto/encrypt_decrypt.rs @@ -0,0 +1,237 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── Encrypt ─────────────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_EncryptInit( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + h_key: CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() { return CKR_ARGUMENTS_BAD; } + let mech = &*p_mechanism; + let (iv, aad, tag_len) = extract_cipher_params(mech); + ck_try!(session::with_session_mut(h_session, |s| { + if s.encrypt_ctx.is_some() { return Err(Pkcs11Error::OperationActive); } + s.encrypt_ctx = Some(CipherContext { + mechanism: mech.mechanism, key_handle: h_key, + iv: Some(iv), aad, tag_len, accumulated: Vec::new(), + }); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_Encrypt( + h_session: CK_SESSION_HANDLE, + p_data: *const CK_BYTE, + ul_data_len: CK_ULONG, + p_encrypted: *mut CK_BYTE, + pul_encrypted_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_data.is_null() || pul_encrypted_len.is_null() { return CKR_ARGUMENTS_BAD; } + let data = std::slice::from_raw_parts(p_data, ul_data_len as usize); + let (ctx, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.encrypt_ctx.take().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok((ctx, s.slot_id)) + })); + let ct = if backend::is_rsa_enc_mechanism(ctx.mechanism) { + ck_try!(with_object(ctx.key_handle, |obj| { + backend::rsa_encrypt(slot_id, ctx.mechanism, obj, data) + })) + } else { + let iv = ctx.iv.as_deref().unwrap_or(&[]); + let aad = ctx.aad.as_deref(); + ck_try!(with_object(ctx.key_handle, |obj| { + backend::encrypt_symmetric(slot_id, ctx.mechanism, obj, iv, aad, data) + })) + }; + write_to_output(p_encrypted, pul_encrypted_len, &ct) +} + +#[no_mangle] +pub unsafe extern "C" fn C_EncryptUpdate( + h_session: CK_SESSION_HANDLE, + p_part: *const CK_BYTE, + ul_part_len: CK_ULONG, + _p_encrypted_part: *mut CK_BYTE, + pul_encrypted_part_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_part.is_null() { return CKR_ARGUMENTS_BAD; } + let part = std::slice::from_raw_parts(p_part, ul_part_len as usize); + ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.encrypt_ctx.as_mut().ok_or(Pkcs11Error::OperationNotInitialised)?; + ctx.accumulated.extend_from_slice(part); + Ok(()) + })); + if !pul_encrypted_part_len.is_null() { *pul_encrypted_part_len = 0; } + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_EncryptFinal( + h_session: CK_SESSION_HANDLE, + p_last_part: *mut CK_BYTE, + pul_last_part_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if pul_last_part_len.is_null() { return CKR_ARGUMENTS_BAD; } + let (ctx, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.encrypt_ctx.take().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok((ctx, s.slot_id)) + })); + let iv = ctx.iv.as_deref().unwrap_or(&[]); + let aad = ctx.aad.as_deref(); + let ct = ck_try!(with_object(ctx.key_handle, |obj| { + backend::encrypt_symmetric(slot_id, ctx.mechanism, obj, iv, aad, &ctx.accumulated) + })); + write_to_output(p_last_part, pul_last_part_len, &ct) +} + +// ── Decrypt ─────────────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_DecryptInit( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + h_key: CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() { return CKR_ARGUMENTS_BAD; } + let mech = &*p_mechanism; + let (iv, aad, tag_len) = extract_cipher_params(mech); + ck_try!(session::with_session_mut(h_session, |s| { + if s.decrypt_ctx.is_some() { return Err(Pkcs11Error::OperationActive); } + // Wipe any old ghost tickets. + s.context_specific_authed = false; + s.decrypt_ctx = Some(CipherContext { + mechanism: mech.mechanism, key_handle: h_key, + iv: Some(iv), aad, tag_len, accumulated: Vec::new(), + }); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_Decrypt( + h_session: CK_SESSION_HANDLE, + p_encrypted: *const CK_BYTE, + ul_enc_len: CK_ULONG, + p_data: *mut CK_BYTE, + pul_data_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_encrypted.is_null() || pul_data_len.is_null() { return CKR_ARGUMENTS_BAD; } + + let is_length_req = p_data.is_null(); + let ct = std::slice::from_raw_parts(p_encrypted, ul_enc_len as usize); + + let (ctx, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.decrypt_ctx.take().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok((ctx, s.slot_id)) + })); + + // Gate on CKA_ALWAYS_AUTHENTICATE before the engine call. + ck_try!(with_object(ctx.key_handle, |obj| { + session::with_session_mut(h_session, |s| s.require_context_auth(obj)) + })); + + let pt = if backend::is_rsa_enc_mechanism(ctx.mechanism) { + ck_try!(with_object(ctx.key_handle, |obj| { + backend::rsa_decrypt(slot_id, ctx.mechanism, obj, ct) + })) + } else { + let iv = ctx.iv.as_deref().unwrap_or(&[]); + let aad = ctx.aad.as_deref(); + ck_try!(with_object(ctx.key_handle, |obj| { + backend::decrypt_symmetric(slot_id, ctx.mechanism, obj, iv, aad, ct, ctx.tag_len) + })) + }; + + // Restore the context OR consume the ticket. + ck_try!(session::with_session_mut(h_session, |s| { + if is_length_req { + s.decrypt_ctx = Some(ctx); // It was a length request, put it back! + } else { + s.context_specific_authed = false; // Real call done, burn the ticket. + } + Ok(()) + })); + write_to_output(p_data, pul_data_len, &pt) +} + +#[no_mangle] +pub unsafe extern "C" fn C_DecryptUpdate( + h_session: CK_SESSION_HANDLE, + p_encrypted_part: *const CK_BYTE, + ul_enc_part_len: CK_ULONG, + _p_part: *mut CK_BYTE, + pul_part_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_encrypted_part.is_null() { return CKR_ARGUMENTS_BAD; } + let part = std::slice::from_raw_parts(p_encrypted_part, ul_enc_part_len as usize); + ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.decrypt_ctx.as_mut().ok_or(Pkcs11Error::OperationNotInitialised)?; + ctx.accumulated.extend_from_slice(part); + Ok(()) + })); + if !pul_part_len.is_null() { *pul_part_len = 0; } + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_DecryptFinal( + h_session: CK_SESSION_HANDLE, + p_last_part: *mut CK_BYTE, + pul_last_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if pul_last_len.is_null() { return CKR_ARGUMENTS_BAD; } + let is_length_req = p_last_part.is_null(); + + let (ctx, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.decrypt_ctx.take().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok((ctx, s.slot_id)) + })); + + // Check CKA_ALWAYS_AUTHENTICATE + ck_try!(with_object(ctx.key_handle, |obj| { + session::with_session_mut(h_session, |s| s.require_context_auth(obj)) + })); + + let iv = ctx.iv.as_deref().unwrap_or(&[]); + let aad = ctx.aad.as_deref(); + let pt = ck_try!(with_object(ctx.key_handle, |obj| { + backend::decrypt_symmetric(slot_id, ctx.mechanism, obj, iv, aad, &ctx.accumulated, ctx.tag_len) + })); + + // put the context back if it's a length request! + ck_try!(session::with_session_mut(h_session, |s| { + if is_length_req { + s.decrypt_ctx = Some(ctx); + } else { + s.context_specific_authed = false; + } + Ok(()) + })); + + write_to_output(p_last_part, pul_last_len, &pt) +} diff --git a/src/pkcs11/ffi_api_crypto/helpers.rs b/src/pkcs11/ffi_api_crypto/helpers.rs new file mode 100644 index 0000000..d81b1bc --- /dev/null +++ b/src/pkcs11/ffi_api_crypto/helpers.rs @@ -0,0 +1,113 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── Private helpers ─────────────────────────────────────────────────────── + +pub(crate) unsafe fn collect_template( + p_template: *const CK_ATTRIBUTE, + ul_count: CK_ULONG, +) -> HashMap> { + let mut map = HashMap::new(); + if p_template.is_null() { return map; } + let attrs = std::slice::from_raw_parts(p_template, ul_count as usize); + for attr in attrs { + if !attr.pValue.is_null() && attr.ulValueLen > 0 { + let bytes = std::slice::from_raw_parts( + attr.pValue as *const u8, attr.ulValueLen as usize, + ); + map.insert(attr.r#type, bytes.to_vec()); + } else if attr.ulValueLen == 0 { + map.insert(attr.r#type, vec![]); + } + } + map +} + +pub(crate) unsafe fn collect_template_vec( + p_template: *const CK_ATTRIBUTE, + ul_count: CK_ULONG, +) -> Vec<(CK_ATTRIBUTE_TYPE, Vec)> { + if p_template.is_null() { return Vec::new(); } + let attrs = std::slice::from_raw_parts(p_template, ul_count as usize); + let mut out = Vec::with_capacity(attrs.len()); + for attr in attrs { + if !attr.pValue.is_null() && attr.ulValueLen > 0 { + let bytes = std::slice::from_raw_parts( + attr.pValue as *const u8, attr.ulValueLen as usize, + ); + out.push((attr.r#type, bytes.to_vec())); + } else if attr.ulValueLen == 0 { + out.push((attr.r#type, vec![])); + } + } + out +} + +pub(crate) unsafe fn extract_cipher_params(mech: &CK_MECHANISM) -> (Vec, Option>, usize) { + match mech.mechanism { + CKM_AES_CBC | CKM_AES_CBC_PAD => { + let iv = if !mech.pParameter.is_null() && mech.ulParameterLen >= 16 { + std::slice::from_raw_parts(mech.pParameter as *const u8, 16).to_vec() + } else { + vec![0u8; 16] + }; + (iv, None, 0) + } + CKM_DES_CBC | CKM_DES3_CBC => { + let iv = if !mech.pParameter.is_null() && mech.ulParameterLen >= 8 { + std::slice::from_raw_parts(mech.pParameter as *const u8, 8).to_vec() + } else { + vec![0u8; 8] + }; + (iv, None, 0) + } + CKM_AES_GCM => { + if mech.pParameter.is_null() { + return (vec![0u8; 12], None, 16); + } + let p = &*(mech.pParameter as *const CK_GCM_PARAMS); + let iv = if !p.pIv.is_null() && p.ulIvLen > 0 { + std::slice::from_raw_parts(p.pIv, p.ulIvLen as usize).to_vec() + } else { + vec![0u8; 12] + }; + let aad = if !p.pAAD.is_null() && p.ulAADLen > 0 { + Some(std::slice::from_raw_parts(p.pAAD, p.ulAADLen as usize).to_vec()) + } else { + None + }; + let tag_len = (p.ulTagBits / 8) as usize; + (iv, aad, if tag_len == 0 { 16 } else { tag_len }) + } + CKM_CHACHA20_POLY1305 => { + // Reuse GCM_PARAMS structure for nonce/AAD (common pattern) + if mech.pParameter.is_null() { + return (vec![0u8; 12], None, 16); + } + let p = &*(mech.pParameter as *const CK_GCM_PARAMS); + let nonce = if !p.pIv.is_null() && p.ulIvLen > 0 { + std::slice::from_raw_parts(p.pIv, p.ulIvLen as usize).to_vec() + } else { + vec![0u8; 12] + }; + let aad = if !p.pAAD.is_null() && p.ulAADLen > 0 { + Some(std::slice::from_raw_parts(p.pAAD, p.ulAADLen as usize).to_vec()) + } else { + None + }; + (nonce, aad, 16) + } + _ => (Vec::new(), None, 0), + } +} diff --git a/src/pkcs11/ffi_api_crypto/key_wrap_derive.rs b/src/pkcs11/ffi_api_crypto/key_wrap_derive.rs new file mode 100644 index 0000000..b95569c --- /dev/null +++ b/src/pkcs11/ffi_api_crypto/key_wrap_derive.rs @@ -0,0 +1,272 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── C_WrapKey ───────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_WrapKey( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + h_wrapping_key: CK_OBJECT_HANDLE, + h_key: CK_OBJECT_HANDLE, + p_wrapped_key: *mut CK_BYTE, + pul_wrapped_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() || pul_wrapped_len.is_null() { + return CKR_ARGUMENTS_BAD; + } + let slot_id = ck_try!(session_slot(h_session)); + + let is_length_req = p_wrapped_key.is_null(); + let mech = &*p_mechanism; + if matches!(mech.mechanism, CKM_DES_ECB | CKM_DES_CBC | CKM_DES3_ECB | CKM_DES3_CBC) { + return CKR_FUNCTION_NOT_SUPPORTED; + } + + // Access control checks. + // Each check uses a separate with_object call so the object-store read lock + // is released between acquisitions. Nesting with_object calls would deadlock + // under parking_lot's writer-preferring RwLock when another thread is waiting + // to store a new object (write lock). + + // 1. Wrapping key must have CKA_WRAP == TRUE. + let wrap_ok = match object_store::with_object_for_slot(h_wrapping_key, slot_id, |obj| Ok(bool_attr_true(obj, CKA_WRAP))) { + Ok(v) => v, + Err(Pkcs11Error::InvalidObjectHandle) => return CKR_WRAPPING_KEY_HANDLE_INVALID, + Err(e) => return e.to_ckr(), + }; + if !wrap_ok { return CKR_KEY_FUNCTION_NOT_PERMITTED; } + + // 2. Target key must have CKA_EXTRACTABLE == TRUE. + let extractable = match object_store::with_object_for_slot(h_key, slot_id, |obj| Ok(bool_attr_true(obj, CKA_EXTRACTABLE))) { + Ok(v) => v, + Err(Pkcs11Error::InvalidObjectHandle) => return CKR_KEY_HANDLE_INVALID, + Err(e) => return e.to_ckr(), + }; + if !extractable { return CKR_KEY_UNEXTRACTABLE; } + + // 3. If target has CKA_WRAP_WITH_TRUSTED == TRUE, wrapping key must have CKA_TRUSTED == TRUE. + let wrap_with_trusted = ck_try!(object_store::with_object_for_slot(h_key, slot_id, |obj| Ok(bool_attr_true(obj, CKA_WRAP_WITH_TRUSTED)))); + if wrap_with_trusted { + let trusted = ck_try!(object_store::with_object_for_slot(h_wrapping_key, slot_id, |obj| Ok(bool_attr_true(obj, CKA_TRUSTED)))); + if !trusted { return CKR_KEY_NOT_WRAPPABLE; } + } + + let wrapped_bytes = match mech.mechanism { + CKM_AES_KEY_WRAP => { + // Clone key refs out of the store before the engine call so we do not + // hold a read lock during the (potentially slow) crypto operation and + // avoid any nested-lock scenario. + let wrap_ref = ck_try!(object_store::with_object_for_slot(h_wrapping_key, slot_id, |obj| Ok(obj.key_ref.clone()))); + let target_ref = ck_try!(object_store::with_object_for_slot(h_key, slot_id, |obj| Ok(obj.key_ref.clone()))); + ck_try!(backend::aes_wrap_key_refs(slot_id, &wrap_ref, &target_ref)) + } + _ => return CKR_MECHANISM_INVALID, + }; + + if !is_length_req { + ck_try!(session::with_session_mut(h_session, |s| { + s.context_specific_authed = false; + Ok(()) + })); + } + + write_to_output(p_wrapped_key, pul_wrapped_len, &wrapped_bytes) +} + +// ── C_UnwrapKey ─────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_UnwrapKey( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + h_unwrapping_key: CK_OBJECT_HANDLE, + p_wrapped_key: *const CK_BYTE, + ul_wrapped_len: CK_ULONG, + p_template: *const CK_ATTRIBUTE, + ul_count: CK_ULONG, + ph_key: *mut CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() || p_wrapped_key.is_null() || ph_key.is_null() { + return CKR_ARGUMENTS_BAD; + } + let mech = &*p_mechanism; + let wrapped = std::slice::from_raw_parts(p_wrapped_key, ul_wrapped_len as usize); + if ul_count > 0 && p_template.is_null() { return CKR_ARGUMENTS_BAD; } + let attrs = collect_template(p_template, ul_count); + + let slot_id = ck_try!(session_slot(h_session)); + let is_token = attrs.get(&CKA_TOKEN).is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + if is_token { + ck_try!(require_rw_session(h_session)); + } + + // Check authorization AND access rights + let auth_rv = object_store::with_object_for_slot(h_unwrapping_key, slot_id, |obj| { + if !bool_attr_true(obj, CKA_UNWRAP) { + return Err(Pkcs11Error::KeyFunctionNotPermitted); + } + session::with_session_mut(h_session, |s| s.require_context_auth(obj)) + }); + match auth_rv { + Ok(()) => {} + Err(Pkcs11Error::InvalidObjectHandle) => return CKR_UNWRAPPING_KEY_HANDLE_INVALID, + Err(e) => return e.to_ckr(), + } + + // The Math & Key Creation + match mech.mechanism { + CKM_AES_KEY_WRAP => { + let key_bytes = ck_try!(object_store::with_object_for_slot(h_unwrapping_key, slot_id, |unwrap_obj| { + backend::aes_unwrap_key(slot_id, unwrap_obj, wrapped) + })); + + let key_len = key_bytes.len(); + if !matches!(key_len, 16 | 24 | 32) { + return CKR_KEY_SIZE_RANGE; + } + + let handle = object_store::next_handle(); + let mut obj_attrs = attrs; + obj_attrs.entry(CKA_CLASS).or_insert_with(|| backend::ulong_bytes(CKO_SECRET_KEY)); + obj_attrs.entry(CKA_KEY_TYPE).or_insert_with(|| backend::ulong_bytes(CKK_AES)); + obj_attrs.insert(CKA_VALUE_LEN, backend::ulong_bytes(key_len as CK_ULONG)); + let mut obj = object_store::KeyObject::new( + handle, slot_id, object_store::KeyType::AesSecret, + crate::traits::EngineKeyRef::from_bytes(key_bytes.to_vec()), obj_attrs, + ); + // Unwrapped keys are NOT locally generated (§4.2, §4.5). + obj.local = false; + obj.key_gen_mechanism = mech.mechanism; + object_store::store_object(obj, Some(h_session)); + *ph_key = handle; + } + _ => return CKR_MECHANISM_INVALID, + } + + // BURN THE TICKET + ck_try!(session::with_session_mut(h_session, |s| { + s.context_specific_authed = false; + Ok(()) + })); + CKR_OK +} + +// ── C_DeriveKey ─────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_DeriveKey( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + h_base_key: CK_OBJECT_HANDLE, + p_template: *const CK_ATTRIBUTE, + ul_count: CK_ULONG, + ph_key: *mut CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() || ph_key.is_null() { return CKR_ARGUMENTS_BAD; } + let mech = &*p_mechanism; + let attrs = collect_template(p_template, ul_count); + + // Evaluate Token Status for Session R/W requirements + let is_token = attrs.get(&CKA_TOKEN).is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE); + if is_token { + ck_try!(require_rw_session(h_session)); + } + + let slot_id = ck_try!(session_slot(h_session)); + + // Gate on auth, check CKA_DERIVE, AND extract base key audit flags + let (base_always_sensitive, base_never_extractable) = ck_try!(with_object(h_base_key, |obj| { + if !bool_attr_true(obj, CKA_DERIVE) { + return Err(Pkcs11Error::KeyFunctionNotPermitted); + } + session::with_session_mut(h_session, |s| s.require_context_auth(obj))?; + Ok((obj.always_sensitive, obj.never_extractable)) + })); + + match mech.mechanism { + CKM_HKDF_DERIVE => { + if mech.pParameter.is_null() { return CKR_MECHANISM_PARAM_INVALID; } + let p = &*(mech.pParameter as *const CK_HKDF_PARAMS); + + let hash = match p.prfHashMechanism { + CKM_SHA256 => crate::types::HashAlgorithm::Sha256, + CKM_SHA384 => crate::types::HashAlgorithm::Sha384, + CKM_SHA512 => crate::types::HashAlgorithm::Sha512, + CKM_SHA_1 => crate::types::HashAlgorithm::Sha1, + _ => return CKR_MECHANISM_PARAM_INVALID, + }; + + let salt = if !p.pSalt.is_null() && p.ulSaltLen > 0 { + std::slice::from_raw_parts(p.pSalt, p.ulSaltLen as usize) + } else { + &[] + }; + let info = if !p.pInfo.is_null() && p.ulInfoLen > 0 { + std::slice::from_raw_parts(p.pInfo, p.ulInfoLen as usize) + } else { + &[] + }; + + let okm_len = attrs + .get(&CKA_VALUE_LEN) + .map(|b| backend::bytes_to_ulong(b) as usize) + .unwrap_or(32); + + let derived_bytes = ck_try!(with_object(h_base_key, |base_obj| { + backend::hkdf_derive(slot_id, base_obj, hash, salt, info, okm_len) + })); + + // Generate CKA_UNIQUE_ID for the derived key + let mut unique_id = vec![0u8; 16]; + ck_try!(backend::generate_random(slot_id, &mut unique_id)); + + let handle = object_store::next_handle(); + let mut obj_attrs = attrs; + obj_attrs.entry(CKA_CLASS).or_insert_with(|| backend::ulong_bytes(CKO_SECRET_KEY)); + obj_attrs.entry(CKA_KEY_TYPE).or_insert_with(|| backend::ulong_bytes(CKK_HKDF)); + obj_attrs.insert(CKA_VALUE_LEN, backend::ulong_bytes(okm_len as CK_ULONG)); + obj_attrs.insert(CKA_UNIQUE_ID, unique_id); // Inject Unique ID + + let mut obj = object_store::KeyObject::new( + handle, + slot_id, + object_store::KeyType::AesSecret, + crate::traits::EngineKeyRef::from_bytes(derived_bytes.to_vec()), + obj_attrs, + ); + + // Derived keys are NEVER local + obj.local = false; + obj.key_gen_mechanism = mech.mechanism; + // Inherit historical audit flags from the base key + obj.always_sensitive = base_always_sensitive; + obj.never_extractable = base_never_extractable; + object_store::store_object(obj, Some(h_session)); + *ph_key = handle; + } + _ => return CKR_MECHANISM_INVALID, + } + + // BURN THE TICKET UNCONDITIONALLY + ck_try!(session::with_session_mut(h_session, |s| { + s.context_specific_authed = false; + Ok(()) + })); + + CKR_OK +} diff --git a/src/pkcs11/ffi_api_crypto/misc_v240.rs b/src/pkcs11/ffi_api_crypto/misc_v240.rs new file mode 100644 index 0000000..fce1fb1 --- /dev/null +++ b/src/pkcs11/ffi_api_crypto/misc_v240.rs @@ -0,0 +1,278 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── Remaining v2.40 functions ───────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_CopyObject( + h_session: CK_SESSION_HANDLE, + h_object: CK_OBJECT_HANDLE, + p_template: *const CK_ATTRIBUTE, + ul_count: CK_ULONG, + ph_new_object: *mut CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + + if ph_new_object.is_null() { return CKR_ARGUMENTS_BAD; } + if ul_count > 0 && p_template.is_null() { return CKR_ARGUMENTS_BAD; } + let overrides = collect_template(p_template, ul_count); + let slot_id = ck_try!(session_slot(h_session)); + + // Read the source object and grab everything we need, including its CKA_TOKEN state. + let (new_handle, new_key_type, new_key_ref, mut new_attrs, src_always_sensitive, src_never_extractable, src_is_token) = + ck_try!(object_store::with_object_for_slot(h_object, slot_id, |obj| { + Ok(( + object_store::next_handle(), + obj.key_type, + obj.key_ref.clone(), + obj.attributes.clone(), + obj.always_sensitive, + obj.never_extractable, + bool_attr_true(obj, CKA_TOKEN) // Check if source is a token object + )) + })); + + // If the template specifies CKA_TOKEN, use that. Otherwise, inherit from source. + let is_target_token = overrides + .get(&CKA_TOKEN) + .map(|v| !v.is_empty() && v[0] == CK_TRUE) + .unwrap_or(src_is_token); + + // Only enforce Read/Write session if we are actually writing to the token. + if is_target_token { + ck_try!(require_rw_session(h_session)); + } + + // Apply template overrides with attribute policy enforcement. + let mut override_keys: Vec = Vec::new(); + for (k, v) in overrides { + let old_val = new_attrs.get(&k).map(|b| b.as_slice()); + ck_try!(attribute_policy::validate_attribute_change(k, old_val, &v)); + new_attrs.insert(k, v); + override_keys.push(k); + } + let mut new_obj = object_store::KeyObject::new(new_handle, slot_id, new_key_type, new_key_ref, new_attrs); + + // Sync derived fields for any attribute the template overrode. + for changed in override_keys { + attribute_policy::update_derived_attributes(&mut new_obj, changed); + } + + // By setting these AFTER update_derived_attributes, we guarantee that + // the policy engine cannot accidentally upgrade them to TRUE if the user + // passed CKA_EXTRACTABLE=FALSE or CKA_SENSITIVE=TRUE in the copy template. + new_obj.always_sensitive = src_always_sensitive; + new_obj.never_extractable = src_never_extractable; + + // Copies are NEVER considered locally generated. + new_obj.local = false; + + object_store::store_object(new_obj, Some(h_session)); + *ph_new_object = new_handle; + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GetObjectSize( + h_session: CK_SESSION_HANDLE, + h_object: CK_OBJECT_HANDLE, + pul_size: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if pul_size.is_null() { return CKR_ARGUMENTS_BAD; } + let slot_id = ck_try!(session_slot(h_session)); + let size = ck_try!(object_store::with_object_for_slot(h_object, slot_id, |obj| { + // Approximate size: key DER + attribute storage overhead + let attr_size: usize = obj.attributes.values().map(|v| v.len() + 16).sum(); + Ok(obj.key_ref.as_bytes().len() + attr_size) + })); + *pul_size = size as CK_ULONG; + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GetOperationState( + h_session: CK_SESSION_HANDLE, + p_operation_state: *mut CK_BYTE, + pul_state_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session(h_session, |_| Ok(()))); + CKR_FUNCTION_NOT_SUPPORTED +} + +#[no_mangle] +pub unsafe extern "C" fn C_SetOperationState( + h_session: CK_SESSION_HANDLE, + p_operation_state: *const CK_BYTE, + ul_state_len: CK_ULONG, + h_encryption_key: CK_OBJECT_HANDLE, + h_authentication_key: CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session(h_session, |_| Ok(()))); + CKR_FUNCTION_NOT_SUPPORTED +} + +#[no_mangle] +pub unsafe extern "C" fn C_DigestKey( + h_session: CK_SESSION_HANDLE, + h_key: CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + let slot_id = ck_try!(session_slot(h_session)); + + // Intercept the error. Do NOT use ck_try! here. + let key_bytes_res = with_object(h_key, |obj| { + backend::key_value_for_digest(slot_id, obj) + }); + + // Translate a missing object to KEY_HANDLE_INVALID + let key_bytes = match key_bytes_res { + Ok(bytes) => bytes, + Err(Pkcs11Error::KeyHandleInvalid | Pkcs11Error::InvalidObjectHandle) => { + return CKR_KEY_HANDLE_INVALID; + } + Err(_) => return CKR_KEY_INDIGESTIBLE, // Reject digesting AES/Secret keys + }; + + let mut ctx = ck_try!(session::with_session_mut(h_session, |s| { + s.digest_ctx.take().ok_or(Pkcs11Error::OperationNotInitialised) + })); + + let result = || -> CK_RV { + if ctx.is_single_part { return CKR_OPERATION_ACTIVE; } + ctx.is_multi_part = true; + + ctx.data.extend_from_slice(&key_bytes); + CKR_OK + }(); + + if result == CKR_OK { + let _ = session::with_session_mut(h_session, |s| { + s.digest_ctx = Some(ctx); + Ok(()) + }); + } + result +} + +#[no_mangle] +pub unsafe extern "C" fn C_SignRecoverInit( + _h_session: CK_SESSION_HANDLE, + _p_mechanism: *const CK_MECHANISM, + _h_key: CK_OBJECT_HANDLE, +) -> CK_RV { CKR_FUNCTION_NOT_SUPPORTED } + +#[no_mangle] +pub unsafe extern "C" fn C_SignRecover( + _h_session: CK_SESSION_HANDLE, + _p_data: *const CK_BYTE, + _ul_data_len: CK_ULONG, + _p_signature: *mut CK_BYTE, + _pul_sig_len: *mut CK_ULONG, +) -> CK_RV { CKR_FUNCTION_NOT_SUPPORTED } + +#[no_mangle] +pub unsafe extern "C" fn C_VerifyRecoverInit( + _h_session: CK_SESSION_HANDLE, + _p_mechanism: *const CK_MECHANISM, + _h_key: CK_OBJECT_HANDLE, +) -> CK_RV { CKR_FUNCTION_NOT_SUPPORTED } + +#[no_mangle] +pub unsafe extern "C" fn C_VerifyRecover( + _h_session: CK_SESSION_HANDLE, + _p_signature: *const CK_BYTE, + _ul_sig_len: CK_ULONG, + _p_data: *mut CK_BYTE, + _pul_data_len: *mut CK_ULONG, +) -> CK_RV { CKR_FUNCTION_NOT_SUPPORTED } + +#[no_mangle] +pub unsafe extern "C" fn C_DigestEncryptUpdate( + _h_session: CK_SESSION_HANDLE, + _p_part: *const CK_BYTE, + _ul_part_len: CK_ULONG, + _p_encrypted_part: *mut CK_BYTE, + _pul_encrypted_part_len: *mut CK_ULONG, +) -> CK_RV { CKR_FUNCTION_NOT_SUPPORTED } + +#[no_mangle] +pub unsafe extern "C" fn C_DecryptDigestUpdate( + _h_session: CK_SESSION_HANDLE, + _p_encrypted_part: *const CK_BYTE, + _ul_encrypted_part_len: CK_ULONG, + _p_part: *mut CK_BYTE, + _pul_part_len: *mut CK_ULONG, +) -> CK_RV { CKR_FUNCTION_NOT_SUPPORTED } + +#[no_mangle] +pub unsafe extern "C" fn C_SignEncryptUpdate( + _h_session: CK_SESSION_HANDLE, + _p_part: *const CK_BYTE, + _ul_part_len: CK_ULONG, + _p_encrypted_part: *mut CK_BYTE, + _pul_encrypted_part_len: *mut CK_ULONG, +) -> CK_RV { CKR_FUNCTION_NOT_SUPPORTED } + +#[no_mangle] +pub unsafe extern "C" fn C_DecryptVerifyUpdate( + _h_session: CK_SESSION_HANDLE, + _p_encrypted_part: *const CK_BYTE, + _ul_encrypted_part_len: CK_ULONG, + _p_part: *mut CK_BYTE, + _pul_part_len: *mut CK_ULONG, +) -> CK_RV { CKR_FUNCTION_NOT_SUPPORTED } + +#[no_mangle] +pub unsafe extern "C" fn C_SeedRandom( + _h_session: CK_SESSION_HANDLE, + _p_seed: *const CK_BYTE, + _ul_seed_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session(_h_session, |_| Ok(()))); + if _p_seed.is_null() && _ul_seed_len > 0 { + return CKR_ARGUMENTS_BAD; + } + // Per PKCS#11: libraries that don't support manual seeding must + // return CKR_RANDOM_SEED_NOT_SUPPORTED (not CKR_OK). + CKR_RANDOM_SEED_NOT_SUPPORTED +} + +#[no_mangle] +pub extern "C" fn C_GetFunctionStatus( + _h_session: CK_SESSION_HANDLE, +) -> CK_RV { + CKR_FUNCTION_NOT_PARALLEL +} + +#[no_mangle] +pub extern "C" fn C_CancelFunction( + _h_session: CK_SESSION_HANDLE, +) -> CK_RV { + CKR_FUNCTION_NOT_PARALLEL +} + +#[no_mangle] +pub unsafe extern "C" fn C_WaitForSlotEvent( + _flags: CK_FLAGS, + _p_slot: *mut CK_SLOT_ID, + _p_reserved: *mut c_void, +) -> CK_RV { + ck_try!(check_init()); + // Software tokens have no hardware events to wait for. + CKR_FUNCTION_NOT_SUPPORTED +} diff --git a/src/pkcs11/ffi_api_crypto/sign_verify.rs b/src/pkcs11/ffi_api_crypto/sign_verify.rs new file mode 100644 index 0000000..d3870c2 --- /dev/null +++ b/src/pkcs11/ffi_api_crypto/sign_verify.rs @@ -0,0 +1,279 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── Sign ────────────────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_SignInit( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + h_key: CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() { return CKR_ARGUMENTS_BAD; } + let mech_type = (*p_mechanism).mechanism; + + match mech_type { + // Standard and Legacy RSA + CKM_RSA_PKCS | CKM_SHA1_RSA_PKCS | CKM_SHA256_RSA_PKCS | + CKM_SHA384_RSA_PKCS | CKM_SHA512_RSA_PKCS => { + // These are standard PKCS#1 v1.5 signing mechanisms + }, + + // RSA-PSS + CKM_RSA_PKCS_PSS | CKM_SHA1_RSA_PKCS_PSS | CKM_SHA256_RSA_PKCS_PSS | + CKM_SHA384_RSA_PKCS_PSS | CKM_SHA512_RSA_PKCS_PSS => { + // Probabilistic Signature Scheme + }, + + // Elliptic Curve + CKM_ECDSA | CKM_ECDSA_SHA256 | CKM_EDDSA => { + // EC and EdDSA signatures + }, + + _ => return CKR_MECHANISM_INVALID, + } + + ck_try!(session::with_session_mut(h_session, |s| { + if s.sign_ctx.is_some() { return Err(Pkcs11Error::OperationActive); } + // Wipe any old ghost tickets. + s.context_specific_authed = false; + s.sign_ctx = Some(SignContext { mechanism: mech_type, key_handle: h_key, data: Vec::new() }); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_Sign( + h_session: CK_SESSION_HANDLE, + p_data: *const CK_BYTE, + ul_data_len: CK_ULONG, + p_signature: *mut CK_BYTE, + pul_sig_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_data.is_null() || pul_sig_len.is_null() { return CKR_ARGUMENTS_BAD; } + + let is_length_req = p_signature.is_null(); + let data = std::slice::from_raw_parts(p_data, ul_data_len as usize); + + let (ctx, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.sign_ctx.take().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok((ctx, s.slot_id)) // take() gives ownership, no need to clone + })); + + // Gate on CKA_ALWAYS_AUTHENTICATE before the engine call. + ck_try!(with_object(ctx.key_handle, |obj| { + session::with_session_mut(h_session, |s| s.require_context_auth(obj)) + })); + let sig = ck_try!(with_object(ctx.key_handle, |obj| { + backend::sign(slot_id, ctx.mechanism, obj, data) + })); + // RESTORE OR CONSUME + ck_try!(session::with_session_mut(h_session, |s| { + if is_length_req { + s.sign_ctx = Some(ctx); // THE FIX: Put it back for the real call! + } else { + s.context_specific_authed = false; // Burn the ticket + } + Ok(()) + })); + + write_to_output(p_signature, pul_sig_len, &sig) +} + +#[no_mangle] +pub unsafe extern "C" fn C_SignUpdate( + h_session: CK_SESSION_HANDLE, + p_part: *const CK_BYTE, + ul_part_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_part.is_null() { return CKR_ARGUMENTS_BAD; } + let part = std::slice::from_raw_parts(p_part, ul_part_len as usize); + ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.sign_ctx.as_mut().ok_or(Pkcs11Error::OperationNotInitialised)?; + ctx.data.extend_from_slice(part); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_SignFinal( + h_session: CK_SESSION_HANDLE, + p_signature: *mut CK_BYTE, + pul_sig_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if pul_sig_len.is_null() { return CKR_ARGUMENTS_BAD; } + let is_length_req = p_signature.is_null(); + + let (ctx, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.sign_ctx.take().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok((ctx, s.slot_id)) + })); + + // Check CKA_ALWAYS_AUTHENTICATE + ck_try!(with_object(ctx.key_handle, |obj| { + session::with_session_mut(h_session, |s| s.require_context_auth(obj)) + })); + + let sig = ck_try!(with_object(ctx.key_handle, |obj| { + backend::sign(slot_id, ctx.mechanism, obj, &ctx.data) + })); + + // RESTORE OR CONSUME + ck_try!(session::with_session_mut(h_session, |s| { + if is_length_req { + s.sign_ctx = Some(ctx); // THE FIX: Put it back for the real call! + } else { + s.context_specific_authed = false; // Burn the ticket + } + Ok(()) + })); + write_to_output(p_signature, pul_sig_len, &sig) +} + +// ── Verify ──────────────────────────────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_VerifyInit( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + h_key: CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() { return CKR_ARGUMENTS_BAD; } + let mech_type = (*p_mechanism).mechanism; + match mech_type { + // Standard and Legacy RSA + CKM_RSA_PKCS | CKM_SHA1_RSA_PKCS | CKM_SHA256_RSA_PKCS | + CKM_SHA384_RSA_PKCS | CKM_SHA512_RSA_PKCS => { + // These are standard PKCS#1 v1.5 signing mechanisms + }, + + // RSA-PSS + CKM_RSA_PKCS_PSS | CKM_SHA1_RSA_PKCS_PSS | CKM_SHA256_RSA_PKCS_PSS | + CKM_SHA384_RSA_PKCS_PSS | CKM_SHA512_RSA_PKCS_PSS => { + // Probabilistic Signature Scheme + }, + + // Elliptic Curve + CKM_ECDSA | CKM_ECDSA_SHA256 | CKM_EDDSA => { + // EC and EdDSA signatures + }, + + _ => return CKR_MECHANISM_INVALID, + } + ck_try!(session::with_session_mut(h_session, |s| { + if s.verify_ctx.is_some() { return Err(Pkcs11Error::OperationActive); } + s.verify_ctx = Some(SignContext { mechanism: mech_type, key_handle: h_key, data: Vec::new() }); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_Verify( + h_session: CK_SESSION_HANDLE, + p_data: *const CK_BYTE, + ul_data_len: CK_ULONG, + p_signature: *const CK_BYTE, + ul_sig_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_data.is_null() || p_signature.is_null() { return CKR_ARGUMENTS_BAD; } + let key_handle = ck_try!(session::with_session(h_session, |s| { + s.verify_ctx.as_ref().map(|ctx| ctx.key_handle).ok_or(Pkcs11Error::OperationNotInitialised) + })); + + let is_rsa = ck_try!(object_store::with_object(key_handle, |obj| { + Ok(obj.key_type == object_store::KeyType::RsaPublic || obj.key_type == object_store::KeyType::RsaPrivate) + })); + + if is_rsa { + let modulus_size = ck_try!(object_store::with_object(key_handle, |obj| { + object_store::get_modulus_len(obj) + })); + if ul_sig_len != modulus_size as CK_ULONG { + return CKR_SIGNATURE_LEN_RANGE; + } + } + let data = std::slice::from_raw_parts(p_data, ul_data_len as usize); + let sig = std::slice::from_raw_parts(p_signature, ul_sig_len as usize); + let (ctx, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.verify_ctx.take().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok((ctx, s.slot_id)) + })); + ck_try!(with_object(ctx.key_handle, |obj| { + backend::verify(slot_id, ctx.mechanism, obj, data, sig) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_VerifyUpdate( + h_session: CK_SESSION_HANDLE, + p_part: *const CK_BYTE, + ul_part_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_part.is_null() { return CKR_ARGUMENTS_BAD; } + let part = std::slice::from_raw_parts(p_part, ul_part_len as usize); + ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.verify_ctx.as_mut().ok_or(Pkcs11Error::OperationNotInitialised)?; + ctx.data.extend_from_slice(part); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_VerifyFinal( + h_session: CK_SESSION_HANDLE, + p_signature: *const CK_BYTE, + ul_sig_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_signature.is_null() { return CKR_ARGUMENTS_BAD; } + + let key_handle = ck_try!(session::with_session(h_session, |s| { + s.verify_ctx.as_ref().map(|ctx| ctx.key_handle).ok_or(Pkcs11Error::OperationNotInitialised) + })); + + let is_rsa = ck_try!(object_store::with_object(key_handle, |obj| { + Ok(obj.key_type == object_store::KeyType::RsaPublic || obj.key_type == object_store::KeyType::RsaPrivate) + })); + + if is_rsa { + let modulus_size = ck_try!(object_store::with_object(key_handle, |obj| { + object_store::get_modulus_len(obj) + })); + if ul_sig_len != modulus_size as CK_ULONG { + return CKR_SIGNATURE_LEN_RANGE; + } + } + + let sig = std::slice::from_raw_parts(p_signature, ul_sig_len as usize); + let (ctx, slot_id) = ck_try!(session::with_session_mut(h_session, |s| { + let ctx = s.verify_ctx.take().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok((ctx, s.slot_id)) + })); + ck_try!(with_object(ctx.key_handle, |obj| { + backend::verify(slot_id, ctx.mechanism, obj, &ctx.data, sig) + })); + CKR_OK +} diff --git a/src/pkcs11/ffi_api_v3.rs b/src/pkcs11/ffi_api_v3.rs new file mode 100644 index 0000000..0057cf4 --- /dev/null +++ b/src/pkcs11/ffi_api_v3.rs @@ -0,0 +1,14 @@ +//! Hub for PKCS#11 v3.0 API entry points. +//! +//! Owns v3 session/user extensions, message APIs, and interface discovery. +use super::*; + +mod session_user; +mod message_encrypt_decrypt; +mod message_sign_verify; +mod interface_discovery; + +pub use session_user::*; +pub use message_encrypt_decrypt::*; +pub use message_sign_verify::*; +pub use interface_discovery::*; diff --git a/src/pkcs11/ffi_api_v3/interface_discovery.rs b/src/pkcs11/ffi_api_v3/interface_discovery.rs new file mode 100644 index 0000000..331a818 --- /dev/null +++ b/src/pkcs11/ffi_api_v3/interface_discovery.rs @@ -0,0 +1,68 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── C_GetInterfaceList / C_GetInterface (v3.0 interface discovery) ──────── + +#[no_mangle] +pub unsafe extern "C" fn C_GetInterfaceList( + p_interfaces_list: *mut CK_INTERFACE, + pul_count: *mut CK_ULONG, +) -> CK_RV { + if pul_count.is_null() { return CKR_ARGUMENTS_BAD; } + if p_interfaces_list.is_null() { + *pul_count = 1; + return CKR_OK; + } + if *pul_count < 1 { + *pul_count = 1; + return CKR_BUFFER_TOO_SMALL; + } + *p_interfaces_list = CK_INTERFACE { + pInterfaceName: PKCS11_INTERFACE_NAME.as_ptr(), + pFunctionList: &FUNCTION_LIST_3_0 as *const _ as *const c_void, + flags: CKF_INTERFACE_FORK_SAFE, + }; + *pul_count = 1; + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_GetInterface( + p_interface_name: *const CK_UTF8CHAR, + p_version: *mut CK_VERSION, + pp_interface: *mut *const CK_INTERFACE, + flags: CK_FLAGS, +) -> CK_RV { + if pp_interface.is_null() { return CKR_ARGUMENTS_BAD; } + + // If name is NULL, return the default (latest) interface + if !p_interface_name.is_null() { + // Verify the name matches "PKCS 11" + let name = std::ffi::CStr::from_ptr(p_interface_name as *const libc::c_char); + if name.to_bytes() != b"PKCS 11" { + return CKR_ARGUMENTS_BAD; + } + } + + // If version is specified, check compatibility + if !p_version.is_null() { + let ver = &*p_version; + if ver.major > 3 || (ver.major == 3 && ver.minor > 0) { + return CKR_ARGUMENTS_BAD; + } + } + + *pp_interface = &INTERFACE_3_0; + CKR_OK +} diff --git a/src/pkcs11/ffi_api_v3/message_encrypt_decrypt.rs b/src/pkcs11/ffi_api_v3/message_encrypt_decrypt.rs new file mode 100644 index 0000000..0132330 --- /dev/null +++ b/src/pkcs11/ffi_api_v3/message_encrypt_decrypt.rs @@ -0,0 +1,230 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── Message-based Encrypt API (v3.0) ────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_MessageEncryptInit( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + h_key: CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() { return CKR_ARGUMENTS_BAD; } + let mech_type = (*p_mechanism).mechanism; + // Only AES-GCM and ChaCha20-Poly1305 support per-message IV semantics. + if mech_type != CKM_AES_GCM && mech_type != CKM_CHACHA20_POLY1305 { + return CKR_MECHANISM_INVALID; + } + ck_try!(session::with_session_mut(h_session, |s| { + if s.msg_encrypt_ctx.is_some() { return Err(Pkcs11Error::OperationActive); } + s.msg_encrypt_ctx = Some(MessageCipherContext { mechanism: mech_type, key_handle: h_key }); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_EncryptMessage( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, // *const CK_GCM_MESSAGE_PARAMS or CK_CHACHA20_POLY1305_MESSAGE_PARAMS (tag written back via interior pointer) + ul_param_len: CK_ULONG, + p_aad: *const CK_BYTE, + ul_aad_len: CK_ULONG, + p_plaintext: *const CK_BYTE, + ul_plain_len: CK_ULONG, + p_ciphertext: *mut CK_BYTE, + pul_cipher_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_plaintext.is_null() || pul_cipher_len.is_null() || p_parameter.is_null() { + return CKR_ARGUMENTS_BAD; + } + let plaintext = std::slice::from_raw_parts(p_plaintext, ul_plain_len as usize); + let aad = if !p_aad.is_null() && ul_aad_len > 0 { + std::slice::from_raw_parts(p_aad, ul_aad_len as usize) + } else { &[] }; + + let (ctx, slot_id) = ck_try!(session::with_session(h_session, |s| { + Ok((s.msg_encrypt_ctx.as_ref().cloned().ok_or(Pkcs11Error::OperationNotInitialised)?, s.slot_id)) + })); + + // Extract IV and tag buffer from the per-message params struct. + // CK_GCM_MESSAGE_PARAMS and CK_CHACHA20_POLY1305_MESSAGE_PARAMS share the same layout. + // Cast: params is the caller's mutable struct passed through a const void pointer. + let params = p_parameter as *const CK_GCM_MESSAGE_PARAMS; + if (*params).pIv.is_null() || (*params).pTag.is_null() { return CKR_ARGUMENTS_BAD; } + let iv = std::slice::from_raw_parts((*params).pIv, (*params).ulIvLen as usize); + let tag_len = ((*params).ulTagBits as usize).div_ceil(8); + + let (ct, tag) = ck_try!(with_object(ctx.key_handle, |obj| { + backend::encrypt_message(slot_id, ctx.mechanism, obj, iv, aad, plaintext) + })); + + // Write tag back to caller's pTag buffer (pTag is *mut CK_BYTE inside the struct). + if tag.len() != tag_len { + return CKR_GENERAL_ERROR; + } + std::ptr::copy_nonoverlapping(tag.as_ptr(), (*params).pTag, tag_len); + + write_to_output(p_ciphertext, pul_cipher_len, &ct) +} + +#[no_mangle] +pub unsafe extern "C" fn C_EncryptMessageBegin( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, + p_aad: *const CK_BYTE, + ul_aad_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + // For single-part AEAD messages, Begin is a no-op that just validates state + ck_try!(session::with_session(h_session, |s| { + s.msg_encrypt_ctx.as_ref().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_EncryptMessageNext( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, + p_plaintext: *const CK_BYTE, + ul_plain_len: CK_ULONG, + p_ciphertext: *mut CK_BYTE, + pul_cipher_len: *mut CK_ULONG, + flags: CK_FLAGS, +) -> CK_RV { + // Streaming not supported for AEAD — use C_EncryptMessage for one-shot + CKR_FUNCTION_NOT_SUPPORTED +} + +#[no_mangle] +pub unsafe extern "C" fn C_MessageEncryptFinal( + h_session: CK_SESSION_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session_mut(h_session, |s| { + s.msg_encrypt_ctx = None; + Ok(()) + })); + CKR_OK +} + +// ── Message-based Decrypt API (v3.0) ────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_MessageDecryptInit( + h_session: CK_SESSION_HANDLE, + p_mechanism: *const CK_MECHANISM, + h_key: CK_OBJECT_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + if p_mechanism.is_null() { return CKR_ARGUMENTS_BAD; } + let mech_type = (*p_mechanism).mechanism; + // Only AES-GCM and ChaCha20-Poly1305 support per-message IV semantics. + if mech_type != CKM_AES_GCM && mech_type != CKM_CHACHA20_POLY1305 { + return CKR_MECHANISM_INVALID; + } + ck_try!(session::with_session_mut(h_session, |s| { + if s.msg_decrypt_ctx.is_some() { return Err(Pkcs11Error::OperationActive); } + s.msg_decrypt_ctx = Some(MessageCipherContext { mechanism: mech_type, key_handle: h_key }); + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_DecryptMessage( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, // *const CK_GCM_MESSAGE_PARAMS or CK_CHACHA20_POLY1305_MESSAGE_PARAMS + ul_param_len: CK_ULONG, + p_aad: *const CK_BYTE, + ul_aad_len: CK_ULONG, + p_ciphertext: *const CK_BYTE, + ul_cipher_len: CK_ULONG, + p_plaintext: *mut CK_BYTE, + pul_plain_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_ciphertext.is_null() || pul_plain_len.is_null() || p_parameter.is_null() { + return CKR_ARGUMENTS_BAD; + } + let ciphertext = std::slice::from_raw_parts(p_ciphertext, ul_cipher_len as usize); + let aad = if !p_aad.is_null() && ul_aad_len > 0 { + std::slice::from_raw_parts(p_aad, ul_aad_len as usize) + } else { &[] }; + + let (ctx, slot_id) = ck_try!(session::with_session(h_session, |s| { + Ok((s.msg_decrypt_ctx.as_ref().cloned().ok_or(Pkcs11Error::OperationNotInitialised)?, s.slot_id)) + })); + + // Extract IV and tag from the per-message params struct. + // CK_GCM_MESSAGE_PARAMS and CK_CHACHA20_POLY1305_MESSAGE_PARAMS share the same layout. + let params = p_parameter as *const CK_GCM_MESSAGE_PARAMS; + if (*params).pIv.is_null() || (*params).pTag.is_null() { return CKR_ARGUMENTS_BAD; } + let iv = std::slice::from_raw_parts((*params).pIv, (*params).ulIvLen as usize); + let tag_len = ((*params).ulTagBits as usize).div_ceil(8); + let tag = std::slice::from_raw_parts((*params).pTag, tag_len); + + let pt = ck_try!(with_object(ctx.key_handle, |obj| { + backend::decrypt_message(slot_id, ctx.mechanism, obj, iv, aad, ciphertext, tag) + })); + write_to_output(p_plaintext, pul_plain_len, &pt) +} + +#[no_mangle] +pub unsafe extern "C" fn C_DecryptMessageBegin( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, + p_aad: *const CK_BYTE, + ul_aad_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session(h_session, |s| { + s.msg_decrypt_ctx.as_ref().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_DecryptMessageNext( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, + p_ciphertext: *const CK_BYTE, + ul_cipher_len: CK_ULONG, + p_plaintext: *mut CK_BYTE, + pul_plain_len: *mut CK_ULONG, + flags: CK_FLAGS, +) -> CK_RV { + CKR_FUNCTION_NOT_SUPPORTED +} + +#[no_mangle] +pub unsafe extern "C" fn C_MessageDecryptFinal( + h_session: CK_SESSION_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session_mut(h_session, |s| { + s.msg_decrypt_ctx = None; + Ok(()) + })); + CKR_OK +} diff --git a/src/pkcs11/ffi_api_v3/message_sign_verify.rs b/src/pkcs11/ffi_api_v3/message_sign_verify.rs new file mode 100644 index 0000000..70ab952 --- /dev/null +++ b/src/pkcs11/ffi_api_v3/message_sign_verify.rs @@ -0,0 +1,160 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── Message-based Sign API (v3.0) ──────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_MessageSignInit( + _h_session: CK_SESSION_HANDLE, + _p_mechanism: *const CK_MECHANISM, + _h_key: CK_OBJECT_HANDLE, +) -> CK_RV { + // No mechanism currently supports per-message signing semantics. + CKR_FUNCTION_NOT_SUPPORTED +} + +#[no_mangle] +pub unsafe extern "C" fn C_SignMessage( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, + p_data: *const CK_BYTE, + ul_data_len: CK_ULONG, + p_signature: *mut CK_BYTE, + pul_sig_len: *mut CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_data.is_null() || pul_sig_len.is_null() { return CKR_ARGUMENTS_BAD; } + let data = std::slice::from_raw_parts(p_data, ul_data_len as usize); + let (ctx, slot_id) = ck_try!(session::with_session(h_session, |s| { + Ok((s.msg_sign_ctx.as_ref().cloned().ok_or(Pkcs11Error::OperationNotInitialised)?, s.slot_id)) + })); + let sig = ck_try!(with_object(ctx.key_handle, |obj| { + backend::sign(slot_id, ctx.mechanism, obj, data) + })); + write_to_output(p_signature, pul_sig_len, &sig) +} + +#[no_mangle] +pub unsafe extern "C" fn C_SignMessageBegin( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session(h_session, |s| { + s.msg_sign_ctx.as_ref().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_SignMessageNext( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, + p_data: *const CK_BYTE, + ul_data_len: CK_ULONG, + p_signature: *mut CK_BYTE, + pul_sig_len: *mut CK_ULONG, +) -> CK_RV { + CKR_FUNCTION_NOT_SUPPORTED +} + +#[no_mangle] +pub unsafe extern "C" fn C_MessageSignFinal( + h_session: CK_SESSION_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session_mut(h_session, |s| { + s.msg_sign_ctx = None; + Ok(()) + })); + CKR_OK +} + +// ── Message-based Verify API (v3.0) ────────────────────────────────────── + +#[no_mangle] +pub unsafe extern "C" fn C_MessageVerifyInit( + _h_session: CK_SESSION_HANDLE, + _p_mechanism: *const CK_MECHANISM, + _h_key: CK_OBJECT_HANDLE, +) -> CK_RV { + // No mechanism currently supports per-message verify semantics. + CKR_FUNCTION_NOT_SUPPORTED +} + +#[no_mangle] +pub unsafe extern "C" fn C_VerifyMessage( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, + p_data: *const CK_BYTE, + ul_data_len: CK_ULONG, + p_signature: *const CK_BYTE, + ul_sig_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + if p_data.is_null() || p_signature.is_null() { return CKR_ARGUMENTS_BAD; } + let data = std::slice::from_raw_parts(p_data, ul_data_len as usize); + let sig = std::slice::from_raw_parts(p_signature, ul_sig_len as usize); + let (ctx, slot_id) = ck_try!(session::with_session(h_session, |s| { + Ok((s.msg_verify_ctx.as_ref().cloned().ok_or(Pkcs11Error::OperationNotInitialised)?, s.slot_id)) + })); + ck_try!(with_object(ctx.key_handle, |obj| { + backend::verify(slot_id, ctx.mechanism, obj, data, sig) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_VerifyMessageBegin( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session(h_session, |s| { + s.msg_verify_ctx.as_ref().ok_or(Pkcs11Error::OperationNotInitialised)?; + Ok(()) + })); + CKR_OK +} + +#[no_mangle] +pub unsafe extern "C" fn C_VerifyMessageNext( + h_session: CK_SESSION_HANDLE, + p_parameter: *const c_void, + ul_param_len: CK_ULONG, + p_data: *const CK_BYTE, + ul_data_len: CK_ULONG, + p_signature: *const CK_BYTE, + ul_sig_len: CK_ULONG, +) -> CK_RV { + CKR_FUNCTION_NOT_SUPPORTED +} + +#[no_mangle] +pub unsafe extern "C" fn C_MessageVerifyFinal( + h_session: CK_SESSION_HANDLE, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session_mut(h_session, |s| { + s.msg_verify_ctx = None; + Ok(()) + })); + CKR_OK +} diff --git a/src/pkcs11/ffi_api_v3/session_user.rs b/src/pkcs11/ffi_api_v3/session_user.rs new file mode 100644 index 0000000..a7ef280 --- /dev/null +++ b/src/pkcs11/ffi_api_v3/session_user.rs @@ -0,0 +1,109 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::*; + +// ── v3.0 new functions ──────────────────────────────────────────────────── + +/// C_SessionCancel — cancel active cryptographic operations on a session. +#[no_mangle] +pub unsafe extern "C" fn C_SessionCancel( + h_session: CK_SESSION_HANDLE, + flags: CK_FLAGS, +) -> CK_RV { + ck_try!(check_init()); + ck_try!(session::with_session_mut(h_session, |s| { + // Cancel all active operations + s.sign_ctx = None; + s.verify_ctx = None; + s.encrypt_ctx = None; + s.decrypt_ctx = None; + s.digest_ctx = None; + s.find_ctx = None; + Ok(()) + })); + CKR_OK +} + +/// C_LoginUser — extended login with username parameter (v3.0). +#[no_mangle] +pub unsafe extern "C" fn C_LoginUser( + h_session: CK_SESSION_HANDLE, + user_type: CK_USER_TYPE, + p_pin: *const CK_UTF8CHAR, + ul_pin_len: CK_ULONG, + p_username: *const CK_UTF8CHAR, + ul_username_len: CK_ULONG, +) -> CK_RV { + ck_try!(check_init()); + // For CKU_CONTEXT_SPECIFIC, we just verify the PIN. + // For CKU_USER / CKU_SO, delegate to the normal login path. + let pin: &[u8] = if p_pin.is_null() || ul_pin_len == 0 { + &[] + } else { + std::slice::from_raw_parts(p_pin, ul_pin_len as usize) + }; + + let slot_id = ck_try!(session_slot(h_session)); + if user_type == CKU_CONTEXT_SPECIFIC { + // Context-specific login requires the user to already be logged in. + let current = session::login_state_for_slot(slot_id); + if current != LoginState::UserLoggedIn { + return CKR_USER_NOT_LOGGED_IN; + } + // Verify PIN without lockout counters; on success, arm the one-shot flag. + ck_try!(token::with_token(slot_id, |tok| tok.verify_user_pin_no_lockout(pin))); + ck_try!(session::with_session_mut(h_session, |s| { + s.context_specific_authed = true; + Ok(()) + })); + return CKR_OK; + } + + // Validate user type early. + let new_state = match user_type { + CKU_USER => LoginState::UserLoggedIn, + CKU_SO => LoginState::SoLoggedIn, + _ => return CKR_USER_TYPE_INVALID, + }; + + // Check if already logged in on this token (any session). + let current = session::login_state_for_slot(slot_id); + if current != LoginState::NotLoggedIn { + if (current == LoginState::UserLoggedIn && user_type == CKU_USER) + || (current == LoginState::SoLoggedIn && user_type == CKU_SO) + { + return CKR_USER_ALREADY_LOGGED_IN; + } + return CKR_USER_ANOTHER_ALREADY_LOGGED_IN; + } + + // SO login requires no RO sessions exist on this token. + if user_type == CKU_SO && session::has_ro_sessions_on_slot(slot_id) { + return CKR_SESSION_READ_ONLY_EXISTS; + } + + // User login requires user PIN to have been initialized. + if user_type == CKU_USER { + let pin_init = token::with_token(slot_id, |tok| tok.user_pin.is_some()); + if !pin_init { + return CKR_USER_PIN_NOT_INITIALIZED; + } + } + + // Verify PIN against the token (mutable for failure counter tracking). + ck_try!(token::with_token_mut(slot_id, |tok| tok.verify_pin(user_type, pin))); + + // Propagate login state to ALL sessions on this token. + session::login_all_sessions_on_slot(slot_id, new_state); + CKR_OK +} diff --git a/src/pkcs11/mechanisms.rs b/src/pkcs11/mechanisms.rs new file mode 100644 index 0000000..c0dd937 --- /dev/null +++ b/src/pkcs11/mechanisms.rs @@ -0,0 +1,68 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Mechanism tier policy — classifies PKCS#11 mechanisms as Standard, Legacy, +//! and gates weak legacy mechanisms behind an environment variable. +//! +//! ## Tiers +//! +//! | Tier | Behaviour | +//! |------------|-------------------------------------------------| +//! | Standard | Always advertised and usable | +//! | Legacy | Hidden and rejected unless `CRYPTOKI_LEGACY=1` | +//! | Forbidden | Never available (e.g. RSA keygen < 2048 bits) | + +use super::constants::*; +use super::types::CK_MECHANISM_TYPE; + +/// Classification of a PKCS#11 mechanism. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MechanismTier { + /// Available by default. + Standard, + /// Only available when `CRYPTOKI_LEGACY=1`. + Legacy, +} + +/// Classify a mechanism type. +/// +/// Pass `None` when the key size is not known (e.g. during +/// `C_GetMechanismList`). +pub fn classify(mech: CK_MECHANISM_TYPE, key_bits: Option) -> MechanismTier { + let _ = key_bits; + match mech { + // Weak / legacy hash and signature mechanisms. + CKM_MD5 | CKM_SHA_1 | CKM_SHA1_RSA_PKCS | CKM_SHA1_RSA_PKCS_PSS => { + MechanismTier::Legacy + } + _ => MechanismTier::Standard, + } +} + +/// Returns `true` when the `CRYPTOKI_LEGACY` environment variable is set +/// to `"1"`. +pub fn legacy_enabled() -> bool { + std::env::var("CRYPTOKI_LEGACY").is_ok_and(|v| v == "1") +} + +/// Returns `true` when the mechanism (with optional key size) is allowed under +/// the current policy. +/// +/// - `Standard` mechanisms are always allowed. +/// - `Legacy` mechanisms are allowed only when `CRYPTOKI_LEGACY=1`. +pub fn is_mechanism_allowed(mech: CK_MECHANISM_TYPE, key_bits: Option) -> bool { + match classify(mech, key_bits) { + MechanismTier::Standard => true, + MechanismTier::Legacy => legacy_enabled(), + } +} diff --git a/src/pkcs11/mod.rs b/src/pkcs11/mod.rs new file mode 100644 index 0000000..f087100 --- /dev/null +++ b/src/pkcs11/mod.rs @@ -0,0 +1,404 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! PKCS#11 v3.0 FFI layer — all C_* functions inline. +//! +//! Architecture: +//! C caller → mod.rs (FFI, ck_try!) → session.rs → token.rs → object_store.rs → backend.rs +#![allow(non_snake_case, dead_code, unused_variables)] +// C FFI exports: safety contracts are defined by the PKCS#11 specification, +// not by inline Rust doc comments. +#![allow(clippy::missing_safety_doc)] + +pub mod attribute_policy; +pub mod backend; +pub mod constants; +pub mod error; +pub mod mechanisms; +pub mod object_store; +pub mod session; +pub mod storage; +pub mod token; +pub mod types; + +use std::collections::HashMap; +use std::ffi::c_void; +use std::sync::{Once, OnceLock, RwLock}; + + + +use constants::*; +use error::Pkcs11Error; +use object_store::with_object; +use session::{CipherContext, DigestContext, FindContext, LoginState, SignContext, + MessageCipherContext}; +use types::*; + +// ── GlobalState ─────────────────────────────────────────────────────────── + +/// Lightweight lifecycle coordinator. All actual stores (sessions, objects, +/// tokens, registry) live in their own module-level statics; `GlobalState` +/// coordinates initialization order and drives the shutdown sequence. +struct GlobalState; + +impl GlobalState { + fn shutdown(&mut self) { + // 1. Drop active sessions (operation contexts, login state). + session::clear_sessions(); + // 2. Persist token objects while the engine/registry is still live. + object_store::persist_to_disk(); + // 3. Zeroize and drop all in-memory key material. + object_store::clear_objects(); + // 4. Clear token metadata (PINs, flags). + token::clear_tokens(); + // 5. Drop engine Arc refs — engine destructors run here. + crate::registry::reset_registry(); + } + + /// Reseed the process RNG after fork so parent and child do not share state. + /// + /// OpenSSL 1.1+ reseeds automatically when the PID changes, but an explicit + /// call to `rand_bytes` guarantees the reseed happens immediately in the child. + fn reseed_rng() { + let mut buf = [0u8; 32]; + let _ = openssl::rand::rand_bytes(&mut buf); + } +} + +/// The initialized state of the library. +/// +/// `OnceLock` initialises the `RwLock` exactly once for the lifetime of the +/// process. The `Option` inside toggles between `None` (not +/// initialised) and `Some` (initialised), enabling repeated Init/Finalize cycles. +static GLOBAL: OnceLock>> = OnceLock::new(); + +fn global() -> &'static RwLock> { + GLOBAL.get_or_init(|| RwLock::new(None)) +} + +/// Registered via `pthread_atfork(None, None, Some(child_after_fork))` exactly +/// once. Called in the child process immediately after `fork(2)`. +/// +/// The child inherits open file descriptors (and any associated `flock` locks) +/// from the parent. We release those locks, reseed the RNG to avoid parent/child +/// RNG correlation, and let each engine perform its own post-fork cleanup. +/// +/// We use `try_write()` rather than `write()`: if another thread held the global +/// lock at the moment of `fork`, `write()` would deadlock in the child (that +/// thread no longer exists). `try_write()` fails gracefully in that case. +extern "C" fn child_after_fork() { + if let Some(global) = GLOBAL.get() { + if let Ok(guard) = global.try_write() { + if guard.is_some() { + storage::release_locks(); + GlobalState::reseed_rng(); + crate::registry::for_each_engine(|e| e.post_fork_child()); + } + } + } +} + +/// Ensures `pthread_atfork` is registered at most once per process. +static ATFORK_REGISTERED: Once = Once::new(); + +// ── Macro ───────────────────────────────────────────────────────────────── + +macro_rules! ck_try { + ($expr:expr) => { + match $expr { + Ok(v) => v, + Err(e) => return e.to_ckr(), + } + }; +} + +// ── Helpers ─────────────────────────────────────────────────────────────── + +fn check_init() -> error::Result<()> { + if global().read().is_ok_and(|g| g.is_some()) { + Ok(()) + } else { + Err(Pkcs11Error::NotInitialised) + } +} + +/// Extract the slot_id from a session handle. +fn session_slot(h_session: CK_SESSION_HANDLE) -> error::Result { + session::with_session(h_session, |s| Ok(s.slot_id)) +} + +/// Return `Err(SessionReadOnly)` if the session is not a read-write session. +fn require_rw_session(h_session: CK_SESSION_HANDLE) -> error::Result<()> { + session::with_session(h_session, |s| s.require_rw()) +} + +/// Return `true` if `attr_type` is present in `obj.attributes` and its first byte +/// equals `CK_TRUE`. Any other value (absent, empty, or `CK_FALSE`) returns `false`. +fn bool_attr_true(obj: &object_store::KeyObject, attr_type: CK_ATTRIBUTE_TYPE) -> bool { + obj.attributes + .get(&attr_type) + .is_some_and(|v| !v.is_empty() && v[0] == CK_TRUE) +} + +fn is_private_component_attr(attr_type: CK_ATTRIBUTE_TYPE) -> bool { + matches!( + attr_type, + CKA_PRIVATE_EXPONENT + | CKA_PRIME_1 + | CKA_PRIME_2 + | CKA_EXPONENT_1 + | CKA_EXPONENT_2 + | CKA_COEFFICIENT + ) +} + +fn fill_padded(dst: &mut [u8], src: &[u8]) { + let n = src.len().min(dst.len()); + dst[..n].copy_from_slice(&src[..n]); + for b in &mut dst[n..] { *b = b' '; } +} + +/// Copy `data` into a caller-provided PKCS#11 output buffer, handling the +/// three-phase protocol: size query (null pointer), buffer-too-small, or copy. +unsafe fn write_to_output(p_out: *mut CK_BYTE, pul_len: *mut CK_ULONG, data: &[u8]) -> CK_RV { + if p_out.is_null() { + *pul_len = data.len() as CK_ULONG; + return CKR_OK; + } + if (*pul_len as usize) < data.len() { + *pul_len = data.len() as CK_ULONG; + return CKR_BUFFER_TOO_SMALL; + } + std::ptr::copy_nonoverlapping(data.as_ptr(), p_out, data.len()); + *pul_len = data.len() as CK_ULONG; + CKR_OK +} + + +mod ffi_api_core; +mod ffi_api_crypto; +mod ffi_api_v3; + +pub use ffi_api_core::*; +pub use ffi_api_crypto::*; +pub use ffi_api_v3::*; + +// ── Static CK_INTERFACE (v3.0) ────────────────────────────────────────── + +static INTERFACE_3_0: CK_INTERFACE = CK_INTERFACE { + pInterfaceName: PKCS11_INTERFACE_NAME.as_ptr() as *const libc::c_char as *mut _, + pFunctionList: &FUNCTION_LIST_3_0 as *const CK_FUNCTION_LIST_3_0 as *const c_void, + flags: CKF_INTERFACE_FORK_SAFE, +}; + +// ── Static FUNCTION_LIST_3_0 (v3.0 extended) ───────────────────────────── + +pub static FUNCTION_LIST_3_0: CK_FUNCTION_LIST_3_0 = CK_FUNCTION_LIST_3_0 { + version: CK_VERSION { major: 3, minor: 0 }, + + C_Initialize: Some(C_Initialize), + C_Finalize: Some(C_Finalize), + C_GetInfo: Some(C_GetInfo), + C_GetFunctionList: Some(C_GetFunctionList), + C_GetSlotList: Some(C_GetSlotList), + C_GetSlotInfo: Some(C_GetSlotInfo), + C_GetTokenInfo: Some(C_GetTokenInfo), + C_GetMechanismList: Some(C_GetMechanismList), + C_GetMechanismInfo: Some(C_GetMechanismInfo), + + C_InitToken: Some(C_InitToken), + C_InitPIN: Some(C_InitPIN), + C_SetPIN: Some(C_SetPIN), + + C_OpenSession: Some(C_OpenSession), + C_CloseSession: Some(C_CloseSession), + C_CloseAllSessions: Some(C_CloseAllSessions), + C_GetSessionInfo: Some(C_GetSessionInfo), + C_GetOperationState: Some(C_GetOperationState), + C_SetOperationState: Some(C_SetOperationState), + C_Login: Some(C_Login), + C_Logout: Some(C_Logout), + + C_CreateObject: Some(C_CreateObject), + C_CopyObject: Some(C_CopyObject), + C_DestroyObject: Some(C_DestroyObject), + C_GetObjectSize: Some(C_GetObjectSize), + C_GetAttributeValue: Some(C_GetAttributeValue), + C_SetAttributeValue: Some(C_SetAttributeValue), + + C_FindObjectsInit: Some(C_FindObjectsInit), + C_FindObjects: Some(C_FindObjects), + C_FindObjectsFinal: Some(C_FindObjectsFinal), + + C_EncryptInit: Some(C_EncryptInit), + C_Encrypt: Some(C_Encrypt), + C_EncryptUpdate: Some(C_EncryptUpdate), + C_EncryptFinal: Some(C_EncryptFinal), + + C_DecryptInit: Some(C_DecryptInit), + C_Decrypt: Some(C_Decrypt), + C_DecryptUpdate: Some(C_DecryptUpdate), + C_DecryptFinal: Some(C_DecryptFinal), + + C_DigestInit: Some(C_DigestInit), + C_Digest: Some(C_Digest), + C_DigestUpdate: Some(C_DigestUpdate), + C_DigestKey: Some(C_DigestKey), + C_DigestFinal: Some(C_DigestFinal), + + C_SignInit: Some(C_SignInit), + C_Sign: Some(C_Sign), + C_SignUpdate: Some(C_SignUpdate), + C_SignFinal: Some(C_SignFinal), + C_SignRecoverInit: Some(C_SignRecoverInit), + C_SignRecover: Some(C_SignRecover), + + C_VerifyInit: Some(C_VerifyInit), + C_Verify: Some(C_Verify), + C_VerifyUpdate: Some(C_VerifyUpdate), + C_VerifyFinal: Some(C_VerifyFinal), + C_VerifyRecoverInit: Some(C_VerifyRecoverInit), + C_VerifyRecover: Some(C_VerifyRecover), + + C_DigestEncryptUpdate: Some(C_DigestEncryptUpdate), + C_DecryptDigestUpdate: Some(C_DecryptDigestUpdate), + C_SignEncryptUpdate: Some(C_SignEncryptUpdate), + C_DecryptVerifyUpdate: Some(C_DecryptVerifyUpdate), + + C_GenerateKey: Some(C_GenerateKey), + C_GenerateKeyPair: Some(C_GenerateKeyPair), + C_WrapKey: Some(C_WrapKey), + C_UnwrapKey: Some(C_UnwrapKey), + C_DeriveKey: Some(C_DeriveKey), + C_SeedRandom: Some(C_SeedRandom), + C_GenerateRandom: Some(C_GenerateRandom), + C_GetFunctionStatus: Some(C_GetFunctionStatus), + C_CancelFunction: Some(C_CancelFunction), + C_WaitForSlotEvent: Some(C_WaitForSlotEvent), + + // v3.0 new functions + C_GetInterfaceList: Some(C_GetInterfaceList), + C_GetInterface: Some(C_GetInterface), + C_LoginUser: Some(C_LoginUser), + C_SessionCancel: Some(C_SessionCancel), + + C_MessageEncryptInit: Some(C_MessageEncryptInit), + C_EncryptMessage: Some(C_EncryptMessage), + C_EncryptMessageBegin: Some(C_EncryptMessageBegin), + C_EncryptMessageNext: Some(C_EncryptMessageNext), + C_MessageEncryptFinal: Some(C_MessageEncryptFinal), + + C_MessageDecryptInit: Some(C_MessageDecryptInit), + C_DecryptMessage: Some(C_DecryptMessage), + C_DecryptMessageBegin: Some(C_DecryptMessageBegin), + C_DecryptMessageNext: Some(C_DecryptMessageNext), + C_MessageDecryptFinal: Some(C_MessageDecryptFinal), + + C_MessageSignInit: Some(C_MessageSignInit), + C_SignMessage: Some(C_SignMessage), + C_SignMessageBegin: Some(C_SignMessageBegin), + C_SignMessageNext: Some(C_SignMessageNext), + C_MessageSignFinal: Some(C_MessageSignFinal), + + C_MessageVerifyInit: Some(C_MessageVerifyInit), + C_VerifyMessage: Some(C_VerifyMessage), + C_VerifyMessageBegin: Some(C_VerifyMessageBegin), + C_VerifyMessageNext: Some(C_VerifyMessageNext), + C_MessageVerifyFinal: Some(C_MessageVerifyFinal), +}; + +// ── Static FUNCTION_LIST (v2.40 compat) + top-level #[no_mangle] export ── + +pub static FUNCTION_LIST: CK_FUNCTION_LIST = CK_FUNCTION_LIST { + version: CK_VERSION { major: 3, minor: 0 }, + + C_Initialize: Some(C_Initialize), + C_Finalize: Some(C_Finalize), + C_GetInfo: Some(C_GetInfo), + C_GetFunctionList: Some(C_GetFunctionList), + C_GetSlotList: Some(C_GetSlotList), + C_GetSlotInfo: Some(C_GetSlotInfo), + C_GetTokenInfo: Some(C_GetTokenInfo), + C_GetMechanismList: Some(C_GetMechanismList), + C_GetMechanismInfo: Some(C_GetMechanismInfo), + + C_InitToken: Some(C_InitToken), + C_InitPIN: Some(C_InitPIN), + C_SetPIN: Some(C_SetPIN), + + C_OpenSession: Some(C_OpenSession), + C_CloseSession: Some(C_CloseSession), + C_CloseAllSessions: Some(C_CloseAllSessions), + C_GetSessionInfo: Some(C_GetSessionInfo), + C_GetOperationState: Some(C_GetOperationState), + C_SetOperationState: Some(C_SetOperationState), + C_Login: Some(C_Login), + C_Logout: Some(C_Logout), + + C_CreateObject: Some(C_CreateObject), + C_CopyObject: Some(C_CopyObject), + C_DestroyObject: Some(C_DestroyObject), + C_GetObjectSize: Some(C_GetObjectSize), + C_GetAttributeValue: Some(C_GetAttributeValue), + C_SetAttributeValue: Some(C_SetAttributeValue), + + C_FindObjectsInit: Some(C_FindObjectsInit), + C_FindObjects: Some(C_FindObjects), + C_FindObjectsFinal: Some(C_FindObjectsFinal), + + C_EncryptInit: Some(C_EncryptInit), + C_Encrypt: Some(C_Encrypt), + C_EncryptUpdate: Some(C_EncryptUpdate), + C_EncryptFinal: Some(C_EncryptFinal), + + C_DecryptInit: Some(C_DecryptInit), + C_Decrypt: Some(C_Decrypt), + C_DecryptUpdate: Some(C_DecryptUpdate), + C_DecryptFinal: Some(C_DecryptFinal), + + C_DigestInit: Some(C_DigestInit), + C_Digest: Some(C_Digest), + C_DigestUpdate: Some(C_DigestUpdate), + C_DigestKey: Some(C_DigestKey), + C_DigestFinal: Some(C_DigestFinal), + + C_SignInit: Some(C_SignInit), + C_Sign: Some(C_Sign), + C_SignUpdate: Some(C_SignUpdate), + C_SignFinal: Some(C_SignFinal), + C_SignRecoverInit: Some(C_SignRecoverInit), + C_SignRecover: Some(C_SignRecover), + + C_VerifyInit: Some(C_VerifyInit), + C_Verify: Some(C_Verify), + C_VerifyUpdate: Some(C_VerifyUpdate), + C_VerifyFinal: Some(C_VerifyFinal), + C_VerifyRecoverInit: Some(C_VerifyRecoverInit), + C_VerifyRecover: Some(C_VerifyRecover), + + C_DigestEncryptUpdate: Some(C_DigestEncryptUpdate), + C_DecryptDigestUpdate: Some(C_DecryptDigestUpdate), + C_SignEncryptUpdate: Some(C_SignEncryptUpdate), + C_DecryptVerifyUpdate: Some(C_DecryptVerifyUpdate), + + C_GenerateKey: Some(C_GenerateKey), + C_GenerateKeyPair: Some(C_GenerateKeyPair), + C_WrapKey: Some(C_WrapKey), + C_UnwrapKey: Some(C_UnwrapKey), + C_DeriveKey: Some(C_DeriveKey), + C_SeedRandom: Some(C_SeedRandom), + C_GenerateRandom: Some(C_GenerateRandom), + C_GetFunctionStatus: Some(C_GetFunctionStatus), + C_CancelFunction: Some(C_CancelFunction), + C_WaitForSlotEvent: Some(C_WaitForSlotEvent), +}; diff --git a/src/pkcs11/object_store.rs b/src/pkcs11/object_store.rs new file mode 100644 index 0000000..1a6862e --- /dev/null +++ b/src/pkcs11/object_store.rs @@ -0,0 +1,425 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! In-memory object store — engine-agnostic key descriptors + CKA_* attribute maps. +//! Global store backed by `once_cell::Lazy>`. + +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; + +use once_cell::sync::Lazy; +use parking_lot::RwLock; + +use crate::traits::EngineKeyRef; + +use super::constants::*; +use super::error::{Pkcs11Error, Result}; +use super::types::*; + +// ── Key type discriminant ───────────────────────────────────────────────── + +/// Identifies what kind of key a `KeyObject` represents. +/// No crypto-library types — just a tag used for dispatch in `backend.rs`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeyType { + RsaPrivate, + RsaPublic, + EcPrivate, + EcPublic, + AesSecret, + GenericSecret, + EdPrivate, // v3.0 — Ed25519, Ed448 + EdPublic, // v3.0 — Ed25519, Ed448 + ChaCha20Secret, // v3.0 + /// `CKO_PROFILE` — not a key; holds `CKA_PROFILE_ID` describing which + /// PKCS#11 v3.0 profile this token claims to implement. `key_ref` is empty. + Profile, // v3.0 +} + +// ── KeyObject ───────────────────────────────────────────────────────────── + +pub struct KeyObject { + pub handle: CK_OBJECT_HANDLE, + /// The slot this object belongs to. + pub slot_id: CK_SLOT_ID, + pub key_type: KeyType, + /// Opaque key reference used by the `CryptoProvider`. + /// For software engines this wraps DER-encoded bytes; for HSM/TPM backends + /// it could be a handle. The PKCS#11 layer never inspects the contents. + pub key_ref: EngineKeyRef, + /// All CKA_* attributes encoded as raw bytes: + /// - `CK_ULONG` → 8-byte little-endian + /// - `CK_BBOOL` → 1 byte (0 = false, 1 = true) + /// - byte arrays → raw bytes + pub attributes: HashMap>, + /// The session that created this object. Session objects (CKA_TOKEN=false) + /// are destroyed when their creating session closes. Token objects are + /// persisted and this field is ignored for them. + pub creating_session: Option, + /// CKA_ALWAYS_AUTHENTICATE — user must re-authenticate before each use. + pub always_authenticate: bool, + /// CKA_LOCAL — true when the key was generated on the token (not imported). + pub local: bool, + /// CKA_ALWAYS_SENSITIVE — has been sensitive since creation (never changed). + pub always_sensitive: bool, + /// CKA_NEVER_EXTRACTABLE — has never been extractable since creation. + pub never_extractable: bool, + /// CKA_KEY_GEN_MECHANISM — mechanism used to generate the key, or + /// `CK_UNAVAILABLE_INFORMATION` if not applicable / unknown. + pub key_gen_mechanism: CK_MECHANISM_TYPE, +} + +impl KeyObject { + pub fn new( + handle: CK_OBJECT_HANDLE, + slot_id: CK_SLOT_ID, + key_type: KeyType, + key_ref: EngineKeyRef, + attrs: HashMap>, + ) -> Self { + let mut attributes = attrs; + attributes.entry(CKA_TOKEN).or_insert_with(|| vec![CK_FALSE]); + KeyObject { + handle, + slot_id, + key_type, + key_ref, + attributes, + creating_session: None, + always_authenticate: false, + local: false, + always_sensitive: false, + never_extractable: false, + key_gen_mechanism: CK_UNAVAILABLE_INFORMATION, + } + } + + pub fn get_attr(&self, attr_type: CK_ATTRIBUTE_TYPE) -> Result<&[u8]> { + self.attributes + .get(&attr_type) + .map(|v| v.as_slice()) + .ok_or(Pkcs11Error::InvalidAttributeType) + } + + /// Determines if this object matches the provided search template. + /// + /// In PKCS#11, a template is a list of attributes that an object must possess + /// with exact matching values to be considered a "match." + /// + /// # Arguments + /// * `template` - A slice of tuples containing the attribute type and the expected raw bytes. + /// + /// # Returns + /// * `true` if the object contains all attributes in the template with matching values, + /// or if the template is empty (vacuous truth). + /// * `false` if any attribute is missing from the object or has a different value. + pub fn matches_template(&self, template: &[(CK_ATTRIBUTE_TYPE, Vec)]) -> bool { + // Iterate through every (Type, Value) pair in the search criteria. + for (attr_type, expected) in template { + // Look up the attribute in this object's internal attribute map. + match self.attributes.get(attr_type) { + // Success case: The attribute exists AND the bytes match exactly. + // We use a 'Match Guard' (if v == expected) to verify the contents. + Some(v) if v == expected => {} + // Failure case: The attribute is either missing (None) + // or the value did not match the guard condition. + _ => return false, + } + } + // If the loop completes without returning false, all criteria + // in the template (if any) were satisfied by this object. + // Note: An empty template (resulting from a NULL pTemplate) + // matches all objects. + true + } +} + +// ── Global store ────────────────────────────────────────────────────────── + +static OBJECT_STORE: Lazy>> = + Lazy::new(|| RwLock::new(HashMap::new())); + +static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1); + +pub fn next_handle() -> CK_OBJECT_HANDLE { + NEXT_HANDLE.fetch_add(1, Ordering::SeqCst) +} + +/// Store an object, optionally tagging it with the creating session handle. +/// Session objects (CKA_TOKEN=false) are tagged so they can be destroyed when +/// their creating session closes. +/// +/// Only token objects (`CKA_TOKEN = CK_TRUE`) trigger a disk write. +/// Session objects never touch the storage layer. +pub fn store_object(mut obj: KeyObject, session_handle: Option) -> CK_OBJECT_HANDLE { + let is_token = !is_session_object(&obj); + if !is_token { + obj.creating_session = session_handle; + } + let h = obj.handle; + OBJECT_STORE.write().insert(h, obj); + if is_token { + persist_if_needed(); + } + h +} + +/// Call `f` with a shared reference to the object; fails if handle is unknown. +pub fn with_object(handle: CK_OBJECT_HANDLE, f: F) -> Result +where + F: FnOnce(&KeyObject) -> Result, +{ + let store = OBJECT_STORE.read(); + let obj = store.get(&handle).ok_or(Pkcs11Error::InvalidObjectHandle)?; + f(obj) +} + +/// Call `f` with a shared reference to the object, enforcing slot isolation. +/// +/// Returns `InvalidObjectHandle` if the object doesn't exist **or** belongs to +/// a different slot. This prevents cross-slot object access. +pub fn with_object_for_slot(handle: CK_OBJECT_HANDLE, slot_id: CK_SLOT_ID, f: F) -> Result +where + F: FnOnce(&KeyObject) -> Result, +{ + let store = OBJECT_STORE.read(); + let obj = store.get(&handle).ok_or(Pkcs11Error::InvalidObjectHandle)?; + if obj.slot_id != slot_id { + return Err(Pkcs11Error::InvalidObjectHandle); + } + f(obj) +} + +/// Call `f` with a mutable reference to the object; fails if handle is unknown. +pub fn with_object_mut(handle: CK_OBJECT_HANDLE, f: F) -> Result +where + F: FnOnce(&mut KeyObject) -> Result, +{ + let mut store = OBJECT_STORE.write(); + let obj = store.get_mut(&handle).ok_or(Pkcs11Error::InvalidObjectHandle)?; + f(obj) +} + +/// Find objects matching a template, respecting private object visibility. +/// +/// Private objects (CKA_PRIVATE = CK_TRUE) are only visible when `logged_in` +/// is true. +pub fn find_objects( + slot_id: CK_SLOT_ID, + template: &[(CK_ATTRIBUTE_TYPE, Vec)], + logged_in: bool, +) -> Vec { + OBJECT_STORE + .read() + .values() + .filter(|o| { + o.slot_id == slot_id + && o.matches_template(template) + && (logged_in || !is_private_object(o)) + }) + .map(|o| o.handle) + .collect() +} + +/// Remove an object from the store. +/// +/// Only triggers a disk write when the destroyed object was a token object +/// (`CKA_TOKEN = CK_TRUE`). Destroying a session object never touches disk. +pub fn destroy_object(handle: CK_OBJECT_HANDLE) -> Result<()> { + let obj = OBJECT_STORE + .write() + .remove(&handle) + .ok_or(Pkcs11Error::InvalidObjectHandle)?; + if !is_session_object(&obj) { + persist_if_needed(); + } + Ok(()) +} + +pub fn clear_objects() { OBJECT_STORE.write().clear(); } + +/// Clear only objects belonging to a specific slot (used by C_InitToken). +pub fn clear_objects_for_slot(slot_id: CK_SLOT_ID) { + OBJECT_STORE.write().retain(|_, obj| obj.slot_id != slot_id); + persist_if_needed(); +} + +pub fn object_count() -> usize { OBJECT_STORE.read().len() } + +/// Ensure a `CKP_BASELINE_PROVIDER` profile object exists for `slot_id`. +/// +/// Idempotent: if a profile object already exists on this slot, this is a no-op. +/// Otherwise it creates a `CKO_PROFILE` object with `CKA_PROFILE_ID = +/// CKP_BASELINE_PROVIDER`, `CKA_TOKEN = TRUE`, `CKA_PRIVATE = FALSE`, and an +/// empty `EngineKeyRef`. +/// +/// Called from `C_Initialize` (per slot) and `C_InitToken`, so every initialized +/// token advertises at least one profile as required by PKCS#11 v3.0. +pub fn ensure_baseline_profile(slot_id: CK_SLOT_ID) -> CK_OBJECT_HANDLE { + // Already present on this slot? + { + let store = OBJECT_STORE.read(); + for obj in store.values() { + if obj.slot_id == slot_id && obj.key_type == KeyType::Profile { + return obj.handle; + } + } + } + + let handle = next_handle(); + let mut attrs: HashMap> = HashMap::new(); + attrs.insert(CKA_CLASS, (CKO_PROFILE as CK_ULONG).to_le_bytes().to_vec()); + attrs.insert(CKA_TOKEN, vec![CK_TRUE]); + attrs.insert(CKA_PRIVATE, vec![CK_FALSE]); + attrs.insert(CKA_DESTROYABLE, vec![CK_FALSE]); + attrs.insert(CKA_PROFILE_ID, (CKP_BASELINE_PROVIDER as CK_ULONG).to_le_bytes().to_vec()); + + let obj = KeyObject::new( + handle, + slot_id, + KeyType::Profile, + EngineKeyRef::from_bytes(Vec::new()), + attrs, + ); + OBJECT_STORE.write().insert(handle, obj); + handle +} + +/// Check if an object has CKA_PRIVATE = CK_TRUE. +pub fn is_private_object(obj: &KeyObject) -> bool { + obj.attributes + .get(&CKA_PRIVATE) + .map(|v| !v.is_empty() && v[0] == CK_TRUE) + .unwrap_or(false) +} + +/// Check if an object is a session object (CKA_TOKEN = CK_FALSE or absent). +pub fn is_session_object(obj: &KeyObject) -> bool { + !obj.attributes + .get(&CKA_TOKEN) + .map(|v| !v.is_empty() && v[0] == CK_TRUE) + .unwrap_or(false) +} + +/// Check if CKA_TOKEN = CK_TRUE +pub fn is_token_object(obj: &KeyObject) -> bool { + obj.attributes + .get(&CKA_TOKEN) + .map(|v| !v.is_empty() && v[0] == CK_TRUE) + .unwrap_or(false) + } + +/// Returns the length of the RSA modulus in bytes. +/// Used to validate signature length in C_Verify. +pub fn get_modulus_len(obj: &KeyObject) -> Result { + obj.attributes + .get(&CKA_MODULUS) + .map(|v| v.len()) + .ok_or(Pkcs11Error::InvalidAttributeType) +} + +/// Destroy session objects owned by a specific session. +/// Called by C_CloseSession when that session closes. +pub fn destroy_objects_for_session(session_handle: CK_SESSION_HANDLE) { + OBJECT_STORE.write().retain(|_, obj| { + obj.creating_session != Some(session_handle) + }); +} + +/// Destroy all session objects belonging to a specific slot. +/// Called by C_CloseAllSessions. +pub fn destroy_session_objects_for_slot(slot_id: CK_SLOT_ID) { + OBJECT_STORE.write().retain(|_, obj| { + !(obj.slot_id == slot_id && is_session_object(obj)) + }); +} + +/// Destroy private session objects on a slot (called by C_Logout). +pub fn destroy_private_session_objects(slot_id: CK_SLOT_ID) { + OBJECT_STORE.write().retain(|_, obj| { + !(obj.slot_id == slot_id && is_session_object(obj) && is_private_object(obj)) + }); +} + +// ── Persistence integration ────────────────────────────────────────────── + +use super::storage; + +/// Save all token objects (CKA_TOKEN = CK_TRUE) to disk. +/// Called automatically after store_object / destroy_object, and explicitly by C_Finalize. +pub fn persist_to_disk() { + persist_if_needed(); +} + +fn persist_if_needed() { + let store = OBJECT_STORE.read(); + let mut objects: Vec = Vec::new(); + for obj in store.values().filter(|o| storage::is_token_object(o) && o.key_type != KeyType::Profile) { + match crate::registry::engine_for_slot(obj.slot_id) { + Ok((engine, _)) => match storage::StoredObject::from_key_object(obj, engine.as_ref()) { + Ok(stored) => objects.push(stored), + Err(e) => eprintln!("cryptoki:serialize error: {e}"), + }, + Err(_) => eprintln!("cryptoki:no engine for slot {} — skipping object {}", obj.slot_id, obj.handle), + } + } + + // Collect token state for every registered slot. + let mut tokens = HashMap::new(); + for slot_id in crate::registry::slot_ids() { + let token_state = super::token::with_token(slot_id, |t| storage::StoredToken::from(t)); + tokens.insert(slot_id, token_state); + } + + let state = storage::StoredState { + version: 1, + tokens, + token: None, + objects, + next_handle: NEXT_HANDLE.load(Ordering::SeqCst), + }; + let _ = storage::save_state(&state); +} + +/// Load persisted objects from disk into the object store. +/// Called by `C_Initialize` to restore token objects from a previous session. +pub fn load_persisted_objects() { + if let Some(state) = storage::load_state() { + let mut store = OBJECT_STORE.write(); + for stored in state.objects { + let slot_id = stored.slot_id; + let h = stored.handle; + match crate::registry::engine_for_slot(slot_id) { + Ok((engine, _)) => match stored.into_key_object_with_engine(engine.as_ref()) { + Ok(obj) => { store.insert(h, obj); } + Err(e) => eprintln!("cryptoki:deserialize error: {e}"), + }, + Err(_) => eprintln!("cryptoki:no engine for slot {slot_id} — skipping object {h} on load"), + } + } + // Ensure the handle counter is past all loaded handles to avoid collisions. + let max_loaded = store.keys().copied().max().unwrap_or(0); + let counter = state.next_handle.max(max_loaded + 1); + NEXT_HANDLE.store(counter, Ordering::SeqCst); + + // Restore per-slot token state. + if !state.tokens.is_empty() { + for (slot_id, stored_token) in &state.tokens { + super::token::with_token_mut(*slot_id, |t| stored_token.apply_to(t)); + } + } else if let Some(ref legacy_token) = state.token { + // Backward compat: legacy single-token format → apply to slot 0. + super::token::with_token_mut(0, |t| legacy_token.apply_to(t)); + } + } +} diff --git a/src/pkcs11/session.rs b/src/pkcs11/session.rs new file mode 100644 index 0000000..3d55621 --- /dev/null +++ b/src/pkcs11/session.rs @@ -0,0 +1,292 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Per-session state: operation contexts, login state, global session store. +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; + +use once_cell::sync::Lazy; +use parking_lot::RwLock; + +use super::constants::*; +use super::error::{Pkcs11Error, Result}; +use super::object_store::KeyObject; +use super::types::*; + +// ── Operation contexts ──────────────────────────────────────────────────── + +/// State for an active C_SignInit / C_VerifyInit operation. +#[derive(Debug, Clone)] +pub struct SignContext { + pub mechanism: CK_MECHANISM_TYPE, + pub key_handle: CK_OBJECT_HANDLE, + /// Accumulated message bytes (multi-part). + pub data: Vec, +} + +/// State for an active C_EncryptInit / C_DecryptInit operation. +#[derive(Debug, Clone)] +pub struct CipherContext { + pub mechanism: CK_MECHANISM_TYPE, + pub key_handle: CK_OBJECT_HANDLE, + pub iv: Option>, + pub aad: Option>, + pub tag_len: usize, + pub accumulated: Vec, +} + +/// State for an active C_DigestInit operation. +#[derive(Debug, Clone)] +pub struct DigestContext { + pub mechanism: CK_MECHANISM_TYPE, + pub data: Vec, + pub is_single_part: bool, + pub is_multi_part: bool, +} + +/// State for an active C_FindObjectsInit operation. +#[derive(Debug, Clone)] +pub struct FindContext { + pub results: Vec, + pub index: usize, +} + +/// State for v3.0 message-based encrypt/decrypt/sign/verify operations. +#[derive(Debug, Clone)] +pub struct MessageCipherContext { + pub mechanism: CK_MECHANISM_TYPE, + pub key_handle: CK_OBJECT_HANDLE, +} + +/// State for v3.0 message-based signing/verification operations. +#[derive(Debug, Clone)] +pub struct MessageSignContext { + pub mechanism: CK_MECHANISM_TYPE, + pub key_handle: CK_OBJECT_HANDLE, +} + +// ── Session ─────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, PartialEq)] +pub enum LoginState { + NotLoggedIn, + UserLoggedIn, + SoLoggedIn, +} + +#[derive(Debug)] +pub struct Session { + pub handle: CK_SESSION_HANDLE, + pub slot_id: CK_SLOT_ID, + pub flags: CK_FLAGS, + pub login_state: LoginState, + + /// Set to `true` after a successful `C_Login(CKU_CONTEXT_SPECIFIC)`. + /// Consumed (reset to `false`) by the next private-key operation. + /// Guards keys with `CKA_ALWAYS_AUTHENTICATE = TRUE`. + pub context_specific_authed: bool, + + pub sign_ctx: Option, + pub verify_ctx: Option, + pub encrypt_ctx: Option, + pub decrypt_ctx: Option, + pub digest_ctx: Option, + pub find_ctx: Option, + + // v3.0 message-based operation contexts + pub msg_encrypt_ctx: Option, + pub msg_decrypt_ctx: Option, + pub msg_sign_ctx: Option, + pub msg_verify_ctx: Option, +} + +impl Session { + pub fn new(handle: CK_SESSION_HANDLE, slot_id: CK_SLOT_ID, flags: CK_FLAGS) -> Self { + Session { + handle, + slot_id, + flags, + login_state: LoginState::NotLoggedIn, + context_specific_authed: false, + sign_ctx: None, + verify_ctx: None, + encrypt_ctx: None, + decrypt_ctx: None, + digest_ctx: None, + find_ctx: None, + msg_encrypt_ctx: None, + msg_decrypt_ctx: None, + msg_sign_ctx: None, + msg_verify_ctx: None, + } + } + + pub fn is_rw(&self) -> bool { self.flags & CKF_RW_SESSION != 0 } + + pub fn require_rw(&self) -> Result<()> { + if self.is_rw() { Ok(()) } else { Err(Pkcs11Error::SessionReadOnly) } + } + + /// Gate a private-key operation on `CKA_ALWAYS_AUTHENTICATE`. + /// + /// * If `obj.always_authenticate` is false — permit unconditionally. + /// * If true and `context_specific_authed` is false — deny. + /// * If true and `context_specific_authed` is true — permit **and consume** the flag + /// (one-shot per spec: the caller must call `C_Login(CKU_CONTEXT_SPECIFIC)` again + /// before the next operation on the same key). + pub fn require_context_auth(&mut self, obj: &KeyObject) -> Result<()> { + if obj.always_authenticate { + // Check if the "single-use ticket" exists + if !self.context_specific_authed { + return Err(Pkcs11Error::UserNotLoggedIn); + } + // // consume the authentication immediately + // self.context_specific_authed = false; + } + Ok(()) + } +} + +// ── Global store ────────────────────────────────────────────────────────── + +static SESSIONS: Lazy>> = + Lazy::new(|| RwLock::new(HashMap::new())); + +static NEXT_SESSION: AtomicU64 = AtomicU64::new(1); + +fn next_session_handle() -> CK_SESSION_HANDLE { + NEXT_SESSION.fetch_add(1, Ordering::SeqCst) +} + +pub fn open_session(slot_id: CK_SLOT_ID, flags: CK_FLAGS) -> Result { + if flags & CKF_SERIAL_SESSION == 0 { + return Err(Pkcs11Error::SessionParallelNotSupported); + } + + let is_rw = (flags & CKF_RW_SESSION) != 0; + + let mut sessions = SESSIONS.write(); + + // Determine the current login state for this token + let current_login_state = sessions.values() + .find(|s| s.slot_id == slot_id) + .map(|s| s.login_state.clone()) + .unwrap_or(LoginState::NotLoggedIn); + + // Security Officer cannot have Read-Only sessions + if current_login_state == LoginState::SoLoggedIn && !is_rw { + return Err(Pkcs11Error::SessionReadWriteSoExists); + } + let handle = next_session_handle(); + let mut session = Session::new(handle, slot_id, flags); + // Inherit the safely validated login state + session.login_state = current_login_state; + sessions.insert(handle, session); + Ok(handle) +} + +pub fn close_session(handle: CK_SESSION_HANDLE) -> Result<()> { + SESSIONS.write().remove(&handle).ok_or(Pkcs11Error::InvalidSessionHandle)?; + Ok(()) +} + +pub fn close_all_sessions(slot_id: CK_SLOT_ID) { + SESSIONS.write().retain(|_, s| s.slot_id != slot_id); +} + +pub fn with_session_mut(handle: CK_SESSION_HANDLE, f: F) -> Result +where + F: FnOnce(&mut Session) -> Result, +{ + let mut sessions = SESSIONS.write(); + let session = sessions.get_mut(&handle).ok_or(Pkcs11Error::InvalidSessionHandle)?; + f(session) +} + +pub fn with_session(handle: CK_SESSION_HANDLE, f: F) -> Result +where + F: FnOnce(&Session) -> Result, +{ + let sessions = SESSIONS.read(); + let session = sessions.get(&handle).ok_or(Pkcs11Error::InvalidSessionHandle)?; + f(session) +} + +pub fn get_session_info(handle: CK_SESSION_HANDLE) -> Result { + with_session(handle, |s| { + let state: CK_ULONG = match s.login_state { + LoginState::NotLoggedIn => if s.is_rw() { CKS_RW_PUBLIC_SESSION } else { CKS_RO_PUBLIC_SESSION }, + LoginState::UserLoggedIn => if s.is_rw() { CKS_RW_USER_FUNCTIONS } else { CKS_RO_USER_FUNCTIONS }, + LoginState::SoLoggedIn => CKS_RW_SO_FUNCTIONS, + }; + Ok(CK_SESSION_INFO { + slotID: s.slot_id, + state, + flags: s.flags, + ulDeviceError: 0, + }) + }) +} + +pub fn clear_sessions() { SESSIONS.write().clear(); } + +pub fn session_count() -> usize { SESSIONS.read().len() } + +/// Count sessions on a specific slot. +pub fn session_count_for_slot(slot_id: CK_SLOT_ID) -> usize { + SESSIONS.read().values().filter(|s| s.slot_id == slot_id).count() +} + +/// Count RW sessions on a specific slot. +pub fn rw_session_count_for_slot(slot_id: CK_SLOT_ID) -> usize { + SESSIONS.read().values().filter(|s| s.slot_id == slot_id && s.is_rw()).count() +} + +/// Check if any RO sessions exist on a specific slot. +pub fn has_ro_sessions_on_slot(slot_id: CK_SLOT_ID) -> bool { + SESSIONS.read().values().any(|s| s.slot_id == slot_id && !s.is_rw()) +} + +/// Set login state for ALL sessions on the given slot. +pub fn login_all_sessions_on_slot(slot_id: CK_SLOT_ID, state: LoginState) { + let mut sessions = SESSIONS.write(); + for s in sessions.values_mut().filter(|s| s.slot_id == slot_id) { + s.login_state = state.clone(); + } +} + +/// Release active find-object contexts on every session for a slot. +/// Called during logout because object visibility changes (private objects +/// become hidden), so any in-progress C_FindObjects must be invalidated. +pub fn release_find_contexts_on_slot(slot_id: CK_SLOT_ID) { + let mut sessions = SESSIONS.write(); + for s in sessions.values_mut().filter(|s| s.slot_id == slot_id) { + s.find_ctx = None; + } +} + +/// Get the current login state for a slot (from any session on that slot). +pub fn login_state_for_slot(slot_id: CK_SLOT_ID) -> LoginState { + SESSIONS.read() + .values() + .find(|s| s.slot_id == slot_id) + .map(|s| s.login_state.clone()) + .unwrap_or(LoginState::NotLoggedIn) +} + +/// Check if any sessions (Read-Only or Read-Write) exist on a specific slot. +pub fn has_open_sessions(slot_id: CK_SLOT_ID) -> bool { + SESSIONS.read() + .values() + .any(|s| s.slot_id == slot_id) +} diff --git a/src/pkcs11/storage.rs b/src/pkcs11/storage.rs new file mode 100644 index 0000000..50123a6 --- /dev/null +++ b/src/pkcs11/storage.rs @@ -0,0 +1,31 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Persistent token storage — serializes token objects to disk. +//! +//! Only objects with `CKA_TOKEN = CK_TRUE` are persisted. +//! Storage path: `$CRYPTOKI_STORE` or `~/.cryptoki/token.json`. +//! +//! This is a thin hub that re-exports storage model, IO, locking, and helper modules. + +mod helpers; +mod io; +mod locks; +mod models; +mod path; + +pub use helpers::*; +pub use io::*; +pub use locks::*; +pub use models::*; +pub use path::*; diff --git a/src/pkcs11/storage/helpers.rs b/src/pkcs11/storage/helpers.rs new file mode 100644 index 0000000..16acf23 --- /dev/null +++ b/src/pkcs11/storage/helpers.rs @@ -0,0 +1,22 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use super::super::constants::CKA_TOKEN; +use super::super::object_store::KeyObject; +use super::super::types::CK_TRUE; + +pub fn is_token_object(obj: &KeyObject) -> bool { + obj.attributes + .get(&CKA_TOKEN) + .map(|v| !v.is_empty() && v[0] == CK_TRUE) + .unwrap_or(false) +} diff --git a/src/pkcs11/storage/io.rs b/src/pkcs11/storage/io.rs new file mode 100644 index 0000000..6e9633a --- /dev/null +++ b/src/pkcs11/storage/io.rs @@ -0,0 +1,89 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use std::fs; +use std::io::Write as _; + +use super::locks::LOCK_FILE_FD; +use super::models::StoredState; +use super::path::storage_path; + +pub fn load_state() -> Option { + let path = storage_path(); + let data = fs::read_to_string(&path).ok()?; + serde_json::from_str(&data).ok() +} + +pub fn save_state(state: &StoredState) -> Result<(), String> { + let path = storage_path(); + let parent = path.parent().unwrap_or_else(|| std::path::Path::new(".")); + + fs::create_dir_all(parent).map_err(|e| format!("mkdir: {e}"))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + let _ = fs::set_permissions(parent, fs::Permissions::from_mode(0o700)); + } + + let lock_path = path.with_extension("lock"); + let lock_file = fs::OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(&lock_path) + .map_err(|e| format!("open lock file: {e}"))?; + #[cfg(unix)] + { + use std::os::unix::io::AsRawFd as _; + use std::sync::atomic::Ordering; + let fd = lock_file.as_raw_fd(); + LOCK_FILE_FD.store(fd, Ordering::Release); + let ret = unsafe { libc::flock(fd, libc::LOCK_EX) }; + if ret != 0 { + return Err(format!("flock exclusive failed: {}", std::io::Error::last_os_error())); + } + } + + let json = serde_json::to_string_pretty(state).map_err(|e| format!("serialize: {e}"))?; + + let mut tmp = tempfile::NamedTempFile::new_in(parent).map_err(|e| format!("create tempfile: {e}"))?; + tmp.write_all(json.as_bytes()).map_err(|e| format!("write tempfile: {e}"))?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + fs::set_permissions(tmp.path(), fs::Permissions::from_mode(0o600)) + .map_err(|e| format!("chmod tempfile 0600: {e}"))?; + } + + tmp.as_file().sync_all().map_err(|e| format!("fsync tempfile: {e}"))?; + tmp.persist(&path).map_err(|e| format!("persist (rename): {e}"))?; + + let dir = fs::File::open(parent).map_err(|e| format!("open parent dir: {e}"))?; + dir.sync_all().map_err(|e| format!("fsync parent dir: {e}"))?; + + drop(lock_file); + #[cfg(unix)] + { + use std::sync::atomic::Ordering; + LOCK_FILE_FD.store(-1, Ordering::Release); + } + Ok(()) +} + +pub fn delete_storage() -> Result<(), String> { + let path = storage_path(); + if path.exists() { + fs::remove_file(&path).map_err(|e| format!("delete: {e}"))?; + } + Ok(()) +} diff --git a/src/pkcs11/storage/locks.rs b/src/pkcs11/storage/locks.rs new file mode 100644 index 0000000..3419742 --- /dev/null +++ b/src/pkcs11/storage/locks.rs @@ -0,0 +1,29 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +#[cfg(unix)] +use std::sync::atomic::{AtomicI32, Ordering}; + +#[cfg(unix)] +pub(crate) static LOCK_FILE_FD: AtomicI32 = AtomicI32::new(-1); + +pub fn release_locks() { + #[cfg(unix)] + { + let fd = LOCK_FILE_FD.swap(-1, Ordering::SeqCst); + if fd >= 0 { + unsafe { + libc::close(fd); + } + } + } +} diff --git a/src/pkcs11/storage/models.rs b/src/pkcs11/storage/models.rs new file mode 100644 index 0000000..217268e --- /dev/null +++ b/src/pkcs11/storage/models.rs @@ -0,0 +1,219 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::traits::CryptoProvider; + +use super::super::object_store::{KeyObject, KeyType}; +use super::super::token::{HashedPin, Token, TokenState}; +use super::super::types::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum StoredKeyType { + RsaPrivate, + RsaPublic, + EcPrivate, + EcPublic, + AesSecret, + EdPrivate, + EdPublic, + ChaCha20Secret, + GenericSecret, +} + +impl From for StoredKeyType { + fn from(kt: KeyType) -> Self { + match kt { + KeyType::RsaPrivate => StoredKeyType::RsaPrivate, + KeyType::RsaPublic => StoredKeyType::RsaPublic, + KeyType::EcPrivate => StoredKeyType::EcPrivate, + KeyType::EcPublic => StoredKeyType::EcPublic, + KeyType::AesSecret => StoredKeyType::AesSecret, + KeyType::EdPrivate => StoredKeyType::EdPrivate, + KeyType::EdPublic => StoredKeyType::EdPublic, + KeyType::ChaCha20Secret => StoredKeyType::ChaCha20Secret, + KeyType::GenericSecret => StoredKeyType::GenericSecret, + KeyType::Profile => unreachable!("profile objects are not persisted"), + } + } +} + +impl From for KeyType { + fn from(skt: StoredKeyType) -> Self { + match skt { + StoredKeyType::RsaPrivate => KeyType::RsaPrivate, + StoredKeyType::RsaPublic => KeyType::RsaPublic, + StoredKeyType::EcPrivate => KeyType::EcPrivate, + StoredKeyType::EcPublic => KeyType::EcPublic, + StoredKeyType::AesSecret => KeyType::AesSecret, + StoredKeyType::EdPrivate => KeyType::EdPrivate, + StoredKeyType::EdPublic => KeyType::EdPublic, + StoredKeyType::ChaCha20Secret => KeyType::ChaCha20Secret, + StoredKeyType::GenericSecret => KeyType::GenericSecret, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredObject { + pub handle: CK_OBJECT_HANDLE, + #[serde(default)] + pub slot_id: CK_SLOT_ID, + pub key_type: StoredKeyType, + pub key_der: Vec, + pub attributes: HashMap>, +} + +impl StoredObject { + pub fn from_key_object(obj: &KeyObject, engine: &dyn CryptoProvider) -> Result { + let key_bytes = engine + .serialize_key(&obj.key_ref) + .map_err(|e| format!("serialize_key slot {}: {e}", obj.slot_id))?; + Ok(StoredObject { + handle: obj.handle, + slot_id: obj.slot_id, + key_type: obj.key_type.into(), + key_der: key_bytes, + attributes: obj.attributes.clone(), + }) + } + + pub fn into_key_object_with_engine(self, engine: &dyn CryptoProvider) -> Result { + let key_ref = engine + .deserialize_key(&self.key_der) + .map_err(|e| format!("deserialize_key slot {}: {e}", self.slot_id))?; + Ok(KeyObject::new(self.handle, self.slot_id, self.key_type.into(), key_ref, self.attributes)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredHashedPin { + #[serde(default)] + pub phc_string: Option, + #[serde(default)] + pub salt: Option>, + #[serde(default)] + pub hash: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredToken { + pub label: Vec, + #[serde(default = "default_state_str")] + pub state: String, + #[serde(default)] + pub initialized: Option, + #[serde(default)] + pub so_pin: Option>, + #[serde(default)] + pub user_pin: Option>, + #[serde(default)] + pub so_pin_hash: Option, + #[serde(default)] + pub user_pin_hash: Option, + #[serde(default)] + pub so_pin_failures: u32, + #[serde(default)] + pub user_pin_failures: u32, + pub login_required: bool, + pub serial_number: Vec, +} + +fn default_state_str() -> String { + "read_write".to_string() +} + +impl From<&Token> for StoredToken { + fn from(t: &Token) -> Self { + let state = match t.state { + TokenState::Reset => "reset", + TokenState::ReadWrite => "read_write", + TokenState::ReadOnly => "read_only", + }; + StoredToken { + label: t.label.to_vec(), + state: state.to_string(), + initialized: None, + so_pin: None, + user_pin: None, + so_pin_hash: t.so_pin.as_ref().map(|p| StoredHashedPin { + phc_string: Some(p.phc_string.clone()), + salt: None, + hash: None, + }), + user_pin_hash: t.user_pin.as_ref().map(|p| StoredHashedPin { + phc_string: Some(p.phc_string.clone()), + salt: None, + hash: None, + }), + so_pin_failures: t.so_pin_failures, + user_pin_failures: t.user_pin_failures, + login_required: t.login_required, + serial_number: t.serial_number.to_vec(), + } + } +} + +impl StoredToken { + pub fn apply_to(&self, token: &mut Token) { + let mut label = [b' '; 32]; + let n = self.label.len().min(32); + label[..n].copy_from_slice(&self.label[..n]); + token.label = label; + + token.state = match self.state.as_str() { + "reset" => TokenState::Reset, + "read_only" => TokenState::ReadOnly, + _ => { + if let Some(false) = self.initialized { + TokenState::Reset + } else { + TokenState::ReadWrite + } + } + }; + + token.so_pin = match (&self.so_pin_hash, &self.so_pin) { + (Some(h), _) if h.phc_string.is_some() => Some(HashedPin::from_phc(h.phc_string.clone().unwrap())), + (_, Some(p)) => Some(HashedPin::new(p)), + _ => None, + }; + token.user_pin = match (&self.user_pin_hash, &self.user_pin) { + (Some(h), _) if h.phc_string.is_some() => Some(HashedPin::from_phc(h.phc_string.clone().unwrap())), + (_, Some(p)) => Some(HashedPin::new(p)), + _ => None, + }; + + token.so_pin_failures = self.so_pin_failures; + token.user_pin_failures = self.user_pin_failures; + token.login_required = self.login_required; + + let mut serial = [b' '; 16]; + let n = self.serial_number.len().min(16); + serial[..n].copy_from_slice(&self.serial_number[..n]); + token.serial_number = serial; + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredState { + pub version: u32, + #[serde(default)] + pub tokens: HashMap, + #[serde(default)] + pub token: Option, + pub objects: Vec, + pub next_handle: u64, +} diff --git a/src/pkcs11/storage/path.rs b/src/pkcs11/storage/path.rs new file mode 100644 index 0000000..0fb2d9a --- /dev/null +++ b/src/pkcs11/storage/path.rs @@ -0,0 +1,21 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use std::path::PathBuf; + +pub fn storage_path() -> PathBuf { + if let Ok(p) = std::env::var("CRYPTOKI_STORE") { + return PathBuf::from(p); + } + let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); + home.join(".cryptoki").join("token.json") +} diff --git a/src/pkcs11/token.rs b/src/pkcs11/token.rs new file mode 100644 index 0000000..ecdf917 --- /dev/null +++ b/src/pkcs11/token.rs @@ -0,0 +1,330 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Per-slot token state management — proper PKCS#11 v3.0 token model. +//! +//! Each registered slot has its own `Token` with: +//! - Three-state machine: Reset → ReadWrite → ReadOnly (on SO PIN lockout) +//! - Label (32-byte padded UTF-8) +//! - Argon2id PIN hashes (SO and User) +//! - PIN failure counters with lockout threshold +//! - PIN policies (min/max length) +//! - Token flags derived from state + +use std::collections::HashMap; + +use argon2::{Argon2, PasswordHasher, PasswordVerifier}; +use argon2::password_hash::{SaltString, rand_core::OsRng}; +use once_cell::sync::Lazy; +use parking_lot::RwLock; + +use super::constants::*; +use super::error::{Pkcs11Error, Result}; +use super::types::*; + +// ── PIN hashing ───────────────────────────────────────────────────────── + +/// An Argon2id PIN hash stored as a PHC-format string. +#[derive(Debug, Clone)] +pub struct HashedPin { + pub phc_string: String, +} + +impl HashedPin { + /// Hash a PIN with Argon2id and a fresh random salt. + pub fn new(pin: &[u8]) -> Self { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2::Params::new(65_536, 3, 4, Some(32)).expect("invalid Argon2 params"), + ); + let phc_string = argon2 + .hash_password(pin, &salt) + .expect("Argon2 hashing failed") + .to_string(); + HashedPin { phc_string } + } + + /// Verify a PIN against this Argon2id hash (constant-time). + pub fn verify(&self, pin: &[u8]) -> bool { + let parsed = argon2::PasswordHash::new(&self.phc_string); + parsed.is_ok_and(|h| Argon2::default().verify_password(pin, &h).is_ok()) + } + + /// Reconstruct from a PHC-format string (for deserialization). + pub fn from_phc(phc_string: String) -> Self { + HashedPin { phc_string } + } +} + +// ── Token state machine ───────────────────────────────────────────────── + +/// PKCS#11 token state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TokenState { + /// Uninitialized — C_InitToken has never been called. + Reset, + /// Normal operation. + ReadWrite, + /// SO PIN locked after too many failures — token is read-only. + ReadOnly, +} + +/// Maximum consecutive PIN failures before lockout. +const MAX_PIN_FAILURES: u32 = 10; + +// ── Token ─────────────────────────────────────────────────────────────── + +/// Represents the full state of a PKCS#11 token. +#[derive(Debug, Clone)] +pub struct Token { + pub slot_id: CK_SLOT_ID, + pub label: [CK_UTF8CHAR; 32], + pub state: TokenState, + pub so_pin: Option, + pub user_pin: Option, + pub so_pin_failures: u32, + pub user_pin_failures: u32, + pub login_required: bool, + pub min_pin_len: usize, + pub max_pin_len: usize, + pub serial_number: [CK_CHAR; 16], +} + + +impl Token { + /// Create a new uninitialized token for the given slot. + pub fn new(slot_id: CK_SLOT_ID) -> Self { + let mut label = [b' '; 32]; + let src = b"Uninitialized Token"; + label[..src.len()].copy_from_slice(src); + + let mut serial = [b' '; 16]; + let sn = b"00000001"; + serial[..sn.len()].copy_from_slice(sn); + + Token { + slot_id, + label, + state: TokenState::Reset, + so_pin: None, + user_pin: None, + so_pin_failures: 0, + user_pin_failures: 0, + login_required: true, + min_pin_len: 4, + max_pin_len: 64, + serial_number: serial, + } + } + + /// Create a pre-initialized token with a default label and PINs. + /// + /// Used when a slot is first accessed before `C_InitToken` is called, + /// so that the token is immediately usable for testing and demos. + pub fn new_default(slot_id: CK_SLOT_ID) -> Self { + let mut token = Token::new(slot_id); + let mut label = [b' '; 32]; + let src = b"Cryptoki Token"; + label[..src.len()].copy_from_slice(src); + token.label = label; + token.state = TokenState::ReadWrite; + token.so_pin = Some(HashedPin::new(b"so-pin")); + token.user_pin = Some(HashedPin::new(b"1234")); + token + } + + /// Whether the token has been initialized (state != Reset). + pub fn initialized(&self) -> bool { + self.state != TokenState::Reset + } + + /// Initialize the token with SO PIN and label (C_InitToken). + pub fn init_token(&mut self, so_pin: &[u8], label: &[CK_UTF8CHAR; 32]) -> Result<()> { + if so_pin.len() < self.min_pin_len || so_pin.len() > self.max_pin_len { + return Err(Pkcs11Error::PinLenRange); + } + // If already initialized, verify old SO PIN. + if self.initialized() { + if let Some(ref existing) = self.so_pin { + if !existing.verify(so_pin) { + return Err(Pkcs11Error::PinIncorrect); + } + } + } + self.label = *label; + self.so_pin = Some(HashedPin::new(so_pin)); + self.user_pin = None; + self.so_pin_failures = 0; + self.user_pin_failures = 0; + self.state = TokenState::ReadWrite; + Ok(()) + } + + /// Initialize/set the user PIN (C_InitPIN — SO must be logged in). + pub fn init_pin(&mut self, pin: &[u8]) -> Result<()> { + if !self.initialized() { + return Err(Pkcs11Error::GeneralError); + } + if pin.len() < self.min_pin_len || pin.len() > self.max_pin_len { + return Err(Pkcs11Error::PinLenRange); + } + self.user_pin = Some(HashedPin::new(pin)); + self.user_pin_failures = 0; + Ok(()) + } + + /// Change a PIN (C_SetPIN). Old PIN must already be verified by the caller. + pub fn set_pin(&mut self, user_type: CK_USER_TYPE, new_pin: &[u8]) -> Result<()> { + if new_pin.len() < self.min_pin_len || new_pin.len() > self.max_pin_len { + return Err(Pkcs11Error::PinLenRange); + } + match user_type { + CKU_SO => { + self.so_pin = Some(HashedPin::new(new_pin)); + self.so_pin_failures = 0; + } + CKU_USER => { + self.user_pin = Some(HashedPin::new(new_pin)); + self.user_pin_failures = 0; + } + _ => return Err(Pkcs11Error::UserTypeInvalid), + } + Ok(()) + } + + /// Verify a PIN for login, with failure counting and lockout. + pub fn verify_pin(&mut self, user_type: CK_USER_TYPE, pin: &[u8]) -> Result<()> { + match user_type { + CKU_SO => { + if self.state == TokenState::ReadOnly { + return Err(Pkcs11Error::PinLocked); + } + let ok = self.so_pin.as_ref().is_some_and(|p| p.verify(pin)); + if !ok { + self.so_pin_failures += 1; + if self.so_pin_failures >= MAX_PIN_FAILURES { + self.state = TokenState::ReadOnly; + return Err(Pkcs11Error::PinLocked); + } + return Err(Pkcs11Error::PinIncorrect); + } + self.so_pin_failures = 0; + } + CKU_USER => { + if self.user_pin_failures >= MAX_PIN_FAILURES { + return Err(Pkcs11Error::PinLocked); + } + let ok = self.user_pin.as_ref().is_some_and(|p| p.verify(pin)); + if !ok { + self.user_pin_failures += 1; + return Err(Pkcs11Error::PinIncorrect); + } + self.user_pin_failures = 0; + } + _ => return Err(Pkcs11Error::ArgumentsBad), + } + Ok(()) + } + + /// Verify user PIN for context-specific re-auth without touching lockout counters. + pub fn verify_user_pin_no_lockout(&self, pin: &[u8]) -> Result<()> { + let ok = self.user_pin.as_ref().is_some_and(|p| p.verify(pin)); + if !ok { Err(Pkcs11Error::PinIncorrect) } else { Ok(()) } + } + + /// Build the CK_FLAGS for CK_TOKEN_INFO. + pub fn token_flags(&self) -> CK_FLAGS { + let mut flags: CK_FLAGS = CKF_RNG; + if self.initialized() { + flags |= CKF_TOKEN_INITIALIZED; + } + if self.login_required { + flags |= CKF_LOGIN_REQUIRED; + } + if self.user_pin.is_some() { + flags |= CKF_USER_PIN_INITIALIZED; + } + if self.state == TokenState::ReadOnly { + flags |= CKF_WRITE_PROTECTED; + } + if self.so_pin_failures > 0 && self.so_pin_failures < MAX_PIN_FAILURES { + flags |= CKF_SO_PIN_COUNT_LOW; + if self.so_pin_failures == MAX_PIN_FAILURES - 1 { + flags |= CKF_SO_PIN_FINAL_TRY; + } + } + if self.so_pin_failures >= MAX_PIN_FAILURES { + flags |= CKF_SO_PIN_LOCKED; + } + if self.user_pin_failures > 0 && self.user_pin_failures < MAX_PIN_FAILURES { + flags |= CKF_USER_PIN_COUNT_LOW; + if self.user_pin_failures == MAX_PIN_FAILURES - 1 { + flags |= CKF_USER_PIN_FINAL_TRY; + } + } + if self.user_pin_failures >= MAX_PIN_FAILURES { + flags |= CKF_USER_PIN_LOCKED; + } + flags + } +} + +// ── Per-slot token store ───────────────────────────────────────────────── + +static TOKENS: Lazy>> = Lazy::new(|| RwLock::new(HashMap::new())); + +/// Ensure a token exists for the given slot (creates a default if missing). +pub fn ensure_token(slot_id: CK_SLOT_ID) { + let mut tokens = TOKENS.write(); + // Only insert if missing + tokens.entry(slot_id).or_insert_with(|| Token::new_default(slot_id)); +} + +/// Access a slot's token immutably. +pub fn with_token(slot_id: CK_SLOT_ID, f: F) -> T +where + F: FnOnce(&Token) -> T, +{ + let tokens = TOKENS.read(); + if let Some(tok) = tokens.get(&slot_id) { + f(tok) + } else { + drop(tokens); + ensure_token(slot_id); + let tokens = TOKENS.read(); + f(tokens.get(&slot_id).unwrap()) + } +} + +/// Access a slot's token mutably. +pub fn with_token_mut(slot_id: CK_SLOT_ID, f: F) -> T +where + F: FnOnce(&mut Token) -> T, +{ + let mut tokens = TOKENS.write(); + tokens.entry(slot_id).or_insert_with(|| Token::new_default(slot_id)); + f(tokens.get_mut(&slot_id).unwrap()) +} + +/// Reset a single slot's token to default state. +pub fn reset_token(slot_id: CK_SLOT_ID) { + let mut tokens = TOKENS.write(); + tokens.insert(slot_id, Token::new_default(slot_id)); +} + +/// Clear all tokens (called by C_Finalize). +pub fn clear_tokens() { + TOKENS.write().clear(); +} diff --git a/src/pkcs11/types.rs b/src/pkcs11/types.rs new file mode 100644 index 0000000..3d44a30 --- /dev/null +++ b/src/pkcs11/types.rs @@ -0,0 +1,428 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! PKCS#11 v3.0 C-compatible type definitions. +#![allow(non_camel_case_types, non_snake_case, dead_code)] + +use std::ffi::c_void; + +// ── Primitive type aliases ───────────────────────────────────────────────── + +pub type CK_BYTE = u8; +pub type CK_CHAR = u8; +pub type CK_UTF8CHAR = u8; +pub type CK_BBOOL = CK_BYTE; +/// LP64 (Linux x86-64): `unsigned long` = 8 bytes. +pub type CK_ULONG = u64; +pub type CK_LONG = i64; +pub type CK_FLAGS = CK_ULONG; +pub type CK_SLOT_ID = CK_ULONG; +pub type CK_SESSION_HANDLE = CK_ULONG; +pub type CK_OBJECT_HANDLE = CK_ULONG; +pub type CK_OBJECT_CLASS = CK_ULONG; +pub type CK_KEY_TYPE = CK_ULONG; +pub type CK_ATTRIBUTE_TYPE = CK_ULONG; +pub type CK_MECHANISM_TYPE = CK_ULONG; +pub type CK_RV = CK_ULONG; +pub type CK_NOTIFICATION = CK_ULONG; +pub type CK_USER_TYPE = CK_ULONG; +pub type CK_STATE = CK_ULONG; + +pub const CK_TRUE: CK_BBOOL = 1; +pub const CK_FALSE: CK_BBOOL = 0; +/// Sentinel for CK_ATTRIBUTE.ulValueLen when an error occurred on that attribute. +pub const CK_UNAVAILABLE_INFORMATION: CK_ULONG = CK_ULONG::MAX; +pub const CK_EFFECTIVELY_INFINITE: CK_ULONG = 0; +pub const CK_INVALID_HANDLE: CK_ULONG = 0; + +// ── Structs ──────────────────────────────────────────────────────────────── + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct CK_VERSION { + pub major: CK_BYTE, + pub minor: CK_BYTE, +} + +#[repr(C)] +pub struct CK_INFO { + pub cryptokiVersion: CK_VERSION, + pub manufacturerID: [CK_UTF8CHAR; 32], + pub flags: CK_FLAGS, + pub libraryDescription: [CK_UTF8CHAR; 32], + pub libraryVersion: CK_VERSION, +} + +#[repr(C)] +pub struct CK_SLOT_INFO { + pub slotDescription: [CK_UTF8CHAR; 64], + pub manufacturerID: [CK_UTF8CHAR; 32], + pub flags: CK_FLAGS, + pub hardwareVersion: CK_VERSION, + pub firmwareVersion: CK_VERSION, +} + +#[repr(C)] +pub struct CK_TOKEN_INFO { + pub label: [CK_UTF8CHAR; 32], + pub manufacturerID: [CK_UTF8CHAR; 32], + pub model: [CK_UTF8CHAR; 16], + pub serialNumber: [CK_CHAR; 16], + pub flags: CK_FLAGS, + pub ulMaxSessionCount: CK_ULONG, + pub ulSessionCount: CK_ULONG, + pub ulMaxRwSessionCount: CK_ULONG, + pub ulRwSessionCount: CK_ULONG, + pub ulMaxPinLen: CK_ULONG, + pub ulMinPinLen: CK_ULONG, + pub ulTotalPublicMemory: CK_ULONG, + pub ulFreePublicMemory: CK_ULONG, + pub ulTotalPrivateMemory: CK_ULONG, + pub ulFreePrivateMemory: CK_ULONG, + pub hardwareVersion: CK_VERSION, + pub firmwareVersion: CK_VERSION, + pub utcTime: [CK_CHAR; 16], +} + +#[repr(C)] +pub struct CK_SESSION_INFO { + pub slotID: CK_SLOT_ID, + pub state: CK_STATE, + pub flags: CK_FLAGS, + pub ulDeviceError: CK_ULONG, +} + +#[repr(C)] +pub struct CK_MECHANISM { + pub mechanism: CK_MECHANISM_TYPE, + pub pParameter: *const c_void, + pub ulParameterLen: CK_ULONG, +} + +#[repr(C)] +pub struct CK_MECHANISM_INFO { + pub ulMinKeySize: CK_ULONG, + pub ulMaxKeySize: CK_ULONG, + pub flags: CK_FLAGS, +} + +#[repr(C)] +pub struct CK_ATTRIBUTE { + pub r#type: CK_ATTRIBUTE_TYPE, + pub pValue: *mut c_void, + pub ulValueLen: CK_ULONG, +} + +// ── Mechanism parameter structs ──────────────────────────────────────────── + +/// `CK_AES_CTR_PARAMS` — for `CKM_AES_CTR`. +#[repr(C)] +pub struct CK_AES_CTR_PARAMS { + pub ulCounterBits: CK_ULONG, + pub cb: [CK_BYTE; 16], +} + +/// `CK_GCM_PARAMS` — for `CKM_AES_GCM`. +#[repr(C)] +pub struct CK_GCM_PARAMS { + pub pIv: *const CK_BYTE, + pub ulIvLen: CK_ULONG, + pub ulIvBits: CK_ULONG, + pub pAAD: *const CK_BYTE, + pub ulAADLen: CK_ULONG, + pub ulTagBits: CK_ULONG, +} + +/// `CK_GCM_MESSAGE_PARAMS` — per-message params for `C_EncryptMessage` / `C_DecryptMessage` +/// with `CKM_AES_GCM`. `pIv` and `pTag` are caller-owned mutable buffers. +#[repr(C)] +pub struct CK_GCM_MESSAGE_PARAMS { + pub pIv: *mut CK_BYTE, // IV buffer (input on encrypt, written on decrypt) + pub ulIvLen: CK_ULONG, // IV length in bytes (12 for standard GCM) + pub ulIvFixedBits: CK_ULONG, // bits of IV that are fixed (ignored, we take full IV) + pub ivGenerator: CK_ULONG, // CK_GCM_GENERATOR_FUNCTION — ignored (caller supplies IV) + pub pTag: *mut CK_BYTE, // tag buffer: written by encrypt, read by decrypt + pub ulTagBits: CK_ULONG, // tag length in bits (128 = 16 bytes for AES-GCM) +} + +/// `CK_CHACHA20_POLY1305_MESSAGE_PARAMS` — per-message params for `C_EncryptMessage` / +/// `C_DecryptMessage` with `CKM_CHACHA20_POLY1305`. Same field layout as +/// `CK_GCM_MESSAGE_PARAMS`. +#[repr(C)] +pub struct CK_CHACHA20_POLY1305_MESSAGE_PARAMS { + pub pNonce: *mut CK_BYTE, + pub ulNonceLen: CK_ULONG, + pub ulNonceFixedBits: CK_ULONG, + pub nonceGenerator: CK_ULONG, + pub pTag: *mut CK_BYTE, + pub ulTagBits: CK_ULONG, +} + +/// `CK_ECDH1_DERIVE_PARAMS` — for `CKM_ECDH1_DERIVE`. +#[repr(C)] +pub struct CK_ECDH1_DERIVE_PARAMS { + /// KDF to apply to shared secret (CKD_NULL = raw secret). + pub kdf: CK_ULONG, + pub ulSharedDataLen: CK_ULONG, + pub pSharedData: *const CK_BYTE, + /// Other party's public key: raw uncompressed point (04||x||y). + pub ulPublicDataLen: CK_ULONG, + pub pPublicData: *const CK_BYTE, +} + +// ── Callback / init args ─────────────────────────────────────────────────── + +pub type CK_NOTIFY = Option CK_RV>; + +pub type CK_CREATEMUTEX = Option CK_RV>; +pub type CK_DESTROYMUTEX = Option CK_RV>; +pub type CK_LOCKMUTEX = Option CK_RV>; +pub type CK_UNLOCKMUTEX = Option CK_RV>; + +#[repr(C)] +pub struct CK_C_INITIALIZE_ARGS { + pub CreateMutex: CK_CREATEMUTEX, + pub DestroyMutex: CK_DESTROYMUTEX, + pub LockMutex: CK_LOCKMUTEX, + pub UnlockMutex: CK_UNLOCKMUTEX, + pub flags: CK_FLAGS, + pub pReserved: *mut c_void, +} + +// ── CK_FUNCTION_LIST ─────────────────────────────────────────────────────── + +/// PKCS#11 v2.40 function list — 68 function-pointer slots. +#[repr(C)] +pub struct CK_FUNCTION_LIST { + pub version: CK_VERSION, + pub C_Initialize: Option CK_RV>, + pub C_Finalize: Option CK_RV>, + pub C_GetInfo: Option CK_RV>, + pub C_GetFunctionList: Option CK_RV>, + pub C_GetSlotList: Option CK_RV>, + pub C_GetSlotInfo: Option CK_RV>, + pub C_GetTokenInfo: Option CK_RV>, + pub C_GetMechanismList: Option CK_RV>, + pub C_GetMechanismInfo: Option CK_RV>, + pub C_InitToken: Option CK_RV>, + pub C_InitPIN: Option CK_RV>, + pub C_SetPIN: Option CK_RV>, + pub C_OpenSession: Option CK_RV>, + pub C_CloseSession: Option CK_RV>, + pub C_CloseAllSessions: Option CK_RV>, + pub C_GetSessionInfo: Option CK_RV>, + pub C_GetOperationState: Option CK_RV>, + pub C_SetOperationState: Option CK_RV>, + pub C_Login: Option CK_RV>, + pub C_Logout: Option CK_RV>, + pub C_CreateObject: Option CK_RV>, + pub C_CopyObject: Option CK_RV>, + pub C_DestroyObject: Option CK_RV>, + pub C_GetObjectSize: Option CK_RV>, + pub C_GetAttributeValue: Option CK_RV>, + pub C_SetAttributeValue: Option CK_RV>, + pub C_FindObjectsInit: Option CK_RV>, + pub C_FindObjects: Option CK_RV>, + pub C_FindObjectsFinal: Option CK_RV>, + pub C_EncryptInit: Option CK_RV>, + pub C_Encrypt: Option CK_RV>, + pub C_EncryptUpdate: Option CK_RV>, + pub C_EncryptFinal: Option CK_RV>, + pub C_DecryptInit: Option CK_RV>, + pub C_Decrypt: Option CK_RV>, + pub C_DecryptUpdate: Option CK_RV>, + pub C_DecryptFinal: Option CK_RV>, + pub C_DigestInit: Option CK_RV>, + pub C_Digest: Option CK_RV>, + pub C_DigestUpdate: Option CK_RV>, + pub C_DigestKey: Option CK_RV>, + pub C_DigestFinal: Option CK_RV>, + pub C_SignInit: Option CK_RV>, + pub C_Sign: Option CK_RV>, + pub C_SignUpdate: Option CK_RV>, + pub C_SignFinal: Option CK_RV>, + pub C_SignRecoverInit: Option CK_RV>, + pub C_SignRecover: Option CK_RV>, + pub C_VerifyInit: Option CK_RV>, + pub C_Verify: Option CK_RV>, + pub C_VerifyUpdate: Option CK_RV>, + pub C_VerifyFinal: Option CK_RV>, + pub C_VerifyRecoverInit: Option CK_RV>, + pub C_VerifyRecover: Option CK_RV>, + pub C_DigestEncryptUpdate: Option CK_RV>, + pub C_DecryptDigestUpdate: Option CK_RV>, + pub C_SignEncryptUpdate: Option CK_RV>, + pub C_DecryptVerifyUpdate: Option CK_RV>, + pub C_GenerateKey: Option CK_RV>, + pub C_GenerateKeyPair: Option CK_RV>, + pub C_WrapKey: Option CK_RV>, + pub C_UnwrapKey: Option CK_RV>, + pub C_DeriveKey: Option CK_RV>, + pub C_SeedRandom: Option CK_RV>, + pub C_GenerateRandom: Option CK_RV>, + pub C_GetFunctionStatus: Option CK_RV>, + pub C_CancelFunction: Option CK_RV>, + pub C_WaitForSlotEvent: Option CK_RV>, +} + +// SAFETY: all fields are function pointers (inherently Sync) or CK_VERSION (Copy). +unsafe impl Sync for CK_FUNCTION_LIST {} + +// ── PKCS#11 v3.0 additions ─────────────────────────────────────────────── + +/// `CK_INTERFACE` — v3.0 interface discovery struct. +#[repr(C)] +pub struct CK_INTERFACE { + pub pInterfaceName: *const CK_CHAR, + pub pFunctionList: *const c_void, + pub flags: CK_FLAGS, +} + +// SAFETY: all fields are raw pointers to immutable statics or CK_FLAGS (Copy). +unsafe impl Sync for CK_INTERFACE {} +unsafe impl Send for CK_INTERFACE {} + +/// `CK_FUNCTION_LIST_3_0` — extended function list with v3.0 functions. +/// Extends v2.40 with message-based APIs, C_SessionCancel, C_LoginUser, etc. +#[repr(C)] +pub struct CK_FUNCTION_LIST_3_0 { + pub version: CK_VERSION, + + // ── v2.40 functions (same order as CK_FUNCTION_LIST) ───────────────── + pub C_Initialize: Option CK_RV>, + pub C_Finalize: Option CK_RV>, + pub C_GetInfo: Option CK_RV>, + pub C_GetFunctionList: Option CK_RV>, + pub C_GetSlotList: Option CK_RV>, + pub C_GetSlotInfo: Option CK_RV>, + pub C_GetTokenInfo: Option CK_RV>, + pub C_GetMechanismList: Option CK_RV>, + pub C_GetMechanismInfo: Option CK_RV>, + pub C_InitToken: Option CK_RV>, + pub C_InitPIN: Option CK_RV>, + pub C_SetPIN: Option CK_RV>, + pub C_OpenSession: Option CK_RV>, + pub C_CloseSession: Option CK_RV>, + pub C_CloseAllSessions: Option CK_RV>, + pub C_GetSessionInfo: Option CK_RV>, + pub C_GetOperationState: Option CK_RV>, + pub C_SetOperationState: Option CK_RV>, + pub C_Login: Option CK_RV>, + pub C_Logout: Option CK_RV>, + pub C_CreateObject: Option CK_RV>, + pub C_CopyObject: Option CK_RV>, + pub C_DestroyObject: Option CK_RV>, + pub C_GetObjectSize: Option CK_RV>, + pub C_GetAttributeValue: Option CK_RV>, + pub C_SetAttributeValue: Option CK_RV>, + pub C_FindObjectsInit: Option CK_RV>, + pub C_FindObjects: Option CK_RV>, + pub C_FindObjectsFinal: Option CK_RV>, + pub C_EncryptInit: Option CK_RV>, + pub C_Encrypt: Option CK_RV>, + pub C_EncryptUpdate: Option CK_RV>, + pub C_EncryptFinal: Option CK_RV>, + pub C_DecryptInit: Option CK_RV>, + pub C_Decrypt: Option CK_RV>, + pub C_DecryptUpdate: Option CK_RV>, + pub C_DecryptFinal: Option CK_RV>, + pub C_DigestInit: Option CK_RV>, + pub C_Digest: Option CK_RV>, + pub C_DigestUpdate: Option CK_RV>, + pub C_DigestKey: Option CK_RV>, + pub C_DigestFinal: Option CK_RV>, + pub C_SignInit: Option CK_RV>, + pub C_Sign: Option CK_RV>, + pub C_SignUpdate: Option CK_RV>, + pub C_SignFinal: Option CK_RV>, + pub C_SignRecoverInit: Option CK_RV>, + pub C_SignRecover: Option CK_RV>, + pub C_VerifyInit: Option CK_RV>, + pub C_Verify: Option CK_RV>, + pub C_VerifyUpdate: Option CK_RV>, + pub C_VerifyFinal: Option CK_RV>, + pub C_VerifyRecoverInit: Option CK_RV>, + pub C_VerifyRecover: Option CK_RV>, + pub C_DigestEncryptUpdate: Option CK_RV>, + pub C_DecryptDigestUpdate: Option CK_RV>, + pub C_SignEncryptUpdate: Option CK_RV>, + pub C_DecryptVerifyUpdate: Option CK_RV>, + pub C_GenerateKey: Option CK_RV>, + pub C_GenerateKeyPair: Option CK_RV>, + pub C_WrapKey: Option CK_RV>, + pub C_UnwrapKey: Option CK_RV>, + pub C_DeriveKey: Option CK_RV>, + pub C_SeedRandom: Option CK_RV>, + pub C_GenerateRandom: Option CK_RV>, + pub C_GetFunctionStatus: Option CK_RV>, + pub C_CancelFunction: Option CK_RV>, + pub C_WaitForSlotEvent: Option CK_RV>, + + // ── v3.0 new functions ─────────────────────────────────────────────── + pub C_GetInterfaceList: Option CK_RV>, + pub C_GetInterface: Option CK_RV>, + pub C_LoginUser: Option CK_RV>, + pub C_SessionCancel: Option CK_RV>, + + // Message-based encryption + pub C_MessageEncryptInit: Option CK_RV>, + pub C_EncryptMessage: Option CK_RV>, + pub C_EncryptMessageBegin: Option CK_RV>, + pub C_EncryptMessageNext: Option CK_RV>, + pub C_MessageEncryptFinal: Option CK_RV>, + + // Message-based decryption + pub C_MessageDecryptInit: Option CK_RV>, + pub C_DecryptMessage: Option CK_RV>, + pub C_DecryptMessageBegin: Option CK_RV>, + pub C_DecryptMessageNext: Option CK_RV>, + pub C_MessageDecryptFinal: Option CK_RV>, + + // Message-based signing + pub C_MessageSignInit: Option CK_RV>, + pub C_SignMessage: Option CK_RV>, + pub C_SignMessageBegin: Option CK_RV>, + pub C_SignMessageNext: Option CK_RV>, + pub C_MessageSignFinal: Option CK_RV>, + + // Message-based verification + pub C_MessageVerifyInit: Option CK_RV>, + pub C_VerifyMessage: Option CK_RV>, + pub C_VerifyMessageBegin: Option CK_RV>, + pub C_VerifyMessageNext: Option CK_RV>, + pub C_MessageVerifyFinal: Option CK_RV>, +} + +// SAFETY: all fields are function pointers or CK_VERSION. +unsafe impl Sync for CK_FUNCTION_LIST_3_0 {} + +/// Profile ID type (v3.0). +pub type CK_PROFILE_ID = CK_ULONG; + +/// `CK_HKDF_PARAMS` — for `CKM_HKDF_DERIVE` (v3.0). +#[repr(C)] +pub struct CK_HKDF_PARAMS { + pub bExtract: CK_BBOOL, + pub bExpand: CK_BBOOL, + pub prfHashMechanism: CK_MECHANISM_TYPE, + pub ulSaltType: CK_ULONG, + pub pSalt: *const CK_BYTE, + pub ulSaltLen: CK_ULONG, + pub hSaltKey: CK_OBJECT_HANDLE, + pub pInfo: *const CK_BYTE, + pub ulInfoLen: CK_ULONG, +} diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 0000000..787a566 --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,182 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Multi-engine registry — maps **global** slot IDs to crypto engine backends. +//! +//! Real PKCS#11 consumers can load multiple providers (e.g. a software token + +//! a hardware HSM). Each provider registers via [`register_engine`], which +//! assigns one or more **global** slot IDs. All subsequent C_* calls route +//! through [`engine_for_slot`] to the correct backend. +//! +//! ## Slot ID model +//! +//! There are two levels of slot ID: +//! +//! | Level | Assigned by | Used by | +//! |-------|------------|---------| +//! | **Global** (0, 1, 2, …) | Registry, sequentially | Application / C_* API | +//! | **Internal** (0-based per engine) | Engine itself | Engine trait methods | +//! +//! `engine_for_slot(global_id)` returns `(Arc, internal_id)`. +//! +//! ## Example +//! +//! ```text +//! register_engine(SoftEngine) → global [0] (internal 0) +//! register_engine(HsmEngine) → global [1, 2] (internal 0, 1) +//! +//! C_OpenSession(slot=2) → session.slot_id = 2 (global) +//! C_Sign(session) → engine_for_slot(2) → (HsmEngine, internal=1) +//! ``` + +use std::collections::HashMap; +use std::sync::{Arc, RwLock, OnceLock}; + +use crate::error::CryptoError; +use crate::traits::CryptoProvider; +use crate::pkcs11::types::CK_SLOT_ID; + +// ── Virtual slot ───────────────────────────────────────────────────────── + +/// Maps a global slot ID to a specific engine and its internal slot index. +struct VirtualSlot { + engine: Arc, + /// The engine's own slot index (0-based). For a single-slot engine this is always 0. + internal_slot_id: CK_SLOT_ID, +} + +// ── Registry ───────────────────────────────────────────────────────────── + +struct Registry { + engines: Vec>, + slots: HashMap, + next_slot_id: CK_SLOT_ID, +} + +impl Registry { + fn new() -> Self { + Registry { + engines: Vec::new(), + slots: HashMap::new(), + next_slot_id: 0, + } + } +} + +static REGISTRY: OnceLock> = OnceLock::new(); + +fn get_registry() -> &'static RwLock { + REGISTRY.get_or_init(|| RwLock::new(Registry::new())) +} + +// ── Public API ─────────────────────────────────────────────────────────── + +/// Register a crypto engine and assign it virtual slot(s). +/// +/// Returns the global slot IDs assigned to this engine. +/// Each engine gets `engine.slot_count()` slots (default: 1). +pub fn register_engine(engine: impl CryptoProvider + 'static) -> Result, CryptoError> { + let engine: Arc = Arc::new(engine); + let mut reg = get_registry().write().map_err(|_| CryptoError::GeneralError { message: "registry lock poisoned".into() })?; + + let count = engine.slot_count(); + let mut assigned = Vec::with_capacity(count); + + for i in 0..count { + let global_id = reg.next_slot_id; + reg.slots.insert(global_id, VirtualSlot { + engine: Arc::clone(&engine), + internal_slot_id: i as CK_SLOT_ID, + }); + assigned.push(global_id); + reg.next_slot_id += 1; + } + + reg.engines.push(engine); + Ok(assigned) +} + +/// Retrieve the engine and its internal slot ID for a given global slot ID. +/// +/// The returned `CK_SLOT_ID` is the engine's own slot index (the "internal" +/// ID) which must be used when talking to the engine. The global ID is what +/// the application sees; the internal ID is what the engine understands. +pub fn engine_for_slot(slot_id: CK_SLOT_ID) -> Result<(Arc, CK_SLOT_ID), CryptoError> { + let reg = get_registry().read().map_err(|_| CryptoError::GeneralError { message: "registry lock poisoned".into() })?; + reg.slots + .get(&slot_id) + .map(|vs| (Arc::clone(&vs.engine), vs.internal_slot_id)) + .ok_or(CryptoError::SlotIdInvalid) +} + +/// Check whether a slot ID is registered. +pub fn is_valid_slot(slot_id: CK_SLOT_ID) -> bool { + get_registry() + .read() + .map(|reg| reg.slots.contains_key(&slot_id)) + .unwrap_or(false) +} + +/// Return all registered slot IDs, sorted. +pub fn slot_ids() -> Vec { + let reg = get_registry().read().unwrap(); + let mut ids: Vec = reg.slots.keys().copied().collect(); + ids.sort(); + ids +} + +/// Number of registered slots. +pub fn slot_count() -> usize { + get_registry().read().map(|r| r.slots.len()).unwrap_or(0) +} + +/// Convenience: retrieve the first registered engine. +/// +/// Equivalent to `engine_for_slot(0)` when only one engine is registered. +/// Kept for backward compatibility with code that uses a single engine. +pub fn engine() -> Result, CryptoError> { + let reg = get_registry().read().map_err(|_| CryptoError::GeneralError { message: "registry lock poisoned".into() })?; + if reg.engines.is_empty() { + return Err(CryptoError::NotInitialized); + } + Ok(Arc::clone(®.engines[0])) +} + +/// Non-erroring convenience accessor — returns `None` when no engine is registered. +pub fn try_engine() -> Option> { + let reg = get_registry().read().ok()?; + reg.engines.first().map(Arc::clone) +} + +/// Call `f` for every registered engine (used by the atfork child handler). +pub fn for_each_engine(f: F) { + if let Ok(reg) = get_registry().read() { + for engine in ®.engines { + f(engine.as_ref()); + } + } +} + +/// Reset the registry (called by C_Finalize). +/// +/// Clears all engines and slot mappings so a subsequent C_Initialize +/// can re-register engines. +pub fn reset_registry() { + if let Some(lock) = REGISTRY.get() { + if let Ok(mut reg) = lock.write() { + reg.engines.clear(); + reg.slots.clear(); + reg.next_slot_id = 0; + } + } +} diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 0000000..d9e0df3 --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,330 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use zeroize::Zeroizing; + +use crate::attributes::{AttributeType, AttributeValue}; +use crate::error::CryptoError; +use crate::types::{EcCurve, EcKeyPair, EdKeyPair, EdwardsCurve, HashAlgorithm, RsaKeyPair}; + +// ── EngineKeyRef ──────────────────────────────────────────────────────────── + +/// Opaque key reference passed between the PKCS#11 layer and the engine. +/// +/// For software engines (e.g. OpenSSL) this wraps DER-encoded key bytes. +/// For HSM/TPM/TEE backends the inner bytes could be a handle or label +/// rather than extractable key material. The PKCS#11 layer never +/// interprets the contents — it just stores, clones, and passes them back. +#[derive(Debug, Clone)] +pub struct EngineKeyRef { + inner: Zeroizing>, +} + +impl EngineKeyRef { + /// Construct from raw bytes (DER, handle, etc.). + pub fn from_bytes(b: Vec) -> Self { + Self { inner: Zeroizing::new(b) } + } + + /// View the inner bytes. Only the engine should call this. + pub fn as_bytes(&self) -> &[u8] { + &self.inner + } +} + +// ── EngineMechanismInfo ───────────────────────────────────────────────────── + +/// Per-mechanism capability descriptor returned by +/// [`CryptoProvider::mechanism_info`]. +/// +/// Fields mirror `CK_MECHANISM_INFO`. The PKCS#11 layer +/// applies additional policy constraints on top (e.g. clamping RSA +/// `min_key_size` to 2048). +#[derive(Debug, Clone, Copy)] +pub struct EngineMechanismInfo { + /// Smallest key size (in bits) supported by this mechanism on this slot. + pub min_key_size: u32, + /// Largest key size (in bits) supported by this mechanism on this slot. + pub max_key_size: u32, + /// `CKF_*` capability flags (e.g. `CKF_SIGN | CKF_VERIFY`). + pub flags: u32, +} + +/// A stateful, streaming hash context for multi-part digest operations. +pub trait StreamHasher: Send { + /// Feed the next chunk into the running hash state. + fn update(&mut self, data: &[u8]) -> Result<(), CryptoError>; + /// Finalise and return the digest bytes; consumes the hasher. + fn finish(self: Box) -> Result, CryptoError>; +} + +/// Engine trait — all crypto operations go through here. +/// +/// Key material is represented by [`EngineKeyRef`], an opaque wrapper that +/// the PKCS#11 layer stores but never interprets. The engine creates refs +/// during key generation or deserialization and consumes them in crypto ops. +/// +/// ## Multi-slot model +/// +/// An engine can expose one or more **slots** (default: 1). When an engine +/// is registered via [`register_engine`](crate::registry::register_engine), +/// the registry assigns sequential **global** slot IDs (what the application +/// sees) and maps each to the engine plus an **internal** slot index +/// (0-based, what the engine sees). +/// +/// ```text +/// SoftEngine.slot_count() == 1 → global 0 ↔ internal 0 +/// HsmEngine.slot_count() == 3 → global 1 ↔ internal 0 +/// global 2 ↔ internal 1 +/// global 3 ↔ internal 2 +/// ``` +/// +/// Slot-aware query methods (`slot_description`, `token_model`, +/// `supported_mechanisms`) receive the **internal** slot index so the engine +/// can return per-partition information. Stateless crypto methods (signing, +/// encryption, …) are slot-agnostic — they operate purely on key refs. +pub trait CryptoProvider: Send + Sync { + + // ── Slot / capability discovery ───────────────────────────────────── + + /// How many virtual slots this engine provides. Default: 1. + /// + /// Called once during [`register_engine`](crate::registry::register_engine) + /// to allocate global slot IDs. + fn slot_count(&self) -> usize { 1 } + + /// Human-readable slot description (≤64 UTF-8 bytes, space-padded by caller). + /// + /// `internal_slot_id` is the engine's own 0-based slot index (not the + /// global ID the application uses). + fn slot_description(&self, _internal_slot_id: u64) -> &str { "Virtual Slot" } + + /// Human-readable token model string (≤16 UTF-8 bytes). + /// + /// `internal_slot_id` is the engine's own 0-based slot index. + fn token_model(&self, _internal_slot_id: u64) -> &str { "SoftToken" } + + /// The set of `CKM_*` mechanism types this engine supports on the given slot. + /// + /// `internal_slot_id` is the engine's own 0-based slot index. + /// Default returns an empty slice — the PKCS#11 layer falls back to the + /// global `SUPPORTED_MECHANISMS` list when this is empty (backward compat). + fn supported_mechanisms(&self, _internal_slot_id: u64) -> &[u64] { &[] } + + /// Return capability information for one mechanism on one slot. + /// + /// `slot` is the engine's own 0-based internal slot index. + /// `mechanism` is a `CKM_*` constant (underlying type `u64`). + /// + /// Returns `None` when the engine has no opinion — the PKCS#11 layer + /// falls back to its built-in hardcoded table in that case. + /// + /// Default: always returns `None` (backward-compatible for engines that + /// have not yet implemented per-mechanism capability reporting). + fn mechanism_info(&self, _slot: usize, _mechanism: u64) -> Option { + None + } + + /// Called in the child process immediately after `fork(2)`. + /// + /// Engines that hold state that must not be shared between parent and + /// child (e.g. hardware session handles, connection pools) should reset + /// that state here. Software engines that are stateless can rely on + /// the default no-op implementation. + fn post_fork_child(&self) {} + + // ── Key generation ──────────────────────────────────────────────────── + + /// Generate an RSA key pair (`bits` ≥ 2048, public exponent fixed at 65537). + fn generate_rsa_key_pair(&self, bits: u32) -> Result; + /// Generate an EC key pair on the given named curve. + fn generate_ec_key_pair(&self, curve: EcCurve) -> Result; + /// Generate an AES key of `len` bytes (16, 24, or 32). + fn generate_aes_key(&self, len: usize) -> Result; + + // ── Random ──────────────────────────────────────────────────────────── + + /// Fill `buf` with CSPRNG bytes. + fn generate_random(&self, buf: &mut [u8]) -> Result<(), CryptoError>; + + // ── AES ─────────────────────────────────────────────────────────────── + + /// AES-CBC encrypt with PKCS#7 padding. `iv`: 16 bytes. + fn aes_cbc_encrypt(&self, key: &EngineKeyRef, iv: &[u8], plaintext: &[u8]) -> Result, CryptoError>; + /// AES-CBC decrypt, stripping PKCS#7 padding. + fn aes_cbc_decrypt(&self, key: &EngineKeyRef, iv: &[u8], ciphertext: &[u8]) -> Result>, CryptoError>; + /// AES-CTR encrypt/decrypt (symmetric XOR with keystream). `iv`: 16-byte counter block. + fn aes_ctr_crypt(&self, key: &EngineKeyRef, iv: &[u8], input: &[u8]) -> Result, CryptoError>; + /// AES-GCM authenticated encrypt. Returns `(ciphertext, 16-byte tag)`. + fn aes_gcm_encrypt(&self, key: &EngineKeyRef, iv: &[u8], aad: &[u8], plaintext: &[u8]) -> Result<(Vec, Vec), CryptoError>; + /// AES-GCM authenticated decrypt. `tag` must be 16 bytes. + fn aes_gcm_decrypt(&self, key: &EngineKeyRef, iv: &[u8], aad: &[u8], ciphertext: &[u8], tag: &[u8]) -> Result>, CryptoError>; + + // ── RSA encryption ──────────────────────────────────────────────────── + + /// RSA PKCS#1 v1.5 encrypt. + fn rsa_pkcs1_encrypt(&self, key: &EngineKeyRef, plaintext: &[u8]) -> Result, CryptoError>; + /// RSA PKCS#1 v1.5 decrypt. + fn rsa_pkcs1_decrypt(&self, key: &EngineKeyRef, ciphertext: &[u8]) -> Result>, CryptoError>; + /// RSA-OAEP encrypt (SHA-1, MGF1-SHA-1, empty label). + fn rsa_oaep_encrypt(&self, key: &EngineKeyRef, plaintext: &[u8]) -> Result, CryptoError>; + /// RSA-OAEP decrypt. + fn rsa_oaep_decrypt(&self, key: &EngineKeyRef, ciphertext: &[u8]) -> Result>, CryptoError>; + + // ── Signing / Verification ──────────────────────────────────────────── + + /// RSA PKCS#1 v1.5 sign (SHA-256 hash computed internally). + fn rsa_pkcs1_sign(&self, key: &EngineKeyRef, message: &[u8]) -> Result, CryptoError>; + /// RSA PKCS#1 v1.5 verify. Returns `true` if signature is valid. + fn rsa_pkcs1_verify(&self, key: &EngineKeyRef, message: &[u8], signature: &[u8]) -> Result; + /// RSA-PSS sign (SHA-256, MGF1-SHA-256, salt = 32 bytes). + fn rsa_pss_sign(&self, key: &EngineKeyRef, message: &[u8]) -> Result, CryptoError>; + /// RSA-PSS verify. + fn rsa_pss_verify(&self, key: &EngineKeyRef, message: &[u8], signature: &[u8]) -> Result; + /// ECDSA sign over P-256 (SHA-256 hash computed internally). Returns DER `(r, s)`. + fn ecdsa_sign(&self, key: &EngineKeyRef, message: &[u8]) -> Result, CryptoError>; + /// ECDSA verify. `signature` must be DER-encoded `(r, s)`. + fn ecdsa_verify(&self, key: &EngineKeyRef, message: &[u8], signature: &[u8]) -> Result; + /// ECDSA sign over a **pre-computed** digest. The caller is responsible + /// for hashing `message` with the appropriate algorithm before calling + /// this method. Returns a DER-encoded `(r, s)` signature. + /// + /// This is the primitive used by the ECDSA dispatch in `backend.rs` + /// `CKM_ECDSA_SHA256/384/512` each hash the message, then call + /// this once with the resulting digest bytes. + /// + /// Default: returns [`CryptoError::MechanismInvalid`] so that engines + /// that only implement `ecdsa_sign` continue to compile without changes. + fn ecdsa_sign_prehashed(&self, key: &EngineKeyRef, digest: &[u8]) -> Result, CryptoError> { + let _ = (key, digest); + Err(CryptoError::MechanismInvalid { name: "ecdsa_sign_prehashed not implemented" }) + } + /// ECDSA verify against a **pre-computed** digest. The caller is responsible + /// for hashing the message before calling this method. + /// + /// Default: returns [`CryptoError::MechanismInvalid`]. + fn ecdsa_verify_prehashed(&self, key: &EngineKeyRef, digest: &[u8], signature: &[u8]) -> Result { + let _ = (key, digest, signature); + Err(CryptoError::MechanismInvalid { name: "ecdsa_verify_prehashed not implemented" }) + } + + // ── Hashing ─────────────────────────────────────────────────────────── + + /// Single-part (one-shot) hash. + fn hash(&self, algorithm: HashAlgorithm, data: &[u8]) -> Result, CryptoError>; + /// Create a streaming hash context for multi-part digesting. + fn new_stream_hasher(&self, algorithm: HashAlgorithm) -> Result, CryptoError>; + + // ── EdDSA (v3.0) ────────────────────────────────────────────────────── + + /// Generate an EdDSA key pair (Ed25519 or Ed448). + fn generate_ed_key_pair(&self, curve: EdwardsCurve) -> Result; + /// EdDSA sign (pure mode, no prehash). Returns raw signature bytes. + fn eddsa_sign(&self, key: &EngineKeyRef, message: &[u8]) -> Result, CryptoError>; + /// EdDSA verify. Returns `true` if signature is valid. + fn eddsa_verify(&self, key: &EngineKeyRef, message: &[u8], signature: &[u8]) -> Result; + + // ── ChaCha20-Poly1305 (v3.0) ──────────────────────────────────────── + + /// Generate a 256-bit ChaCha20 key. + fn generate_chacha20_key(&self) -> Result; + /// ChaCha20-Poly1305 AEAD encrypt. Returns `(ciphertext, 16-byte tag)`. + fn chacha20_poly1305_encrypt(&self, key: &EngineKeyRef, nonce: &[u8], aad: &[u8], plaintext: &[u8]) -> Result<(Vec, Vec), CryptoError>; + /// ChaCha20-Poly1305 AEAD decrypt. + fn chacha20_poly1305_decrypt(&self, key: &EngineKeyRef, nonce: &[u8], aad: &[u8], ciphertext: &[u8], tag: &[u8]) -> Result>, CryptoError>; + + // ── HKDF (v3.0) ───────────────────────────────────────────────────── + + /// HKDF-Extract + HKDF-Expand. Returns derived key material of `okm_len` bytes. + fn hkdf_derive(&self, hash: HashAlgorithm, ikm: &EngineKeyRef, salt: &[u8], info: &[u8], okm_len: usize) -> Result>, CryptoError>; + + // ── Hash-parameterized RSA signing (v2.40 SHA-384/512 + v3.0) ───── + + /// RSA PKCS#1 v1.5 sign with caller-chosen hash. + fn rsa_pkcs1_sign_hash(&self, key: &EngineKeyRef, message: &[u8], hash: HashAlgorithm) -> Result, CryptoError> { + let _ = (key, message, hash); + Err(CryptoError::MechanismInvalid { name: "rsa_pkcs1_sign_hash not implemented" }) + } + /// RSA PKCS#1 v1.5 verify with caller-chosen hash. + fn rsa_pkcs1_verify_hash(&self, key: &EngineKeyRef, message: &[u8], signature: &[u8], hash: HashAlgorithm) -> Result { + let _ = (key, message, signature, hash); + Err(CryptoError::MechanismInvalid { name: "rsa_pkcs1_verify_hash not implemented" }) + } + /// RSA-PSS sign with caller-chosen hash. + fn rsa_pss_sign_hash(&self, key: &EngineKeyRef, message: &[u8], hash: HashAlgorithm) -> Result, CryptoError> { + let _ = (key, message, hash); + Err(CryptoError::MechanismInvalid { name: "rsa_pss_sign_hash not implemented" }) + } + /// RSA-PSS verify with caller-chosen hash. + fn rsa_pss_verify_hash(&self, key: &EngineKeyRef, message: &[u8], signature: &[u8], hash: HashAlgorithm) -> Result { + let _ = (key, message, signature, hash); + Err(CryptoError::MechanismInvalid { name: "rsa_pss_verify_hash not implemented" }) + } + /// ECDSA sign with caller-chosen hash. + fn ecdsa_sign_hash(&self, key: &EngineKeyRef, message: &[u8], hash: HashAlgorithm) -> Result, CryptoError> { + let _ = (key, message, hash); + Err(CryptoError::MechanismInvalid { name: "ecdsa_sign_hash not implemented" }) + } + /// ECDSA verify with caller-chosen hash. + fn ecdsa_verify_hash(&self, key: &EngineKeyRef, message: &[u8], signature: &[u8], hash: HashAlgorithm) -> Result { + let _ = (key, message, signature, hash); + Err(CryptoError::MechanismInvalid { name: "ecdsa_verify_hash not implemented" }) + } + + // ── AES Key Wrap (RFC 3394) ───────────────────────────────────────── + + /// AES Key Wrap (encrypt). Returns wrapped key bytes. + fn aes_key_wrap(&self, kek: &EngineKeyRef, plaintext_key: &EngineKeyRef) -> Result, CryptoError> { + let _ = (kek, plaintext_key); + Err(CryptoError::MechanismInvalid { name: "aes_key_wrap not implemented" }) + } + /// AES Key Unwrap (decrypt). Returns unwrapped key bytes. + fn aes_key_unwrap(&self, kek: &EngineKeyRef, wrapped_key: &[u8]) -> Result>, CryptoError> { + let _ = (kek, wrapped_key); + Err(CryptoError::MechanismInvalid { name: "aes_key_unwrap not implemented" }) + } + + /// Return the raw key value bytes to be fed into `C_DigestKey`. + /// + /// For software engines: returns the secret key bytes directly. + /// For HSM/TEE engines: extract via the HSM if the key policy permits it, + /// or return `Err` to surface `CKR_KEY_INDIGESTIBLE` to the caller. + /// + /// Default: returns `Err` — engines must explicitly opt in. This is the + /// fail-safe for opaque backends where `as_bytes()` yields a handle token, + /// not extractable key material. + fn key_value_for_digest(&self, key_ref: &EngineKeyRef) -> Result, CryptoError> { + let _ = key_ref; + Err(CryptoError::MechanismInvalid { name: "key_value_for_digest not supported" }) + } + + // ── Attribute access ────────────────────────────────────────────────── + + /// Return a crypto-derived attribute for an RSA key (modulus, bits, exponent, etc.). + fn rsa_attribute(&self, key: &EngineKeyRef, is_private: bool, attr: AttributeType) -> Result; + /// Return a crypto-derived attribute for an EC key (ec_params, ec_point, etc.). + fn ec_attribute(&self, key: &EngineKeyRef, is_private: bool, attr: AttributeType) -> Result; + /// Return a crypto-derived attribute for an AES key (value_len, value). + fn aes_attribute(&self, key: &EngineKeyRef, attr: AttributeType) -> Result; + /// Return a crypto-derived attribute for an EdDSA key (ec_params, ec_point, etc.). + fn ed_attribute(&self, _key: &EngineKeyRef, _is_private: bool, _attr: AttributeType) -> Result { + Err(CryptoError::AttributeTypeInvalid) + } + + // ── Key persistence ────────────────────────────────────────────────── + + /// Serialize an opaque key ref to bytes for persistent storage. + fn serialize_key(&self, key: &EngineKeyRef) -> Result, CryptoError>; + /// Reconstruct an opaque key ref from previously serialized bytes. + fn deserialize_key(&self, data: &[u8]) -> Result; +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..b331a7b --- /dev/null +++ b/src/types.rs @@ -0,0 +1,140 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use zeroize::Zeroizing; + +/// An RSA key pair with pre-parsed attribute fields. +/// +/// Both DER fields use standard encodings that every crypto library understands: +/// - `private_der` — PKCS#8 `PrivateKeyInfo` (RFC 5958) +/// - `public_der` — `SubjectPublicKeyInfo` (RFC 5480 / X.509) +/// +/// The pre-parsed fields (`bits`, `modulus`, `public_exponent`) allow the +/// PKCS#11 layer to answer `C_GetAttributeValue` for common read-only attributes +/// without round-tripping through the engine's `rsa_attribute` method on every call. +/// +/// # PKCS#11 attribute mapping +/// | Field | CKA_* | +/// |--------------------|--------------------------| +/// | `private_der` | CKA_VALUE (sensitive) | +/// | `public_der` | (SubjectPublicKeyInfo) | +/// | `bits` | CKA_MODULUS_BITS | +/// | `modulus` | CKA_MODULUS | +/// | `public_exponent` | CKA_PUBLIC_EXPONENT | +#[derive(Clone)] +pub struct RsaKeyPair { + /// PKCS#8 PrivateKeyInfo DER — corresponds to CKA_VALUE on the private-key object. + /// Marked sensitive; the PKCS#11 layer must not expose it unless CKA_EXTRACTABLE is true. + /// Wrapped in `Zeroizing` to ensure secure erasure on drop. + pub private_der: Zeroizing>, + /// SubjectPublicKeyInfo DER — the public half. + pub public_der: Vec, + /// Modulus bit length (e.g. 2048, 4096). Corresponds to CKA_MODULUS_BITS. + pub bits: u32, + /// RSA modulus `n` in big-endian bytes. Corresponds to CKA_MODULUS. + pub modulus: Vec, + /// RSA public exponent `e` in big-endian bytes. Corresponds to CKA_PUBLIC_EXPONENT. + pub public_exponent: Vec, +} + +/// An EC key pair with pre-parsed attribute fields. +/// +/// | Field | CKA_* | +/// |--------------------------|--------------------------| +/// | `private_der` | CKA_VALUE (sensitive) | +/// | `public_der` | (SubjectPublicKeyInfo) | +/// | `curve` | (engine-internal) | +/// | `ec_params_der` | CKA_EC_PARAMS | +/// | `ec_point_uncompressed` | CKA_EC_POINT | +#[derive(Clone)] +pub struct EcKeyPair { + /// PKCS#8 PrivateKeyInfo DER. Wrapped in `Zeroizing` for secure erasure on drop. + pub private_der: Zeroizing>, + /// SubjectPublicKeyInfo DER. + pub public_der: Vec, + /// Which named curve was used. + pub curve: EcCurve, + /// DER-encoded OID of the curve — CKA_EC_PARAMS. + /// For P-256: `06 08 2a 86 48 ce 3d 03 01 07`. + pub ec_params_der: Vec, + /// DER OCTET STRING wrapping the uncompressed point (0x04 || x || y). + /// Corresponds to CKA_EC_POINT. + pub ec_point_uncompressed: Vec, +} + +/// Named elliptic curves supported by the abstraction layer. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EcCurve { + /// P-256 / secp256r1 / prime256v1. + P256, + /// P-384 / secp384r1. + P384, + /// P-521 / secp521r1. + P521, +} + +/// Edwards curves for EdDSA (v3.0). +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EdwardsCurve { + /// Ed25519 — RFC 8032. + Ed25519, + /// Ed448 — RFC 8032. + Ed448, +} + +/// EdDSA key pair returned by the engine. +#[derive(Clone)] +pub struct EdKeyPair { + /// PKCS#8 PrivateKeyInfo DER. Wrapped in `Zeroizing` for secure erasure on drop. + pub private_der: Zeroizing>, + /// SubjectPublicKeyInfo DER. + pub public_der: Vec, + /// Which Edwards curve was used. + pub curve: EdwardsCurve, + /// DER-encoded OID of the curve — CKA_EC_PARAMS. + pub ec_params_der: Vec, + /// Raw public key bytes (32 for Ed25519, 57 for Ed448). + pub ec_point: Vec, +} + +/// Hash algorithms supported by the engine. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HashAlgorithm { + Md5, + Sha1, + Sha256, + Sha384, + Sha512, + Sha3_256, + Sha3_384, + Sha3_512, +} + +impl HashAlgorithm { + /// Digest output length in bytes. + pub fn digest_len(self) -> usize { + match self { + HashAlgorithm::Md5 => 16, + HashAlgorithm::Sha1 => 20, + HashAlgorithm::Sha256 => 32, + HashAlgorithm::Sha384 => 48, + HashAlgorithm::Sha512 => 64, + HashAlgorithm::Sha3_256 => 32, + HashAlgorithm::Sha3_384 => 48, + HashAlgorithm::Sha3_512 => 64, + } + } +} diff --git a/tests/BUILD b/tests/BUILD new file mode 100644 index 0000000..e1130fe --- /dev/null +++ b/tests/BUILD @@ -0,0 +1,69 @@ +load("@crates//:defs.bzl", "aliases", "all_crate_deps") +load("@rules_rust//rust:defs.bzl", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +_COMMON_DEPS = ["//src:cryptoki_lib"] + +_TESTS_WITH_COMMON = [ + "always_authenticate", + "attribute_policy", + "copy_object", + "lifecycle", + "mechanism_policy", + "message_api", + "persistence_integration", + "pkcs11_integration", + "pkcs11_v3_integration", + "profile_objects", + "ro_session", + "session_vs_token_objects", + "token_info", + "wrap_acl", +] + +_TESTS_NO_COMMON = [ + "connect_disconnect", + "encryption", + "engine_integration", + "hashing", + "misc", + "object_management", + "signing", + "slots_and_tokens", + "storage_atomic_writes", +] + +[rust_test( + name = name, + crate_root = name + ".rs", + srcs = [ + name + ".rs", + "common/mod.rs", + ], + aliases = aliases( + normal_dev = True, + proc_macro_dev = True, + package_name = "", + ), + deps = _COMMON_DEPS + all_crate_deps(normal = True, package_name = "") + all_crate_deps(normal_dev = True, package_name = ""), + proc_macro_deps = all_crate_deps(proc_macro_dev = True, package_name = ""), +) for name in _TESTS_WITH_COMMON] + +[rust_test( + name = name, + crate_root = name + ".rs", + srcs = [name + ".rs"], + aliases = aliases( + normal_dev = True, + proc_macro_dev = True, + package_name = "", + ), + deps = _COMMON_DEPS + all_crate_deps(normal = True, package_name = "") + all_crate_deps(normal_dev = True, package_name = ""), + proc_macro_deps = all_crate_deps(proc_macro_dev = True, package_name = ""), +) for name in _TESTS_NO_COMMON] + +test_suite( + name = "integration_tests", + tests = [":" + name for name in _TESTS_WITH_COMMON + _TESTS_NO_COMMON], +) diff --git a/tests/always_authenticate.rs b/tests/always_authenticate.rs new file mode 100644 index 0000000..91c23ea --- /dev/null +++ b/tests/always_authenticate.rs @@ -0,0 +1,254 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for: CKA_ALWAYS_AUTHENTICATE per-operation gating. +//! +//! Tests exercise: +//! - A key with `CKA_ALWAYS_AUTHENTICATE=TRUE` blocks `C_Sign` without a +//! preceding `C_Login(CKU_CONTEXT_SPECIFIC)`. +//! - The operation succeeds after `C_Login(CKU_CONTEXT_SPECIFIC)` with the +//! correct PIN. +//! - The context-specific auth is one-shot: a second `C_Sign` (without a new +//! context login) is rejected. + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use serial_test::serial; +use std::ffi::c_void; +use std::ptr; + +use std::sync::Once; +static INIT: Once = Once::new(); + +const SLOT_PIN: &[u8] = b"1234"; + +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}" + ); + }); +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +/// Open an RW session and log in as CKU_USER. +unsafe fn open_user_session() -> CK_SESSION_HANDLE { + let fl = common::fn_list(); + let h = common::open_session(fl); + let rv = p11!(fl, C_Login, h, CKU_USER, SLOT_PIN.as_ptr(), SLOT_PIN.len() as CK_ULONG); + assert!(rv == CKR_OK || rv == CKR_USER_ALREADY_LOGGED_IN, + "C_Login(USER) failed: {rv:#010x}"); + h +} + +/// Generate an EC P-256 key pair. Returns (priv_handle, pub_handle). +/// +/// `always_authenticate` controls whether `CKA_ALWAYS_AUTHENTICATE` is set on +/// the private key template. +unsafe fn generate_ec_keypair( + session: CK_SESSION_HANDLE, + always_authenticate: bool, +) -> (CK_OBJECT_HANDLE, CK_OBJECT_HANDLE) { + let fl = common::fn_list(); + // P-256 OID DER encoding. + let p256_oid: &[u8] = &[0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; + let token_false: &[u8] = &[CK_FALSE]; + let always_auth_byte: &[u8] = &[if always_authenticate { CK_TRUE } else { CK_FALSE }]; + + let mut pub_attrs = [ + CK_ATTRIBUTE { r#type: CKA_EC_PARAMS, pValue: p256_oid.as_ptr() as *mut c_void, ulValueLen: p256_oid.len() as CK_ULONG }, + CK_ATTRIBUTE { r#type: CKA_TOKEN, pValue: token_false.as_ptr() as *mut c_void, ulValueLen: 1 }, + ]; + let mut priv_attrs = [ + CK_ATTRIBUTE { r#type: CKA_TOKEN, pValue: token_false.as_ptr() as *mut c_void, ulValueLen: 1 }, + CK_ATTRIBUTE { r#type: CKA_ALWAYS_AUTHENTICATE, pValue: always_auth_byte.as_ptr() as *mut c_void, ulValueLen: 1 }, + ]; + let mech = CK_MECHANISM { mechanism: CKM_EC_KEY_PAIR_GEN, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let mut pub_h: CK_OBJECT_HANDLE = 0; + let mut priv_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, + session, &mech, + pub_attrs.as_mut_ptr(), pub_attrs.len() as CK_ULONG, + priv_attrs.as_mut_ptr(), priv_attrs.len() as CK_ULONG, + &mut pub_h, &mut priv_h); + assert_eq!(rv, CKR_OK, "C_GenerateKeyPair failed: {rv:#010x}"); + (priv_h, pub_h) +} + +/// Call C_SignInit + C_Sign with ECDSA on `priv_handle`. Returns the raw CK_RV. +unsafe fn do_sign(session: CK_SESSION_HANDLE, priv_handle: CK_OBJECT_HANDLE) -> CK_RV { + let fl = common::fn_list(); + let mech = CK_MECHANISM { mechanism: CKM_ECDSA, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl, C_SignInit, session, &mech, priv_handle); + if rv != CKR_OK { return rv; } + // 32-byte prehashed digest (SHA-256 of b"test") + let digest = [0u8; 32]; + let mut sig_buf = [0u8; 72]; + let mut sig_len: CK_ULONG = sig_buf.len() as CK_ULONG; + p11!(fl, C_Sign, session, digest.as_ptr(), digest.len() as CK_ULONG, + sig_buf.as_mut_ptr(), &mut sig_len) +} + +/// Call C_Login(CKU_CONTEXT_SPECIFIC) on `session` with the correct PIN. +unsafe fn context_login(session: CK_SESSION_HANDLE) -> CK_RV { + let fl = common::fn_list(); + p11!(fl, C_Login, session, CKU_CONTEXT_SPECIFIC, + SLOT_PIN.as_ptr(), SLOT_PIN.len() as CK_ULONG) +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +/// A key WITHOUT CKA_ALWAYS_AUTHENTICATE signs normally — no context login needed. +#[test] +#[serial] +fn normal_key_signs_without_context_login() { + init(); + unsafe { + let session = open_user_session(); + let (priv_h, _pub_h) = generate_ec_keypair(session, false); + let rv = do_sign(session, priv_h); + assert_eq!(rv, CKR_OK, "sign with normal key must succeed, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// A key WITH CKA_ALWAYS_AUTHENTICATE=TRUE blocks C_Sign unless a preceding +/// C_Login(CKU_CONTEXT_SPECIFIC) was issued on the same session. +#[test] +#[serial] +fn always_auth_key_fails_sign_without_context_login() { + init(); + unsafe { + let session = open_user_session(); + let (priv_h, _pub_h) = generate_ec_keypair(session, true); + let rv = do_sign(session, priv_h); + assert_eq!(rv, CKR_USER_NOT_LOGGED_IN, + "sign without context login must return CKR_USER_NOT_LOGGED_IN, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// After C_Login(CKU_CONTEXT_SPECIFIC) with the correct PIN, C_Sign succeeds. +#[test] +#[serial] +fn always_auth_key_succeeds_after_context_login() { + init(); + unsafe { + let session = open_user_session(); + let (priv_h, _pub_h) = generate_ec_keypair(session, true); + + let mech = CK_MECHANISM { mechanism: CKM_ECDSA, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(common::fn_list(), C_SignInit, session, &mech, priv_h); + assert_eq!(rv, CKR_OK); + + let rv = context_login(session); + assert_eq!(rv, CKR_OK, "C_Login must succeed because SignInit is active"); + + let digest = [0u8; 32]; + let mut sig = [0u8; 72]; + let mut sig_len = sig.len() as CK_ULONG; + let rv = p11!(common::fn_list(), C_Sign, session, digest.as_ptr(), digest.len() as CK_ULONG, sig.as_mut_ptr(), &mut sig_len); + assert_eq!(rv, CKR_OK); + + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// The context-specific auth is one-shot: a second C_Sign (without a new +/// C_Login(CKU_CONTEXT_SPECIFIC)) must be rejected. +#[test] +#[serial] +fn always_auth_consumed_after_one_sign() { + init(); + unsafe { + let session = open_user_session(); + let (priv_h, _pub_h) = generate_ec_keypair(session, true); + + let mech = CK_MECHANISM { mechanism: CKM_ECDSA, pParameter: std::ptr::null_mut(), ulParameterLen: 0 }; + p11!(common::fn_list(), C_SignInit, session, &mech, priv_h); + + let rv = context_login(session); + assert_eq!(rv, CKR_OK, "C_Login(CKU_CONTEXT_SPECIFIC) failed: {rv:#010x}"); + + let digest = [0u8; 32]; + let mut sig = [0u8; 72]; + let mut sig_len = sig.len() as CK_ULONG; + let rv = p11!(common::fn_list(), C_Sign, session, digest.as_ptr(), digest.len() as CK_ULONG, sig.as_mut_ptr(), &mut sig_len); + assert_eq!(rv, CKR_OK, "first sign must succeed, got {rv:#010x}"); + + p11!(common::fn_list(), C_SignInit, session, &mech, priv_h); + + let mut sig_len = sig.len() as CK_ULONG; + let rv = p11!(common::fn_list(), C_Sign, session, digest.as_ptr(), digest.len() as CK_ULONG, sig.as_mut_ptr(), &mut sig_len); + assert_eq!(rv, CKR_USER_NOT_LOGGED_IN, + "second sign without re-login must fail, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// Two sessions on the same slot are independent: context login on session A +/// does not arm the flag on session B. +#[test] +#[serial] +fn context_auth_is_per_session() { + init(); + unsafe { + let fl = common::fn_list(); + let session_a = open_user_session(); + let (priv_h, _pub_h) = generate_ec_keypair(session_a, true); + let mut session_b: CK_SESSION_HANDLE = 0; + p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut session_b); + + let mech = CK_MECHANISM { mechanism: CKM_ECDSA, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + + p11!(fl, C_SignInit, session_a, &mech, priv_h); + p11!(fl, C_SignInit, session_b, &mech, priv_h); + let rv = context_login(session_a); + assert_eq!(rv, CKR_OK, "Login must succeed because session_a has an active operation"); + let digest = [0u8; 32]; + let mut sig = [0u8; 72]; + let mut sig_len = sig.len() as CK_ULONG; + let rv = p11!(fl, C_Sign, session_b, digest.as_ptr(), digest.len() as CK_ULONG, sig.as_mut_ptr(), &mut sig_len); + assert_eq!(rv, CKR_USER_NOT_LOGGED_IN, "Session B should not be authorized"); + + let mut sig_len = sig.len() as CK_ULONG; + let rv = p11!(fl, C_Sign, session_a, digest.as_ptr(), digest.len() as CK_ULONG, sig.as_mut_ptr(), &mut sig_len); + assert_eq!(rv, CKR_OK); + + p11!(fl, C_CloseSession, session_a); + p11!(fl, C_CloseSession, session_b); + } +} + +/// Wrong PIN for CKU_CONTEXT_SPECIFIC login must return CKR_PIN_INCORRECT. +#[test] +#[serial] +fn context_login_wrong_pin_rejected() { + init(); + unsafe { + let session = open_user_session(); + let wrong_pin = b"wrong"; + let fl = common::fn_list(); + let rv = p11!(fl, C_Login, session, CKU_CONTEXT_SPECIFIC, + wrong_pin.as_ptr(), wrong_pin.len() as CK_ULONG); + assert_eq!(rv, CKR_PIN_INCORRECT, + "wrong PIN for context login must return CKR_PIN_INCORRECT, got {rv:#010x}"); + p11!(fl, C_CloseSession, session); + } +} diff --git a/tests/attribute_policy.rs b/tests/attribute_policy.rs new file mode 100644 index 0000000..986f49a --- /dev/null +++ b/tests/attribute_policy.rs @@ -0,0 +1,425 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! +//! Integration Tests for: attribute policy — one-way ratchets, immutability, +//! access control, and key-generation defaults. +//! +//! Tests exercise the policy through the public C_* API (C_SetAttributeValue, +//! C_GetAttributeValue, C_GenerateKey, C_GenerateKeyPair) so that both the +//! attribute_policy module and its integration in mod.rs are covered. + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use std::ptr; + +// ── Process-level init ─────────────────────────────────────────────────────── + +use std::sync::Once; +static INIT: Once = Once::new(); +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}" + ); + }); +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +unsafe fn open_session() -> CK_SESSION_HANDLE { + common::open_session(common::fn_list()) +} + +fn bool_attr(val: bool) -> Vec { + vec![if val { CK_TRUE } else { CK_FALSE }] +} + +fn ulong_attr(val: CK_ULONG) -> Vec { + val.to_le_bytes().to_vec() +} + +/// Generate a session AES-128 key with the given extra attributes. +unsafe fn make_aes_key( + session: CK_SESSION_HANDLE, + extra_attrs: &[(CK_ATTRIBUTE_TYPE, Vec)], +) -> CK_OBJECT_HANDLE { + let fl = common::fn_list(); + // Build the base template: token=false, value_len=16. + let mut attrs_data: Vec<(CK_ATTRIBUTE_TYPE, Vec)> = vec![ + (CKA_TOKEN, bool_attr(false)), + (CKA_VALUE_LEN, ulong_attr(16)), + ]; + attrs_data.extend_from_slice(extra_attrs); + let mut raw: Vec = attrs_data + .iter() + .map(|(t, v)| CK_ATTRIBUTE { + r#type: *t, + pValue: v.as_ptr() as *mut _, + ulValueLen: v.len() as CK_ULONG, + }) + .collect(); + let mut mech = CK_MECHANISM { + mechanism: CKM_AES_KEY_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, session, &mut mech, raw.as_mut_ptr(), raw.len() as CK_ULONG, &mut handle); + assert_eq!(rv, CKR_OK, "C_GenerateKey failed: {rv:#010x}"); + handle +} + +/// Read a single boolean attribute from an object. Returns `None` if unavailable. +unsafe fn get_bool_attr(session: CK_SESSION_HANDLE, handle: CK_OBJECT_HANDLE, attr_type: CK_ATTRIBUTE_TYPE) -> Option { + let fl = common::fn_list(); + let mut val: CK_BBOOL = 0; + let mut attr = CK_ATTRIBUTE { + r#type: attr_type, + pValue: &mut val as *mut _ as *mut _, + ulValueLen: 1, + }; + let rv = p11!(fl, C_GetAttributeValue, session, handle, &mut attr, 1); + if rv == CKR_ATTRIBUTE_SENSITIVE || rv == CKR_ATTRIBUTE_TYPE_INVALID { + return None; + } + assert_eq!(rv, CKR_OK, "C_GetAttributeValue failed: {rv:#010x}"); + Some(val == CK_TRUE) +} + +/// Set a boolean attribute. Returns the CK_RV directly. +unsafe fn set_bool_attr( + session: CK_SESSION_HANDLE, + handle: CK_OBJECT_HANDLE, + attr_type: CK_ATTRIBUTE_TYPE, + value: bool, +) -> CK_RV { + let fl = common::fn_list(); + let data = bool_attr(value); + let mut attr = CK_ATTRIBUTE { + r#type: attr_type, + pValue: data.as_ptr() as *mut _, + ulValueLen: 1, + }; + p11!(fl, C_SetAttributeValue, session, handle, &mut attr, 1) +} + +// ── Ratchet: CKA_SENSITIVE ──────────────────────────────────────────── + +/// CKA_SENSITIVE can go FALSE → TRUE (allowed). +#[test] +fn sensitive_false_to_true_is_allowed() { + init(); + unsafe { + let session = open_session(); + // Create a key explicitly with SENSITIVE=FALSE. + let handle = make_aes_key(session, &[(CKA_SENSITIVE, bool_attr(false)), (CKA_EXTRACTABLE, bool_attr(true))]); + assert_eq!(get_bool_attr(session, handle, CKA_SENSITIVE), Some(false)); + // Ratchet it to TRUE — must succeed. + let rv = set_bool_attr(session, handle, CKA_SENSITIVE, true); + assert_eq!(rv, CKR_OK, "SENSITIVE FALSE → TRUE must be allowed, got {rv:#010x}"); + assert_eq!(get_bool_attr(session, handle, CKA_SENSITIVE), Some(true)); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// CKA_SENSITIVE cannot go TRUE → FALSE (ratchet). +#[test] +fn sensitive_true_to_false_is_rejected() { + init(); + unsafe { + let session = open_session(); + // Default generated key has SENSITIVE=TRUE. + let handle = make_aes_key(session, &[]); + assert_eq!(get_bool_attr(session, handle, CKA_SENSITIVE), Some(true)); + // Attempt the forbidden direction. + let rv = set_bool_attr(session, handle, CKA_SENSITIVE, false); + assert_eq!(rv, CKR_ATTRIBUTE_READ_ONLY, + "SENSITIVE TRUE → FALSE must return CKR_ATTRIBUTE_READ_ONLY, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +// ── Ratchet: CKA_EXTRACTABLE ───────────────────────────────────────── + +/// CKA_EXTRACTABLE can go TRUE → FALSE (allowed). +#[test] +fn extractable_true_to_false_is_allowed() { + init(); + unsafe { + let session = open_session(); + // Create a key with SENSITIVE=FALSE, EXTRACTABLE=TRUE to start. + let handle = make_aes_key(session, &[ + (CKA_SENSITIVE, bool_attr(false)), + (CKA_EXTRACTABLE, bool_attr(true)), + ]); + assert_eq!(get_bool_attr(session, handle, CKA_EXTRACTABLE), Some(true)); + let rv = set_bool_attr(session, handle, CKA_EXTRACTABLE, false); + assert_eq!(rv, CKR_OK, "EXTRACTABLE TRUE → FALSE must be allowed, got {rv:#010x}"); + assert_eq!(get_bool_attr(session, handle, CKA_EXTRACTABLE), Some(false)); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// CKA_EXTRACTABLE cannot go FALSE → TRUE (ratchet). +#[test] +fn extractable_false_to_true_is_rejected() { + init(); + unsafe { + let session = open_session(); + // Default generated key has EXTRACTABLE=FALSE. + let handle = make_aes_key(session, &[]); + assert_eq!(get_bool_attr(session, handle, CKA_EXTRACTABLE), Some(false)); + let rv = set_bool_attr(session, handle, CKA_EXTRACTABLE, true); + assert_eq!(rv, CKR_ATTRIBUTE_READ_ONLY, + "EXTRACTABLE FALSE → TRUE must return CKR_ATTRIBUTE_READ_ONLY, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +// ── Immutable attributes ────────────────────────────────────────────── + +/// CKA_KEY_TYPE is immutable after creation. +#[test] +fn key_type_is_immutable() { + init(); + unsafe { + let session = open_session(); + let handle = make_aes_key(session, &[]); + // Attempt to change CKA_KEY_TYPE to something else (CKK_DES = 0x13). + let new_type = ulong_attr(0x13); + let mut attr = CK_ATTRIBUTE { + r#type: CKA_KEY_TYPE, + pValue: new_type.as_ptr() as *mut _, + ulValueLen: new_type.len() as CK_ULONG, + }; + let rv = p11!(common::fn_list(), C_SetAttributeValue, session, handle, &mut attr, 1); + assert_eq!(rv, CKR_ATTRIBUTE_READ_ONLY, + "CKA_KEY_TYPE must be immutable, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// CKA_CLASS is immutable after creation. +#[test] +fn class_is_immutable() { + init(); + unsafe { + let session = open_session(); + let handle = make_aes_key(session, &[]); + let new_class = ulong_attr(CKO_PUBLIC_KEY); + let mut attr = CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: new_class.as_ptr() as *mut _, + ulValueLen: new_class.len() as CK_ULONG, + }; + let rv = p11!(common::fn_list(), C_SetAttributeValue, session, handle, &mut attr, 1); + assert_eq!(rv, CKR_ATTRIBUTE_READ_ONLY, + "CKA_CLASS must be immutable, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// CKA_VALUE_LEN is immutable after creation. +#[test] +fn value_len_is_immutable() { + init(); + unsafe { + let session = open_session(); + let handle = make_aes_key(session, &[]); + let new_len = ulong_attr(32); + let mut attr = CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: new_len.as_ptr() as *mut _, + ulValueLen: new_len.len() as CK_ULONG, + }; + let rv = p11!(common::fn_list(), C_SetAttributeValue, session, handle, &mut attr, 1); + assert_eq!(rv, CKR_ATTRIBUTE_READ_ONLY, + "CKA_VALUE_LEN must be immutable, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +// ── CKA_VALUE access control ───────────────────────────────────────── + +/// CKA_VALUE must be blocked on a sensitive key. +#[test] +fn value_blocked_when_sensitive() { + init(); + unsafe { + let session = open_session(); + // Default key: SENSITIVE=TRUE. + let handle = make_aes_key(session, &[]); + assert_eq!(get_bool_attr(session, handle, CKA_SENSITIVE), Some(true)); + + let mut val_buf = vec![0u8; 32]; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_VALUE, + pValue: val_buf.as_mut_ptr() as *mut _, + ulValueLen: 32, + }; + let rv = p11!(common::fn_list(), C_GetAttributeValue, session, handle, &mut attr, 1); + assert_eq!(rv, CKR_ATTRIBUTE_SENSITIVE, + "CKA_VALUE on a sensitive key must return CKR_ATTRIBUTE_SENSITIVE, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// CKA_VALUE must be blocked on a non-extractable key (even if not sensitive). +#[test] +fn value_blocked_when_not_extractable() { + init(); + unsafe { + let session = open_session(); + // Explicitly sensitive=false but extractable=false. + let handle = make_aes_key(session, &[ + (CKA_SENSITIVE, bool_attr(false)), + (CKA_EXTRACTABLE, bool_attr(false)), + ]); + assert_eq!(get_bool_attr(session, handle, CKA_SENSITIVE), Some(false)); + assert_eq!(get_bool_attr(session, handle, CKA_EXTRACTABLE), Some(false)); + + let mut val_buf = vec![0u8; 32]; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_VALUE, + pValue: val_buf.as_mut_ptr() as *mut _, + ulValueLen: 32, + }; + let rv = p11!(common::fn_list(), C_GetAttributeValue, session, handle, &mut attr, 1); + assert_eq!(rv, CKR_ATTRIBUTE_SENSITIVE, + "CKA_VALUE on a non-extractable key must return CKR_ATTRIBUTE_SENSITIVE, got {rv:#010x}"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +// ── Key-generation defaults ────────────────────────────────────────── + +/// A generated AES key has SENSITIVE=TRUE and EXTRACTABLE=FALSE by default. +#[test] +fn generated_aes_key_has_secure_defaults() { + init(); + unsafe { + let session = open_session(); + let handle = make_aes_key(session, &[]); + assert_eq!(get_bool_attr(session, handle, CKA_SENSITIVE), Some(true), "default SENSITIVE must be TRUE"); + assert_eq!(get_bool_attr(session, handle, CKA_EXTRACTABLE), Some(false), "default EXTRACTABLE must be FALSE"); + p11!(common::fn_list(), C_CloseSession, session); + } +} + +/// A generated RSA key pair: private key has SENSITIVE=TRUE, EXTRACTABLE=FALSE. +#[test] +fn generated_rsa_private_key_has_secure_defaults() { + init(); + unsafe { + let fl = common::fn_list(); + let session = open_session(); + + let modulus_bits_val = 2048u64.to_le_bytes().to_vec(); + let token_false = bool_attr(false); + let mut pub_template = vec![ + CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: token_false.as_ptr() as *mut _, + ulValueLen: 1, + }, + CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: modulus_bits_val.as_ptr() as *mut _, + ulValueLen: 8, + }, + ]; + let mut priv_template = vec![ + CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: token_false.as_ptr() as *mut _, + ulValueLen: 1, + }, + ]; + let mut mech = CK_MECHANISM { + mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut pub_h: CK_OBJECT_HANDLE = 0; + let mut priv_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, + session, &mut mech, + pub_template.as_mut_ptr(), pub_template.len() as CK_ULONG, + priv_template.as_mut_ptr(), priv_template.len() as CK_ULONG, + &mut pub_h, &mut priv_h, + ); + assert_eq!(rv, CKR_OK, "C_GenerateKeyPair failed: {rv:#010x}"); + + assert_eq!(get_bool_attr(session, priv_h, CKA_SENSITIVE), Some(true), "private key default SENSITIVE must be TRUE"); + assert_eq!(get_bool_attr(session, priv_h, CKA_EXTRACTABLE), Some(false), "private key default EXTRACTABLE must be FALSE"); + + p11!(fl, C_CloseSession, session); + } +} + +// ── update_derived_attributes ──────────────────────────────────────── + +/// Setting SENSITIVE FALSE → TRUE on a key that started non-sensitive leaves +/// always_sensitive = false (the key was not always sensitive). +/// We verify indirectly: after the ratchet, CKA_ALWAYS_SENSITIVE should be FALSE. +#[test] +fn always_sensitive_stays_false_after_ratchet_up() { + init(); + unsafe { + let fl = common::fn_list(); + let session = open_session(); + // Key created with SENSITIVE=FALSE → always_sensitive starts false. + let handle = make_aes_key(session, &[ + (CKA_SENSITIVE, bool_attr(false)), + (CKA_EXTRACTABLE, bool_attr(true)), + ]); + // Ratchet SENSITIVE to TRUE. + let rv = set_bool_attr(session, handle, CKA_SENSITIVE, true); + assert_eq!(rv, CKR_OK); + // always_sensitive should remain FALSE (key was not always sensitive). + assert_eq!(get_bool_attr(session, handle, CKA_ALWAYS_SENSITIVE), Some(false), + "CKA_ALWAYS_SENSITIVE must stay FALSE when key was created non-sensitive"); + p11!(fl, C_CloseSession, session); + } +} + +/// A key generated by C_GenerateKey with default SENSITIVE=TRUE has +/// CKA_ALWAYS_SENSITIVE=TRUE and CKA_NEVER_EXTRACTABLE=TRUE. +#[test] +fn generated_key_has_always_sensitive_and_never_extractable() { + init(); + unsafe { + let fl = common::fn_list(); + let session = open_session(); + let handle = make_aes_key(session, &[]); + // These are struct fields exposed as attributes via C_GetAttributeValue. + // The PKCS#11 spec requires them to be readable. + assert_eq!( + get_bool_attr(session, handle, CKA_ALWAYS_SENSITIVE), + Some(true), + "CKA_ALWAYS_SENSITIVE must be TRUE for a key generated with SENSITIVE=TRUE" + ); + assert_eq!( + get_bool_attr(session, handle, CKA_NEVER_EXTRACTABLE), + Some(true), + "CKA_NEVER_EXTRACTABLE must be TRUE for a key generated with EXTRACTABLE=FALSE" + ); + p11!(fl, C_CloseSession, session); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..0cdcd86 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,103 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Shared test helpers — function-list dispatch. +//! +//! Real PKCS#11 consumers never call C_* symbols directly. They: +//! 1. `dlopen` the shared library +//! 2. `dlsym("C_GetFunctionList")` to obtain `CK_FUNCTION_LIST*` +//! 3. Dispatch every call through the returned function pointers +//! +//! These helpers let our tests follow the same pattern. + +#![allow(dead_code)] + +use std::ptr; + +use cryptoki::pkcs11::C_GetFunctionList; + +// Re-export for convenience so test files only need `mod common;`. +pub use cryptoki::pkcs11::constants::*; +pub use cryptoki::pkcs11::types::*; + +/// Obtain the v2.40-compatible function list via `C_GetFunctionList`. +/// +/// In production this would be the *only* symbol obtained via `dlsym`; +/// every subsequent PKCS#11 call goes through the returned table. +pub unsafe fn fn_list() -> &'static CK_FUNCTION_LIST { + let mut fl: *const CK_FUNCTION_LIST = ptr::null(); + let rv = C_GetFunctionList(&mut fl); + assert_eq!(rv, CKR_OK, "C_GetFunctionList failed: {rv:#010x}"); + assert!(!fl.is_null(), "C_GetFunctionList returned null"); + &*fl +} + +/// Obtain the v3.0 extended function list via `C_GetInterface`. +/// +/// This is the v3.0 bootstrap path: `dlsym("C_GetInterface")` → +/// `CK_INTERFACE` → cast `pFunctionList` to `CK_FUNCTION_LIST_3_0*`. +pub unsafe fn fn_list_3_0() -> &'static CK_FUNCTION_LIST_3_0 { + use cryptoki::pkcs11::C_GetInterface; + let mut iface_ptr: *const CK_INTERFACE = ptr::null(); + let rv = C_GetInterface(ptr::null(), ptr::null_mut(), &mut iface_ptr, 0); + assert_eq!(rv, CKR_OK, "C_GetInterface failed: {rv:#010x}"); + assert!(!iface_ptr.is_null()); + let iface = &*iface_ptr; + assert!(!iface.pFunctionList.is_null()); + &*(iface.pFunctionList as *const CK_FUNCTION_LIST_3_0) +} + +/// Call a PKCS#11 function through a function-list pointer. +/// +/// Usage: `p11!(fl, C_Initialize, ptr::null_mut())` +/// +/// Mirrors the C pattern: `fn_list->C_Initialize(NULL)` +#[macro_export] +macro_rules! p11 { + ($fl:expr, $func:ident $(, $arg:expr)* $(,)?) => { + ($fl.$func.unwrap())($($arg),*) + } +} + +/// Initialize the library through the function list and open a logged-in R/W session. +/// Returns `(function_list, session_handle)`. +pub unsafe fn init_and_open_session(fl: &CK_FUNCTION_LIST) -> CK_SESSION_HANDLE { + let rv = (fl.C_Initialize.unwrap())(ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + + let mut h: CK_SESSION_HANDLE = 0; + let rv = p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession failed: {rv:#010x}"); + h +} + +/// Open a R/W session (assumes library is already initialized). +pub unsafe fn open_session(fl: &CK_FUNCTION_LIST) -> CK_SESSION_HANDLE { + let mut h: CK_SESSION_HANDLE = 0; + let rv = p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession failed: {rv:#010x}"); + h +} + +/// Open a session and log in as CKU_USER with the default PIN. +pub unsafe fn open_logged_in_session(fl: &CK_FUNCTION_LIST) -> CK_SESSION_HANDLE { + let h = open_session(fl); + let pin = b"1234"; + let rv = p11!(fl, C_Login, h, CKU_USER, pin.as_ptr(), pin.len() as CK_ULONG); + assert!(rv == CKR_OK || rv == CKR_USER_ALREADY_LOGGED_IN, + "C_Login failed: {rv:#010x}"); + h +} diff --git a/tests/connect_disconnect.rs b/tests/connect_disconnect.rs new file mode 100644 index 0000000..88bab8a --- /dev/null +++ b/tests/connect_disconnect.rs @@ -0,0 +1,106 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! The Test demonstrates: +//! - Loading the PKCS#11 library. +//! - Connecting to a token: C_Initialize → C_OpenSession → C_Login. +//! - Disconnecting from a token: C_Logout → C_CloseSession → C_Finalize. + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::{ + C_Initialize, + C_OpenSession, C_CloseSession, + C_Login, C_Logout, + C_GetSessionInfo, +}; +use std::ptr; +use std::sync::Once; + +const SLOT_PIN: &[u8] = b"1234"; + +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let rv = C_Initialize(ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}", + ); + }); +} + +// ── connectToSlot / disconnectFromSlot ─────── + +/// Demonstrates the full connect/disconnect lifecycle: +/// C_Initialize → C_OpenSession → C_Login → C_Logout → C_CloseSession +/// +/// connectToSlot(): +/// C_Initialize(NULL_PTR) +/// C_OpenSession(slotId, CKF_SERIAL_SESSION|CKF_RW_SESSION, ...) +/// C_Login(hSession, CKU_USER, slotPin, pinLen) +/// +/// disconnectFromSlot(): +/// C_Logout(hSession) +/// C_CloseSession(hSession) +/// C_Finalize(NULL_PTR) +#[test] +fn connect_disconnect() { + init(); + unsafe { + // Step 1: Initialize the library + // (shared via Once — mirrors C_Initialize(NULL_PTR) in loadHSMLibrary) + + // Step 2: Open a read-write session on the slot + // (C_OpenSession(slotId, CKF_SERIAL_SESSION | CKF_RW_SESSION, NULL_PTR, NULL_PTR, &hSession)) + let mut h_session: CK_SESSION_HANDLE = 0; + assert_eq!( + C_OpenSession( + 0, + CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), + None, + &mut h_session, + ), + CKR_OK, + "C_OpenSession failed", + ); + assert_ne!(h_session, 0, "session handle must be non-zero"); + + // Step 3: Login as normal user + // (C_Login(hSession, CKU_USER, slotPin, strlen(slotPin))) + assert_eq!( + C_Login(h_session, CKU_USER, SLOT_PIN.as_ptr(), SLOT_PIN.len() as CK_ULONG), + CKR_OK, + "C_Login failed", + ); + + // Step 4: Verify session state reflects logged-in user + let mut info: CK_SESSION_INFO = std::mem::zeroed(); + assert_eq!(C_GetSessionInfo(h_session, &mut info), CKR_OK); + assert_eq!(info.state, CKS_RW_USER_FUNCTIONS, "state must be CKS_RW_USER_FUNCTIONS after login"); + + // Step 5: Logout + // (C_Logout(hSession)) + assert_eq!(C_Logout(h_session), CKR_OK, "C_Logout failed"); + + // Step 6: Verify session state reverts to public read-write + assert_eq!(C_GetSessionInfo(h_session, &mut info), CKR_OK); + assert_eq!(info.state, CKS_RW_PUBLIC_SESSION, "state must revert after logout"); + + // Step 7: Close the session + // (C_CloseSession(hSession)) + assert_eq!(C_CloseSession(h_session), CKR_OK, "C_CloseSession failed"); + } +} diff --git a/tests/copy_object.rs b/tests/copy_object.rs new file mode 100644 index 0000000..fce9915 --- /dev/null +++ b/tests/copy_object.rs @@ -0,0 +1,248 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for: C_CopyObject. +//! +//! Verifies: basic copy (handle differs, attributes identical), copy with +//! attribute override, ratchet enforcement on copy template, and RO session +//! rejection. + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use std::ptr; +use std::sync::Once; + +static INIT: Once = Once::new(); +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}" + ); + }); +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +fn bool_attr(val: bool) -> Vec { + vec![if val { CK_TRUE } else { CK_FALSE }] +} + +fn ulong_attr(val: CK_ULONG) -> Vec { + val.to_le_bytes().to_vec() +} + +/// Open an RW session on slot 0. +unsafe fn open_rw() -> CK_SESSION_HANDLE { + let fl = common::fn_list(); + let mut h: CK_SESSION_HANDLE = 0; + let rv = p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession (RW) failed: {rv:#010x}"); + h +} + +/// Open an RO session on slot 0. +unsafe fn open_ro() -> CK_SESSION_HANDLE { + let fl = common::fn_list(); + let mut h: CK_SESSION_HANDLE = 0; + let rv = p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION, ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession (RO) failed: {rv:#010x}"); + h +} + +/// Generate a session AES-128 key. +unsafe fn make_aes_key( + session: CK_SESSION_HANDLE, + extra: &[(CK_ATTRIBUTE_TYPE, Vec)], +) -> CK_OBJECT_HANDLE { + let fl = common::fn_list(); + let mut attrs_data: Vec<(CK_ATTRIBUTE_TYPE, Vec)> = vec![ + (CKA_TOKEN, bool_attr(false)), + (CKA_VALUE_LEN, ulong_attr(16)), + ]; + attrs_data.extend_from_slice(extra); + let mut raw: Vec = attrs_data + .iter() + .map(|(t, v)| CK_ATTRIBUTE { + r#type: *t, + pValue: v.as_ptr() as *mut _, + ulValueLen: v.len() as CK_ULONG, + }) + .collect(); + let mut mech = CK_MECHANISM { + mechanism: CKM_AES_KEY_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, session, &mut mech, + raw.as_mut_ptr(), raw.len() as CK_ULONG, &mut handle); + assert_eq!(rv, CKR_OK, "C_GenerateKey failed: {rv:#010x}"); + handle +} + +/// Read a single BBOOL attribute. Returns `None` if the call fails. +unsafe fn get_bool(session: CK_SESSION_HANDLE, obj: CK_OBJECT_HANDLE, attr_type: CK_ATTRIBUTE_TYPE) -> Option { + let fl = common::fn_list(); + let mut val: CK_BBOOL = 0; + let mut attr = CK_ATTRIBUTE { + r#type: attr_type, + pValue: &mut val as *mut CK_BBOOL as *mut _, + ulValueLen: 1, + }; + let rv = p11!(fl, C_GetAttributeValue, session, obj, &mut attr, 1u64); + if rv == CKR_OK { Some(val != 0) } else { None } +} + +/// Copy an object with the given template overrides. +unsafe fn copy(session: CK_SESSION_HANDLE, src: CK_OBJECT_HANDLE, + overrides: &[(CK_ATTRIBUTE_TYPE, Vec)]) -> (CK_RV, CK_OBJECT_HANDLE) { + let fl = common::fn_list(); + let mut raw: Vec = overrides + .iter() + .map(|(t, v)| CK_ATTRIBUTE { + r#type: *t, + pValue: v.as_ptr() as *mut _, + ulValueLen: v.len() as CK_ULONG, + }) + .collect(); + let mut new_handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_CopyObject, session, src, + raw.as_mut_ptr(), raw.len() as CK_ULONG, &mut new_handle); + (rv, new_handle) +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +/// A basic copy produces a distinct handle whose attributes match the source. +#[test] +fn copy_basic_produces_new_handle() { + init(); + unsafe { + let session = open_rw(); + let fl = common::fn_list(); + let src = make_aes_key(session, &[(CKA_ENCRYPT, bool_attr(true))]); + let (rv, copy_h) = copy(session, src, &[]); + assert_eq!(rv, CKR_OK, "C_CopyObject failed: {rv:#010x}"); + assert_ne!(copy_h, src, "copy must have a different handle"); + // CKA_ENCRYPT must be TRUE on the copy too. + let enc = get_bool(session, copy_h, CKA_ENCRYPT); + assert_eq!(enc, Some(true), "copy should inherit CKA_ENCRYPT=TRUE"); + p11!(fl, C_CloseSession, session); + } +} + +/// A copy with an attribute override reflects the new value. +#[test] +fn copy_with_attribute_override() { + init(); + unsafe { + let session = open_rw(); + let fl = common::fn_list(); + // Source has CKA_ENCRYPT=TRUE, CKA_DECRYPT=FALSE. + let src = make_aes_key(session, &[ + (CKA_ENCRYPT, bool_attr(true)), + (CKA_DECRYPT, bool_attr(false)), + ]); + // Override: flip DECRYPT to TRUE on the copy. + let (rv, copy_h) = copy(session, src, &[(CKA_DECRYPT, bool_attr(true))]); + assert_eq!(rv, CKR_OK, "C_CopyObject with override failed: {rv:#010x}"); + let enc = get_bool(session, copy_h, CKA_ENCRYPT); + assert_eq!(enc, Some(true), "CKA_ENCRYPT should still be TRUE"); + let dec = get_bool(session, copy_h, CKA_DECRYPT); + assert_eq!(dec, Some(true), "CKA_DECRYPT override should be TRUE"); + p11!(fl, C_CloseSession, session); + } +} + +/// Ratchet enforcement: trying to copy a SENSITIVE key as non-sensitive must fail. +#[test] +fn copy_ratchet_sensitive_true_to_false_rejected() { + init(); + unsafe { + let session = open_rw(); + let fl = common::fn_list(); + let src = make_aes_key(session, &[(CKA_SENSITIVE, bool_attr(true))]); + // Attempt to lower SENSITIVE on the copy — must be blocked. + let (rv, _) = copy(session, src, &[(CKA_SENSITIVE, bool_attr(false))]); + assert_eq!(rv, CKR_ATTRIBUTE_READ_ONLY, + "ratchet must prevent SENSITIVE TRUE→FALSE on copy: {rv:#010x}"); + p11!(fl, C_CloseSession, session); + } +} + +/// Ratchet enforcement: trying to copy a non-extractable key as extractable must fail. +#[test] +fn copy_ratchet_extractable_false_to_true_rejected() { + init(); + unsafe { + let session = open_rw(); + let fl = common::fn_list(); + let src = make_aes_key(session, &[(CKA_EXTRACTABLE, bool_attr(false))]); + // Attempt to raise EXTRACTABLE on the copy — must be blocked. + let (rv, _) = copy(session, src, &[(CKA_EXTRACTABLE, bool_attr(true))]); + assert_eq!(rv, CKR_ATTRIBUTE_READ_ONLY, + "ratchet must prevent EXTRACTABLE FALSE→TRUE on copy: {rv:#010x}"); + p11!(fl, C_CloseSession, session); + } +} + +/// Attempting to copy via an RO session must return CKR_SESSION_READ_ONLY. +#[test] +fn copy_ro_session_rejected() { + init(); + unsafe { + let rw = open_rw(); + let ro = open_ro(); + let fl = common::fn_list(); + let src = make_aes_key(rw, &[]); + let (rv, _) = copy(ro, src, &[(CKA_TOKEN, bool_attr(true))]); + assert_eq!(rv, CKR_SESSION_READ_ONLY, + "RO session must be rejected for C_CopyObject: {rv:#010x}"); + p11!(fl, C_CloseSession, rw); + p11!(fl, C_CloseSession, ro); + } +} + +/// The copied object is independent: destroying the copy does not affect the source. +#[test] +fn copy_is_independent_of_source() { + init(); + unsafe { + let session = open_rw(); + let fl = common::fn_list(); + let src = make_aes_key(session, &[]); + let (rv, copy_h) = copy(session, src, &[]); + assert_eq!(rv, CKR_OK); + // Destroy the copy; source must still be usable. + let rv2 = p11!(fl, C_DestroyObject, session, copy_h); + assert_eq!(rv2, CKR_OK, "destroy copy failed: {rv2:#010x}"); + // Confirm source still exists by reading an attribute. + let class = { + let mut val: CK_ULONG = 0; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: &mut val as *mut CK_ULONG as *mut _, + ulValueLen: std::mem::size_of::() as CK_ULONG, + }; + p11!(fl, C_GetAttributeValue, session, src, &mut attr, 1u64) + }; + assert_eq!(class, CKR_OK, "source must still exist after copy is destroyed"); + p11!(fl, C_CloseSession, session); + } +} diff --git a/tests/cpp/BUILD b/tests/cpp/BUILD index e3b354e..8ac4d79 100644 --- a/tests/cpp/BUILD +++ b/tests/cpp/BUILD @@ -1,19 +1,41 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +package(default_visibility = ["//visibility:public"]) + +# Smoke scaffold (no dlopen needed) cc_test( name = "cpp_test_main", srcs = ["test_main.cpp"], - deps = [ - "@googletest//:gtest_main", # GoogleTest dependency via Bazel Modules +) + +# Native wrapper test: cpp/test_cpp.cpp via dlopen → libcryptoki.so +sh_test( + name = "test_cpp", + srcs = ["run_test_cpp.sh"], + data = [ + "//cpp:test_cpp_bin", + "//src:cryptoki_cdylib", + ], + timeout = "short", +) + +# Google PKCS#11 conformance suite via dlopen → libcryptoki.so +sh_test( + name = "pkcs11test", + srcs = ["run_pkcs11test.sh"], + data = [ + "//cpp/pkcs11test:pkcs11test", + "//src:cryptoki_cdylib", + ], + timeout = "long", +) + +# Legacy cmake-based wrapper (kept for reference) +sh_test( + name = "cpp_tests", + srcs = ["run_cpp_tests.sh"], + data = [ + "//:cpp_srcs", ], + timeout = "long", ) diff --git a/tests/cpp/run_cpp_tests.sh b/tests/cpp/run_cpp_tests.sh new file mode 100755 index 0000000..f73c7c3 --- /dev/null +++ b/tests/cpp/run_cpp_tests.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/../.." && pwd)" +build_dir="$repo_root/cpp/build" + +if [[ ! -f "$repo_root/cpp/CMakeLists.txt" ]]; then + echo "cpp/CMakeLists.txt not found" + exit 1 +fi + +mkdir -p "$build_dir" +cd "$build_dir" +cmake .. >/dev/null +cmake --build . >/dev/null + +if [[ -x "$build_dir/test_cpp" ]]; then + "$build_dir/test_cpp" +fi diff --git a/tests/cpp/run_pkcs11test.sh b/tests/cpp/run_pkcs11test.sh new file mode 100644 index 0000000..c06dec1 --- /dev/null +++ b/tests/cpp/run_pkcs11test.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Bazel sets TEST_SRCDIR to the runfiles root. +SO_DIR="${TEST_SRCDIR}/cryptoki/src" +export LD_LIBRARY_PATH="${SO_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +exec "${TEST_SRCDIR}/cryptoki/cpp/pkcs11test/pkcs11test" \ + -m libcryptoki.so \ + -l "${SO_DIR}" \ + -s 0 \ + -u 1234 \ + -o so-pin \ + -I \ + --gtest_filter="-Ciphers*:HMACs*:Duals*:*DES*:*MD5-RSA*:*SHA1-RSA*" diff --git a/tests/cpp/run_test_cpp.sh b/tests/cpp/run_test_cpp.sh new file mode 100644 index 0000000..26d66eb --- /dev/null +++ b/tests/cpp/run_test_cpp.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Bazel sets TEST_SRCDIR to the runfiles root. +SO_DIR="${TEST_SRCDIR}/cryptoki/src" +export LD_LIBRARY_PATH="${SO_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +exec "${TEST_SRCDIR}/cryptoki/cpp/test_cpp_bin" diff --git a/tests/cpp/test_main.cpp b/tests/cpp/test_main.cpp index 4d14df3..516e77e 100644 --- a/tests/cpp/test_main.cpp +++ b/tests/cpp/test_main.cpp @@ -1,41 +1,6 @@ -/******************************************************************************** - * Copyright (c) 2025 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ -#include +#include -// Function to be tested -int add(int a, int b) { - return a + b; -} - -// Test case -TEST(AdditionTest, HandlesPositiveNumbers) { - EXPECT_EQ(add(2, 3), 5); - EXPECT_EQ(add(10, 20), 30); -} - -TEST(AdditionTest, HandlesNegativeNumbers) { - EXPECT_EQ(add(-2, -3), -5); - EXPECT_EQ(add(-10, 5), -5); -} - -TEST(AdditionTest, HandlesZero) { - EXPECT_EQ(add(0, 0), 0); - EXPECT_EQ(add(0, 5), 5); - EXPECT_EQ(add(5, 0), 5); -} - -// Main function for running tests -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); +int main() { + std::cout << "Bazel C++ test scaffold for Cryptoki." << std::endl; + return 0; } diff --git a/tests/encryption.rs b/tests/encryption.rs new file mode 100644 index 0000000..3f48f76 --- /dev/null +++ b/tests/encryption.rs @@ -0,0 +1,423 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! +//! Each test follows the same lifecycle: +//! loadHSMLibrary → connectToSlot (Initialize + OpenSession + Login) +//! → generateKey → encryptData → decryptData +//! → disconnectFromSlot (Logout + CloseSession + Finalize) + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::{ + C_Initialize, + C_OpenSession, C_CloseSession, + C_Login, C_Logout, + C_EncryptInit, C_Encrypt, + C_DecryptInit, C_Decrypt, + C_GenerateKey, C_GenerateKeyPair, +}; +use std::ffi::c_void; +use std::ptr; +use std::sync::Once; + +const SLOT_PIN: &[u8] = b"1234"; + +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let rv = C_Initialize(ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}", + ); + }); +} + +// ── Shared helper: open RW session + login ──────────────────────────────── + +unsafe fn connect_to_slot() -> CK_SESSION_HANDLE { + let mut h: CK_SESSION_HANDLE = 0; + assert_eq!( + C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h), + CKR_OK, + ); + // Login — tolerate CKR_USER_ALREADY_LOGGED_IN (token-wide login from parallel test). + let rv = C_Login(h, CKU_USER, SLOT_PIN.as_ptr(), SLOT_PIN.len() as CK_ULONG); + assert!(rv == CKR_OK || rv == CKR_USER_ALREADY_LOGGED_IN, "C_Login failed: {rv:#x}"); + h +} + +unsafe fn disconnect_from_slot(h: CK_SESSION_HANDLE) { + let rv = C_Logout(h); + assert!(rv == CKR_OK || rv == CKR_USER_NOT_LOGGED_IN, "C_Logout failed: {rv:#x}"); + assert_eq!(C_CloseSession(h), CKR_OK); +} + +// ── Shared helper: generate AES key ────────────────────────────────────── + +unsafe fn generate_aes_key(h: CK_SESSION_HANDLE, key_size_bytes: u64) -> CK_OBJECT_HANDLE { + // generateAesKey(): CK_ATTRIBUTE attrib[] = { ..., {CKA_VALUE_LEN, &keySize, sizeof(CK_ULONG)} } + let key_len_le = key_size_bytes.to_le_bytes(); + let mut attribs = [CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: key_len_le.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + let mech = CK_MECHANISM { + mechanism: CKM_AES_KEY_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut key_handle: CK_OBJECT_HANDLE = 0; + assert_eq!( + C_GenerateKey(h, &mech, attribs.as_mut_ptr(), 1, &mut key_handle), + CKR_OK, + ); + key_handle +} + +// ── Shared helper: generate RSA-2048 key pair ───────────────────────────── + +unsafe fn generate_rsa_key_pair(h: CK_SESSION_HANDLE) -> (CK_OBJECT_HANDLE, CK_OBJECT_HANDLE) { + // generateRsaKeyPair(): CKM_RSA_PKCS_KEY_PAIR_GEN, keySize=2048 + let key_bits: u64 = 2048; + let bits_le = key_bits.to_le_bytes(); + let mut pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: bits_le.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { + mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut h_public: CK_OBJECT_HANDLE = 0; + let mut h_private: CK_OBJECT_HANDLE = 0; + assert_eq!( + C_GenerateKeyPair( + h, &mech, + pub_attrs.as_mut_ptr(), 1, + priv_attrs.as_mut_ptr(), 0, + &mut h_public, &mut h_private, + ), + CKR_OK, + ); + (h_public, h_private) +} + +// ── GCM parameter block (mirrors CK_GCM_PARAMS from cryptoki.h) ────────── + +#[repr(C)] +#[allow(non_snake_case)] +struct GcmParams { + pIv: *const u8, + ulIvLen: u64, + ulIvBits: u64, + pAAD: *const u8, + ulAADLen: u64, + ulTagBits: u64, +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_AES_CBC_PAD +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateAesKey() → +/// encryptData() → decryptData() → disconnectFromSlot() +/// +/// encryptData(): C_EncryptInit(CKM_AES_CBC_PAD, IV) → C_Encrypt(NULL, &len) → C_Encrypt(buf, &len) +/// decryptData(): C_DecryptInit(CKM_AES_CBC_PAD, IV) → C_Decrypt(NULL, &len) → C_Decrypt(buf, &len) +#[test] +fn ckm_aes_cbc_pad() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Generate AES-256 key + // (CK_ULONG keySize = 32; attrib CKA_VALUE_LEN = &keySize) + let obj_handle = generate_aes_key(h_session, 32); + + // IV for CBC mode (CK_BYTE IV[] = "1234567812345678") + let iv = b"1234567812345678"; + let mech = CK_MECHANISM { + mechanism: CKM_AES_CBC_PAD, + pParameter: iv.as_ptr() as *const c_void, + ulParameterLen: 16, + }; + + // Plaintext (unsigned char plainData[] = "Earth is the third planet...") + let plain_data = b"Earth is the third planet of our Solar System."; + + // Step 4: Encrypt — C_EncryptInit then C_Encrypt + // (C_EncryptInit → C_Encrypt(NULL, &encLen) → C_Encrypt(encryptedData, &encLen)) + assert_eq!(C_EncryptInit(h_session, &mech, obj_handle), CKR_OK); + let mut enc_len: CK_ULONG = 128; + let mut encrypted_data = vec![0u8; 128]; + assert_eq!( + C_Encrypt( + h_session, + plain_data.as_ptr(), plain_data.len() as CK_ULONG, + encrypted_data.as_mut_ptr(), &mut enc_len, + ), + CKR_OK, + ); + encrypted_data.truncate(enc_len as usize); + assert!(enc_len > 0); + assert_ne!(encrypted_data.as_slice(), plain_data.as_slice(), "ciphertext must differ from plaintext"); + + // Step 5: Decrypt — C_DecryptInit then C_Decrypt + // (C_DecryptInit → C_Decrypt(NULL, &decLen) → C_Decrypt(decryptedData, &decLen)) + assert_eq!(C_DecryptInit(h_session, &mech, obj_handle), CKR_OK); + let mut dec_len: CK_ULONG = 128; + let mut decrypted_data = vec![0u8; 128]; + assert_eq!( + C_Decrypt( + h_session, + encrypted_data.as_ptr(), enc_len, + decrypted_data.as_mut_ptr(), &mut dec_len, + ), + CKR_OK, + ); + decrypted_data.truncate(dec_len as usize); + assert_eq!(decrypted_data, plain_data.as_slice(), "decrypted must equal original plaintext"); + + // Step 6: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_AES_GCM +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateAesKey() → +/// initGCMParam() → encryptData() → decryptData() → disconnectFromSlot() +/// +/// GCM params (gcmParam.pIv / .ulIvLen / .pAAD / .ulAADLen / .ulTagBits = 128) +/// encryptData(): C_EncryptInit(CKM_AES_GCM, &gcmParam) → C_Encrypt(NULL, &encLen) → C_Encrypt(buf, &encLen) +/// decryptData(): C_DecryptInit(CKM_AES_GCM, &gcmParam) → C_Decrypt → verify plaintext +#[test] +fn ckm_aes_gcm() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Generate AES-256 key + let obj_handle = generate_aes_key(h_session, 32); + + // Step 4: Initialize GCM parameters + // (IV[] = "1234567812345678", AAD[] = "127.0.0.1", ulTagBits = 128) + let iv = b"1234567812345678"; + let aad = b"127.0.0.1"; + let gcm_params = GcmParams { + pIv: iv.as_ptr(), ulIvLen: 16, ulIvBits: 128, + pAAD: aad.as_ptr(), ulAADLen: aad.len() as u64, + ulTagBits: 128, + }; + let mech = CK_MECHANISM { + mechanism: CKM_AES_GCM, + pParameter: &gcm_params as *const _ as *const c_void, + ulParameterLen: std::mem::size_of::() as CK_ULONG, + }; + + let plain_data = b"Earth is the third planet of our Solar System."; + + // Step 5: Encrypt — output is ciphertext || 16-byte authentication tag + // (C_EncryptInit → C_Encrypt(NULL, &encLen) → allocate → C_Encrypt(buf, &encLen)) + assert_eq!(C_EncryptInit(h_session, &mech, obj_handle), CKR_OK); + let mut enc_len: CK_ULONG = 256; + let mut encrypted_data = vec![0u8; 256]; + assert_eq!( + C_Encrypt( + h_session, + plain_data.as_ptr(), plain_data.len() as CK_ULONG, + encrypted_data.as_mut_ptr(), &mut enc_len, + ), + CKR_OK, + ); + encrypted_data.truncate(enc_len as usize); + // GCM output = plaintext_len + 16-byte tag + assert_eq!(enc_len as usize, plain_data.len() + 16, "GCM output must be plaintext + 16-byte tag"); + + // Step 6: Decrypt with same GCM params (AAD must match for auth to pass) + // (initGCMParam() → C_DecryptInit → C_Decrypt(NULL, &decLen) → C_Decrypt(buf, &decLen)) + assert_eq!(C_DecryptInit(h_session, &mech, obj_handle), CKR_OK); + let mut dec_len: CK_ULONG = 256; + let mut decrypted_data = vec![0u8; 256]; + assert_eq!( + C_Decrypt( + h_session, + encrypted_data.as_ptr(), enc_len, + decrypted_data.as_mut_ptr(), &mut dec_len, + ), + CKR_OK, + ); + decrypted_data.truncate(dec_len as usize); + assert_eq!(decrypted_data, plain_data.as_slice()); + + // Step 7: Tamper test — flip a ciphertext byte; decryption must fail + encrypted_data[0] ^= 0xFF; + assert_eq!(C_DecryptInit(h_session, &mech, obj_handle), CKR_OK); + let mut bad_len: CK_ULONG = 256; + let mut bad = vec![0u8; 256]; + let rv = C_Decrypt(h_session, encrypted_data.as_ptr(), enc_len, bad.as_mut_ptr(), &mut bad_len); + assert_ne!(rv, CKR_OK, "tampered GCM ciphertext must not decrypt successfully"); + + // Step 8: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_RSA_PKCS +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateRsaKeyPair() → +/// encryptData() → decryptData() → disconnectFromSlot() +/// +/// encryptData(): C_EncryptInit(CKM_RSA_PKCS, hPublic) → C_Encrypt(NULL, &encLen) → C_Encrypt(buf, &encLen) +/// decryptData(): C_DecryptInit(CKM_RSA_PKCS, hPrivate) → C_Decrypt(NULL, &decLen) → C_Decrypt(buf, &decLen) +#[test] +fn ckm_rsa_pkcs() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Generate RSA-2048 key pair + // (generateRsaKeyPair() → C_GenerateKeyPair(CKM_RSA_PKCS_KEY_PAIR_GEN, ...)) + let (h_public, h_private) = generate_rsa_key_pair(h_session); + + let plain_data = b"Earth is the third planet of our Solar System."; + + // Step 4: Encrypt with the public key + // (CK_MECHANISM mech = {CKM_RSA_PKCS}; C_EncryptInit(hSession, &mech, hPublic)) + let mech = CK_MECHANISM { + mechanism: CKM_RSA_PKCS, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + assert_eq!(C_EncryptInit(h_session, &mech, h_public), CKR_OK); + let mut enc_len: CK_ULONG = 512; + let mut encrypted_data = vec![0u8; 512]; + assert_eq!( + C_Encrypt( + h_session, + plain_data.as_ptr(), plain_data.len() as CK_ULONG, + encrypted_data.as_mut_ptr(), &mut enc_len, + ), + CKR_OK, + ); + encrypted_data.truncate(enc_len as usize); + // RSA-2048 ciphertext is always 256 bytes + assert_eq!(enc_len, 256, "RSA-2048 ciphertext must be 256 bytes"); + + // Step 5: Decrypt with the private key + // (CK_MECHANISM mech = {CKM_RSA_PKCS}; C_DecryptInit(hSession, &mech, hPrivate)) + assert_eq!(C_DecryptInit(h_session, &mech, h_private), CKR_OK); + let mut dec_len: CK_ULONG = 512; + let mut decrypted_data = vec![0u8; 512]; + assert_eq!( + C_Decrypt( + h_session, + encrypted_data.as_ptr(), enc_len, + decrypted_data.as_mut_ptr(), &mut dec_len, + ), + CKR_OK, + ); + decrypted_data.truncate(dec_len as usize); + assert_eq!(decrypted_data, plain_data.as_slice(), "decrypted must equal original plaintext"); + + // Step 6: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_RSA_PKCS_OAEP +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateRsaKeyPair() → +/// initOAEP() → encryptData() → decryptData() → disconnectFromSlot() +/// +/// initOAEP(): oaepParam.hashAlg = CKM_SHA_1; oaepParam.mgf = CKG_MGF1_SHA1 +/// encryptData(): C_EncryptInit(CKM_RSA_PKCS_OAEP, &oaepParam, hPublic) → C_Encrypt +/// decryptData(): C_DecryptInit(CKM_RSA_PKCS_OAEP, &oaepParam, hPrivate) → C_Decrypt +/// +/// Note: our backend ignores the OAEP parameter struct and uses OpenSSL defaults. +#[test] +fn ckm_rsa_pkcs_oaep() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Generate RSA-2048 key pair + let (h_public, h_private) = generate_rsa_key_pair(h_session); + + let plain_data = b"Earth is the third planet of our Solar System."; + + // Step 4: Encrypt with OAEP padding + // (CK_MECHANISM mech = {CKM_RSA_PKCS_OAEP, &oaepParam, sizeof(oaepParam)}) + let mech = CK_MECHANISM { + mechanism: CKM_RSA_PKCS_OAEP, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + assert_eq!(C_EncryptInit(h_session, &mech, h_public), CKR_OK); + let mut enc_len: CK_ULONG = 512; + let mut encrypted_data = vec![0u8; 512]; + assert_eq!( + C_Encrypt( + h_session, + plain_data.as_ptr(), plain_data.len() as CK_ULONG, + encrypted_data.as_mut_ptr(), &mut enc_len, + ), + CKR_OK, + ); + encrypted_data.truncate(enc_len as usize); + assert_eq!(enc_len, 256, "RSA-2048 OAEP ciphertext must be 256 bytes"); + + // Step 5: Decrypt with OAEP padding + assert_eq!(C_DecryptInit(h_session, &mech, h_private), CKR_OK); + let mut dec_len: CK_ULONG = 512; + let mut decrypted_data = vec![0u8; 512]; + assert_eq!( + C_Decrypt( + h_session, + encrypted_data.as_ptr(), enc_len, + decrypted_data.as_mut_ptr(), &mut dec_len, + ), + CKR_OK, + ); + decrypted_data.truncate(dec_len as usize); + assert_eq!(decrypted_data, plain_data.as_slice(), "OAEP decrypted must equal original plaintext"); + + // Step 6: Logout and close session + disconnect_from_slot(h_session); + } +} diff --git a/tests/engine_integration.rs b/tests/engine_integration.rs new file mode 100644 index 0000000..f06c6a0 --- /dev/null +++ b/tests/engine_integration.rs @@ -0,0 +1,396 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests — PKCS#11-style call sequences through the engine trait. +//! +//! All tests share the same process, so `init()` uses `try_engine` to +//! avoid `CKR_CRYPTOKI_ALREADY_INITIALIZED` on the second test that runs. + +use cryptoki::{ + engine, register_engine, try_engine, AttributeType, EcCurve, EngineKeyRef, HashAlgorithm, + OpenSslEngine, +}; + +fn init() { + if try_engine().is_none() { + register_engine(OpenSslEngine).expect("engine registration failed"); + } +} + +// ── Random ──────────────────────────────────────────────────────────────────── + +#[test] +fn test_generate_random_fills_buffer() { + init(); + let eng = engine().unwrap(); + let mut buf = vec![0u8; 32]; + eng.generate_random(&mut buf).unwrap(); + assert_ne!(buf, vec![0u8; 32]); +} + +// ── Key generation ──────────────────────────────────────────────────────────── + +#[test] +fn test_generate_aes_key_128() { + init(); + let key = engine().unwrap().generate_aes_key(16).unwrap(); + assert_eq!(key.as_bytes().len(), 16); +} + +#[test] +fn test_generate_aes_key_256() { + init(); + let key = engine().unwrap().generate_aes_key(32).unwrap(); + assert_eq!(key.as_bytes().len(), 32); +} + +#[test] +fn test_generate_rsa_key_pair_2048() { + init(); + let kp = engine().unwrap().generate_rsa_key_pair(2048).unwrap(); + assert_eq!(kp.bits, 2048); + assert!(!kp.private_der.is_empty()); + assert!(!kp.public_der.is_empty()); + assert_eq!(kp.modulus.len(), 256); // 2048 bits / 8 + assert!(!kp.public_exponent.is_empty()); +} + +#[test] +fn test_generate_ec_key_pair_p256() { + init(); + let kp = engine().unwrap().generate_ec_key_pair(EcCurve::P256).unwrap(); + assert!(!kp.private_der.is_empty()); + assert!(!kp.public_der.is_empty()); + // P-256 OID is 10 bytes + assert_eq!(kp.ec_params_der.len(), 10); + // Uncompressed point = 04 + 32 + 32 = 65 bytes; DER OCTET STRING adds 2 bytes header + assert_eq!(kp.ec_point_uncompressed.len(), 67); +} + +// ── AES-CBC ─────────────────────────────────────────────────────────────────── + +#[test] +fn test_aes_cbc_roundtrip() { + init(); + let eng = engine().unwrap(); + let key = eng.generate_aes_key(16).unwrap(); + let mut iv = vec![0u8; 16]; + eng.generate_random(&mut iv).unwrap(); + let plaintext = b"C_EncryptInit(CKM_AES_CBC_PAD) + C_Encrypt + C_Decrypt"; + + let ciphertext = eng.aes_cbc_encrypt(&key, &iv, plaintext).unwrap(); + let recovered = eng.aes_cbc_decrypt(&key, &iv, &ciphertext).unwrap(); + assert_eq!(&*recovered, plaintext); +} + +#[test] +fn test_aes_cbc_256_roundtrip() { + init(); + let eng = engine().unwrap(); + let key = eng.generate_aes_key(32).unwrap(); + let mut iv = vec![0u8; 16]; + eng.generate_random(&mut iv).unwrap(); + + let ciphertext = eng.aes_cbc_encrypt(&key, &iv, b"hello world").unwrap(); + let recovered = eng.aes_cbc_decrypt(&key, &iv, &ciphertext).unwrap(); + assert_eq!(&*recovered, b"hello world"); +} + +// ── AES-CTR ─────────────────────────────────────────────────────────────────── + +#[test] +fn test_aes_ctr_roundtrip() { + init(); + let eng = engine().unwrap(); + let key = eng.generate_aes_key(16).unwrap(); + let mut iv = vec![0u8; 16]; + eng.generate_random(&mut iv).unwrap(); + let plaintext = b"C_EncryptInit(CKM_AES_CTR) stream cipher"; + + let ciphertext = eng.aes_ctr_crypt(&key, &iv, plaintext).unwrap(); + let recovered = eng.aes_ctr_crypt(&key, &iv, &ciphertext).unwrap(); // CTR decrypt == encrypt + assert_eq!(&*recovered, plaintext); +} + +// ── AES-GCM ─────────────────────────────────────────────────────────────────── + +#[test] +fn test_aes_gcm_roundtrip() { + init(); + let eng = engine().unwrap(); + let key = eng.generate_aes_key(16).unwrap(); + let mut iv = vec![0u8; 12]; + eng.generate_random(&mut iv).unwrap(); + let aad = b"additional authenticated data"; + let plaintext = b"C_EncryptInit(CKM_AES_GCM) + C_Encrypt"; + + let (ct, tag) = eng.aes_gcm_encrypt(&key, &iv, aad, plaintext).unwrap(); + assert_eq!(tag.len(), 16); + + let recovered = eng.aes_gcm_decrypt(&key, &iv, aad, &ct, &tag).unwrap(); + assert_eq!(&*recovered, plaintext); +} + +#[test] +fn test_aes_gcm_tampered_ciphertext_fails() { + init(); + let eng = engine().unwrap(); + let key = eng.generate_aes_key(16).unwrap(); + let iv = vec![0u8; 12]; + + let (mut ct, tag) = eng.aes_gcm_encrypt(&key, &iv, b"", b"secret").unwrap(); + ct[0] ^= 0xFF; // tamper + + let result = eng.aes_gcm_decrypt(&key, &iv, b"", &ct, &tag); + assert!(result.is_err()); + // Maps to CKR_ENCRYPTED_DATA_INVALID + assert_eq!(result.unwrap_err().ckr_code(), 0x00000040); +} + +// ── RSA encryption ──────────────────────────────────────────────────────────── + +#[test] +fn test_rsa_pkcs1_encrypt_decrypt() { + init(); + let eng = engine().unwrap(); + let kp = eng.generate_rsa_key_pair(2048).unwrap(); + let plaintext = b"RSA PKCS1 v1.5 encrypt test"; + + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + let priv_ref = EngineKeyRef::from_bytes(kp.private_der.to_vec()); + let ct = eng.rsa_pkcs1_encrypt(&pub_ref, plaintext).unwrap(); + let recovered = eng.rsa_pkcs1_decrypt(&priv_ref, &ct).unwrap(); + assert_eq!(&*recovered, plaintext); +} + +#[test] +fn test_rsa_oaep_encrypt_decrypt() { + init(); + let eng = engine().unwrap(); + let kp = eng.generate_rsa_key_pair(2048).unwrap(); + let plaintext = b"RSA OAEP encrypt test"; + + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + let priv_ref = EngineKeyRef::from_bytes(kp.private_der.to_vec()); + let ct = eng.rsa_oaep_encrypt(&pub_ref, plaintext).unwrap(); + let recovered = eng.rsa_oaep_decrypt(&priv_ref, &ct).unwrap(); + assert_eq!(&*recovered, plaintext); +} + +// ── RSA signing ─────────────────────────────────────────────────────────────── + +#[test] +fn test_rsa_pkcs1_sign_verify() { + init(); + let eng = engine().unwrap(); + let kp = eng.generate_rsa_key_pair(2048).unwrap(); + let priv_ref = EngineKeyRef::from_bytes(kp.private_der.to_vec()); + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + let msg = b"C_Sign(CKM_SHA256_RSA_PKCS)"; + + let sig = eng.rsa_pkcs1_sign(&priv_ref, msg).unwrap(); + let valid = eng.rsa_pkcs1_verify(&pub_ref, msg, &sig).unwrap(); + assert!(valid); + assert_eq!(sig.len(), 256); // 2048-bit key → 256-byte signature +} + +#[test] +fn test_rsa_pkcs1_tampered_message_fails() { + init(); + let eng = engine().unwrap(); + let kp = eng.generate_rsa_key_pair(2048).unwrap(); + let priv_ref = EngineKeyRef::from_bytes(kp.private_der.to_vec()); + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + let sig = eng.rsa_pkcs1_sign(&priv_ref, b"original").unwrap(); + let valid = eng.rsa_pkcs1_verify(&pub_ref, b"tampered", &sig).unwrap(); + assert!(!valid); +} + +#[test] +fn test_rsa_pss_sign_verify() { + init(); + let eng = engine().unwrap(); + let kp = eng.generate_rsa_key_pair(2048).unwrap(); + let priv_ref = EngineKeyRef::from_bytes(kp.private_der.to_vec()); + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + let msg = b"C_Sign(CKM_SHA256_RSA_PKCS_PSS)"; + + let sig = eng.rsa_pss_sign(&priv_ref, msg).unwrap(); + let valid = eng.rsa_pss_verify(&pub_ref, msg, &sig).unwrap(); + assert!(valid); +} + +#[test] +fn test_rsa_pss_is_randomised() { + init(); + let eng = engine().unwrap(); + let kp = eng.generate_rsa_key_pair(2048).unwrap(); + let priv_ref = EngineKeyRef::from_bytes(kp.private_der.to_vec()); + let msg = b"same message"; + + let sig1 = eng.rsa_pss_sign(&priv_ref, msg).unwrap(); + let sig2 = eng.rsa_pss_sign(&priv_ref, msg).unwrap(); + assert_ne!(sig1, sig2); // random salt → different ciphertexts +} + +// ── ECDSA signing ───────────────────────────────────────────────────────────── + +#[test] +fn test_ecdsa_sign_verify() { + init(); + let eng = engine().unwrap(); + let kp = eng.generate_ec_key_pair(EcCurve::P256).unwrap(); + let priv_ref = EngineKeyRef::from_bytes(kp.private_der.to_vec()); + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + let msg = b"C_Sign(CKM_ECDSA) over P-256"; + + let sig = eng.ecdsa_sign(&priv_ref, msg).unwrap(); + let valid = eng.ecdsa_verify(&pub_ref, msg, &sig).unwrap(); + assert!(valid); +} + +#[test] +fn test_ecdsa_tampered_message_fails() { + init(); + let eng = engine().unwrap(); + let kp = eng.generate_ec_key_pair(EcCurve::P256).unwrap(); + + let priv_ref = EngineKeyRef::from_bytes(kp.private_der.to_vec()); + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + let sig = eng.ecdsa_sign(&priv_ref, b"original").unwrap(); + let valid = eng.ecdsa_verify(&pub_ref, b"tampered", &sig).unwrap(); + assert!(!valid); +} + +// ── Hashing ─────────────────────────────────────────────────────────────────── + +#[test] +fn test_hash_sha256_known_vector() { + init(); + let digest = engine().unwrap().hash(HashAlgorithm::Sha256, b"hello world").unwrap(); + assert_eq!(digest.len(), 32); +} + +#[test] +fn test_multi_part_hash_matches_single_part() { + init(); + let eng = engine().unwrap(); + let full = b"hello world"; + let reference = eng.hash(HashAlgorithm::Sha256, full).unwrap(); + + // C_DigestInit → C_DigestUpdate × 2 → C_DigestFinal + let mut hasher = eng.new_stream_hasher(HashAlgorithm::Sha256).unwrap(); + hasher.update(b"hello ").unwrap(); + hasher.update(b"world").unwrap(); + let digest = hasher.finish().unwrap(); + + assert_eq!(digest, reference); +} + +// ── Attributes (C_GetAttributeValue) ───────────────────────────────────────── + +#[test] +fn test_rsa_attribute_modulus_bits() { + use cryptoki::AttributeValue; + init(); + let eng = engine().unwrap(); + let kp = eng.generate_rsa_key_pair(2048).unwrap(); + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + + let val = eng.rsa_attribute(&pub_ref, false, AttributeType::ModulusBits).unwrap(); + assert!(matches!(val, AttributeValue::Ulong(2048))); +} + +#[test] +fn test_rsa_attribute_modulus() { + use cryptoki::AttributeValue; + init(); + let eng = engine().unwrap(); + let kp = eng.generate_rsa_key_pair(2048).unwrap(); + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + + let val = eng.rsa_attribute(&pub_ref, false, AttributeType::Modulus).unwrap(); + if let AttributeValue::Bytes(n) = val { + assert_eq!(n, kp.modulus); + } else { + panic!("expected Bytes variant"); + } +} + +#[test] +fn test_rsa_private_key_value_is_sensitive() { + init(); + let eng = engine().unwrap(); + let kp = eng.generate_rsa_key_pair(2048).unwrap(); + let priv_ref = EngineKeyRef::from_bytes(kp.private_der.to_vec()); + + let err = eng.rsa_attribute(&priv_ref, true, AttributeType::Value).unwrap_err(); + assert_eq!(err.ckr_code(), 0x00000011); // CKR_ATTRIBUTE_SENSITIVE +} + +#[test] +fn test_ec_attribute_params_and_point() { + use cryptoki::AttributeValue; + init(); + let eng = engine().unwrap(); + let kp = eng.generate_ec_key_pair(EcCurve::P256).unwrap(); + let pub_ref = EngineKeyRef::from_bytes(kp.public_der.clone()); + + let params = eng.ec_attribute(&pub_ref, false, AttributeType::EcParams).unwrap(); + if let AttributeValue::Bytes(b) = params { + assert_eq!(b, kp.ec_params_der); + } else { + panic!("expected Bytes for EcParams"); + } + + let point = eng.ec_attribute(&pub_ref, false, AttributeType::EcPoint).unwrap(); + if let AttributeValue::Bytes(b) = point { + assert_eq!(b, kp.ec_point_uncompressed); + } else { + panic!("expected Bytes for EcPoint"); + } +} + +#[test] +fn test_aes_attribute_value_len() { + use cryptoki::AttributeValue; + init(); + let eng = engine().unwrap(); + let key = eng.generate_aes_key(32).unwrap(); + + let val = eng.aes_attribute(&key, AttributeType::ValueLen).unwrap(); + assert!(matches!(val, AttributeValue::Ulong(32))); +} + +// ── Error code mapping ──────────────────────────────────────────────────────── + +#[test] +fn test_not_initialized_error_code() { + use cryptoki::CryptoError; + let err = CryptoError::NotInitialized; + assert_eq!(err.ckr_code(), 0x00000190); // CKR_CRYPTOKI_NOT_INITIALIZED +} + +#[test] +fn test_already_initialized_error_code() { + use cryptoki::CryptoError; + let err = CryptoError::AlreadyInitialized; + assert_eq!(err.ckr_code(), 0x00000191); // CKR_CRYPTOKI_ALREADY_INITIALIZED +} + +#[test] +fn test_decrypt_failed_maps_to_encrypted_data_invalid() { + use cryptoki::CryptoError; + let err = CryptoError::DecryptFailed { message: "tag mismatch".into() }; + assert_eq!(err.ckr_code(), 0x00000040); // CKR_ENCRYPTED_DATA_INVALID +} diff --git a/tests/hashing.rs b/tests/hashing.rs new file mode 100644 index 0000000..b839c8c --- /dev/null +++ b/tests/hashing.rs @@ -0,0 +1,377 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! +//! Each test follows: +//! loadHSMLibrary → connectToSlot (Initialize + OpenSession + Login) +//! → generateHash (C_DigestInit → C_Digest or C_DigestUpdate × N → C_DigestFinal) +//! → disconnectFromSlot (Logout + CloseSession + Finalize) + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::{ + C_Initialize, + C_OpenSession, C_CloseSession, + C_Login, C_Logout, + C_DigestInit, C_Digest, C_DigestUpdate, C_DigestFinal, +}; +use std::ptr; +use std::sync::Once; + +const SLOT_PIN: &[u8] = b"1234"; + +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let rv = C_Initialize(ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}", + ); + }); +} + +// ── Shared helpers ──────────────────────────────────────────────────────── + +unsafe fn connect_to_slot() -> CK_SESSION_HANDLE { + let mut h: CK_SESSION_HANDLE = 0; + assert_eq!( + C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h), + CKR_OK, + ); + let rv = C_Login(h, CKU_USER, SLOT_PIN.as_ptr(), SLOT_PIN.len() as CK_ULONG); + assert!(rv == CKR_OK || rv == CKR_USER_ALREADY_LOGGED_IN, "C_Login failed: {rv:#x}"); + h +} + +unsafe fn disconnect_from_slot(h: CK_SESSION_HANDLE) { + let rv = C_Logout(h); + assert!(rv == CKR_OK || rv == CKR_USER_NOT_LOGGED_IN, "C_Logout failed: {rv:#x}"); + assert_eq!(C_CloseSession(h), CKR_OK); +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_SHA256 +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateHash() → disconnectFromSlot() +/// +/// generateHash(): +/// C_DigestInit(CKM_SHA256) → C_Digest(data, NULL, &digestLen) → C_Digest(data, digest, &digestLen) +/// +/// The uses a two-call pattern: first pass NULL to get length, then allocate and call again. +/// We verify against the known SHA-256("abc") vector. +#[test] +fn ckm_sha256() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // plainData (CK_BYTE plainData[] = "Earth is the third planet of our Solar System.") + let plain_data = b"Earth is the third planet of our Solar System."; + + // Step 3: Initialize digest operation with CKM_SHA256 + // (CK_MECHANISM mech = {CKM_SHA256}; C_DigestInit(hSession, &mech)) + let mech = CK_MECHANISM { + mechanism: CKM_SHA256, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + assert_eq!(C_DigestInit(h_session, &mech), CKR_OK); + + // Step 4: First C_Digest call with NULL output buffer to query output length + // (C_Digest(hSession, plainData, sizeof(plainData)-1, NULL, &digestLen)) + let mut digest_len: CK_ULONG = 0; + assert_eq!( + C_Digest(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, ptr::null_mut(), &mut digest_len), + CKR_OK, + ); + assert_eq!(digest_len, 32, "SHA-256 output must be 32 bytes"); + + // Step 5: Second C_Digest call with allocated buffer to retrieve hash + // (digest = new CK_BYTE[digestLen]; C_Digest(hSession, plainData, ..., digest, &digestLen)) + let mut digest = vec![0u8; 32]; + let mut digest_len2: CK_ULONG = 32; + assert_eq!( + C_Digest(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, digest.as_mut_ptr(), &mut digest_len2), + CKR_OK, + ); + assert_ne!(digest, [0u8; 32], "hash output must not be all zeros"); + + // Step 6: Verify known SHA-256("abc") test vector + // SHA-256("abc") = ba7816bf 8f01cfea 414140de 5dae2223 b00361a3 96177a9c b410ff61 f20015ad + assert_eq!(C_DigestInit(h_session, &mech), CKR_OK); + let abc = b"abc"; + let mut abc_hash = vec![0u8; 32]; + let mut abc_len: CK_ULONG = 32; + assert_eq!(C_Digest(h_session, abc.as_ptr(), 3, abc_hash.as_mut_ptr(), &mut abc_len), CKR_OK); + let expected_sha256_abc = [ + 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, + 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, + 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad, + ]; + assert_eq!(abc_hash, expected_sha256_abc, "SHA-256('abc') vector mismatch"); + + // Step 7: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_SHA_1 +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateHash() → disconnectFromSlot() +/// +/// generateHash(): +/// CK_MECHANISM mech = {CKM_SHA_1} +/// C_DigestInit → C_Digest(NULL, &digestLen) → C_Digest(digest, &digestLen) +/// +/// We verify against the known SHA-1("abc") vector. +#[test] +fn ckm_sha1() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // plainData (CK_BYTE plainData[] = "Earth is the third planet of our Solar System.") + let plain_data = b"Earth is the third planet of our Solar System."; + + // Step 3: Initialize SHA-1 digest operation + // (CK_MECHANISM mech = {CKM_SHA_1}; C_DigestInit(hSession, &mech)) + let mech = CK_MECHANISM { + mechanism: CKM_SHA_1, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + assert_eq!(C_DigestInit(h_session, &mech), CKR_OK); + + // Step 4: Query output length (NULL buffer) + // (C_Digest(hSession, plainData, sizeof(plainData)-1, NULL, &digestLen)) + let mut digest_len: CK_ULONG = 0; + assert_eq!( + C_Digest(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, ptr::null_mut(), &mut digest_len), + CKR_OK, + ); + assert_eq!(digest_len, 20, "SHA-1 output must be 20 bytes"); + + // Step 5: Compute hash and retrieve it + // (digest = new CK_BYTE[digestLen]; C_Digest(hSession, plainData, ..., digest, &digestLen)) + let mut digest = vec![0u8; 20]; + let mut digest_len2: CK_ULONG = 20; + assert_eq!( + C_Digest(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, digest.as_mut_ptr(), &mut digest_len2), + CKR_OK, + ); + assert_ne!(digest, [0u8; 20], "hash output must not be all zeros"); + + // Step 6: Verify known SHA-1("abc") test vector + // SHA-1("abc") = a9993e36 4706816a ba3e2571 7850c26c 9cd0d89d + assert_eq!(C_DigestInit(h_session, &mech), CKR_OK); + let mut abc_hash = vec![0u8; 20]; + let mut abc_len: CK_ULONG = 20; + assert_eq!(C_Digest(h_session, b"abc".as_ptr(), 3, abc_hash.as_mut_ptr(), &mut abc_len), CKR_OK); + let expected_sha1_abc = [ + 0xa9u8, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81, 0x6a, + 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50, 0xc2, 0x6c, + 0x9c, 0xd0, 0xd8, 0x9d, + ]; + assert_eq!(abc_hash, expected_sha1_abc, "SHA-1('abc') vector mismatch"); + + // Step 7: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_MD5 +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateHash() → disconnectFromSlot() +/// +/// generateHash(): +/// CK_MECHANISM mech = {CKM_MD5} +/// C_DigestInit → C_Digest(NULL, &digestLen) → C_Digest(digest, &digestLen) +/// +/// We verify against known MD5 test vectors for empty string and "abc". +#[test] +fn ckm_md5() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Initialize MD5 digest operation + // (CK_MECHANISM mech = {CKM_MD5}; C_DigestInit(hSession, &mech)) + let mech = CK_MECHANISM { + mechanism: CKM_MD5, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + assert_eq!(C_DigestInit(h_session, &mech), CKR_OK); + + // Step 4: Query output length + let mut digest_len: CK_ULONG = 0; + assert_eq!(C_Digest(h_session, b"".as_ptr(), 0, ptr::null_mut(), &mut digest_len), CKR_OK); + assert_eq!(digest_len, 16, "MD5 output must be 16 bytes"); + + // Step 5: Known MD5("") = d41d8cd9 8f00b204 e9800998 ecf8427e + // (digest = new CK_BYTE[digestLen]; C_Digest(..., digest, &digestLen)) + let mut digest_empty = vec![0u8; 16]; + let mut len1: CK_ULONG = 16; + assert_eq!(C_Digest(h_session, b"".as_ptr(), 0, digest_empty.as_mut_ptr(), &mut len1), CKR_OK); + let expected_md5_empty = [ + 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, + 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e, + ]; + assert_eq!(digest_empty, expected_md5_empty, "MD5('') vector mismatch"); + + // Step 6: Known MD5("abc") = 90015098 3cd24fb0 d6963f7d 28e17f72 + assert_eq!(C_DigestInit(h_session, &mech), CKR_OK); + let mut digest_abc = vec![0u8; 16]; + let mut len2: CK_ULONG = 16; + assert_eq!(C_Digest(h_session, b"abc".as_ptr(), 3, digest_abc.as_mut_ptr(), &mut len2), CKR_OK); + let expected_md5_abc = [ + 0x90u8, 0x01, 0x50, 0x98, 0x3c, 0xd2, 0x4f, 0xb0, + 0xd6, 0x96, 0x3f, 0x7d, 0x28, 0xe1, 0x7f, 0x72, + ]; + assert_eq!(digest_abc, expected_md5_abc, "MD5('abc') vector mismatch"); + + // Step 7: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// multi_part_digest +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → hash_of_a_file() → disconnectFromSlot() +/// +/// hash_of_a_file(): +/// C_DigestInit(CKM_SHA256) → +/// [loop] C_DigestUpdate(hSession, buffer, bufferLen) for each chunk → +/// C_DigestFinal(hSession, NULL, &digestLen) → +/// C_DigestFinal(hSession, digest, &digestLen) +/// +/// We simulate chunked file reads using in-memory byte slices, then compare +/// against the one-shot result to confirm equivalence. +#[test] +fn multi_part_digest_sha256() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Simulated file chunks (reads file in 32-byte buffers via ifstream) + let chunk1 = b"Earth is the third "; + let chunk2 = b"planet of our "; + let chunk3 = b"Solar System."; + let full = b"Earth is the third planet of our Solar System."; + + let mech = CK_MECHANISM { + mechanism: CKM_SHA256, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + + // Step 3: Initialize the streaming digest + // (C_DigestInit(hSession, &mech)) + assert_eq!(C_DigestInit(h_session, &mech), CKR_OK); + + // Step 4: Feed chunks via C_DigestUpdate + // ([loop] C_DigestUpdate(hSession, buffer, bufferLen)) + assert_eq!(C_DigestUpdate(h_session, chunk1.as_ptr(), chunk1.len() as CK_ULONG), CKR_OK); + assert_eq!(C_DigestUpdate(h_session, chunk2.as_ptr(), chunk2.len() as CK_ULONG), CKR_OK); + assert_eq!(C_DigestUpdate(h_session, chunk3.as_ptr(), chunk3.len() as CK_ULONG), CKR_OK); + + // Step 5: Finalize — first call with NULL to get output length + // (C_DigestFinal(hSession, NULL, &digestLen)) + let mut digest_len: CK_ULONG = 0; + assert_eq!(C_DigestFinal(h_session, ptr::null_mut(), &mut digest_len), CKR_OK); + assert_eq!(digest_len, 32); + + // Step 6: Second call retrieves hash (C_DigestFinal with NULL does NOT consume the context) + // (digest = new CK_BYTE[digestLen]; C_DigestFinal(hSession, digest, &digestLen)) + let mut multi_digest = vec![0u8; 32]; + let mut multi_len: CK_ULONG = 32; + assert_eq!(C_DigestFinal(h_session, multi_digest.as_mut_ptr(), &mut multi_len), CKR_OK); + + // Step 7: One-shot reference digest of the same full data + assert_eq!(C_DigestInit(h_session, &mech), CKR_OK); + let mut one_digest = vec![0u8; 32]; + let mut one_len: CK_ULONG = 32; + assert_eq!( + C_Digest(h_session, full.as_ptr(), full.len() as CK_ULONG, one_digest.as_mut_ptr(), &mut one_len), + CKR_OK, + ); + + // Multi-part result must match the one-shot result + assert_eq!(multi_digest, one_digest, "multi-part digest must equal one-shot digest"); + + // Step 8: Logout and close session + disconnect_from_slot(h_session); + } +} + +/// Extension: multi-part SHA-1 digest +#[test] +fn multi_part_digest_sha1() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + let mech = CK_MECHANISM { + mechanism: CKM_SHA_1, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + + // Step 3: Initialize streaming SHA-1 digest + assert_eq!(C_DigestInit(h_session, &mech), CKR_OK); + + // Step 4: Feed two chunks "ab" + "c" = "abc" + assert_eq!(C_DigestUpdate(h_session, b"ab".as_ptr(), 2), CKR_OK); + assert_eq!(C_DigestUpdate(h_session, b"c".as_ptr(), 1), CKR_OK); + + // Step 5: Finalize and retrieve digest + let mut digest = vec![0u8; 20]; + let mut len: CK_ULONG = 20; + assert_eq!(C_DigestFinal(h_session, digest.as_mut_ptr(), &mut len), CKR_OK); + + // SHA-1("abc") = a9993e36 4706816a ba3e2571 7850c26c 9cd0d89d + let expected = [ + 0xa9u8, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81, 0x6a, + 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50, 0xc2, 0x6c, + 0x9c, 0xd0, 0xd8, 0x9d, + ]; + assert_eq!(digest, expected, "multi-part SHA-1('abc') vector mismatch"); + + // Step 6: Logout and close session + disconnect_from_slot(h_session); + } +} diff --git a/tests/lifecycle.rs b/tests/lifecycle.rs new file mode 100644 index 0000000..8ac6f90 --- /dev/null +++ b/tests/lifecycle.rs @@ -0,0 +1,287 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for: C_Initialize / C_Finalize lifecycle. +//! +//! These tests exercise repeated Init/Finalize cycles and verify that all +//! in-process state is cleaned up between cycles. Because each test mutates +//! global library state (initialised / finalised), the tests MUST NOT run in +//! parallel within the same binary. A process-wide mutex serialises them. +//! +//! Run with: +//! cargo test --test lifecycle -- --test-threads=1 +//! or just `cargo test --test lifecycle` (single-file binary always sequential). + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::{C_Initialize, C_Finalize, C_OpenSession, C_CloseSession}; +use std::ffi::c_void; +use std::ptr; +use std::sync::OnceLock; + +// ── Serialisation ───────────────────────────────────────────────────────────── +// All tests in this file mutate global library state; they must not overlap. +static LOCK: OnceLock> = OnceLock::new(); +fn serial_lock() -> std::sync::MutexGuard<'static, ()> { + LOCK.get_or_init(|| std::sync::Mutex::new(())).lock().unwrap_or_else(|e| e.into_inner()) +} + +/// Bring the library to a known-uninitialised state regardless of where it +/// currently is. Swallows `CKR_CRYPTOKI_NOT_INITIALIZED` (already finalized). +unsafe fn ensure_finalized() { + let rv = C_Finalize(ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_NOT_INITIALIZED, + "ensure_finalized: unexpected rv {rv:#010x}" + ); +} + +unsafe fn do_initialize() -> CK_RV { + C_Initialize(ptr::null_mut()) +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +/// C_Initialize / C_Finalize / C_Initialize cycle succeeds cleanly. +#[test] +fn init_finalize_init_cycle() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + + // First initialization. + let rv = do_initialize(); + assert_eq!(rv, CKR_OK, "first C_Initialize failed: {rv:#010x}"); + + // Open a session to create some state. + let mut h: CK_SESSION_HANDLE = 0; + let rv = C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession after first init failed: {rv:#010x}"); + + // Finalize — should close the session and clear all state. + let rv = C_Finalize(ptr::null_mut()); + assert_eq!(rv, CKR_OK, "C_Finalize failed: {rv:#010x}"); + + // Second initialization must succeed (not return ALREADY_INITIALIZED). + let rv = do_initialize(); + assert_eq!(rv, CKR_OK, "second C_Initialize after Finalize failed: {rv:#010x}"); + + // Verify the library is usable again. + let mut h2: CK_SESSION_HANDLE = 0; + let rv = C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h2); + assert_eq!(rv, CKR_OK, "C_OpenSession after re-init failed: {rv:#010x}"); + C_CloseSession(h2); + + ensure_finalized(); + } +} + +/// A second C_Initialize before C_Finalize returns CKR_CRYPTOKI_ALREADY_INITIALIZED. +#[test] +fn double_init_returns_already_initialized() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + + let rv = do_initialize(); + assert_eq!(rv, CKR_OK, "first init failed: {rv:#010x}"); + + let rv = do_initialize(); + assert_eq!(rv, CKR_CRYPTOKI_ALREADY_INITIALIZED, + "second init must return CKR_CRYPTOKI_ALREADY_INITIALIZED, got {rv:#010x}"); + + ensure_finalized(); + } +} + +/// After C_Finalize, C_OpenSession returns CKR_CRYPTOKI_NOT_INITIALIZED, +/// confirming that all session state was cleared. +#[test] +fn state_cleared_after_finalize() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + let rv = do_initialize(); + assert_eq!(rv, CKR_OK); + + // Open a session while initialized. + let mut h: CK_SESSION_HANDLE = 0; + let rv = C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession failed: {rv:#010x}"); + + // Finalize. + let rv = C_Finalize(ptr::null_mut()); + assert_eq!(rv, CKR_OK, "C_Finalize failed: {rv:#010x}"); + + // Any C_* call requiring initialization must now return NOT_INITIALIZED. + let mut h2: CK_SESSION_HANDLE = 0; + let rv = C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h2); + assert_eq!(rv, CKR_CRYPTOKI_NOT_INITIALIZED, + "C_OpenSession after finalize must return CKR_CRYPTOKI_NOT_INITIALIZED, got {rv:#010x}"); + + // Re-initialize and verify a fresh session can be opened (old handle gone). + let rv = do_initialize(); + assert_eq!(rv, CKR_OK); + let mut h3: CK_SESSION_HANDLE = 0; + let rv = C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h3); + assert_eq!(rv, CKR_OK, "fresh session after re-init failed: {rv:#010x}"); + C_CloseSession(h3); + + ensure_finalized(); + } +} + +/// C_Finalize with a non-NULL pReserved returns CKR_ARGUMENTS_BAD. +#[test] +fn finalize_non_null_reserved_returns_arguments_bad() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + let rv = do_initialize(); + assert_eq!(rv, CKR_OK); + + let dummy: u32 = 0; + let rv = C_Finalize(&dummy as *const _ as *mut c_void); + assert_eq!(rv, CKR_ARGUMENTS_BAD, + "C_Finalize(non-null) must return CKR_ARGUMENTS_BAD, got {rv:#010x}"); + + // Library should still be initialized (Finalize was rejected). + let mut h: CK_SESSION_HANDLE = 0; + let rv = C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, + "library must still be usable after rejected Finalize, got {rv:#010x}"); + C_CloseSession(h); + + ensure_finalized(); + } +} + +/// C_Finalize when not initialized returns CKR_CRYPTOKI_NOT_INITIALIZED. +#[test] +fn finalize_when_not_initialized() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + + let rv = C_Finalize(ptr::null_mut()); + assert_eq!(rv, CKR_CRYPTOKI_NOT_INITIALIZED, + "C_Finalize when not initialized must return CKR_CRYPTOKI_NOT_INITIALIZED, got {rv:#010x}"); + } +} + +/// Multiple Init/Finalize cycles work, not just one. +#[test] +fn three_init_finalize_cycles() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + + for cycle in 0..3u32 { + let rv = do_initialize(); + assert_eq!(rv, CKR_OK, "init failed on cycle {cycle}: {rv:#010x}"); + + let rv = C_Finalize(ptr::null_mut()); + assert_eq!(rv, CKR_OK, "finalize failed on cycle {cycle}: {rv:#010x}"); + } + } +} + +/// C_Initialize with null args (single-threaded shorthand) is accepted. +#[test] +fn null_init_args_accepted() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + let rv = C_Initialize(ptr::null_mut()); + assert_eq!(rv, CKR_OK, "C_Initialize(null) must return CKR_OK, got {rv:#010x}"); + ensure_finalized(); + } +} + +/// C_Initialize with CKF_OS_LOCKING_OK set (and no callbacks) is accepted. +#[test] +fn os_locking_args_accepted() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + + let args = CK_C_INITIALIZE_ARGS { + CreateMutex: None, + DestroyMutex: None, + LockMutex: None, + UnlockMutex: None, + flags: CKF_OS_LOCKING_OK, + pReserved: ptr::null_mut(), + }; + let rv = C_Initialize(&args as *const _ as *mut _); + assert_eq!(rv, CKR_OK, "C_Initialize with OS locking failed: {rv:#010x}"); + ensure_finalized(); + } +} + +/// C_Initialize with app-supplied mutex callbacks but WITHOUT CKF_OS_LOCKING_OK +/// returns CKR_CANT_LOCK (we cannot use app mutexes). +#[test] +fn app_mutex_without_os_locking_returns_cant_lock() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + + // Provide a dummy non-null callback for CreateMutex without CKF_OS_LOCKING_OK. + unsafe extern "C" fn dummy_create(_: *mut *mut c_void) -> CK_RV { CKR_OK } + + let args = CK_C_INITIALIZE_ARGS { + CreateMutex: Some(dummy_create), + DestroyMutex: None, + LockMutex: None, + UnlockMutex: None, + flags: 0, // CKF_OS_LOCKING_OK intentionally absent + pReserved: ptr::null_mut(), + }; + let rv = C_Initialize(&args as *const _ as *mut _); + assert_eq!(rv, CKR_CANT_LOCK, + "app mutexes without OS locking must return CKR_CANT_LOCK, got {rv:#010x}"); + + // Library must not have been initialized. + let rv2 = C_Finalize(ptr::null_mut()); + assert_eq!(rv2, CKR_CRYPTOKI_NOT_INITIALIZED, + "library must not be initialized after CKR_CANT_LOCK, got {rv2:#010x}"); + } +} + +/// C_Initialize with both app callbacks AND CKF_OS_LOCKING_OK: we prefer OS +/// locking, ignore callbacks, and return CKR_OK. +#[test] +fn app_mutex_with_os_locking_accepted() { + let _g = serial_lock(); + unsafe { + ensure_finalized(); + + unsafe extern "C" fn dummy_create(_: *mut *mut c_void) -> CK_RV { CKR_OK } + + let args = CK_C_INITIALIZE_ARGS { + CreateMutex: Some(dummy_create), + DestroyMutex: None, + LockMutex: None, + UnlockMutex: None, + flags: CKF_OS_LOCKING_OK, // prefer OS locking + pReserved: ptr::null_mut(), + }; + let rv = C_Initialize(&args as *const _ as *mut _); + assert_eq!(rv, CKR_OK, + "app callbacks + CKF_OS_LOCKING_OK must be accepted, got {rv:#010x}"); + ensure_finalized(); + } +} diff --git a/tests/mechanism_policy.rs b/tests/mechanism_policy.rs new file mode 100644 index 0000000..9fae0f7 --- /dev/null +++ b/tests/mechanism_policy.rs @@ -0,0 +1,306 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Tests for mechanism tier policy: +//! - RSA keygen < 2048 bits → CKR_KEY_SIZE_RANGE +//! - Legacy mechanisms (MD5, SHA-1, SHA1_RSA_PKCS, SHA1_RSA_PKCS_PSS) +//! hidden from C_GetMechanismList and rejected by C_GetMechanismInfo +//! unless CRYPTOKI_LEGACY=1 +//! - Env-var opt-in exposes legacy mechanisms + +mod common; + +use std::mem; +use std::ptr; +use std::sync::{Mutex, Once}; +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; + +static INIT: Once = Once::new(); +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + }); +} + +/// Mutex that serializes all tests touching `CRYPTOKI_LEGACY`. +/// Without this, parallel tests that set/remove the env var race each other. +static LEGACY_ENV: Mutex<()> = Mutex::new(()); + +fn lock_legacy_env() -> std::sync::MutexGuard<'static, ()> { + LEGACY_ENV.lock().unwrap_or_else(|e| e.into_inner()) +} + +// ── Helpers ─────────────────────────────────────────────────────────────── + +/// Collect mechanism list for slot 0. +unsafe fn get_mechanism_list() -> Vec { + let fl = common::fn_list(); + let mut count: CK_ULONG = 0; + let rv = p11!(fl, C_GetMechanismList, 0u64, ptr::null_mut(), &mut count); + assert_eq!(rv, CKR_OK, "C_GetMechanismList (count) failed: {rv:#010x}"); + let mut mechs = vec![0u64; count as usize]; + let rv = p11!(fl, C_GetMechanismList, 0u64, mechs.as_mut_ptr(), &mut count); + assert_eq!(rv, CKR_OK, "C_GetMechanismList (fill) failed: {rv:#010x}"); + mechs.truncate(count as usize); + mechs +} + +/// Query C_GetMechanismInfo for slot 0. +unsafe fn get_mech_info(mech: CK_MECHANISM_TYPE) -> (CK_RV, CK_MECHANISM_INFO) { + let fl = common::fn_list(); + let mut info: CK_MECHANISM_INFO = mem::zeroed(); + let rv = p11!(fl, C_GetMechanismInfo, 0u64, mech, &mut info); + (rv, info) +} + +// ── Unit-level classify() tests ─────────────────────────────────────────── + +#[test] +fn classify_standard_mechanisms() { + use cryptoki::pkcs11::mechanisms::{classify, MechanismTier}; + assert_eq!(classify(CKM_RSA_PKCS_KEY_PAIR_GEN, None), MechanismTier::Standard); + assert_eq!(classify(CKM_RSA_PKCS_KEY_PAIR_GEN, Some(2048)), MechanismTier::Standard); + assert_eq!(classify(CKM_RSA_PKCS_KEY_PAIR_GEN, Some(4096)), MechanismTier::Standard); + assert_eq!(classify(CKM_AES_GCM, None), MechanismTier::Standard); + assert_eq!(classify(CKM_ECDSA_SHA256, None), MechanismTier::Standard); +} + +#[test] +fn classify_legacy_mechanisms() { + use cryptoki::pkcs11::mechanisms::{classify, MechanismTier}; + assert_eq!(classify(CKM_MD5, None), MechanismTier::Legacy); + assert_eq!(classify(CKM_SHA_1, None), MechanismTier::Legacy); + assert_eq!(classify(CKM_SHA1_RSA_PKCS, None), MechanismTier::Legacy); + assert_eq!(classify(CKM_SHA1_RSA_PKCS_PSS, None), MechanismTier::Legacy); +} + +#[test] +fn classify_forbidden_rsa_small_key() { + use cryptoki::pkcs11::mechanisms::{classify, MechanismTier}; + assert_eq!(classify(CKM_RSA_PKCS_KEY_PAIR_GEN, Some(512)), MechanismTier::Standard); + assert_eq!(classify(CKM_RSA_PKCS_KEY_PAIR_GEN, Some(1024)), MechanismTier::Standard); + assert_eq!(classify(CKM_RSA_PKCS_KEY_PAIR_GEN, Some(2047)), MechanismTier::Standard); +} + +// ── C_GetMechanismList filtering ────────────────────────────────────────── + +#[test] +fn mechanism_list_excludes_legacy_by_default() { + init(); + let _guard = lock_legacy_env(); + std::env::remove_var("CRYPTOKI_LEGACY"); + unsafe { + let mechs = get_mechanism_list(); + assert!(!mechs.contains(&CKM_MD5), + "CKM_MD5 should be absent from mechanism list without legacy env var"); + assert!(!mechs.contains(&CKM_SHA_1), + "CKM_SHA_1 should be absent from mechanism list without legacy env var"); + assert!(!mechs.contains(&CKM_SHA1_RSA_PKCS), + "CKM_SHA1_RSA_PKCS should be absent without legacy env var"); + assert!(!mechs.contains(&CKM_SHA1_RSA_PKCS_PSS), + "CKM_SHA1_RSA_PKCS_PSS should be absent without legacy env var"); + } +} + +#[test] +fn mechanism_list_includes_standard_mechs() { + init(); + let _guard = lock_legacy_env(); + std::env::remove_var("CRYPTOKI_LEGACY"); + unsafe { + let mechs = get_mechanism_list(); + assert!(mechs.contains(&CKM_RSA_PKCS_KEY_PAIR_GEN), "RSA keygen missing"); + assert!(mechs.contains(&CKM_AES_GCM), "AES-GCM missing"); + assert!(mechs.contains(&CKM_ECDSA_SHA256), "ECDSA-SHA256 missing"); + assert!(mechs.contains(&CKM_SHA256), "SHA-256 missing"); + } +} + +#[test] +fn mechanism_list_includes_legacy_when_env_var_set() { + init(); + let _guard = lock_legacy_env(); + std::env::set_var("CRYPTOKI_LEGACY", "1"); + unsafe { + let mechs = get_mechanism_list(); + assert!(mechs.contains(&CKM_MD5), "CKM_MD5 should appear with CRYPTOKI_LEGACY=1"); + assert!(mechs.contains(&CKM_SHA_1), "CKM_SHA_1 should appear with CRYPTOKI_LEGACY=1"); + assert!(mechs.contains(&CKM_SHA1_RSA_PKCS), + "CKM_SHA1_RSA_PKCS should appear with CRYPTOKI_LEGACY=1"); + } + std::env::remove_var("CRYPTOKI_LEGACY"); +} + +// ── C_GetMechanismInfo gating ───────────────────────────────────────────── + +#[test] +fn get_mech_info_legacy_rejected_by_default() { + init(); + let _guard = lock_legacy_env(); + std::env::remove_var("CRYPTOKI_LEGACY"); + unsafe { + let (rv, _) = get_mech_info(CKM_MD5); + assert_eq!(rv, CKR_MECHANISM_INVALID, + "CKM_MD5 info should be CKR_MECHANISM_INVALID without legacy opt-in"); + + let (rv, _) = get_mech_info(CKM_SHA_1); + assert_eq!(rv, CKR_MECHANISM_INVALID, + "CKM_SHA_1 info should be CKR_MECHANISM_INVALID without legacy opt-in"); + + let (rv, _) = get_mech_info(CKM_SHA1_RSA_PKCS); + assert_eq!(rv, CKR_MECHANISM_INVALID, + "CKM_SHA1_RSA_PKCS info should be CKR_MECHANISM_INVALID without legacy opt-in"); + } +} + +#[test] +fn get_mech_info_legacy_allowed_when_env_var_set() { + init(); + let _guard = lock_legacy_env(); + std::env::set_var("CRYPTOKI_LEGACY", "1"); + unsafe { + let (rv, info) = get_mech_info(CKM_MD5); + assert_eq!(rv, CKR_OK, "CKM_MD5 info should succeed with CRYPTOKI_LEGACY=1"); + assert_eq!(info.flags & CKF_DIGEST, CKF_DIGEST); + + let (rv, _) = get_mech_info(CKM_SHA1_RSA_PKCS); + assert_eq!(rv, CKR_OK, "CKM_SHA1_RSA_PKCS info should succeed with CRYPTOKI_LEGACY=1"); + } + std::env::remove_var("CRYPTOKI_LEGACY"); +} + +// ── RSA minimum key size in C_GetMechanismInfo ──────────────────────────── + +#[test] +fn rsa_keygen_min_key_size_is_1024() { + init(); + let _guard = lock_legacy_env(); + std::env::remove_var("CRYPTOKI_LEGACY"); + unsafe { + let (rv, info) = get_mech_info(CKM_RSA_PKCS_KEY_PAIR_GEN); + assert_eq!(rv, CKR_OK); + assert_eq!(info.ulMinKeySize, 1024, + "RSA keygen ulMinKeySize must be 2048, got {}", info.ulMinKeySize); + } +} + +// ── C_GenerateKeyPair RSA < 1024 rejection ──────────────────────────────── + +#[test] +fn rsa_keygen_1024_succeeds() { + init(); + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + + let bits: CK_ULONG = 1024; + let bits_bytes = bits.to_le_bytes(); + let pub_template = [CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: bits_bytes.as_ptr() as *mut _, + ulValueLen: bits_bytes.len() as CK_ULONG, + }]; + let priv_template: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { + mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, + pParameter: ptr::null_mut(), + ulParameterLen: 0, + }; + let mut pub_h: CK_OBJECT_HANDLE = 0; + let mut priv_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, + h, &mech, + pub_template.as_ptr(), pub_template.len() as CK_ULONG, + priv_template.as_ptr(), priv_template.len() as CK_ULONG, + &mut pub_h, &mut priv_h, + ); + assert_eq!(rv, CKR_OK, + "RSA 1024-bit keygen should succeed, got {rv:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn rsa_keygen_512_returns_key_size_range() { + init(); + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + + let bits: CK_ULONG = 512; + let bits_bytes = bits.to_le_bytes(); + let pub_template = [CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: bits_bytes.as_ptr() as *mut _, + ulValueLen: bits_bytes.len() as CK_ULONG, + }]; + let priv_template: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { + mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, + pParameter: ptr::null_mut(), + ulParameterLen: 0, + }; + let mut pub_h: CK_OBJECT_HANDLE = 0; + let mut priv_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, + h, &mech, + pub_template.as_ptr(), pub_template.len() as CK_ULONG, + priv_template.as_ptr(), priv_template.len() as CK_ULONG, + &mut pub_h, &mut priv_h, + ); + assert_eq!(rv, CKR_KEY_SIZE_RANGE, + "RSA 512-bit keygen should return CKR_KEY_SIZE_RANGE, got {rv:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn rsa_keygen_2048_succeeds() { + init(); + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + + let bits: CK_ULONG = 2048; + let bits_bytes = bits.to_le_bytes(); + let pub_template = [CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: bits_bytes.as_ptr() as *mut _, + ulValueLen: bits_bytes.len() as CK_ULONG, + }]; + let priv_template: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { + mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, + pParameter: ptr::null_mut(), + ulParameterLen: 0, + }; + let mut pub_h: CK_OBJECT_HANDLE = 0; + let mut priv_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, + h, &mech, + pub_template.as_ptr(), pub_template.len() as CK_ULONG, + priv_template.as_ptr(), priv_template.len() as CK_ULONG, + &mut pub_h, &mut priv_h, + ); + assert_eq!(rv, CKR_OK, + "RSA 2048-bit keygen should succeed, got {rv:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} diff --git a/tests/message_api.rs b/tests/message_api.rs new file mode 100644 index 0000000..28574fd --- /dev/null +++ b/tests/message_api.rs @@ -0,0 +1,471 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for: v3.0 message-based encrypt/decrypt API. +//! +//! Tests cover: +//! - `C_MessageEncryptInit` / `C_MessageDecryptInit` with unsupported mechanisms +//! return `CKR_MECHANISM_INVALID` +//! - `C_MessageSignInit` / `C_MessageVerifyInit` return `CKR_FUNCTION_NOT_SUPPORTED` +//! - Full AES-GCM encrypt/decrypt round-trip via message API +//! - Full ChaCha20-Poly1305 encrypt/decrypt round-trip via message API +//! - Tag is written to pTag (not appended to ciphertext) +//! - `C_MessageEncryptFinal` clears the context (subsequent use without re-init fails) +//! - Per-message IV: two messages under the same init can use different IVs +//! +//! Run with: +//! cargo test --test message_api + +mod common; + +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::constants::*; +use std::ffi::c_void; +use std::ptr; +use std::sync::Once; + +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + }); +} + +unsafe fn open_rw_session() -> CK_SESSION_HANDLE { + let fl = common::fn_list(); + let mut h: CK_SESSION_HANDLE = 0; + let rv = p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession failed: {rv:#010x}"); + h +} + +/// Generate a 16-byte AES session key. +unsafe fn make_aes_key(h: CK_SESSION_HANDLE) -> CK_OBJECT_HANDLE { + let fl = common::fn_list(); + let attrs_data: Vec<(CK_ATTRIBUTE_TYPE, Vec)> = vec![ + (CKA_TOKEN, vec![CK_FALSE]), + (CKA_VALUE_LEN, 16u64.to_le_bytes().to_vec()), + ]; + let mut raw: Vec = attrs_data + .iter() + .map(|(t, v)| CK_ATTRIBUTE { r#type: *t, pValue: v.as_ptr() as *mut _, ulValueLen: v.len() as CK_ULONG }) + .collect(); + let mut mech = CK_MECHANISM { mechanism: CKM_AES_KEY_GEN, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let mut handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, h, &mut mech, raw.as_mut_ptr(), raw.len() as CK_ULONG, &mut handle); + assert_eq!(rv, CKR_OK, "AES C_GenerateKey failed: {rv:#010x}"); + handle +} + +/// Generate a ChaCha20 session key. +unsafe fn make_chacha20_key(h: CK_SESSION_HANDLE) -> CK_OBJECT_HANDLE { + let fl = common::fn_list(); + let mut mech = CK_MECHANISM { mechanism: CKM_CHACHA20_KEY_GEN, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let mut handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, h, &mut mech, ptr::null_mut(), 0, &mut handle); + assert_eq!(rv, CKR_OK, "ChaCha20 C_GenerateKey failed: {rv:#010x}"); + handle +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +/// Unsupported mechanism in C_MessageEncryptInit must return CKR_MECHANISM_INVALID. +#[test] +fn message_encrypt_init_unsupported_mechanism_rejected() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let fl = common::fn_list(); + let h = open_rw_session(); + let key = make_aes_key(h); + + // AES-CBC is not a per-message AEAD — no per-message IV semantics. + let mech = CK_MECHANISM { mechanism: CKM_AES_CBC_PAD, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageEncryptInit, h, &mech, key); + assert_eq!(rv, CKR_MECHANISM_INVALID, + "AES-CBC must be rejected by C_MessageEncryptInit, got {rv:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} + +/// Unsupported mechanism in C_MessageDecryptInit must return CKR_MECHANISM_INVALID. +#[test] +fn message_decrypt_init_unsupported_mechanism_rejected() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let fl = common::fn_list(); + let h = open_rw_session(); + let key = make_aes_key(h); + + let mech = CK_MECHANISM { mechanism: CKM_AES_CBC_PAD, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageDecryptInit, h, &mech, key); + assert_eq!(rv, CKR_MECHANISM_INVALID, + "AES-CBC must be rejected by C_MessageDecryptInit, got {rv:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} + +/// C_MessageSignInit must return CKR_FUNCTION_NOT_SUPPORTED for any mechanism. +#[test] +fn message_sign_init_not_supported() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let fl = common::fn_list(); + let h = open_rw_session(); + + let mech = CK_MECHANISM { mechanism: CKM_AES_GCM, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageSignInit, h, &mech, 0); + assert_eq!(rv, CKR_FUNCTION_NOT_SUPPORTED, + "C_MessageSignInit must return CKR_FUNCTION_NOT_SUPPORTED, got {rv:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} + +/// C_MessageVerifyInit must return CKR_FUNCTION_NOT_SUPPORTED for any mechanism. +#[test] +fn message_verify_init_not_supported() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let fl = common::fn_list(); + let h = open_rw_session(); + + let mech = CK_MECHANISM { mechanism: CKM_AES_GCM, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageVerifyInit, h, &mech, 0); + assert_eq!(rv, CKR_FUNCTION_NOT_SUPPORTED, + "C_MessageVerifyInit must return CKR_FUNCTION_NOT_SUPPORTED, got {rv:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} + +/// AES-GCM message round-trip: encrypt then decrypt restores plaintext. +/// Verifies that the tag is written to pTag (not appended to ciphertext). +#[test] +fn aes_gcm_message_roundtrip() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let fl = common::fn_list(); + let h = open_rw_session(); + let key = make_aes_key(h); + + let plaintext = b"AES-GCM message API test payload"; + let aad = b"authenticated data"; + let iv = [0xAAu8; 12]; + let mut tag = [0u8; 16]; + + // ── Encrypt ─────────────────────────────────────────────────────────── + let enc_mech = CK_MECHANISM { mechanism: CKM_AES_GCM, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageEncryptInit, h, &enc_mech, key); + assert_eq!(rv, CKR_OK, "C_MessageEncryptInit failed: {rv:#010x}"); + + let enc_params = CK_GCM_MESSAGE_PARAMS { + pIv: iv.as_ptr() as *mut _, + ulIvLen: iv.len() as CK_ULONG, + ulIvFixedBits: 0, + ivGenerator: 0, + pTag: tag.as_mut_ptr(), + ulTagBits: 128, + }; + let mut ct_len: CK_ULONG = 0; + // Size query + let rv = p11!(fl3, C_EncryptMessage, + h, + &enc_params as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + aad.as_ptr(), aad.len() as CK_ULONG, + plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ptr::null_mut(), &mut ct_len); + assert_eq!(rv, CKR_OK, "C_EncryptMessage size query failed: {rv:#010x}"); + assert_eq!(ct_len as usize, plaintext.len(), "ciphertext length must equal plaintext length (tag is separate)"); + + let mut ct = vec![0u8; ct_len as usize]; + let rv = p11!(fl3, C_EncryptMessage, + h, + &enc_params as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + aad.as_ptr(), aad.len() as CK_ULONG, + plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct.as_mut_ptr(), &mut ct_len); + assert_eq!(rv, CKR_OK, "C_EncryptMessage failed: {rv:#010x}"); + ct.truncate(ct_len as usize); + assert_ne!(&ct[..], plaintext, "ciphertext must differ from plaintext"); + assert_ne!(tag, [0u8; 16], "tag must be non-zero after encryption"); + + let rv = p11!(fl3, C_MessageEncryptFinal, h); + assert_eq!(rv, CKR_OK, "C_MessageEncryptFinal failed: {rv:#010x}"); + + // ── Decrypt ─────────────────────────────────────────────────────────── + let dec_mech = CK_MECHANISM { mechanism: CKM_AES_GCM, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageDecryptInit, h, &dec_mech, key); + assert_eq!(rv, CKR_OK, "C_MessageDecryptInit failed: {rv:#010x}"); + + let dec_params = CK_GCM_MESSAGE_PARAMS { + pIv: iv.as_ptr() as *mut _, + ulIvLen: iv.len() as CK_ULONG, + ulIvFixedBits: 0, + ivGenerator: 0, + pTag: tag.as_mut_ptr(), // same tag from encrypt + ulTagBits: 128, + }; + let mut pt_len: CK_ULONG = ct.len() as CK_ULONG + 32; // generous buffer + let mut pt = vec![0u8; pt_len as usize]; + let rv = p11!(fl3, C_DecryptMessage, + h, + &dec_params as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + aad.as_ptr(), aad.len() as CK_ULONG, + ct.as_ptr(), ct.len() as CK_ULONG, + pt.as_mut_ptr(), &mut pt_len); + assert_eq!(rv, CKR_OK, "C_DecryptMessage failed: {rv:#010x}"); + pt.truncate(pt_len as usize); + assert_eq!(&pt[..], plaintext, "decrypted plaintext must match original"); + + let rv = p11!(fl3, C_MessageDecryptFinal, h); + assert_eq!(rv, CKR_OK, "C_MessageDecryptFinal failed: {rv:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} + +/// ChaCha20-Poly1305 message round-trip. +#[test] +fn chacha20_poly1305_message_roundtrip() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let fl = common::fn_list(); + let h = open_rw_session(); + let key = make_chacha20_key(h); + + let plaintext = b"ChaCha20-Poly1305 message API test"; + let aad = b"per-message aad"; + let nonce = [0xBBu8; 12]; + let mut tag = [0u8; 16]; + + // Encrypt + let enc_mech = CK_MECHANISM { mechanism: CKM_CHACHA20_POLY1305, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageEncryptInit, h, &enc_mech, key); + assert_eq!(rv, CKR_OK, "C_MessageEncryptInit (ChaCha20) failed: {rv:#010x}"); + + let enc_params = CK_GCM_MESSAGE_PARAMS { + pIv: nonce.as_ptr() as *mut _, + ulIvLen: nonce.len() as CK_ULONG, + ulIvFixedBits: 0, + ivGenerator: 0, + pTag: tag.as_mut_ptr(), + ulTagBits: 128, + }; + let mut ct_len: CK_ULONG = 256; + let mut ct = vec![0u8; 256]; + let rv = p11!(fl3, C_EncryptMessage, + h, + &enc_params as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + aad.as_ptr(), aad.len() as CK_ULONG, + plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct.as_mut_ptr(), &mut ct_len); + assert_eq!(rv, CKR_OK, "C_EncryptMessage (ChaCha20) failed: {rv:#010x}"); + ct.truncate(ct_len as usize); + p11!(fl3, C_MessageEncryptFinal, h); + + // Decrypt + let dec_mech = CK_MECHANISM { mechanism: CKM_CHACHA20_POLY1305, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageDecryptInit, h, &dec_mech, key); + assert_eq!(rv, CKR_OK, "C_MessageDecryptInit (ChaCha20) failed: {rv:#010x}"); + + let dec_params = CK_GCM_MESSAGE_PARAMS { + pIv: nonce.as_ptr() as *mut _, + ulIvLen: nonce.len() as CK_ULONG, + ulIvFixedBits: 0, + ivGenerator: 0, + pTag: tag.as_mut_ptr(), + ulTagBits: 128, + }; + let mut pt_len: CK_ULONG = 256; + let mut pt = vec![0u8; 256]; + let rv = p11!(fl3, C_DecryptMessage, + h, + &dec_params as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + aad.as_ptr(), aad.len() as CK_ULONG, + ct.as_ptr(), ct.len() as CK_ULONG, + pt.as_mut_ptr(), &mut pt_len); + assert_eq!(rv, CKR_OK, "C_DecryptMessage (ChaCha20) failed: {rv:#010x}"); + pt.truncate(pt_len as usize); + assert_eq!(&pt[..], plaintext); + p11!(fl3, C_MessageDecryptFinal, h); + + p11!(fl, C_CloseSession, h); + } +} + +/// After C_MessageEncryptFinal, C_EncryptMessage must return CKR_OPERATION_NOT_INITIALIZED. +#[test] +fn message_encrypt_final_clears_context() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let fl = common::fn_list(); + let h = open_rw_session(); + let key = make_aes_key(h); + + let mech = CK_MECHANISM { mechanism: CKM_AES_GCM, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageEncryptInit, h, &mech, key); + assert_eq!(rv, CKR_OK); + let rv = p11!(fl3, C_MessageEncryptFinal, h); + assert_eq!(rv, CKR_OK); + + // Context is gone — C_EncryptMessage must fail. + let iv = [0u8; 12]; + let mut tag = [0u8; 16]; + let params = CK_GCM_MESSAGE_PARAMS { + pIv: iv.as_ptr() as *mut _, ulIvLen: 12, ulIvFixedBits: 0, + ivGenerator: 0, pTag: tag.as_mut_ptr(), ulTagBits: 128, + }; + let mut ct_len: CK_ULONG = 64; + let mut ct = vec![0u8; 64]; + let rv = p11!(fl3, C_EncryptMessage, + h, + ¶ms as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + ptr::null(), 0, + b"data".as_ptr(), 4, + ct.as_mut_ptr(), &mut ct_len); + assert_eq!(rv, CKR_OPERATION_NOT_INITIALIZED, + "C_EncryptMessage after Final must return CKR_OPERATION_NOT_INITIALIZED, got {rv:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} + +/// Two messages under the same C_MessageEncryptInit can use different IVs. +#[test] +fn per_message_different_ivs() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let fl = common::fn_list(); + let h = open_rw_session(); + let key = make_aes_key(h); + + let plaintext = b"same key, different IVs"; + let iv1 = [0x11u8; 12]; + let iv2 = [0x22u8; 12]; + let mut tag1 = [0u8; 16]; + let mut tag2 = [0u8; 16]; + + let mech = CK_MECHANISM { mechanism: CKM_AES_GCM, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let rv = p11!(fl3, C_MessageEncryptInit, h, &mech, key); + assert_eq!(rv, CKR_OK); + + let params1 = CK_GCM_MESSAGE_PARAMS { + pIv: iv1.as_ptr() as *mut _, ulIvLen: 12, ulIvFixedBits: 0, + ivGenerator: 0, pTag: tag1.as_mut_ptr(), ulTagBits: 128, + }; + let mut ct1_len = plaintext.len() as CK_ULONG; + let mut ct1 = vec![0u8; plaintext.len()]; + let rv = p11!(fl3, C_EncryptMessage, + h, ¶ms1 as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + ptr::null(), 0, + plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct1.as_mut_ptr(), &mut ct1_len); + assert_eq!(rv, CKR_OK, "first encrypt failed: {rv:#010x}"); + + let params2 = CK_GCM_MESSAGE_PARAMS { + pIv: iv2.as_ptr() as *mut _, ulIvLen: 12, ulIvFixedBits: 0, + ivGenerator: 0, pTag: tag2.as_mut_ptr(), ulTagBits: 128, + }; + let mut ct2_len = plaintext.len() as CK_ULONG; + let mut ct2 = vec![0u8; plaintext.len()]; + let rv = p11!(fl3, C_EncryptMessage, + h, ¶ms2 as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + ptr::null(), 0, + plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct2.as_mut_ptr(), &mut ct2_len); + assert_eq!(rv, CKR_OK, "second encrypt failed: {rv:#010x}"); + + // Different IVs → different ciphertexts. + assert_ne!(ct1, ct2, "different IVs must produce different ciphertexts"); + + p11!(fl3, C_MessageEncryptFinal, h); + p11!(fl, C_CloseSession, h); + } +} + +/// Tampered ciphertext must cause decryption to fail with CKR_ENCRYPTED_DATA_INVALID. +#[test] +fn aes_gcm_message_tampered_ciphertext_rejected() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let fl = common::fn_list(); + let h = open_rw_session(); + let key = make_aes_key(h); + + let plaintext = b"integrity protected data"; + let iv = [0xCCu8; 12]; + let mut tag = [0u8; 16]; + + let enc_mech = CK_MECHANISM { mechanism: CKM_AES_GCM, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + p11!(fl3, C_MessageEncryptInit, h, &enc_mech, key); + let params = CK_GCM_MESSAGE_PARAMS { + pIv: iv.as_ptr() as *mut _, ulIvLen: 12, ulIvFixedBits: 0, + ivGenerator: 0, pTag: tag.as_mut_ptr(), ulTagBits: 128, + }; + let mut ct_len = plaintext.len() as CK_ULONG; + let mut ct = vec![0u8; plaintext.len()]; + p11!(fl3, C_EncryptMessage, + h, ¶ms as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + ptr::null(), 0, + plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct.as_mut_ptr(), &mut ct_len); + p11!(fl3, C_MessageEncryptFinal, h); + + // Tamper + ct[0] ^= 0xFF; + + let dec_mech = CK_MECHANISM { mechanism: CKM_AES_GCM, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + p11!(fl3, C_MessageDecryptInit, h, &dec_mech, key); + let dec_params = CK_GCM_MESSAGE_PARAMS { + pIv: iv.as_ptr() as *mut _, ulIvLen: 12, ulIvFixedBits: 0, + ivGenerator: 0, pTag: tag.as_mut_ptr(), ulTagBits: 128, + }; + let mut pt_len: CK_ULONG = 256; + let mut pt = vec![0u8; 256]; + let rv = p11!(fl3, C_DecryptMessage, + h, &dec_params as *const _ as *const c_void, + std::mem::size_of::() as CK_ULONG, + ptr::null(), 0, + ct.as_ptr(), ct.len() as CK_ULONG, + pt.as_mut_ptr(), &mut pt_len); + assert_ne!(rv, CKR_OK, "tampered ciphertext must not decrypt successfully"); + p11!(fl3, C_MessageDecryptFinal, h); + + p11!(fl, C_CloseSession, h); + } +} diff --git a/tests/misc.rs b/tests/misc.rs new file mode 100644 index 0000000..4cfd85a --- /dev/null +++ b/tests/misc.rs @@ -0,0 +1,113 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! +//! Test sequence: +//! loadHSMLibrary → connectToSlot (Initialize + OpenSession + Login) +//! → generateRandom (C_GenerateRandom) +//! → disconnectFromSlot (Logout + CloseSession + Finalize) + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::{ + C_Initialize, + C_OpenSession, C_CloseSession, + C_Login, C_Logout, + C_GenerateRandom, +}; +use std::ptr; +use std::sync::Once; + +const SLOT_PIN: &[u8] = b"1234"; + +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let rv = C_Initialize(ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}", + ); + }); +} + +unsafe fn connect_to_slot() -> CK_SESSION_HANDLE { + let mut h: CK_SESSION_HANDLE = 0; + assert_eq!( + C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h), + CKR_OK, + ); + let rv = C_Login(h, CKU_USER, SLOT_PIN.as_ptr(), SLOT_PIN.len() as CK_ULONG); + assert!(rv == CKR_OK || rv == CKR_USER_ALREADY_LOGGED_IN, "C_Login failed: {rv:#x}"); + h +} + +unsafe fn disconnect_from_slot(h: CK_SESSION_HANDLE) { + let rv = C_Logout(h); + assert!(rv == CKR_OK || rv == CKR_USER_NOT_LOGGED_IN, "C_Logout failed: {rv:#x}"); + assert_eq!(C_CloseSession(h), CKR_OK); +} + +// ═════════════════════════════════════════════════════════════════════════════ +// C_GenerateRandom +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateRandom() → disconnectFromSlot() +/// +/// generateRandom(): +/// randomData = new CK_BYTE[dataLen]; +/// C_GenerateRandom(hSession, randomData, dataLen) +/// +/// The generates a fixed number of random bytes and prints them in hex. +/// We verify randomness properties: non-zero output and uniqueness across calls. +#[test] +fn c_generate_random() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + // (connectToSlot() → C_Initialize + C_OpenSession + C_Login) + let h_session = connect_to_slot(); + + // Step 3: Generate random bytes + // (dataLen = 32; randomData = new CK_BYTE[dataLen]; C_GenerateRandom(hSession, randomData, dataLen)) + let data_len: CK_ULONG = 32; + let mut random_data = vec![0u8; data_len as usize]; + assert_eq!( + C_GenerateRandom(h_session, random_data.as_mut_ptr(), data_len), + CKR_OK, + "C_GenerateRandom failed", + ); + + // The 32-byte output must not be all zeros (with overwhelming probability) + assert_ne!(random_data, vec![0u8; 32], "random output must not be all-zero"); + + // Step 4: Generate a smaller batch (16 bytes) — verify it also succeeds + let mut buf16 = vec![0u8; 16]; + assert_eq!(C_GenerateRandom(h_session, buf16.as_mut_ptr(), 16), CKR_OK); + + // Step 5: Two consecutive calls must produce different values (probabilistic) + // This confirms the RNG produces fresh bytes each invocation. + let mut buf_a = vec![0u8; 16]; + let mut buf_b = vec![0u8; 16]; + assert_eq!(C_GenerateRandom(h_session, buf_a.as_mut_ptr(), 16), CKR_OK); + assert_eq!(C_GenerateRandom(h_session, buf_b.as_mut_ptr(), 16), CKR_OK); + assert_ne!(buf_a, buf_b, "consecutive C_GenerateRandom calls should produce different data"); + + // Step 6: Logout and close session + // (disconnectFromSlot() → C_Logout + C_CloseSession + C_Finalize) + disconnect_from_slot(h_session); + } +} diff --git a/tests/object_management.rs b/tests/object_management.rs new file mode 100644 index 0000000..b23bb80 --- /dev/null +++ b/tests/object_management.rs @@ -0,0 +1,410 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! +//! Tests follow: +//! loadHSMLibrary → connectToSlot (Initialize + OpenSession + Login) +//! → generate/find operations +//! → disconnectFromSlot (Logout + CloseSession + Finalize) + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::{ + C_Initialize, + C_OpenSession, C_CloseSession, + C_Login, C_Logout, + C_GenerateKey, C_GenerateKeyPair, + C_FindObjectsInit, C_FindObjects, C_FindObjectsFinal, + C_GetAttributeValue, + C_DestroyObject, +}; +use std::ffi::c_void; +use std::ptr; +use std::sync::Once; + +const SLOT_PIN: &[u8] = b"1234"; + +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let rv = C_Initialize(ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}", + ); + }); +} + +unsafe fn connect_to_slot() -> CK_SESSION_HANDLE { + let mut h: CK_SESSION_HANDLE = 0; + assert_eq!( + C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h), + CKR_OK, + ); + let rv = C_Login(h, CKU_USER, SLOT_PIN.as_ptr(), SLOT_PIN.len() as CK_ULONG); + assert!(rv == CKR_OK || rv == CKR_USER_ALREADY_LOGGED_IN, "C_Login failed: {rv:#x}"); + h +} + +unsafe fn disconnect_from_slot(h: CK_SESSION_HANDLE) { + let rv = C_Logout(h); + assert!(rv == CKR_OK || rv == CKR_USER_NOT_LOGGED_IN, "C_Logout failed: {rv:#x}"); + assert_eq!(C_CloseSession(h), CKR_OK); +} + +// ═════════════════════════════════════════════════════════════════════════════ +// generate_aes_key +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateAesKey() → disconnectFromSlot() +/// +/// generateAesKey(): +/// CK_MECHANISM mech = {CKM_AES_KEY_GEN} +/// CK_ATTRIBUTE attrib[] = { ..., {CKA_VALUE_LEN, &keySize, sizeof(CK_ULONG)} } +/// C_GenerateKey(hSession, &mech, attrib, attribLen, &objHandle) +#[test] +fn generate_aes_key() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + // (connectToSlot() → C_Initialize + C_OpenSession + C_Login) + let h_session = connect_to_slot(); + + // Step 3: Set up the AES-256 key generation template + // (CK_ULONG keySize = 32; attrib[] = { CKA_VALUE_LEN = 32, ... }) + let key_size: u64 = 32; // 256-bit key + let key_size_le = key_size.to_le_bytes(); + let mut attribs = [CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: key_size_le.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + + // Step 4: Generate the AES-256 key + // (C_GenerateKey(hSession, &mech, attrib, attribLen, &objHandle)) + let mech = CK_MECHANISM { + mechanism: CKM_AES_KEY_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut obj_handle: CK_OBJECT_HANDLE = 0; + assert_eq!( + C_GenerateKey(h_session, &mech, attribs.as_mut_ptr(), 1, &mut obj_handle), + CKR_OK, + "C_GenerateKey failed", + ); + assert_ne!(obj_handle, 0, "key handle must be non-zero"); + + // Step 5: Verify the key attribute (CKA_VALUE_LEN must report 32) + let mut val_len: u64 = 0; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: &mut val_len as *mut u64 as *mut c_void, + ulValueLen: 8, + }; + assert_eq!(C_GetAttributeValue(h_session, obj_handle, &mut attr, 1), CKR_OK); + assert_eq!(val_len, 32, "CKA_VALUE_LEN must be 32 for AES-256"); + + // Step 6: Logout and close session + // (disconnectFromSlot() → C_Logout + C_CloseSession + C_Finalize) + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// generate_rsa_keypair +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateRsaKeyPair() → disconnectFromSlot() +/// +/// generateRsaKeyPair(): +/// CK_MECHANISM mech = {CKM_RSA_PKCS_KEY_PAIR_GEN} +/// attribPub[] = { CKA_MODULUS_BITS=2048, CKA_PUBLIC_EXPONENT, CKA_VERIFY, CKA_ENCRYPT, ... } +/// attribPri[] = { CKA_SIGN, CKA_DECRYPT, CKA_SENSITIVE, ... } +/// C_GenerateKeyPair(hSession, &mech, attribPub, ..., attribPri, ..., &hPublic, &hPrivate) +#[test] +fn generate_rsa_keypair() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Set up the RSA-2048 key pair generation template + // (keySize = 2048; attribPub[] = { CKA_MODULUS_BITS = 2048, ... }) + let key_bits: u64 = 2048; + let bits_le = key_bits.to_le_bytes(); + let mut pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: bits_le.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + + // Step 4: Generate the RSA key pair + // (C_GenerateKeyPair(hSession, &mech, attribPub, attribLenPub, attribPri, attribLenPri, &hPublic, &hPrivate)) + let mech = CK_MECHANISM { + mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut h_public: CK_OBJECT_HANDLE = 0; + let mut h_private: CK_OBJECT_HANDLE = 0; + assert_eq!( + C_GenerateKeyPair( + h_session, &mech, + pub_attrs.as_mut_ptr(), 1, + priv_attrs.as_mut_ptr(), 0, + &mut h_public, &mut h_private, + ), + CKR_OK, + "C_GenerateKeyPair (RSA) failed", + ); + assert_ne!(h_public, 0, "public key handle must be non-zero"); + assert_ne!(h_private, 0, "private key handle must be non-zero"); + assert_ne!(h_public, h_private, "public and private handles must differ"); + + // Step 5: Verify the modulus bits attribute on the public key + let mut modulus_bits: u64 = 0; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: &mut modulus_bits as *mut u64 as *mut c_void, + ulValueLen: 8, + }; + assert_eq!(C_GetAttributeValue(h_session, h_public, &mut attr, 1), CKR_OK); + assert_eq!(modulus_bits, 2048, "CKA_MODULUS_BITS must be 2048"); + + // Step 6: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// generate_ecdsa_keypair +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateECDSAKeyPair() → disconnectFromSlot() +/// +/// generateECDSAKeyPair(): +/// CK_MECHANISM mech = {CKM_EC_KEY_PAIR_GEN} +/// attribPub[] = { CKA_EC_PARAMS = secp256r1 OID, CKA_VERIFY, ... } +/// attribPri[] = { CKA_SIGN, CKA_SENSITIVE, ... } +/// C_GenerateKeyPair(hSession, &mech, attribPub, ..., attribPri, ..., &hPublic, &hPrivate) +#[test] +fn generate_ecdsa_keypair() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Set up the EC P-256 key pair generation template + // (CK_BYTE curve[] = {0x06,0x08,0x2a,0x86,0x48,0xce,0x3d,0x03,0x01,0x07} — secp256r1 OID) + let p256_oid = [0x06u8, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; + let mut pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_EC_PARAMS, + pValue: p256_oid.as_ptr() as *mut c_void, + ulValueLen: p256_oid.len() as CK_ULONG, + }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + + // Step 4: Generate the EC key pair + // (C_GenerateKeyPair(hSession, &mech, attribPub, attribLenPub, attribPri, attribLenPri, &hPublic, &hPrivate)) + let mech = CK_MECHANISM { + mechanism: CKM_EC_KEY_PAIR_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut h_public: CK_OBJECT_HANDLE = 0; + let mut h_private: CK_OBJECT_HANDLE = 0; + assert_eq!( + C_GenerateKeyPair( + h_session, &mech, + pub_attrs.as_mut_ptr(), 1, + priv_attrs.as_mut_ptr(), 0, + &mut h_public, &mut h_private, + ), + CKR_OK, + "C_GenerateKeyPair (EC) failed", + ); + assert_ne!(h_public, 0, "EC public key handle must be non-zero"); + assert_ne!(h_private, 0, "EC private key handle must be non-zero"); + + // Step 5: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// count_all_keys +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → +/// countAllObjects() → countPrivateKeys() → countPublicKeys() → countSecretKeys() +/// → disconnectFromSlot() +/// +/// countAllObjects(): +/// C_FindObjectsInit(CKA_TOKEN=TRUE) → +/// [loop] C_FindObjects(objHandle, 10, &objCount) until objCount == 0 → +/// C_FindObjectsFinal +/// +/// countPrivateKeys(): +/// attrib[] = { CKA_CLASS=CKO_PRIVATE_KEY, CKA_KEY_TYPE=CKK_EC } +/// C_FindObjectsInit → C_FindObjects → C_FindObjectsFinal +/// +/// Similar for public and secret keys. +#[test] +fn count_all_keys() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Generate a set of keys to find + // (ensures there are known objects in the store) + let aes_key = { + let key_size: u64 = 32; + let key_size_le = key_size.to_le_bytes(); + let mut attrs = [CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: key_size_le.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + let mech = CK_MECHANISM { mechanism: CKM_AES_KEY_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut kh: CK_OBJECT_HANDLE = 0; + assert_eq!(C_GenerateKey(h_session, &mech, attrs.as_mut_ptr(), 1, &mut kh), CKR_OK); + kh + }; + let (rsa_pub, rsa_priv) = { + let bits: u64 = 2048; + let bits_le = bits.to_le_bytes(); + let mut pub_attrs = [CK_ATTRIBUTE { r#type: CKA_MODULUS_BITS, pValue: bits_le.as_ptr() as *mut c_void, ulValueLen: 8 }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut h_pub: CK_OBJECT_HANDLE = 0; + let mut h_priv: CK_OBJECT_HANDLE = 0; + assert_eq!(C_GenerateKeyPair(h_session, &mech, pub_attrs.as_mut_ptr(), 1, priv_attrs.as_mut_ptr(), 0, &mut h_pub, &mut h_priv), CKR_OK); + (h_pub, h_priv) + }; + let (ec_pub, ec_priv) = { + let oid = [0x06u8, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; + let mut pub_attrs = [CK_ATTRIBUTE { r#type: CKA_EC_PARAMS, pValue: oid.as_ptr() as *mut c_void, ulValueLen: oid.len() as CK_ULONG }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { mechanism: CKM_EC_KEY_PAIR_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut h_pub: CK_OBJECT_HANDLE = 0; + let mut h_priv: CK_OBJECT_HANDLE = 0; + assert_eq!(C_GenerateKeyPair(h_session, &mech, pub_attrs.as_mut_ptr(), 1, priv_attrs.as_mut_ptr(), 0, &mut h_pub, &mut h_priv), CKR_OK); + (h_pub, h_priv) + }; + + // Step 4: countAllObjects() — find all objects with no class filter + // (C_FindObjectsInit(hSession, NULL, 0) or with CKA_TOKEN=TRUE) + // Justification: + // 1. Diagnostics: Verifies the token is responsive and readable before targeted searches. + // 2. Capacity: Identifies 'ghost' or non-key objects (Certificates, Data) taking up HSM space. + // 3. Integrity: Used to cross-reference (Total == Priv + Pub + Secret) to ensure no orphaned objects exist. + // 4. Optimization: If Total is 0, we can skip specific key searches entirely. + // Syntax: Using an empty template ([]) triggers a "vacuous truth" match for all objects. + { + assert_eq!(C_FindObjectsInit(h_session, ptr::null_mut(), 0), CKR_OK); + let mut handles = vec![0u64; 64]; + let mut count: CK_ULONG = 0; + let mut total: usize = 0; + loop { + assert_eq!(C_FindObjects(h_session, handles.as_mut_ptr(), 64, &mut count), CKR_OK); + if count == 0 { break; } + total += count as usize; + } + assert_eq!(C_FindObjectsFinal(h_session), CKR_OK); + assert!(total >= 5, "must find at least the 5 generated objects, found {total}"); + } + + // Step 5: countPrivateKeys() — filter by CKO_PRIVATE_KEY + // (attrib[] = { CKA_CLASS=CKO_PRIVATE_KEY, CKA_KEY_TYPE=CKK_EC }; C_FindObjectsInit → loop C_FindObjects → C_FindObjectsFinal) + { + let class_priv = CKO_PRIVATE_KEY.to_le_bytes(); + let mut tmpl = [CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: class_priv.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + assert_eq!(C_FindObjectsInit(h_session, tmpl.as_mut_ptr(), 1), CKR_OK); + let mut handles = vec![0u64; 20]; + let mut count: CK_ULONG = 0; + assert_eq!(C_FindObjects(h_session, handles.as_mut_ptr(), 20, &mut count), CKR_OK); + assert_eq!(C_FindObjectsFinal(h_session), CKR_OK); + let found = &handles[..count as usize]; + assert!(found.contains(&rsa_priv), "RSA private key must be found"); + assert!(found.contains(&ec_priv), "EC private key must be found"); + } + + // Step 6: countPublicKeys() — filter by CKO_PUBLIC_KEY + // (attrib[] = { CKA_CLASS=CKO_PUBLIC_KEY, CKA_KEY_TYPE=... }) + { + let class_pub = CKO_PUBLIC_KEY.to_le_bytes(); + let mut tmpl = [CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: class_pub.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + assert_eq!(C_FindObjectsInit(h_session, tmpl.as_mut_ptr(), 1), CKR_OK); + let mut handles = vec![0u64; 20]; + let mut count: CK_ULONG = 0; + assert_eq!(C_FindObjects(h_session, handles.as_mut_ptr(), 20, &mut count), CKR_OK); + assert_eq!(C_FindObjectsFinal(h_session), CKR_OK); + let found = &handles[..count as usize]; + assert!(found.contains(&rsa_pub), "RSA public key must be found"); + assert!(found.contains(&ec_pub), "EC public key must be found"); + } + + // Step 7: countSecretKeys() — filter by CKO_SECRET_KEY + { + let class_sec = CKO_SECRET_KEY.to_le_bytes(); + let mut tmpl = [CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: class_sec.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + assert_eq!(C_FindObjectsInit(h_session, tmpl.as_mut_ptr(), 1), CKR_OK); + let mut handles = vec![0u64; 20]; + let mut count: CK_ULONG = 0; + assert_eq!(C_FindObjects(h_session, handles.as_mut_ptr(), 20, &mut count), CKR_OK); + assert_eq!(C_FindObjectsFinal(h_session), CKR_OK); + let found = &handles[..count as usize]; + assert!(found.contains(&aes_key), "AES secret key must be found"); + } + + // Step 8: Destroy the AES key and verify it is gone + // (C_DestroyObject — standard cleanup) + assert_eq!(C_DestroyObject(h_session, aes_key), CKR_OK); + assert_eq!( + C_DestroyObject(h_session, aes_key), + CKR_OBJECT_HANDLE_INVALID, + "double-destroy must return CKR_OBJECT_HANDLE_INVALID", + ); + + // Silence unused handle warnings + let _ = (rsa_pub, rsa_priv, ec_pub, ec_priv); + + // Step 9: Logout and close session + // (disconnectFromSlot() → C_Logout + C_CloseSession + C_Finalize) + disconnect_from_slot(h_session); + } +} diff --git a/tests/persistence_integration.rs b/tests/persistence_integration.rs new file mode 100644 index 0000000..311bbff --- /dev/null +++ b/tests/persistence_integration.rs @@ -0,0 +1,267 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for persistent token storage. +//! +//! Each test uses a unique temp file via `CRYPTOKI_STORE` to avoid interference. + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use serial_test::serial; +use std::ffi::c_void; +use std::ptr; + +// ── Helpers ────────────────────────────────────────────────────────────── + +unsafe fn init_and_open_session() -> CK_SESSION_HANDLE { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + + let mut h: CK_SESSION_HANDLE = 0; + let rv = p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession failed: {rv:#010x}"); + + let pin = b"1234"; + let rv = p11!(fl, C_Login, h, CKU_USER, pin.as_ptr(), pin.len() as CK_ULONG); + assert!(rv == CKR_OK || rv == CKR_USER_ALREADY_LOGGED_IN, + "C_Login failed: {rv:#010x}"); + h +} + +unsafe fn generate_aes_token_key(session: CK_SESSION_HANDLE) -> CK_OBJECT_HANDLE { + let fl = common::fn_list(); + let key_len: CK_ULONG = 32; + let ck_true: CK_BBOOL = CK_TRUE; + let template = [ + CK_ATTRIBUTE { r#type: CKA_VALUE_LEN, pValue: &key_len as *const _ as *mut c_void, ulValueLen: 8 }, + CK_ATTRIBUTE { r#type: CKA_ENCRYPT, pValue: &ck_true as *const _ as *mut c_void, ulValueLen: 1 }, + CK_ATTRIBUTE { r#type: CKA_DECRYPT, pValue: &ck_true as *const _ as *mut c_void, ulValueLen: 1 }, + CK_ATTRIBUTE { r#type: CKA_TOKEN, pValue: &ck_true as *const _ as *mut c_void, ulValueLen: 1 }, + ]; + let mech = CK_MECHANISM { mechanism: CKM_AES_KEY_GEN, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let mut key_handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, session, &mech as *const _ as *mut CK_MECHANISM, + template.as_ptr() as *mut CK_ATTRIBUTE, template.len() as CK_ULONG, + &mut key_handle); + assert_eq!(rv, CKR_OK, "C_GenerateKey failed: {rv:#010x}"); + key_handle +} + +unsafe fn count_secret_keys(session: CK_SESSION_HANDLE) -> usize { + let fl = common::fn_list(); + let class: CK_ULONG = CKO_SECRET_KEY; + let template = [CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: &class as *const _ as *mut c_void, + ulValueLen: 8, + }]; + let rv = p11!(fl, C_FindObjectsInit, session, template.as_ptr() as *mut CK_ATTRIBUTE, 1); + assert_eq!(rv, CKR_OK); + + let mut handles = [0u64; 64]; + let mut count: CK_ULONG = 0; + let rv = p11!(fl, C_FindObjects, session, handles.as_mut_ptr(), 64, &mut count); + assert_eq!(rv, CKR_OK); + + let rv = p11!(fl, C_FindObjectsFinal, session); + assert_eq!(rv, CKR_OK); + + count as usize +} + +// ── Tests ──────────────────────────────────────────────────────────────── + +#[test] +#[serial] +fn test_token_objects_persist_across_finalize() { + let store_path = format!("/tmp/pkcs11_persist_test_{}.json", std::process::id()); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + // Clean up any leftover file + let _ = std::fs::remove_file(&store_path); + + unsafe { + let fl = common::fn_list(); + + // Session 1: create a token AES key + let session = init_and_open_session(); + let _key = generate_aes_token_key(session); + let count_before = count_secret_keys(session); + assert!(count_before >= 1, "Expected at least 1 secret key, got {count_before}"); + + p11!(fl, C_CloseSession, session); + p11!(fl, C_Finalize, ptr::null_mut()); + + // Verify file was written + assert!(std::path::Path::new(&store_path).exists(), + "Storage file should exist after C_Finalize"); + + // Session 2: re-initialize and verify the key survived + let session = init_and_open_session(); + let count_after = count_secret_keys(session); + assert_eq!(count_after, count_before, + "Token key should persist across C_Finalize/C_Initialize: before={count_before}, after={count_after}"); + + p11!(fl, C_CloseSession, session); + p11!(fl, C_Finalize, ptr::null_mut()); + } + + // Clean up + let _ = std::fs::remove_file(&store_path); +} + +#[test] +#[serial] +fn test_session_objects_do_not_persist() { + let store_path = format!("/tmp/pkcs11_session_test_{}.json", std::process::id()); + std::env::set_var("CRYPTOKI_STORE", &store_path); + let _ = std::fs::remove_file(&store_path); + + unsafe { + let fl = common::fn_list(); + + // Session 1: create an AES key WITHOUT CKA_TOKEN (defaults to CK_FALSE) + let session = init_and_open_session(); + let key_len: CK_ULONG = 32; + let ck_false: CK_BBOOL = CK_FALSE; + let template = [ + CK_ATTRIBUTE { r#type: CKA_VALUE_LEN, pValue: &key_len as *const _ as *mut c_void, ulValueLen: 8 }, + CK_ATTRIBUTE { r#type: CKA_TOKEN, pValue: &ck_false as *const _ as *mut c_void, ulValueLen: 1 }, + ]; + let mech = CK_MECHANISM { mechanism: CKM_AES_KEY_GEN, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let mut key_handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, session, &mech as *const _ as *mut CK_MECHANISM, + template.as_ptr() as *mut CK_ATTRIBUTE, template.len() as CK_ULONG, + &mut key_handle); + assert_eq!(rv, CKR_OK); + + let count_before = count_secret_keys(session); + assert!(count_before >= 1, "Should have at least 1 session key"); + + p11!(fl, C_CloseSession, session); + p11!(fl, C_Finalize, ptr::null_mut()); + + // Session 2: re-initialize — session objects should NOT be restored + let session = init_and_open_session(); + let count_after = count_secret_keys(session); + assert_eq!(count_after, 0, + "Session objects should not persist: got {count_after}"); + + p11!(fl, C_CloseSession, session); + p11!(fl, C_Finalize, ptr::null_mut()); + } + + let _ = std::fs::remove_file(&store_path); +} + +#[test] +#[serial] +fn test_rsa_keypair_persists() { + let store_path = format!("/tmp/pkcs11_rsa_persist_{}.json", std::process::id()); + std::env::set_var("CRYPTOKI_STORE", &store_path); + let _ = std::fs::remove_file(&store_path); + + unsafe { + let fl = common::fn_list(); + + // Session 1: generate RSA key pair with CKA_TOKEN = true + let session = init_and_open_session(); + let bits: CK_ULONG = 2048; + let ck_true: CK_BBOOL = CK_TRUE; + let pub_template = [ + CK_ATTRIBUTE { r#type: CKA_TOKEN, pValue: &ck_true as *const _ as *mut c_void, ulValueLen: 1 }, + CK_ATTRIBUTE { r#type: CKA_MODULUS_BITS, pValue: &bits as *const _ as *mut c_void, ulValueLen: 8 }, + ]; + let priv_template = [ + CK_ATTRIBUTE { r#type: CKA_TOKEN, pValue: &ck_true as *const _ as *mut c_void, ulValueLen: 1 }, + CK_ATTRIBUTE { r#type: CKA_SIGN, pValue: &ck_true as *const _ as *mut c_void, ulValueLen: 1 }, + ]; + let mech = CK_MECHANISM { mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, pParameter: ptr::null_mut(), ulParameterLen: 0 }; + let mut pub_h: CK_OBJECT_HANDLE = 0; + let mut priv_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, + session, + &mech as *const _ as *mut CK_MECHANISM, + pub_template.as_ptr() as *mut CK_ATTRIBUTE, pub_template.len() as CK_ULONG, + priv_template.as_ptr() as *mut CK_ATTRIBUTE, priv_template.len() as CK_ULONG, + &mut pub_h, &mut priv_h, + ); + assert_eq!(rv, CKR_OK, "C_GenerateKeyPair failed: {rv:#010x}"); + + // Count private keys + let class: CK_ULONG = CKO_PRIVATE_KEY; + let tmpl = [CK_ATTRIBUTE { r#type: CKA_CLASS, pValue: &class as *const _ as *mut c_void, ulValueLen: 8 }]; + let rv = p11!(fl, C_FindObjectsInit, session, tmpl.as_ptr() as *mut CK_ATTRIBUTE, 1); + assert_eq!(rv, CKR_OK); + let mut found = [0u64; 16]; + let mut n: CK_ULONG = 0; + p11!(fl, C_FindObjects, session, found.as_mut_ptr(), 16, &mut n); + p11!(fl, C_FindObjectsFinal, session); + let priv_count_before = n; + + p11!(fl, C_CloseSession, session); + p11!(fl, C_Finalize, ptr::null_mut()); + + // Session 2: verify key pair survived + let session = init_and_open_session(); + let rv = p11!(fl, C_FindObjectsInit, session, tmpl.as_ptr() as *mut CK_ATTRIBUTE, 1); + assert_eq!(rv, CKR_OK); + let mut n2: CK_ULONG = 0; + p11!(fl, C_FindObjects, session, found.as_mut_ptr(), 16, &mut n2); + p11!(fl, C_FindObjectsFinal, session); + + assert_eq!(n2, priv_count_before, + "RSA private key should persist: before={priv_count_before}, after={n2}"); + + p11!(fl, C_CloseSession, session); + p11!(fl, C_Finalize, ptr::null_mut()); + } + + let _ = std::fs::remove_file(&store_path); +} + +#[test] +#[serial] +fn test_storage_file_created_and_valid_json() { + let store_path = format!("/tmp/pkcs11_json_test_{}.json", std::process::id()); + std::env::set_var("CRYPTOKI_STORE", &store_path); + let _ = std::fs::remove_file(&store_path); + + unsafe { + let fl = common::fn_list(); + + let session = init_and_open_session(); + let _key = generate_aes_token_key(session); + + p11!(fl, C_CloseSession, session); + p11!(fl, C_Finalize, ptr::null_mut()); + } + + // Verify the file is valid JSON + let content = std::fs::read_to_string(&store_path) + .expect("Storage file should exist"); + let parsed: serde_json::Value = serde_json::from_str(&content) + .expect("Storage file should be valid JSON"); + + assert_eq!(parsed["version"], 1); + assert!(!parsed["objects"].as_array().unwrap().is_empty()); + // Per-slot token state: slot 0's token should be in read_write state. + assert_eq!(parsed["tokens"]["0"]["state"].as_str().unwrap(), "read_write"); + + let _ = std::fs::remove_file(&store_path); +} diff --git a/tests/pkcs11_integration.rs b/tests/pkcs11_integration.rs new file mode 100644 index 0000000..4596c7a --- /dev/null +++ b/tests/pkcs11_integration.rs @@ -0,0 +1,632 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for the PKCS#11 C API layer. +//! +//! Exercises the full C_* call sequence through the function-list dispatch: +//! C_Initialize → C_OpenSession → C_Generate* → C_EncryptInit+C_Encrypt +//! → C_DecryptInit+C_Decrypt → C_CloseSession → C_Finalize + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use std::ffi::c_void; +use std::ptr; + +// ── Test fixture ────────────────────────────────────────────────────────── + +/// Open a read-write session on slot 0. +unsafe fn open_session() -> CK_SESSION_HANDLE { + common::open_session(common::fn_list()) +} + +// Shared process-level init guard (tests run in the same process). +use std::sync::Once; +static INIT: Once = Once::new(); +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + }); +} + +// ── Library info ────────────────────────────────────────────────────────── + +#[test] +fn test_get_info() { + init(); + unsafe { + let fl = common::fn_list(); + let mut info: CK_INFO = std::mem::zeroed(); + assert_eq!(p11!(fl, C_GetInfo, &mut info), CKR_OK); + assert_eq!(info.cryptokiVersion.major, 3); + assert_eq!(info.cryptokiVersion.minor, 0); + } +} + +#[test] +fn test_get_slot_list() { + init(); + unsafe { + let fl = common::fn_list(); + let mut count: CK_ULONG = 0; + assert_eq!(p11!(fl, C_GetSlotList, CK_TRUE, ptr::null_mut(), &mut count), CKR_OK); + assert_eq!(count, 1); + let mut slot: CK_SLOT_ID = 0; + assert_eq!(p11!(fl, C_GetSlotList, CK_TRUE, &mut slot, &mut count), CKR_OK); + assert_eq!(slot, 0); + } +} + +#[test] +fn test_get_mechanism_list() { + init(); + unsafe { + let fl = common::fn_list(); + let mut count: CK_ULONG = 0; + assert_eq!(p11!(fl, C_GetMechanismList, 0, ptr::null_mut(), &mut count), CKR_OK); + assert!(count >= 10); + let mut list = vec![0u64; count as usize]; + assert_eq!(p11!(fl, C_GetMechanismList, 0, list.as_mut_ptr(), &mut count), CKR_OK); + assert!(list.contains(&CKM_AES_KEY_GEN)); + assert!(list.contains(&CKM_RSA_PKCS_KEY_PAIR_GEN)); + assert!(list.contains(&CKM_EC_KEY_PAIR_GEN)); + assert!(list.contains(&CKM_AES_GCM)); + assert!(list.contains(&CKM_SHA256)); + } +} + +// ── Session ──────────────────────────────────────────────────────────────── + +#[test] +fn test_open_close_session() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + assert_ne!(h, CK_INVALID_HANDLE); + assert_eq!(p11!(fl, C_CloseSession, h), CKR_OK); + // Closing again → invalid handle + assert_eq!(p11!(fl, C_CloseSession, h), CKR_SESSION_HANDLE_INVALID); + } +} + +#[test] +fn test_get_session_info() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mut info: CK_SESSION_INFO = std::mem::zeroed(); + assert_eq!(p11!(fl, C_GetSessionInfo, h, &mut info), CKR_OK); + assert_eq!(info.slotID, 0); + assert_eq!(info.state, CKS_RW_PUBLIC_SESSION); + let _ = p11!(fl, C_CloseSession, h); + } +} + +// ── AES key generation + encrypt/decrypt ────────────────────────────────── + +unsafe fn generate_aes_key(h: CK_SESSION_HANDLE, key_len_bytes: u64) -> CK_OBJECT_HANDLE { + let fl = common::fn_list(); + let value_len = key_len_bytes.to_le_bytes(); + let mut attrs = [CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: value_len.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + let mech = CK_MECHANISM { mechanism: CKM_AES_KEY_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut key_handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, h, &mech, attrs.as_mut_ptr(), 1, &mut key_handle); + assert_eq!(rv, CKR_OK, "C_GenerateKey failed: {rv:#010x}"); + assert_ne!(key_handle, CK_INVALID_HANDLE); + key_handle +} + +#[test] +fn test_aes_cbc_roundtrip_128() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let key = generate_aes_key(h, 16); + + let iv = [0x42u8; 16]; + let mech = CK_MECHANISM { + mechanism: CKM_AES_CBC_PAD, + pParameter: iv.as_ptr() as *const c_void, + ulParameterLen: 16, + }; + let plaintext = b"PKCS11 AES-CBC test message"; + + // Encrypt + assert_eq!(p11!(fl, C_EncryptInit, h, &mech, key), CKR_OK); + let mut ct_len: CK_ULONG = 64; + let mut ct = vec![0u8; 64]; + let rv = p11!(fl, C_Encrypt, h, plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct.as_mut_ptr(), &mut ct_len); + assert_eq!(rv, CKR_OK); + ct.truncate(ct_len as usize); + + // Decrypt + assert_eq!(p11!(fl, C_DecryptInit, h, &mech, key), CKR_OK); + let mut pt_len: CK_ULONG = 64; + let mut pt = vec![0u8; 64]; + let rv = p11!(fl, C_Decrypt, h, ct.as_ptr(), ct_len, + pt.as_mut_ptr(), &mut pt_len); + assert_eq!(rv, CKR_OK); + pt.truncate(pt_len as usize); + assert_eq!(pt, plaintext); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn test_aes_gcm_roundtrip() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let key = generate_aes_key(h, 32); + + let iv = [0xAAu8; 12]; + let aad = b"additional authenticated data"; + + #[repr(C)] + #[allow(non_snake_case)] + struct GcmParams { + pIv: *const u8, ulIvLen: u64, ulIvBits: u64, + pAAD: *const u8, ulAADLen: u64, ulTagBits: u64, + } + let params = GcmParams { + pIv: iv.as_ptr(), ulIvLen: 12, ulIvBits: 96, + pAAD: aad.as_ptr(), ulAADLen: aad.len() as u64, ulTagBits: 128, + }; + let mech = CK_MECHANISM { + mechanism: CKM_AES_GCM, + pParameter: ¶ms as *const _ as *const c_void, + ulParameterLen: std::mem::size_of::() as CK_ULONG, + }; + let plaintext = b"GCM authenticated encryption"; + + // Encrypt (output = ciphertext || 16-byte tag) + assert_eq!(p11!(fl, C_EncryptInit, h, &mech, key), CKR_OK); + let mut ct_len: CK_ULONG = 64; + let mut ct = vec![0u8; 64]; + assert_eq!(p11!(fl, C_Encrypt, h, plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct.as_mut_ptr(), &mut ct_len), CKR_OK); + ct.truncate(ct_len as usize); + // ct = ciphertext + tag (last 16 bytes) + assert_eq!(ct.len(), plaintext.len() + 16); + + // Decrypt + assert_eq!(p11!(fl, C_DecryptInit, h, &mech, key), CKR_OK); + let mut pt_len: CK_ULONG = 64; + let mut pt = vec![0u8; 64]; + assert_eq!(p11!(fl, C_Decrypt, h, ct.as_ptr(), ct.len() as CK_ULONG, + pt.as_mut_ptr(), &mut pt_len), CKR_OK); + pt.truncate(pt_len as usize); + assert_eq!(pt, plaintext); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +// ── RSA key pair generation + sign/verify ──────────────────────────────── + +unsafe fn generate_rsa_key_pair(h: CK_SESSION_HANDLE) -> (CK_OBJECT_HANDLE, CK_OBJECT_HANDLE) { + let fl = common::fn_list(); + let bits: u64 = 2048u64; + let bits_bytes = bits.to_le_bytes(); + let mut pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: bits_bytes.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut pub_key: CK_OBJECT_HANDLE = 0; + let mut priv_key: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, + h, &mech, + pub_attrs.as_mut_ptr(), 1, + priv_attrs.as_mut_ptr(), 0, + &mut pub_key, &mut priv_key, + ); + assert_eq!(rv, CKR_OK, "C_GenerateKeyPair (RSA) failed: {rv:#010x}"); + (pub_key, priv_key) +} + +#[test] +fn test_rsa_sign_verify() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let (pub_key, priv_key) = generate_rsa_key_pair(h); + let message = b"C_Sign(CKM_SHA256_RSA_PKCS) via PKCS#11 C API"; + let mech = CK_MECHANISM { mechanism: CKM_SHA256_RSA_PKCS, pParameter: ptr::null(), ulParameterLen: 0 }; + + // Sign + assert_eq!(p11!(fl, C_SignInit, h, &mech, priv_key), CKR_OK); + let mut sig_len: CK_ULONG = 512; + let mut sig = vec![0u8; 512]; + let rv = p11!(fl, C_Sign, h, message.as_ptr(), message.len() as CK_ULONG, + sig.as_mut_ptr(), &mut sig_len); + assert_eq!(rv, CKR_OK); + sig.truncate(sig_len as usize); + assert_eq!(sig_len, 256); // 2048-bit key + + // Verify with correct message + assert_eq!(p11!(fl, C_VerifyInit, h, &mech, pub_key), CKR_OK); + assert_eq!(p11!(fl, C_Verify, h, message.as_ptr(), message.len() as CK_ULONG, + sig.as_ptr(), sig_len), CKR_OK); + + // Verify with tampered message + let bad = b"tampered"; + assert_eq!(p11!(fl, C_VerifyInit, h, &mech, pub_key), CKR_OK); + assert_eq!(p11!(fl, C_Verify, h, bad.as_ptr(), bad.len() as CK_ULONG, + sig.as_ptr(), sig_len), CKR_SIGNATURE_INVALID); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn test_rsa_pkcs1_encrypt_decrypt() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let (pub_key, priv_key) = generate_rsa_key_pair(h); + let mech = CK_MECHANISM { mechanism: CKM_RSA_PKCS, pParameter: ptr::null(), ulParameterLen: 0 }; + let plaintext = b"RSA PKCS1 via PKCS#11"; + + assert_eq!(p11!(fl, C_EncryptInit, h, &mech, pub_key), CKR_OK); + let mut ct_len: CK_ULONG = 512; + let mut ct = vec![0u8; 512]; + assert_eq!(p11!(fl, C_Encrypt, h, plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct.as_mut_ptr(), &mut ct_len), CKR_OK); + ct.truncate(ct_len as usize); + + assert_eq!(p11!(fl, C_DecryptInit, h, &mech, priv_key), CKR_OK); + let mut pt_len: CK_ULONG = 512; + let mut pt = vec![0u8; 512]; + assert_eq!(p11!(fl, C_Decrypt, h, ct.as_ptr(), ct.len() as CK_ULONG, + pt.as_mut_ptr(), &mut pt_len), CKR_OK); + pt.truncate(pt_len as usize); + assert_eq!(pt, plaintext); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +// ── EC key pair + ECDSA ─────────────────────────────────────────────────── + +unsafe fn generate_ec_key_pair(h: CK_SESSION_HANDLE) -> (CK_OBJECT_HANDLE, CK_OBJECT_HANDLE) { + let fl = common::fn_list(); + // P-256 OID + let p256_oid = [0x06u8, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; + let mut pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_EC_PARAMS, + pValue: p256_oid.as_ptr() as *mut c_void, + ulValueLen: p256_oid.len() as CK_ULONG, + }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { mechanism: CKM_EC_KEY_PAIR_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut pub_key: CK_OBJECT_HANDLE = 0; + let mut priv_key: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, h, &mech, + pub_attrs.as_mut_ptr(), 1, + priv_attrs.as_mut_ptr(), 0, + &mut pub_key, &mut priv_key); + assert_eq!(rv, CKR_OK, "C_GenerateKeyPair (EC) failed: {rv:#010x}"); + (pub_key, priv_key) +} + +#[test] +fn test_ecdsa_sign_verify() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let (pub_key, priv_key) = generate_ec_key_pair(h); + let message = b"C_Sign(CKM_ECDSA) over P-256"; + let mech = CK_MECHANISM { mechanism: CKM_ECDSA, pParameter: ptr::null(), ulParameterLen: 0 }; + + assert_eq!(p11!(fl, C_SignInit, h, &mech, priv_key), CKR_OK); + let mut sig_len: CK_ULONG = 128; + let mut sig = vec![0u8; 128]; + assert_eq!(p11!(fl, C_Sign, h, message.as_ptr(), message.len() as CK_ULONG, + sig.as_mut_ptr(), &mut sig_len), CKR_OK); + sig.truncate(sig_len as usize); + + assert_eq!(p11!(fl, C_VerifyInit, h, &mech, pub_key), CKR_OK); + assert_eq!(p11!(fl, C_Verify, h, message.as_ptr(), message.len() as CK_ULONG, + sig.as_ptr(), sig_len), CKR_OK); + let _ = p11!(fl, C_CloseSession, h); + } +} + +// ── Digest (multi-part + one-shot) ──────────────────────────────────────── + +#[test] +fn test_digest_sha256_one_shot() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mech = CK_MECHANISM { mechanism: CKM_SHA256, pParameter: ptr::null(), ulParameterLen: 0 }; + let data = b"hello world"; + + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OK); + let mut len: CK_ULONG = 64; + let mut digest = vec![0u8; 64]; + assert_eq!(p11!(fl, C_Digest, h, data.as_ptr(), data.len() as CK_ULONG, + digest.as_mut_ptr(), &mut len), CKR_OK); + assert_eq!(len, 32); + assert_ne!(digest[..32], [0u8; 32]); + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn test_digest_sha256_multi_part() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mech = CK_MECHANISM { mechanism: CKM_SHA256, pParameter: ptr::null(), ulParameterLen: 0 }; + + // C_DigestUpdate × 2 → C_DigestFinal + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OK); + assert_eq!(p11!(fl, C_DigestUpdate, h, b"hello ".as_ptr(), 6), CKR_OK); + assert_eq!(p11!(fl, C_DigestUpdate, h, b"world".as_ptr(), 5), CKR_OK); + let mut len: CK_ULONG = 64; + let mut digest_multi = vec![0u8; 64]; + assert_eq!(p11!(fl, C_DigestFinal, h, digest_multi.as_mut_ptr(), &mut len), CKR_OK); + digest_multi.truncate(32); + + // One-shot reference + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OK); + let mut len2: CK_ULONG = 64; + let mut digest_one = vec![0u8; 64]; + assert_eq!(p11!(fl, C_Digest, h, b"hello world".as_ptr(), 11, digest_one.as_mut_ptr(), &mut len2), CKR_OK); + digest_one.truncate(32); + + assert_eq!(digest_multi, digest_one); + let _ = p11!(fl, C_CloseSession, h); + } +} + +// ── Random ──────────────────────────────────────────────────────────────── + +#[test] +fn test_generate_random() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mut buf = vec![0u8; 32]; + assert_eq!(p11!(fl, C_GenerateRandom, h, buf.as_mut_ptr(), 32), CKR_OK); + assert_ne!(buf, vec![0u8; 32]); + let _ = p11!(fl, C_CloseSession, h); + } +} + +// ── Object management ───────────────────────────────────────────────────── + +#[test] +fn test_find_objects_by_class() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + // Generate two different key types + let _aes = generate_aes_key(h, 16); + let (_pub, _priv) = generate_rsa_key_pair(h); + + // Find all secret keys + let class_aes: u64 = CKO_SECRET_KEY; + let class_bytes = class_aes.to_le_bytes(); + let mut tmpl = [CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: class_bytes.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + assert_eq!(p11!(fl, C_FindObjectsInit, h, tmpl.as_mut_ptr(), 1), CKR_OK); + let mut handles = vec![0u64; 10]; + let mut count: CK_ULONG = 0; + assert_eq!(p11!(fl, C_FindObjects, h, handles.as_mut_ptr(), 10, &mut count), CKR_OK); + assert!(count >= 1); // at least the AES key we just created + assert_eq!(p11!(fl, C_FindObjectsFinal, h), CKR_OK); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn test_get_attribute_value_aes_key_len() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let key = generate_aes_key(h, 32); + + let mut val: u64 = 0; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: &mut val as *mut u64 as *mut c_void, + ulValueLen: 8, + }; + assert_eq!(p11!(fl, C_GetAttributeValue, h, key, &mut attr, 1), CKR_OK); + assert_eq!(val, 32u64); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn test_destroy_object() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let key = generate_aes_key(h, 16); + assert_eq!(p11!(fl, C_DestroyObject, h, key), CKR_OK); + // Second destroy → object already gone + assert_eq!(p11!(fl, C_DestroyObject, h, key), CKR_OBJECT_HANDLE_INVALID); + let _ = p11!(fl, C_CloseSession, h); + } +} + +// ── Attribute engine fallback ───────────────────────────────────────────── + +/// `CKA_VALUE` on an RSA private key must return `CKR_ATTRIBUTE_SENSITIVE`. +/// This exercises the engine fallback path (not in HashMap) with a sensitive attr. +#[test] +fn test_get_attribute_rsa_private_value_is_sensitive() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let (_pub_key, priv_key) = generate_rsa_key_pair(h); + + let mut buf = vec![0u8; 512]; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_VALUE, + pValue: buf.as_mut_ptr() as *mut c_void, + ulValueLen: buf.len() as CK_ULONG, + }; + // CKA_VALUE is not in the HashMap for RSA private keys; + // engine fallback returns AttributeSensitive → CKR_ATTRIBUTE_SENSITIVE. + assert_eq!( + p11!(fl, C_GetAttributeValue, h, priv_key, &mut attr, 1), + CKR_ATTRIBUTE_SENSITIVE, + "CKA_VALUE on RSA private key must be sensitive", + ); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +/// `CKA_EC_POINT` on an EC private key is not pre-cached in the HashMap +/// (only inserted for the public key at generation time). +/// The engine fallback must derive and return it from the private DER. +#[test] +fn test_get_attribute_ec_point_from_private_key() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let (_pub_key, priv_key) = generate_ec_key_pair(h); + + // Step 1: query length (pValue = NULL). + let mut attr = CK_ATTRIBUTE { + r#type: CKA_EC_POINT, + pValue: ptr::null_mut(), + ulValueLen: 0, + }; + assert_eq!( + p11!(fl, C_GetAttributeValue, h, priv_key, &mut attr, 1), + CKR_OK, + "length query for CKA_EC_POINT on EC private key must succeed", + ); + let point_len = attr.ulValueLen as usize; + assert!(point_len > 0, "EC point length must be non-zero"); + + // Step 2: retrieve the value. + let mut buf = vec![0u8; point_len]; + attr.pValue = buf.as_mut_ptr() as *mut c_void; + attr.ulValueLen = point_len as CK_ULONG; + assert_eq!( + p11!(fl, C_GetAttributeValue, h, priv_key, &mut attr, 1), + CKR_OK, + "CKA_EC_POINT must be retrievable from EC private key via engine", + ); + // P-256 uncompressed point is 65 bytes, DER-wrapped adds a few more. + assert!(buf.len() >= 65, "EC point must be at least 65 bytes"); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +/// An unrecognised attribute type must return `CKR_ATTRIBUTE_TYPE_INVALID` +/// (set on the attribute and returned as the function result). +#[test] +fn test_get_attribute_unknown_type_returns_invalid() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let key = generate_aes_key(h, 16); + + let mut attr = CK_ATTRIBUTE { + r#type: 0xDEADBEEF, // not a valid CKA_* constant + pValue: ptr::null_mut(), + ulValueLen: 0, + }; + assert_eq!( + p11!(fl, C_GetAttributeValue, h, key, &mut attr, 1), + CKR_ATTRIBUTE_TYPE_INVALID, + ); + assert_eq!(attr.ulValueLen, CK_UNAVAILABLE_INFORMATION); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +// ── Error cases ──────────────────────────────────────────────────────────── + +#[test] +fn test_invalid_session_handle() { + init(); + unsafe { + let fl = common::fn_list(); + let mech = CK_MECHANISM { mechanism: CKM_SHA256, pParameter: ptr::null(), ulParameterLen: 0 }; + assert_eq!(p11!(fl, C_DigestInit, 9999999, &mech), CKR_SESSION_HANDLE_INVALID); + } +} + +#[test] +fn test_operation_active_error() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mech = CK_MECHANISM { mechanism: CKM_SHA256, pParameter: ptr::null(), ulParameterLen: 0 }; + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OK); + // Second init without finishing first → OPERATION_ACTIVE + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OPERATION_ACTIVE); + // Finish to clean up + let mut len: CK_ULONG = 64; + let mut d = vec![0u8; 64]; + let _ = p11!(fl, C_DigestFinal, h, d.as_mut_ptr(), &mut len); + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn test_double_initialize() { + // C_Initialize already called by INIT; second call should return ALREADY_INITIALIZED. + init(); + unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert_eq!(rv, CKR_CRYPTOKI_ALREADY_INITIALIZED); + } +} diff --git a/tests/pkcs11_v3_integration.rs b/tests/pkcs11_v3_integration.rs new file mode 100644 index 0000000..130675c --- /dev/null +++ b/tests/pkcs11_v3_integration.rs @@ -0,0 +1,487 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for PKCS#11 v3.0 features. +//! +//! Covers: EdDSA, ChaCha20-Poly1305, SHA-3/SHA-384/SHA-512 digests, +//! interface discovery, token management, C_SessionCancel, C_LoginUser. + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use serial_test::serial; +use std::ffi::c_void; +use std::ptr; +use std::sync::Once; + +static INIT: Once = Once::new(); +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + }); +} + +unsafe fn open_session() -> CK_SESSION_HANDLE { + let fl = common::fn_list(); + let mut h: CK_SESSION_HANDLE = 0; + let rv = p11!(fl, C_OpenSession, + 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h, + ); + assert_eq!(rv, CKR_OK, "C_OpenSession failed: {rv:#010x}"); + h +} + +unsafe fn open_logged_in_session() -> CK_SESSION_HANDLE { + let fl = common::fn_list(); + let h = open_session(); + let pin = b"1234"; + let rv = p11!(fl, C_Login, h, CKU_USER, pin.as_ptr(), 4); + assert!(rv == CKR_OK || rv == CKR_USER_ALREADY_LOGGED_IN, "C_Login failed: {rv:#x}"); + h +} + +// -- EdDSA (Ed25519) ---------------------------------------------------------- + +unsafe fn generate_ed_key_pair(h: CK_SESSION_HANDLE) -> (CK_OBJECT_HANDLE, CK_OBJECT_HANDLE) { + let fl = common::fn_list(); + // Ed25519 OID: 06 03 2b 65 70 + let ed25519_oid = [0x06u8, 0x03, 0x2b, 0x65, 0x70]; + let mut pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_EC_PARAMS, + pValue: ed25519_oid.as_ptr() as *mut c_void, + ulValueLen: ed25519_oid.len() as CK_ULONG, + }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { + mechanism: CKM_EC_EDWARDS_KEY_PAIR_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut pub_key: CK_OBJECT_HANDLE = 0; + let mut priv_key: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, + h, &mech, + pub_attrs.as_mut_ptr(), 1, + priv_attrs.as_mut_ptr(), 0, + &mut pub_key, &mut priv_key, + ); + assert_eq!(rv, CKR_OK, "C_GenerateKeyPair (EdDSA) failed: {rv:#010x}"); + (pub_key, priv_key) +} + +#[test] +#[serial] +fn test_eddsa_sign_verify() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let (pub_key, priv_key) = generate_ed_key_pair(h); + let message = b"EdDSA sign/verify via PKCS#11 v3.0"; + let mech = CK_MECHANISM { mechanism: CKM_EDDSA, pParameter: ptr::null(), ulParameterLen: 0 }; + + // Sign + assert_eq!(p11!(fl, C_SignInit, h, &mech, priv_key), CKR_OK); + let mut sig_len: CK_ULONG = 128; + let mut sig = vec![0u8; 128]; + let rv = p11!(fl, C_Sign, h, message.as_ptr(), message.len() as CK_ULONG, + sig.as_mut_ptr(), &mut sig_len); + assert_eq!(rv, CKR_OK); + sig.truncate(sig_len as usize); + assert_eq!(sig_len, 64, "Ed25519 signature must be 64 bytes"); + + // Verify + assert_eq!(p11!(fl, C_VerifyInit, h, &mech, pub_key), CKR_OK); + assert_eq!(p11!(fl, C_Verify, h, message.as_ptr(), message.len() as CK_ULONG, + sig.as_ptr(), sig_len), CKR_OK); + + // Tampered signature must fail + sig[0] ^= 0xFF; + assert_eq!(p11!(fl, C_VerifyInit, h, &mech, pub_key), CKR_OK); + assert_eq!(p11!(fl, C_Verify, h, message.as_ptr(), message.len() as CK_ULONG, + sig.as_ptr(), sig_len), CKR_SIGNATURE_INVALID); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +// -- ChaCha20-Poly1305 -------------------------------------------------------- + +unsafe fn generate_chacha20_key(h: CK_SESSION_HANDLE) -> CK_OBJECT_HANDLE { + let fl = common::fn_list(); + let mech = CK_MECHANISM { mechanism: CKM_CHACHA20_KEY_GEN, pParameter: ptr::null(), ulParameterLen: 0 }; + let mut key_handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, h, &mech, ptr::null(), 0, &mut key_handle); + assert_eq!(rv, CKR_OK, "C_GenerateKey (ChaCha20) failed: {rv:#010x}"); + key_handle +} + +#[repr(C)] +#[allow(non_snake_case)] +struct GcmParams { + pIv: *const u8, + ulIvLen: u64, + ulIvBits: u64, + pAAD: *const u8, + ulAADLen: u64, + ulTagBits: u64, +} + +#[test] +#[serial] +fn test_chacha20_poly1305_roundtrip() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let key = generate_chacha20_key(h); + + let nonce = [0x42u8; 12]; + let aad = b"additional data"; + let params = GcmParams { + pIv: nonce.as_ptr(), ulIvLen: 12, ulIvBits: 96, + pAAD: aad.as_ptr(), ulAADLen: aad.len() as u64, ulTagBits: 128, + }; + let mech = CK_MECHANISM { + mechanism: CKM_CHACHA20_POLY1305, + pParameter: ¶ms as *const _ as *const c_void, + ulParameterLen: std::mem::size_of::() as CK_ULONG, + }; + let plaintext = b"ChaCha20-Poly1305 AEAD test"; + + // Encrypt + assert_eq!(p11!(fl, C_EncryptInit, h, &mech, key), CKR_OK); + let mut ct_len: CK_ULONG = 128; + let mut ct = vec![0u8; 128]; + assert_eq!(p11!(fl, C_Encrypt, h, plaintext.as_ptr(), plaintext.len() as CK_ULONG, + ct.as_mut_ptr(), &mut ct_len), CKR_OK); + ct.truncate(ct_len as usize); + assert_eq!(ct.len(), plaintext.len() + 16, "output = ciphertext + 16-byte tag"); + + // Decrypt + assert_eq!(p11!(fl, C_DecryptInit, h, &mech, key), CKR_OK); + let mut pt_len: CK_ULONG = 128; + let mut pt = vec![0u8; 128]; + assert_eq!(p11!(fl, C_Decrypt, h, ct.as_ptr(), ct.len() as CK_ULONG, + pt.as_mut_ptr(), &mut pt_len), CKR_OK); + pt.truncate(pt_len as usize); + assert_eq!(pt, plaintext); + + // Tamper test -- corrupt ciphertext + ct[0] ^= 0xFF; + assert_eq!(p11!(fl, C_DecryptInit, h, &mech, key), CKR_OK); + let mut bad_len: CK_ULONG = 128; + let mut bad = vec![0u8; 128]; + let rv = p11!(fl, C_Decrypt, h, ct.as_ptr(), ct.len() as CK_ULONG, + bad.as_mut_ptr(), &mut bad_len); + assert_ne!(rv, CKR_OK, "tampered ChaCha20-Poly1305 ciphertext must not decrypt"); + + let _ = p11!(fl, C_CloseSession, h); + } +} + +// -- SHA-3 and SHA-384/512 digests -------------------------------------------- + +#[test] +#[serial] +fn test_digest_sha384() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mech = CK_MECHANISM { mechanism: CKM_SHA384, pParameter: ptr::null(), ulParameterLen: 0 }; + let data = b"hello world"; + + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OK); + let mut len: CK_ULONG = 64; + let mut digest = vec![0u8; 64]; + assert_eq!(p11!(fl, C_Digest, h, data.as_ptr(), data.len() as CK_ULONG, + digest.as_mut_ptr(), &mut len), CKR_OK); + assert_eq!(len, 48, "SHA-384 digest must be 48 bytes"); + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +#[serial] +fn test_digest_sha512() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mech = CK_MECHANISM { mechanism: CKM_SHA512, pParameter: ptr::null(), ulParameterLen: 0 }; + let data = b"hello world"; + + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OK); + let mut len: CK_ULONG = 128; + let mut digest = vec![0u8; 128]; + assert_eq!(p11!(fl, C_Digest, h, data.as_ptr(), data.len() as CK_ULONG, + digest.as_mut_ptr(), &mut len), CKR_OK); + assert_eq!(len, 64, "SHA-512 digest must be 64 bytes"); + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +#[serial] +fn test_digest_sha3_256() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mech = CK_MECHANISM { mechanism: CKM_SHA3_256, pParameter: ptr::null(), ulParameterLen: 0 }; + let data = b"hello world"; + + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OK); + let mut len: CK_ULONG = 64; + let mut digest = vec![0u8; 64]; + assert_eq!(p11!(fl, C_Digest, h, data.as_ptr(), data.len() as CK_ULONG, + digest.as_mut_ptr(), &mut len), CKR_OK); + assert_eq!(len, 32, "SHA3-256 digest must be 32 bytes"); + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +#[serial] +fn test_digest_sha3_384() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mech = CK_MECHANISM { mechanism: CKM_SHA3_384, pParameter: ptr::null(), ulParameterLen: 0 }; + let data = b"hello world"; + + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OK); + let mut len: CK_ULONG = 64; + let mut digest = vec![0u8; 64]; + assert_eq!(p11!(fl, C_Digest, h, data.as_ptr(), data.len() as CK_ULONG, + digest.as_mut_ptr(), &mut len), CKR_OK); + assert_eq!(len, 48, "SHA3-384 digest must be 48 bytes"); + let _ = p11!(fl, C_CloseSession, h); + } +} + +#[test] +#[serial] +fn test_digest_sha3_512() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_session(); + let mech = CK_MECHANISM { mechanism: CKM_SHA3_512, pParameter: ptr::null(), ulParameterLen: 0 }; + let data = b"hello world"; + + assert_eq!(p11!(fl, C_DigestInit, h, &mech), CKR_OK); + let mut len: CK_ULONG = 128; + let mut digest = vec![0u8; 128]; + assert_eq!(p11!(fl, C_Digest, h, data.as_ptr(), data.len() as CK_ULONG, + digest.as_mut_ptr(), &mut len), CKR_OK); + assert_eq!(len, 64, "SHA3-512 digest must be 64 bytes"); + let _ = p11!(fl, C_CloseSession, h); + } +} + +// -- Interface discovery (v3.0) ----------------------------------------------- + +#[test] +#[serial] +fn test_get_interface_list() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + // Query count + let mut count: CK_ULONG = 0; + assert_eq!(p11!(fl3, C_GetInterfaceList, ptr::null_mut(), &mut count), CKR_OK); + assert!(count >= 1, "must have at least 1 interface"); + + // Retrieve + let mut iface: CK_INTERFACE = std::mem::zeroed(); + assert_eq!(p11!(fl3, C_GetInterfaceList, &mut iface, &mut count), CKR_OK); + assert!(!iface.pInterfaceName.is_null()); + assert!(!iface.pFunctionList.is_null()); + } +} + +#[test] +#[serial] +fn test_get_interface_default() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let mut iface_ptr: *const CK_INTERFACE = ptr::null(); + // NULL name + NULL version -> return default interface + let rv = p11!(fl3, C_GetInterface, + ptr::null(), ptr::null_mut(), + &mut iface_ptr, 0, + ); + assert_eq!(rv, CKR_OK); + assert!(!iface_ptr.is_null()); + let iface = &*iface_ptr; + assert!(!iface.pFunctionList.is_null()); + } +} + +// -- Mechanism list includes v3.0 mechanisms ---------------------------------- + +#[test] +#[serial] +fn test_mechanism_list_v3() { + init(); + unsafe { + let fl = common::fn_list(); + let mut count: CK_ULONG = 0; + assert_eq!(p11!(fl, C_GetMechanismList, 0, ptr::null_mut(), &mut count), CKR_OK); + let mut list = vec![0u64; count as usize]; + assert_eq!(p11!(fl, C_GetMechanismList, 0, list.as_mut_ptr(), &mut count), CKR_OK); + + assert!(list.contains(&CKM_EC_EDWARDS_KEY_PAIR_GEN), "missing CKM_EC_EDWARDS_KEY_PAIR_GEN"); + assert!(list.contains(&CKM_EDDSA), "missing CKM_EDDSA"); + assert!(list.contains(&CKM_CHACHA20_POLY1305), "missing CKM_CHACHA20_POLY1305"); + assert!(list.contains(&CKM_CHACHA20_KEY_GEN), "missing CKM_CHACHA20_KEY_GEN"); + assert!(list.contains(&CKM_SHA3_256), "missing CKM_SHA3_256"); + assert!(list.contains(&CKM_SHA384), "missing CKM_SHA384"); + assert!(list.contains(&CKM_SHA512), "missing CKM_SHA512"); + } +} + +// -- Token management --------------------------------------------------------- + +#[test] +#[serial] +fn test_token_info_has_label() { + init(); + unsafe { + let fl = common::fn_list(); + let mut info: CK_TOKEN_INFO = std::mem::zeroed(); + assert_eq!(p11!(fl, C_GetTokenInfo, 0, &mut info), CKR_OK); + // Label should be padded to 32 bytes, not all zeros + let label = &info.label[..]; + assert!(!label.iter().all(|&b| b == 0), "token label must not be empty"); + } +} + +// -- C_SessionCancel ---------------------------------------------------------- + +#[test] +#[serial] +fn test_session_cancel() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let h = open_session(); + // Start a digest operation + let mech = CK_MECHANISM { mechanism: CKM_SHA256, pParameter: ptr::null(), ulParameterLen: 0 }; + assert_eq!(p11!(fl3, C_DigestInit, h, &mech), CKR_OK); + + // Cancel it + assert_eq!(p11!(fl3, C_SessionCancel, h, 0), CKR_OK); + + // Now we should be able to start a new digest (no OPERATION_ACTIVE) + assert_eq!(p11!(fl3, C_DigestInit, h, &mech), CKR_OK); + + // Clean up + let mut len: CK_ULONG = 64; + let mut d = vec![0u8; 64]; + let _ = p11!(fl3, C_Digest, h, b"x".as_ptr(), 1, d.as_mut_ptr(), &mut len); + let _ = p11!(fl3, C_CloseSession, h); + } +} + +// -- C_LoginUser (CKU_CONTEXT_SPECIFIC) --------------------------------------- + +#[test] +#[serial] +fn test_login_user_context_specific() { + init(); + unsafe { + let fl3 = common::fn_list_3_0(); + let h = open_logged_in_session(); + let pin = b"1234"; + + // C_LoginUser with CKU_CONTEXT_SPECIFIC should succeed + // (context-specific login for always-authenticate operations) + let rv = p11!(fl3, C_LoginUser, + h, CKU_CONTEXT_SPECIFIC, + pin.as_ptr(), pin.len() as CK_ULONG, + ptr::null(), 0, + ); + assert_eq!(rv, CKR_OK, "C_LoginUser(CKU_CONTEXT_SPECIFIC) failed: {rv:#010x}"); + + let _ = p11!(fl3, C_Logout, h); + let _ = p11!(fl3, C_CloseSession, h); + } +} + +// -- C_InitToken / C_InitPIN / C_SetPIN --------------------------------------- + +/// This test is destructive (C_InitToken closes all sessions and clears objects). +/// It must not run in parallel with other tests that use sessions. +/// Run with `--test-threads=1` or isolate via `cargo test --test pkcs11_v3_integration -- --test-threads=1`. +#[test] +#[serial] +fn test_init_token_and_pin_management() { + init(); + unsafe { + let fl = common::fn_list(); + + // InitToken with SO PIN -- this closes all sessions and clears objects! + let so_pin = b"so-pin"; + let label = b"TestToken "; // 32 bytes padded + let rv = p11!(fl, C_InitToken, 0, so_pin.as_ptr(), so_pin.len() as CK_ULONG, label.as_ptr()); + assert_eq!(rv, CKR_OK, "C_InitToken failed: {rv:#010x}"); + + // Open session, login as SO + let h = open_session(); + assert_eq!(p11!(fl, C_Login, h, CKU_SO, so_pin.as_ptr(), so_pin.len() as CK_ULONG), CKR_OK); + + // InitPIN -- set user PIN + let new_user_pin = b"newpin"; + assert_eq!(p11!(fl, C_InitPIN, h, new_user_pin.as_ptr(), new_user_pin.len() as CK_ULONG), CKR_OK); + + // Logout SO + assert_eq!(p11!(fl, C_Logout, h), CKR_OK); + + // Login with new user PIN + assert_eq!(p11!(fl, C_Login, h, CKU_USER, new_user_pin.as_ptr(), new_user_pin.len() as CK_ULONG), CKR_OK); + + // SetPIN -- change user PIN back to default for other tests + let default_pin = b"1234"; + assert_eq!(p11!(fl, C_SetPIN, h, + new_user_pin.as_ptr(), new_user_pin.len() as CK_ULONG, + default_pin.as_ptr(), default_pin.len() as CK_ULONG), CKR_OK); + + let _ = p11!(fl, C_Logout, h); + let _ = p11!(fl, C_CloseSession, h); + } +} + +// -- Cryptoki version is 3.0 -------------------------------------------------- + +#[test] +#[serial] +fn test_v3_info() { + init(); + unsafe { + let fl = common::fn_list(); + let mut info: CK_INFO = std::mem::zeroed(); + assert_eq!(p11!(fl, C_GetInfo, &mut info), CKR_OK); + assert_eq!(info.cryptokiVersion.major, 3); + assert_eq!(info.cryptokiVersion.minor, 0); + } +} diff --git a/tests/profile_objects.rs b/tests/profile_objects.rs new file mode 100644 index 0000000..a8a9aef --- /dev/null +++ b/tests/profile_objects.rs @@ -0,0 +1,246 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for: CKO_PROFILE objects. +//! +//! Verifies that every initialized token advertises a `CKP_BASELINE_PROVIDER` +//! profile object and that it can be discovered and queried via the standard +//! `C_FindObjects` / `C_GetAttributeValue` flow. + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use std::ptr; +use std::sync::Once; + +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + }); +} + +unsafe fn open_rw_session() -> CK_SESSION_HANDLE { + let fl = common::fn_list(); + let mut h: CK_SESSION_HANDLE = 0; + let rv = p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession failed: {rv:#010x}"); + h +} + +/// Find objects matching a `(type, value)` template. Returns collected handles. +unsafe fn find_all(h: CK_SESSION_HANDLE, template: &[(CK_ATTRIBUTE_TYPE, Vec)]) -> Vec { + let fl = common::fn_list(); + let raw: Vec = template + .iter() + .map(|(t, v)| CK_ATTRIBUTE { r#type: *t, pValue: v.as_ptr() as *mut _, ulValueLen: v.len() as CK_ULONG }) + .collect(); + let rv = p11!(fl, C_FindObjectsInit, h, raw.as_ptr() as *mut _, raw.len() as CK_ULONG); + assert_eq!(rv, CKR_OK, "C_FindObjectsInit failed: {rv:#010x}"); + let mut handles = [0u64; 32]; + let mut count: CK_ULONG = 0; + let rv = p11!(fl, C_FindObjects, h, handles.as_mut_ptr(), handles.len() as CK_ULONG, &mut count); + assert_eq!(rv, CKR_OK, "C_FindObjects failed: {rv:#010x}"); + let rv = p11!(fl, C_FindObjectsFinal, h); + assert_eq!(rv, CKR_OK, "C_FindObjectsFinal failed: {rv:#010x}"); + handles[..count as usize].to_vec() +} + +/// Fetch a single CK_ULONG attribute from an object. +unsafe fn get_ulong(h: CK_SESSION_HANDLE, obj: CK_OBJECT_HANDLE, attr_type: CK_ATTRIBUTE_TYPE) -> CK_ULONG { + let fl = common::fn_list(); + let mut buf = [0u8; 8]; + let mut attr = CK_ATTRIBUTE { + r#type: attr_type, + pValue: buf.as_mut_ptr() as *mut _, + ulValueLen: buf.len() as CK_ULONG, + }; + let rv = p11!(fl, C_GetAttributeValue, h, obj, &mut attr, 1); + assert_eq!(rv, CKR_OK, "C_GetAttributeValue({attr_type:#010x}) failed: {rv:#010x}"); + assert_eq!(attr.ulValueLen, 8, "ULONG attribute should be 8 bytes"); + CK_ULONG::from_le_bytes(buf) +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +/// C_FindObjects with CKA_CLASS=CKO_PROFILE returns at least one profile object. +#[test] +fn profile_object_discoverable_via_find() { + init(); + unsafe { + let h = open_rw_session(); + let fl = common::fn_list(); + + let template = vec![ + (CKA_CLASS, (CKO_PROFILE as CK_ULONG).to_le_bytes().to_vec()), + ]; + let handles = find_all(h, &template); + assert!(!handles.is_empty(), "at least one CKO_PROFILE object must exist after C_Initialize"); + + p11!(fl, C_CloseSession, h); + } +} + +/// The profile object reports CKA_PROFILE_ID == CKP_BASELINE_PROVIDER. +#[test] +fn profile_object_has_baseline_provider_id() { + init(); + unsafe { + let h = open_rw_session(); + let fl = common::fn_list(); + + let template = vec![ + (CKA_CLASS, (CKO_PROFILE as CK_ULONG).to_le_bytes().to_vec()), + ]; + let handles = find_all(h, &template); + assert!(!handles.is_empty()); + + let profile_id = get_ulong(h, handles[0], CKA_PROFILE_ID); + assert_eq!(profile_id, CKP_BASELINE_PROVIDER, + "profile object must advertise CKP_BASELINE_PROVIDER, got {profile_id:#010x}"); + + p11!(fl, C_CloseSession, h); + } +} + +/// The profile object's CKA_CLASS attribute round-trips as CKO_PROFILE. +#[test] +fn profile_object_class_attribute_correct() { + init(); + unsafe { + let h = open_rw_session(); + let fl = common::fn_list(); + + let template = vec![ + (CKA_CLASS, (CKO_PROFILE as CK_ULONG).to_le_bytes().to_vec()), + ]; + let handles = find_all(h, &template); + assert!(!handles.is_empty()); + + let class = get_ulong(h, handles[0], CKA_CLASS); + assert_eq!(class, CKO_PROFILE, "profile object CKA_CLASS must be CKO_PROFILE"); + + p11!(fl, C_CloseSession, h); + } +} + +/// Profile objects are public (CKA_PRIVATE = FALSE) and therefore visible to +/// sessions that are not logged in. +#[test] +fn profile_object_is_public() { + init(); + unsafe { + let h = open_rw_session(); + let fl = common::fn_list(); + + // Not logged in — find must still return the profile. + let template = vec![ + (CKA_CLASS, (CKO_PROFILE as CK_ULONG).to_le_bytes().to_vec()), + ]; + let handles = find_all(h, &template); + assert!(!handles.is_empty(), "public profile must be visible without login"); + + // Double-check by asking for CKA_PRIVATE directly. + let mut priv_byte = [0u8; 1]; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_PRIVATE, + pValue: priv_byte.as_mut_ptr() as *mut _, + ulValueLen: 1, + }; + let rv = p11!(fl, C_GetAttributeValue, h, handles[0], &mut attr, 1); + assert_eq!(rv, CKR_OK); + assert_eq!(priv_byte[0], CK_FALSE, "profile CKA_PRIVATE must be FALSE"); + + p11!(fl, C_CloseSession, h); + } +} + +/// Profile objects are token objects (CKA_TOKEN = TRUE) so they survive across +/// sessions on the same initialized library. +#[test] +fn profile_object_is_token_object() { + init(); + unsafe { + let h = open_rw_session(); + let fl = common::fn_list(); + + let template = vec![ + (CKA_CLASS, (CKO_PROFILE as CK_ULONG).to_le_bytes().to_vec()), + ]; + let handles = find_all(h, &template); + assert!(!handles.is_empty()); + + let mut tok_byte = [0u8; 1]; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: tok_byte.as_mut_ptr() as *mut _, + ulValueLen: 1, + }; + let rv = p11!(fl, C_GetAttributeValue, h, handles[0], &mut attr, 1); + assert_eq!(rv, CKR_OK); + assert_eq!(tok_byte[0], CK_TRUE, "profile CKA_TOKEN must be TRUE"); + + p11!(fl, C_CloseSession, h); + } +} + +/// Profile object survives across a close/open session cycle. +#[test] +fn profile_object_survives_session_close() { + init(); + unsafe { + let fl = common::fn_list(); + + // First session: find profile, grab handle. + let h1 = open_rw_session(); + let template = vec![ + (CKA_CLASS, (CKO_PROFILE as CK_ULONG).to_le_bytes().to_vec()), + ]; + let handles1 = find_all(h1, &template); + assert!(!handles1.is_empty()); + let original = handles1[0]; + p11!(fl, C_CloseSession, h1); + + // Second session: find profile again — must still exist. + let h2 = open_rw_session(); + let handles2 = find_all(h2, &template); + assert!(!handles2.is_empty(), "profile must survive session close"); + assert_eq!(handles2[0], original, + "profile handle should remain stable across sessions"); + p11!(fl, C_CloseSession, h2); + } +} + +/// Exactly one profile object is created per slot (no duplicates on repeated init). +#[test] +fn profile_object_is_unique_per_slot() { + init(); + unsafe { + let h = open_rw_session(); + let fl = common::fn_list(); + + let template = vec![ + (CKA_CLASS, (CKO_PROFILE as CK_ULONG).to_le_bytes().to_vec()), + ]; + let handles = find_all(h, &template); + assert_eq!(handles.len(), 1, "exactly one baseline profile per slot, got {}", handles.len()); + + p11!(fl, C_CloseSession, h); + } +} diff --git a/tests/ro_session.rs b/tests/ro_session.rs new file mode 100644 index 0000000..f39888a --- /dev/null +++ b/tests/ro_session.rs @@ -0,0 +1,285 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for: read-only session enforcement. +//! +//! Every state-mutating C_* function must return `CKR_SESSION_READ_ONLY` when +//! called on a session opened without `CKF_RW_SESSION`. + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use std::ptr; + +use std::sync::Once; +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}" + ); + }); +} + +/// Open a read-only session on slot 0. `CKF_RW_SESSION` is intentionally absent. +unsafe fn open_ro_session() -> CK_SESSION_HANDLE { + let fl = common::fn_list(); + let mut h: CK_SESSION_HANDLE = 0; + // CKF_SERIAL_SESSION is mandatory; no CKF_RW_SESSION → read-only session. + let rv = p11!(fl, C_OpenSession, 0, CKF_SERIAL_SESSION, ptr::null_mut(), None, &mut h); + assert_eq!(rv, CKR_OK, "C_OpenSession (RO) failed: {rv:#010x}"); + h +} + +fn null_mech() -> CK_MECHANISM { + CK_MECHANISM { + mechanism: CKM_AES_KEY_GEN, + pParameter: ptr::null_mut(), + ulParameterLen: 0, + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[test] +fn ro_session_create_object_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_ro_session(); + let mut dummy: CK_OBJECT_HANDLE = 0; + let token_true = [CK_TRUE]; + let template = [CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: token_true.as_ptr() as *mut _, + ulValueLen: 1, + }]; + let rv = p11!(fl, C_CreateObject, h, template.as_ptr(), 1, &mut dummy); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn ro_session_copy_object_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let rw_h = common::open_session(fl); + let ro_h = open_ro_session(); + + let mech = null_mech(); + let mut src_h = 0; + p11!(fl, C_GenerateKey, rw_h, &mech, ptr::null(), 0, &mut src_h); + + let token_true = [CK_TRUE]; + let template = [CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: token_true.as_ptr() as *mut _, + ulValueLen: 1, + }]; + + let mut new_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_CopyObject, ro_h, src_h, template.as_ptr(), 1, &mut new_h); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, rw_h); + p11!(fl, C_CloseSession, ro_h); + } +} + +#[test] +fn ro_session_destroy_object_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let rw_h = common::open_session(fl); + let h = open_ro_session(); + + let mech = null_mech(); + let token_true = [CK_TRUE]; + let template = [CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: token_true.as_ptr() as *mut _, + ulValueLen: 1, + }]; + let mut obj_h: CK_OBJECT_HANDLE = 0; + p11!(fl, C_GenerateKey, rw_h, &mech, template.as_ptr(), 1, &mut obj_h); + + let rv = p11!(fl, C_DestroyObject, h, obj_h); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, rw_h); + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn ro_session_set_attribute_value_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_ro_session(); + // Non-null template so the null-pointer argument check passes; RO fires first. + let mut attr = CK_ATTRIBUTE { r#type: CKA_LABEL, pValue: ptr::null_mut(), ulValueLen: 0 }; + let rv = p11!(fl, C_SetAttributeValue, h, 999, &mut attr, 1); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn ro_session_generate_key_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_ro_session(); + let mech = null_mech(); + + let token_true = [CK_TRUE]; + let template = [CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: token_true.as_ptr() as *mut _, + ulValueLen: 1, + }]; + let mut key_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, h, &mech, template.as_ptr(), 1, &mut key_h); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn ro_session_generate_key_pair_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_ro_session(); + let mech = null_mech(); + let token_true = [CK_TRUE]; + let template = [CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: token_true.as_ptr() as *mut _, + ulValueLen: 1, + }]; + let mut pub_h: CK_OBJECT_HANDLE = 0; + let mut priv_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKeyPair, + h, &mech, + template.as_ptr(), 1, + ptr::null(), 0, + &mut pub_h, &mut priv_h); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn ro_session_unwrap_key_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_ro_session(); + let mech = null_mech(); + let wrapped: [u8; 8] = [0u8; 8]; + let token_true = [CK_TRUE]; + let template = [CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: token_true.as_ptr() as *mut _, + ulValueLen: 1, + }]; + let mut key_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_UnwrapKey, + h, &mech, + 999, + wrapped.as_ptr(), wrapped.len() as CK_ULONG, + template.as_ptr(), template.len() as CK_ULONG, + &mut key_h); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn ro_session_derive_key_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_ro_session(); + let mech = null_mech(); + + let token_true = [CK_TRUE]; + let template = [CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: token_true.as_ptr() as *mut _, + ulValueLen: 1, + }]; + let mut key_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_DeriveKey, + h, &mech, + 999, + template.as_ptr(), 1, + &mut key_h); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn ro_session_init_pin_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_ro_session(); + let pin = b"1234"; + let rv = p11!(fl, C_InitPIN, h, pin.as_ptr(), pin.len() as CK_ULONG); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, h); + } +} + +#[test] +fn ro_session_set_pin_returns_session_read_only() { + init(); + unsafe { + let fl = common::fn_list(); + let h = open_ro_session(); + let old_pin = b"1234"; + let new_pin = b"5678"; + let rv = p11!(fl, C_SetPIN, + h, + old_pin.as_ptr(), old_pin.len() as CK_ULONG, + new_pin.as_ptr(), new_pin.len() as CK_ULONG); + assert_eq!(rv, CKR_SESSION_READ_ONLY, "expected CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, h); + } +} + +/// Sanity check: an RW session must NOT be rejected by the RW check. +#[test] +fn rw_session_is_not_blocked() { + init(); + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); // opens with CKF_RW_SESSION + let mut dummy: CK_OBJECT_HANDLE = 0; + // null template → CKR_ARGUMENTS_BAD (not CKR_SESSION_READ_ONLY) + let rv = p11!(fl, C_CreateObject, h, ptr::null(), 0, &mut dummy); + assert_ne!(rv, CKR_SESSION_READ_ONLY, + "RW session must not get CKR_SESSION_READ_ONLY, got {rv:#010x}"); + p11!(fl, C_CloseSession, h); + } +} diff --git a/tests/rust/BUILD b/tests/rust/BUILD index 828a001..6272633 100644 --- a/tests/rust/BUILD +++ b/tests/rust/BUILD @@ -1,18 +1,29 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* +load("@crates//:defs.bzl", "aliases", "all_crate_deps") load("@rules_rust//rust:defs.bzl", "rust_test") +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["test_main.rs"]) rust_test( - name = "rust_hello_test", + name = "rust_unit_smoke", + crate_root = "test_main.rs", srcs = ["test_main.rs"], + aliases = aliases( + normal_dev = True, + proc_macro_dev = True, + package_name = "", + ), + deps = ["//src:cryptoki_lib"] + all_crate_deps(normal_dev = True, package_name = ""), + proc_macro_deps = all_crate_deps(proc_macro_dev = True, package_name = ""), +) + +sh_test( + name = "rust_tests", + srcs = ["run_rust_tests.sh"], + data = [ + "//:rust_srcs", + ], + timeout = "long", ) diff --git a/tests/rust/run_rust_tests.sh b/tests/rust/run_rust_tests.sh new file mode 100755 index 0000000..6f92bec --- /dev/null +++ b/tests/rust/run_rust_tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$repo_root" + +cargo test diff --git a/tests/rust/test_main.rs b/tests/rust/test_main.rs index 9390d5e..45f6d74 100644 --- a/tests/rust/test_main.rs +++ b/tests/rust/test_main.rs @@ -1,4 +1,6 @@ #[test] -fn test_hello() { +fn bazel_rust_entrypoint_exists() { + // Bazel Rust entrypoint test scaffold. + // Real PKCS#11 behavior tests are executed via cargo test in //tests/rust:rust_tests. assert_eq!(2 + 2, 4); } diff --git a/tests/session_vs_token_objects.rs b/tests/session_vs_token_objects.rs new file mode 100644 index 0000000..b51756c --- /dev/null +++ b/tests/session_vs_token_objects.rs @@ -0,0 +1,343 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Tests for: session objects must never trigger disk persistence. +//! +//! Strategy: redirect `CRYPTOKI_STORE` to a fresh temp path before each +//! scenario. `persist_if_needed()` always writes to disk (even an empty state), +//! so checking whether the file exists is a reliable signal that persistence was +//! triggered. All env-var tests are serialized with `STORE_LOCK` to prevent +//! parallel tests from racing on the env var. + +mod common; + +use std::path::PathBuf; +use std::ptr; +use std::sync::{Mutex, Once}; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; + +static INIT: Once = Once::new(); +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + }); +} + +/// Serializes all tests that manipulate `CRYPTOKI_STORE`. +static STORE_LOCK: Mutex<()> = Mutex::new(()); + +fn lock_store() -> std::sync::MutexGuard<'static, ()> { + STORE_LOCK.lock().unwrap_or_else(|e| e.into_inner()) +} + +/// Return a unique temp file path and guarantee it does not exist. +fn fresh_store_path(tag: &str) -> PathBuf { + let p = std::env::temp_dir().join(format!("pkcs11_store_test_{tag}.json")); + let _ = std::fs::remove_file(&p); + p +} + +// ── Helpers ─────────────────────────────────────────────────────────────── + +unsafe fn generate_aes_key( + fl: &CK_FUNCTION_LIST, + session: CK_SESSION_HANDLE, + is_token: bool, +) -> (CK_RV, CK_OBJECT_HANDLE) { + let key_len: CK_ULONG = 32; + let key_len_bytes = key_len.to_le_bytes(); + let token_byte: CK_BBOOL = if is_token { CK_TRUE } else { CK_FALSE }; + + let template = [ + CK_ATTRIBUTE { + r#type: CKA_VALUE_LEN, + pValue: key_len_bytes.as_ptr() as *mut _, + ulValueLen: key_len_bytes.len() as CK_ULONG, + }, + CK_ATTRIBUTE { + r#type: CKA_TOKEN, + pValue: &token_byte as *const _ as *mut _, + ulValueLen: 1, + }, + ]; + let mech = CK_MECHANISM { + mechanism: CKM_AES_KEY_GEN, + pParameter: ptr::null_mut(), + ulParameterLen: 0, + }; + let mut key_h: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, + session, &mech, + template.as_ptr(), template.len() as CK_ULONG, + &mut key_h, + ); + (rv, key_h) +} + +// ── Session object: no disk write ───────────────────────────────────────── + +/// Creating a session object (`CKA_TOKEN = CK_FALSE`) must NOT write to disk. +#[test] +fn session_object_create_does_not_write_disk() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("session_create"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + let (rv, _) = generate_aes_key(fl, h, /* is_token */ false); + assert_eq!(rv, CKR_OK, "GenerateKey (session) failed: {rv:#010x}"); + + assert!( + !store_path.exists(), + "session object must NOT trigger a disk write; file {:?} was created", + store_path + ); + p11!(fl, C_CloseSession, h); + } + + let _ = std::fs::remove_file(&store_path); + std::env::remove_var("CRYPTOKI_STORE"); +} + +/// Destroying a session object must NOT write to disk. +#[test] +fn session_object_destroy_does_not_write_disk() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("session_destroy"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + + // Create (still session — no write expected). + let (rv, key_h) = generate_aes_key(fl, h, false); + assert_eq!(rv, CKR_OK); + assert!(!store_path.exists(), "session create must not write disk"); + + // Destroy — also must not write. + let rv = p11!(fl, C_DestroyObject, h, key_h); + assert_eq!(rv, CKR_OK, "C_DestroyObject failed: {rv:#010x}"); + assert!( + !store_path.exists(), + "destroying a session object must NOT trigger a disk write" + ); + + p11!(fl, C_CloseSession, h); + } + + let _ = std::fs::remove_file(&store_path); + std::env::remove_var("CRYPTOKI_STORE"); +} + +/// Mutating an attribute on a session object (`C_SetAttributeValue`) must NOT +/// write to disk. +#[test] +fn session_object_set_attribute_does_not_write_disk() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("session_setattr"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + let (rv, key_h) = generate_aes_key(fl, h, false); + assert_eq!(rv, CKR_OK); + assert!(!store_path.exists(), "session create must not write disk"); + + // Mutate a non-sensitive attribute (CKA_LABEL). + let label = b"test-label"; + let attr = CK_ATTRIBUTE { + r#type: CKA_LABEL, + pValue: label.as_ptr() as *mut _, + ulValueLen: label.len() as CK_ULONG, + }; + let rv = p11!(fl, C_SetAttributeValue, h, key_h, &attr as *const _ as *mut _, 1u64); + assert_eq!(rv, CKR_OK, "C_SetAttributeValue failed: {rv:#010x}"); + assert!( + !store_path.exists(), + "C_SetAttributeValue on a session object must NOT write to disk" + ); + + p11!(fl, C_CloseSession, h); + } + + let _ = std::fs::remove_file(&store_path); + std::env::remove_var("CRYPTOKI_STORE"); +} + +// ── Token object: disk write occurs ────────────────────────────────────── + +/// Creating a token object (`CKA_TOKEN = CK_TRUE`) MUST write to disk. +#[test] +fn token_object_create_writes_disk() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("token_create"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + let (rv, _) = generate_aes_key(fl, h, /* is_token */ true); + assert_eq!(rv, CKR_OK, "GenerateKey (token) failed: {rv:#010x}"); + + assert!( + store_path.exists(), + "token object MUST trigger a disk write; file {:?} was not created", + store_path + ); + p11!(fl, C_CloseSession, h); + } + + let _ = std::fs::remove_file(&store_path); + std::env::remove_var("CRYPTOKI_STORE"); +} + +/// Destroying a token object MUST write to disk (to remove it from the store). +#[test] +fn token_object_destroy_writes_disk() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("token_destroy"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + let (rv, key_h) = generate_aes_key(fl, h, true); + assert_eq!(rv, CKR_OK); + assert!(store_path.exists(), "token create must write disk"); + + // Record mtime before destroy. + let mtime_before = std::fs::metadata(&store_path).unwrap().modified().unwrap(); + + // Small sleep so mtime can advance on coarse-grained filesystems. + std::thread::sleep(std::time::Duration::from_millis(10)); + + let rv = p11!(fl, C_DestroyObject, h, key_h); + assert_eq!(rv, CKR_OK, "C_DestroyObject failed: {rv:#010x}"); + + let mtime_after = std::fs::metadata(&store_path).unwrap().modified().unwrap(); + assert!( + mtime_after > mtime_before, + "destroying a token object must rewrite disk (mtime unchanged)" + ); + + p11!(fl, C_CloseSession, h); + } + + let _ = std::fs::remove_file(&store_path); + std::env::remove_var("CRYPTOKI_STORE"); +} + +/// Mutating an attribute on a token object (`C_SetAttributeValue`) MUST write +/// to disk so the change survives across sessions. +#[test] +fn token_object_set_attribute_writes_disk() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("token_setattr"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + let (rv, key_h) = generate_aes_key(fl, h, true); + assert_eq!(rv, CKR_OK); + assert!(store_path.exists(), "token create must write disk"); + + let mtime_before = std::fs::metadata(&store_path).unwrap().modified().unwrap(); + std::thread::sleep(std::time::Duration::from_millis(10)); + + let label = b"persistent-label"; + let attr = CK_ATTRIBUTE { + r#type: CKA_LABEL, + pValue: label.as_ptr() as *mut _, + ulValueLen: label.len() as CK_ULONG, + }; + let rv = p11!(fl, C_SetAttributeValue, h, key_h, &attr as *const _ as *mut _, 1u64); + assert_eq!(rv, CKR_OK, "C_SetAttributeValue failed: {rv:#010x}"); + + let mtime_after = std::fs::metadata(&store_path).unwrap().modified().unwrap(); + assert!( + mtime_after > mtime_before, + "C_SetAttributeValue on a token object must rewrite disk (mtime unchanged)" + ); + + p11!(fl, C_CloseSession, h); + } + + let _ = std::fs::remove_file(&store_path); + std::env::remove_var("CRYPTOKI_STORE"); +} + +// ── Session object vanishes on session close; token object survives ─────── + +/// A session object must be gone after the creating session closes. +/// A token object must survive. +#[test] +fn session_object_gone_after_session_close_token_object_survives() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("lifetime"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + unsafe { + let fl = common::fn_list(); + let h = common::open_session(fl); + + let (rv, session_key_h) = generate_aes_key(fl, h, false); + assert_eq!(rv, CKR_OK); + let (rv, token_key_h) = generate_aes_key(fl, h, true); + assert_eq!(rv, CKR_OK); + + // Close the session — session objects must be destroyed. + let rv = p11!(fl, C_CloseSession, h); + assert_eq!(rv, CKR_OK); + + // Open a new session and verify object handles. + let h2 = common::open_session(fl); + + // Session object: handle should be invalid now. + let mut class: CK_OBJECT_CLASS = 0; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_CLASS, + pValue: &mut class as *mut _ as *mut _, + ulValueLen: std::mem::size_of::() as CK_ULONG, + }; + let rv = p11!(fl, C_GetAttributeValue, h2, session_key_h, &mut attr, 1u64); + assert_eq!(rv, CKR_OBJECT_HANDLE_INVALID, + "session object must be gone after session close"); + + // Token object: must still be accessible. + let rv = p11!(fl, C_GetAttributeValue, h2, token_key_h, &mut attr, 1u64); + assert_eq!(rv, CKR_OK, + "token object must survive session close"); + + p11!(fl, C_CloseSession, h2); + } + + let _ = std::fs::remove_file(&store_path); + std::env::remove_var("CRYPTOKI_STORE"); +} diff --git a/tests/signing.rs b/tests/signing.rs new file mode 100644 index 0000000..0f479c1 --- /dev/null +++ b/tests/signing.rs @@ -0,0 +1,357 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! +//! Each Test follows: +//! loadHSMLibrary → connectToSlot (Initialize + OpenSession + Login) +//! → generateKeyPair → signData → verifyData +//! → disconnectFromSlot (Logout + CloseSession + Finalize) + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::{ + C_Initialize, + C_OpenSession, C_CloseSession, + C_Login, C_Logout, + C_SignInit, C_Sign, C_SignUpdate, C_SignFinal, + C_VerifyInit, C_Verify, C_VerifyUpdate, C_VerifyFinal, + C_GenerateKeyPair, +}; +use std::ffi::c_void; +use std::ptr; +use std::sync::Once; + +const SLOT_PIN: &[u8] = b"1234"; + +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let rv = C_Initialize(ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}", + ); + }); +} + +// ── Shared helpers ──────────────────────────────────────────────────────── + +unsafe fn connect_to_slot() -> CK_SESSION_HANDLE { + let mut h: CK_SESSION_HANDLE = 0; + assert_eq!( + C_OpenSession(0, CKF_SERIAL_SESSION | CKF_RW_SESSION, ptr::null_mut(), None, &mut h), + CKR_OK, + ); + let rv = C_Login(h, CKU_USER, SLOT_PIN.as_ptr(), SLOT_PIN.len() as CK_ULONG); + assert!(rv == CKR_OK || rv == CKR_USER_ALREADY_LOGGED_IN, "C_Login failed: {rv:#x}"); + h +} + +unsafe fn disconnect_from_slot(h: CK_SESSION_HANDLE) { + let rv = C_Logout(h); + assert!(rv == CKR_OK || rv == CKR_USER_NOT_LOGGED_IN, "C_Logout failed: {rv:#x}"); + assert_eq!(C_CloseSession(h), CKR_OK); +} + +/// Generate RSA-2048 key pair +/// (generateRsaKeyPair() — CKM_RSA_PKCS_KEY_PAIR_GEN, keySize=2048) +unsafe fn generate_rsa_key_pair(h: CK_SESSION_HANDLE) -> (CK_OBJECT_HANDLE, CK_OBJECT_HANDLE) { + let key_bits: u64 = 2048; + let bits_le = key_bits.to_le_bytes(); + let mut pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_MODULUS_BITS, + pValue: bits_le.as_ptr() as *mut c_void, + ulValueLen: 8, + }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { + mechanism: CKM_RSA_PKCS_KEY_PAIR_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut h_public: CK_OBJECT_HANDLE = 0; + let mut h_private: CK_OBJECT_HANDLE = 0; + assert_eq!( + C_GenerateKeyPair(h, &mech, pub_attrs.as_mut_ptr(), 1, priv_attrs.as_mut_ptr(), 0, &mut h_public, &mut h_private), + CKR_OK, + ); + (h_public, h_private) +} + +/// Generate P-256 EC key pair +/// (generateECDSAKeyPair() — CKM_EC_KEY_PAIR_GEN, curve OID for secp256r1) +unsafe fn generate_ec_key_pair(h: CK_SESSION_HANDLE) -> (CK_OBJECT_HANDLE, CK_OBJECT_HANDLE) { + // DER-encoded OID for P-256 (secp256r1): 06 08 2a 86 48 ce 3d 03 01 07 + // (CK_BYTE curve[] = {0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}) + let p256_oid = [0x06u8, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; + let mut pub_attrs = [CK_ATTRIBUTE { + r#type: CKA_EC_PARAMS, + pValue: p256_oid.as_ptr() as *mut c_void, + ulValueLen: p256_oid.len() as CK_ULONG, + }]; + let mut priv_attrs: [CK_ATTRIBUTE; 0] = []; + let mech = CK_MECHANISM { + mechanism: CKM_EC_KEY_PAIR_GEN, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let mut h_public: CK_OBJECT_HANDLE = 0; + let mut h_private: CK_OBJECT_HANDLE = 0; + assert_eq!( + C_GenerateKeyPair(h, &mech, pub_attrs.as_mut_ptr(), 1, priv_attrs.as_mut_ptr(), 0, &mut h_public, &mut h_private), + CKR_OK, + ); + (h_public, h_private) +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_SHA256_RSA_PKCS +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateRsaKeyPair() → +/// signData() → verifyData() → disconnectFromSlot() +/// +/// signData(): C_SignInit(CKM_SHA256_RSA_PKCS, hPrivate) → C_Sign(NULL, &sigLen) → C_Sign(sig, &sigLen) +/// verifyData(): C_VerifyInit(CKM_SHA256_RSA_PKCS, hPublic) → C_Verify(data, sig) +#[test] +fn ckm_sha256_rsa_pkcs() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Generate RSA-2048 key pair + // (generateRsaKeyPair() → C_GenerateKeyPair(..., &hPublic, &hPrivate)) + let (h_public, h_private) = generate_rsa_key_pair(h_session); + + // plainData (CK_BYTE plainData[] = "Earth is the third planet of our Solar System.") + let plain_data = b"Earth is the third planet of our Solar System."; + + // Step 4: Sign with private key + // (CK_MECHANISM mech = {CKM_SHA256_RSA_PKCS}; C_SignInit → C_Sign(NULL, &sigLen) → C_Sign(sig, &sigLen)) + let mech = CK_MECHANISM { + mechanism: CKM_SHA256_RSA_PKCS, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + assert_eq!(C_SignInit(h_session, &mech, h_private), CKR_OK); + let mut sig_len: CK_ULONG = 512; + let mut signature = vec![0u8; 512]; + assert_eq!( + C_Sign(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, signature.as_mut_ptr(), &mut sig_len), + CKR_OK, + ); + signature.truncate(sig_len as usize); + assert_eq!(sig_len, 256, "RSA-2048 signature must be 256 bytes"); + + // Step 5: Verify with public key — must succeed + // (CK_MECHANISM mech = {CKM_SHA256_RSA_PKCS}; C_VerifyInit → C_Verify(data, sig)) + assert_eq!(C_VerifyInit(h_session, &mech, h_public), CKR_OK); + assert_eq!( + C_Verify(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, signature.as_ptr(), sig_len), + CKR_OK, + "signature verification must succeed", + ); + + // Step 6: Tamper test — verify with wrong data must return CKR_SIGNATURE_INVALID + let wrong_data = b"Mars is the fourth planet of our Solar System."; + assert_eq!(C_VerifyInit(h_session, &mech, h_public), CKR_OK); + assert_eq!( + C_Verify(h_session, wrong_data.as_ptr(), wrong_data.len() as CK_ULONG, signature.as_ptr(), sig_len), + CKR_SIGNATURE_INVALID, + "tampered data must fail verification", + ); + + // Step 7: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// Extension: multi-part sign / verify (C_SignUpdate + C_SignFinal) +// ═════════════════════════════════════════════════════════════════════════════ + +/// Multi-part signing via C_SignUpdate + C_SignFinal, then verified both ways: +/// multi-part verify (C_VerifyUpdate + C_VerifyFinal) and one-shot (C_Verify on full message) +#[test] +fn ckm_sha256_rsa_pkcs_multipart() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Generate RSA-2048 key pair + let (h_public, h_private) = generate_rsa_key_pair(h_session); + + let mech = CK_MECHANISM { + mechanism: CKM_SHA256_RSA_PKCS, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + let part1 = b"Earth is the third "; + let part2 = b"planet of our Solar System."; + let full = b"Earth is the third planet of our Solar System."; + + // Step 4: Multi-part sign — C_SignInit → C_SignUpdate × N → C_SignFinal + assert_eq!(C_SignInit(h_session, &mech, h_private), CKR_OK); + assert_eq!(C_SignUpdate(h_session, part1.as_ptr(), part1.len() as CK_ULONG), CKR_OK); + assert_eq!(C_SignUpdate(h_session, part2.as_ptr(), part2.len() as CK_ULONG), CKR_OK); + let mut sig_len: CK_ULONG = 512; + let mut signature = vec![0u8; 512]; + assert_eq!(C_SignFinal(h_session, signature.as_mut_ptr(), &mut sig_len), CKR_OK); + signature.truncate(sig_len as usize); + + // Step 5: One-shot verify against the full concatenated message + assert_eq!(C_VerifyInit(h_session, &mech, h_public), CKR_OK); + assert_eq!( + C_Verify(h_session, full.as_ptr(), full.len() as CK_ULONG, signature.as_ptr(), sig_len), + CKR_OK, + "one-shot verify of multi-part signature must succeed", + ); + + // Step 6: Multi-part verify — C_VerifyInit → C_VerifyUpdate × N → C_VerifyFinal + assert_eq!(C_VerifyInit(h_session, &mech, h_public), CKR_OK); + assert_eq!(C_VerifyUpdate(h_session, part1.as_ptr(), part1.len() as CK_ULONG), CKR_OK); + assert_eq!(C_VerifyUpdate(h_session, part2.as_ptr(), part2.len() as CK_ULONG), CKR_OK); + assert_eq!(C_VerifyFinal(h_session, signature.as_ptr(), sig_len), CKR_OK); + + // Step 7: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_SHA256_RSA_PKCS_PSS +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateRsaKeyPair() → +/// initPSSParam() → signData() → verifyData() → disconnectFromSlot() +/// +/// initPSSParam(): pssParam.hashAlg = CKM_SHA256; pssParam.mgf = CKG_MGF1_SHA256; pssParam.sLen = sizeof(plainData)-1 +/// signData(): C_SignInit(CKM_SHA256_RSA_PKCS_PSS, &pssParam, hPrivate) → C_Sign(NULL, &sigLen) → C_Sign(sig, &sigLen) +/// verifyData(): C_VerifyInit(CKM_SHA256_RSA_PKCS_PSS, &pssParam, hPublic) → C_Verify +/// +/// Note: our backend accepts CKM_SHA256_RSA_PKCS_PSS and ignores the param struct (uses OpenSSL PSS defaults). +#[test] +fn ckm_sha256_rsa_pkcs_pss() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Generate RSA-2048 key pair + let (h_public, h_private) = generate_rsa_key_pair(h_session); + + let plain_data = b"Earth is the third planet of our Solar System."; + + // Step 4: Sign with PSS padding + // (CK_MECHANISM mech = {CKM_SHA256_RSA_PKCS_PSS, &pssParam, sizeof(pssParam)}) + let mech = CK_MECHANISM { + mechanism: CKM_SHA256_RSA_PKCS_PSS, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + assert_eq!(C_SignInit(h_session, &mech, h_private), CKR_OK); + let mut sig_len: CK_ULONG = 512; + let mut signature = vec![0u8; 512]; + assert_eq!( + C_Sign(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, signature.as_mut_ptr(), &mut sig_len), + CKR_OK, + ); + signature.truncate(sig_len as usize); + assert_eq!(sig_len, 256, "RSA-2048 PSS signature must be 256 bytes"); + + // Step 5: Verify PSS signature with public key + // (C_VerifyInit(hSession, &mech, hPublic) → C_Verify(plainData, sigLen, signature, sigLen)) + assert_eq!(C_VerifyInit(h_session, &mech, h_public), CKR_OK); + assert_eq!( + C_Verify(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, signature.as_ptr(), sig_len), + CKR_OK, + "PSS signature verification must succeed", + ); + + // Step 6: Logout and close session + disconnect_from_slot(h_session); + } +} + +// ═════════════════════════════════════════════════════════════════════════════ +// CKM_ECDSA +// ═════════════════════════════════════════════════════════════════════════════ + +/// sequence: +/// loadHSMLibrary() → connectToSlot() → generateECDSAKeyPair() → +/// signData() → verifyData() → disconnectFromSlot() +/// +/// generateECDSAKeyPair(): CKM_EC_KEY_PAIR_GEN with CKA_EC_PARAMS = secp256r1 OID +/// signData(): C_SignInit(CKM_ECDSA, hPrivate) → C_Sign(NULL, &sigLen) → C_Sign(sig, &sigLen) +/// verifyData(): C_VerifyInit(CKM_ECDSA, hPublic) → C_Verify(data, sigLen, sig, sigLen) +#[test] +fn ckm_ecdsa() { + init(); + unsafe { + // Step 1: Initialize (shared) + // Step 2: Open session + login + let h_session = connect_to_slot(); + + // Step 3: Generate EC P-256 key pair + // (generateECDSAKeyPair() — curve OID bytes for secp256r1) + let (h_public, h_private) = generate_ec_key_pair(h_session); + + let plain_data = b"Earth is the third planet of our Solar System."; + + // Step 4: Sign with ECDSA (our backend applies SHA-256 internally) + // (CK_MECHANISM mech = {CKM_ECDSA}; C_SignInit(hSession, &mech, hPrivate)) + let mech = CK_MECHANISM { + mechanism: CKM_ECDSA, + pParameter: ptr::null(), + ulParameterLen: 0, + }; + assert_eq!(C_SignInit(h_session, &mech, h_private), CKR_OK); + let mut sig_len: CK_ULONG = 128; + let mut signature = vec![0u8; 128]; + assert_eq!( + C_Sign(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, signature.as_mut_ptr(), &mut sig_len), + CKR_OK, + ); + signature.truncate(sig_len as usize); + assert!(sig_len > 0, "ECDSA signature must be non-empty"); + + // Step 5: Verify with the public key — must succeed + // (C_VerifyInit(hSession, &mech, hPublic) → C_Verify(plainData, ..., signature, sigLen)) + assert_eq!(C_VerifyInit(h_session, &mech, h_public), CKR_OK); + assert_eq!( + C_Verify(h_session, plain_data.as_ptr(), plain_data.len() as CK_ULONG, signature.as_ptr(), sig_len), + CKR_OK, + "ECDSA verification must succeed", + ); + + // Step 6: Tamper test — wrong data must return CKR_SIGNATURE_INVALID + let wrong_data = b"Mars is the fourth planet of our Solar System."; + assert_eq!(C_VerifyInit(h_session, &mech, h_public), CKR_OK); + assert_eq!( + C_Verify(h_session, wrong_data.as_ptr(), wrong_data.len() as CK_ULONG, signature.as_ptr(), sig_len), + CKR_SIGNATURE_INVALID, + "tampered data must fail ECDSA verification", + ); + + // Step 7: Logout and close session + disconnect_from_slot(h_session); + } +} diff --git a/tests/slots_and_tokens.rs b/tests/slots_and_tokens.rs new file mode 100644 index 0000000..1b9b983 --- /dev/null +++ b/tests/slots_and_tokens.rs @@ -0,0 +1,133 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! +//! The Tests demonstrate: +//! - Retrieving the list of all available slots with tokens. +//! - Displaying information about those slots and tokens. +//! - Listing supported mechanisms. +//! - Querying the library version via C_GetInfo. + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use cryptoki::pkcs11::{ + C_Initialize, + C_GetInfo, C_GetSlotList, C_GetSlotInfo, C_GetTokenInfo, + C_GetMechanismList, +}; +use std::ptr; +use std::sync::Once; + +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + // Step 1: Initialize the PKCS#11 library + // (equivalent: C_Initialize(NULL_PTR) in loadHSMLibrary/connectToSlot) + let rv = C_Initialize(ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}", + ); + }); +} + +// ── slot_and_token_info ────────────────────────────────────── + +/// Demonstrates: C_GetSlotList → C_GetSlotInfo → C_GetTokenInfo → +/// C_GetMechanismList → C_GetInfo +/// +/// main() sequence: +/// loadHSMLibrary() → C_Initialize() → show_all_slots() → C_Finalize() +/// show_all_slots() calls: C_GetSlotList → C_GetSlotInfo → C_GetTokenInfo +#[test] +fn slot_and_token_info() { + init(); + unsafe { + // Step 2: C_GetSlotList — first call with NULL buffer to get count, + // second call to fill the slot ID array + // (checkOperation(p11Func->C_GetSlotList(CK_TRUE, NULL_PTR, &no_of_slots))) + let mut slot_count: CK_ULONG = 0; + assert_eq!( + C_GetSlotList(CK_TRUE, ptr::null_mut(), &mut slot_count), + CKR_OK, + "C_GetSlotList (count) failed", + ); + assert_eq!(slot_count, 1, "expected exactly one slot"); + + let mut slot_id: CK_SLOT_ID = 0; + assert_eq!( + C_GetSlotList(CK_TRUE, &mut slot_id, &mut slot_count), + CKR_OK, + "C_GetSlotList (fill) failed", + ); + assert_eq!(slot_id, 0); + + // Step 3: C_GetSlotInfo — retrieve hardware/firmware version and flags + // (show_slot_info() → p11Func->C_GetSlotInfo(slotId, &slotInfo)) + let mut slot_info: CK_SLOT_INFO = std::mem::zeroed(); + assert_eq!( + C_GetSlotInfo(slot_id, &mut slot_info), + CKR_OK, + "C_GetSlotInfo failed", + ); + assert_ne!( + slot_info.flags & CKF_TOKEN_PRESENT, + 0, + "CKF_TOKEN_PRESENT must be set", + ); + + // Step 4: C_GetTokenInfo — retrieve token label, flags, memory, pin-length limits + // (show_token_info() → p11Func->C_GetTokenInfo(slotId, &tokenInfo)) + let mut token_info: CK_TOKEN_INFO = std::mem::zeroed(); + assert_eq!( + C_GetTokenInfo(slot_id, &mut token_info), + CKR_OK, + "C_GetTokenInfo failed", + ); + assert_ne!( + token_info.flags & CKF_TOKEN_INITIALIZED, + 0, + "CKF_TOKEN_INITIALIZED must be set", + ); + + // Step 5: C_GetMechanismList — first call with NULL to get count, + // second call to retrieve the mechanism type array + let mut mech_count: CK_ULONG = 0; + assert_eq!( + C_GetMechanismList(slot_id, ptr::null_mut(), &mut mech_count), + CKR_OK, + "C_GetMechanismList (count) failed", + ); + assert!(mech_count >= 10, "too few mechanisms: {mech_count}"); + + let mut mechs = vec![0u64; mech_count as usize]; + assert_eq!( + C_GetMechanismList(slot_id, mechs.as_mut_ptr(), &mut mech_count), + CKR_OK, + "C_GetMechanismList (fill) failed", + ); + assert!(mechs.contains(&CKM_AES_KEY_GEN), "missing CKM_AES_KEY_GEN"); + assert!(mechs.contains(&CKM_AES_CBC_PAD), "missing CKM_AES_CBC_PAD"); + assert!(mechs.contains(&CKM_AES_GCM), "missing CKM_AES_GCM"); + assert!(mechs.contains(&CKM_RSA_PKCS_KEY_PAIR_GEN),"missing CKM_RSA_PKCS_KEY_PAIR_GEN"); + assert!(mechs.contains(&CKM_EC_KEY_PAIR_GEN), "missing CKM_EC_KEY_PAIR_GEN"); + assert!(mechs.contains(&CKM_SHA256), "missing CKM_SHA256"); + + // Step 6: C_GetInfo — library version and vendor information + let mut info: CK_INFO = std::mem::zeroed(); + assert_eq!(C_GetInfo(&mut info), CKR_OK, "C_GetInfo failed"); + assert_eq!(info.cryptokiVersion.major, 3); + assert_eq!(info.cryptokiVersion.minor, 0); + } +} diff --git a/tests/storage_atomic_writes.rs b/tests/storage_atomic_writes.rs new file mode 100644 index 0000000..c23518d --- /dev/null +++ b/tests/storage_atomic_writes.rs @@ -0,0 +1,307 @@ +/******************************************************************************** + * Copyright (c) 2026 Valeo + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +//! Tests for: atomic writes + file locking. +//! +//! ## What is verified +//! +//! * After a successful `save_state()` call the storage file has `0600` +//! permissions and the parent directory has `0700` permissions (Unix only). +//! * `save_state()` blocks while another writer holds the exclusive `flock` on +//! the sidecar `.lock` file, and proceeds correctly once the lock is released. +//! This validates that concurrent writers in different **processes** (each of +//! which would open the lock file separately) are serialised. Within a +//! single process the same behaviour is exercised because Linux `flock(2)` +//! locks are per open-file-description: two distinct `open(2)` calls on the +//! same path produce independent descriptions that block each other. +//! +//! All tests that touch `CRYPTOKI_STORE` are serialised with `STORE_LOCK` +//! to prevent parallel tests inside this binary from racing on the env var. + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::{Mutex, Once}; +use std::time::Duration; + +use cryptoki::pkcs11::storage::{save_state, StoredState}; + +// ── Process-wide init ───────────────────────────────────────────────────── + +static INIT: Once = Once::new(); +fn init() { + INIT.call_once(|| { + // Nothing to do here for storage-only tests; marker kept for symmetry + // with other test files that call C_Initialize. + }); +} + +/// Serialise all tests that mutate `CRYPTOKI_STORE`. +static STORE_LOCK: Mutex<()> = Mutex::new(()); + +fn lock_store() -> std::sync::MutexGuard<'static, ()> { + STORE_LOCK.lock().unwrap_or_else(|e| e.into_inner()) +} + +// ── Helpers ─────────────────────────────────────────────────────────────── + +/// Return a path `/pkcs11_storage_test_/token.json`. +/// +/// The *subdirectory* is unique per tag so that the parent-directory +/// permission test can verify a directory we fully own (not `/tmp` itself). +/// Any pre-existing directory is removed first to start from a clean slate. +fn fresh_store_path(tag: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("pkcs11_storage_test_{tag}")); + let _ = std::fs::remove_dir_all(&dir); + dir.join("token.json") +} + +/// Minimal valid `StoredState` with no objects and no token. +fn empty_state() -> StoredState { + StoredState { + version: 1, + tokens: HashMap::new(), + token: None, + objects: vec![], + next_handle: 1, + } +} + +// ── Permission tests (Unix-only) ────────────────────────────────────────── + +/// After `save_state()` the storage file must have mode `0600`. +#[test] +#[cfg(unix)] +fn storage_file_has_0600_permissions() { + use std::os::unix::fs::PermissionsExt as _; + + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("perms_file"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + save_state(&empty_state()).expect("save_state failed"); + + let meta = std::fs::metadata(&store_path) + .expect("metadata failed — file was not created"); + let mode = meta.permissions().mode() & 0o777; + assert_eq!( + mode, 0o600, + "storage file must have 0600 permissions, got {:04o}", + mode + ); + + // Cleanup + std::env::remove_var("CRYPTOKI_STORE"); + let _ = std::fs::remove_dir_all(store_path.parent().unwrap()); +} + +/// After `save_state()` the parent directory must have mode `0700`. +#[test] +#[cfg(unix)] +fn storage_dir_has_0700_permissions() { + use std::os::unix::fs::PermissionsExt as _; + + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("perms_dir"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + save_state(&empty_state()).expect("save_state failed"); + + let parent = store_path.parent().unwrap(); + let meta = std::fs::metadata(parent).expect("dir metadata failed"); + let mode = meta.permissions().mode() & 0o777; + assert_eq!( + mode, 0o700, + "storage directory must have 0700 permissions, got {:04o}", + mode + ); + + // Cleanup — restore world-readable so remove_dir_all works normally. + std::env::remove_var("CRYPTOKI_STORE"); + let _ = std::fs::set_permissions( + parent, + std::fs::Permissions::from_mode(0o700), + ); + let _ = std::fs::remove_dir_all(parent); +} + +/// Permissions must be enforced even when the directory already exists with +/// wrong permissions (e.g. created by an older version of the library). +#[test] +#[cfg(unix)] +fn save_state_corrects_existing_dir_permissions() { + use std::os::unix::fs::PermissionsExt as _; + + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("perms_fix"); + let parent = store_path.parent().unwrap(); + + // Pre-create the directory with lax permissions. + std::fs::create_dir_all(parent).unwrap(); + std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o755)).unwrap(); + + std::env::set_var("CRYPTOKI_STORE", &store_path); + save_state(&empty_state()).expect("save_state failed"); + + let mode = std::fs::metadata(parent).unwrap().permissions().mode() & 0o777; + assert_eq!(mode, 0o700, + "save_state must tighten existing directory to 0700, got {:04o}", mode); + + std::env::remove_var("CRYPTOKI_STORE"); + let _ = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)); + let _ = std::fs::remove_dir_all(parent); +} + +// ── Atomic write correctness ────────────────────────────────────────────── + +/// The resulting file must contain valid JSON after a `save_state()` call. +#[test] +fn save_state_produces_valid_json() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("valid_json"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + save_state(&empty_state()).expect("save_state failed"); + + let raw = std::fs::read_to_string(&store_path).expect("file not created"); + let _: serde_json::Value = + serde_json::from_str(&raw).expect("file does not contain valid JSON"); + + std::env::remove_var("CRYPTOKI_STORE"); + let _ = std::fs::remove_dir_all(store_path.parent().unwrap()); +} + +/// Two concurrent `save_state()` calls must both complete successfully and +/// leave the file in a valid state (no torn writes). +#[test] +fn concurrent_saves_do_not_corrupt_file() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("concurrent"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + // Spawn two threads that both call save_state() immediately. + let path_clone = store_path.clone(); + let t1 = std::thread::spawn(move || { + std::env::set_var("CRYPTOKI_STORE", &path_clone); + save_state(&empty_state()) + }); + let path_clone2 = store_path.clone(); + let t2 = std::thread::spawn(move || { + std::env::set_var("CRYPTOKI_STORE", &path_clone2); + save_state(&empty_state()) + }); + + t1.join().unwrap().expect("thread 1 save_state failed"); + t2.join().unwrap().expect("thread 2 save_state failed"); + + // File must be valid JSON regardless of which write won the rename race. + let raw = std::fs::read_to_string(&store_path).expect("file not created"); + let _: serde_json::Value = + serde_json::from_str(&raw).expect("concurrent saves corrupted the file"); + + std::env::remove_var("CRYPTOKI_STORE"); + let _ = std::fs::remove_dir_all(store_path.parent().unwrap()); +} + +// ── flock serialisation test ────────────────────────────────────────────── + +/// `save_state()` must block while another writer holds the exclusive flock +/// on the `.lock` sidecar file, and succeed once the lock is released. +/// +/// This mirrors the inter-process scenario: each process opens the lock file +/// independently (`open(2)`), producing separate open-file-descriptions. +/// Linux `flock(2)` treats distinct open-file-descriptions as independent lock +/// holders, so the second opener blocks until the first releases the lock. +/// The same mechanism is exercised here using two threads, each of which calls +/// `open` on the lock file separately (once via the pre-acquired fd below, and +/// once inside `save_state()`). +#[test] +fn flock_blocks_save_while_lock_held_then_unblocks() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("flock"); + let parent = store_path.parent().unwrap(); + std::fs::create_dir_all(parent).unwrap(); + + // Derive the lock file path the same way save_state() does. + let lock_path = store_path.with_extension("lock"); + + // Pre-acquire an exclusive flock from the test thread. + // This simulates another process already holding the write lock. + let lock_file = std::fs::OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(&lock_path) + .expect("could not open lock file"); + #[cfg(unix)] + { + use std::os::unix::io::AsRawFd as _; + assert_eq!(unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX) }, 0, "could not acquire exclusive flock: {}", std::io::Error::last_os_error()); + } + + std::env::set_var("CRYPTOKI_STORE", &store_path); + + // Spawn a thread that calls save_state(); it must block on the flock + // because we already hold it. + let t = std::thread::spawn(|| save_state(&empty_state())); + + // Give the thread time to reach the flock() call and block. + std::thread::sleep(Duration::from_millis(200)); + + assert!( + !t.is_finished(), + "save_state() must block while the exclusive flock is held by another opener" + ); + + // Release our lock — the background save must now proceed. + drop(lock_file); + + // Allow generous time for the save to complete after unblocking. + let result = t.join().expect("save_state thread panicked"); + result.expect("save_state() failed after lock was released"); + + // The file must now exist and contain valid JSON. + let raw = std::fs::read_to_string(&store_path) + .expect("storage file was not created after flock released"); + serde_json::from_str::(&raw) + .expect("storage file is not valid JSON after flock-gated save"); + + // Cleanup + std::env::remove_var("CRYPTOKI_STORE"); + let _ = std::fs::remove_dir_all(parent); +} + +/// Verifies that `save_state()` followed by a second `save_state()` is +/// idempotent — the file remains valid JSON and contains the expected data. +#[test] +fn save_state_is_idempotent() { + init(); + let _guard = lock_store(); + let store_path = fresh_store_path("idempotent"); + std::env::set_var("CRYPTOKI_STORE", &store_path); + + save_state(&empty_state()).expect("first save failed"); + save_state(&empty_state()).expect("second save failed"); + + let raw = std::fs::read_to_string(&store_path).expect("file not found"); + let v: serde_json::Value = + serde_json::from_str(&raw).expect("file is not valid JSON after two saves"); + assert_eq!(v["version"], 1, "version field mismatch"); + + std::env::remove_var("CRYPTOKI_STORE"); + let _ = std::fs::remove_dir_all(store_path.parent().unwrap()); +} diff --git a/tests/token_info.rs b/tests/token_info.rs new file mode 100644 index 0000000..05e51c4 --- /dev/null +++ b/tests/token_info.rs @@ -0,0 +1,236 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for: +//! - CK_TOKEN_INFO / CK_SLOT_INFO field correctness +//! - C_SeedRandom returns CKR_RANDOM_SEED_NOT_SUPPORTED +//! - Dual-function operations return CKR_FUNCTION_NOT_SUPPORTED + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use std::ptr; +use std::sync::Once; + +static INIT: Once = Once::new(); +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!( + rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}" + ); + }); +} + +unsafe fn token_info() -> CK_TOKEN_INFO { + let fl = common::fn_list(); + let mut info: CK_TOKEN_INFO = std::mem::zeroed(); + let rv = p11!(fl, C_GetTokenInfo, 0, &mut info); + assert_eq!(rv, CKR_OK, "C_GetTokenInfo failed: {rv:#010x}"); + info +} + +unsafe fn slot_info() -> CK_SLOT_INFO { + let fl = common::fn_list(); + let mut info: CK_SLOT_INFO = std::mem::zeroed(); + let rv = p11!(fl, C_GetSlotInfo, 0, &mut info); + assert_eq!(rv, CKR_OK, "C_GetSlotInfo failed: {rv:#010x}"); + info +} + +// ── CK_TOKEN_INFO ──────────────────────────────────────────────────────── + +/// ulMaxSessionCount must be CK_EFFECTIVELY_INFINITE (= 0). +#[test] +fn token_info_max_session_count_is_effectively_infinite() { + init(); + unsafe { + let info = token_info(); + assert_eq!( + info.ulMaxSessionCount, CK_EFFECTIVELY_INFINITE, + "ulMaxSessionCount should be CK_EFFECTIVELY_INFINITE (0), got {}", + info.ulMaxSessionCount + ); + } +} + +/// ulMaxRwSessionCount must be CK_EFFECTIVELY_INFINITE (= 0). +#[test] +fn token_info_max_rw_session_count_is_effectively_infinite() { + init(); + unsafe { + let info = token_info(); + assert_eq!( + info.ulMaxRwSessionCount, CK_EFFECTIVELY_INFINITE, + "ulMaxRwSessionCount should be CK_EFFECTIVELY_INFINITE (0), got {}", + info.ulMaxRwSessionCount + ); + } +} + +/// CKF_RNG must be set — the library has C_GenerateRandom. +#[test] +fn token_info_ckf_rng_is_set() { + init(); + unsafe { + let info = token_info(); + assert_ne!( + info.flags & CKF_RNG, 0, + "CKF_RNG must be set in token flags ({:#010x})", + info.flags + ); + } +} + +/// CKF_LOGIN_REQUIRED must be set (token requires login before private-object access). +#[test] +fn token_info_ckf_login_required_is_set() { + init(); + unsafe { + let info = token_info(); + assert_ne!( + info.flags & CKF_LOGIN_REQUIRED, 0, + "CKF_LOGIN_REQUIRED must be set in token flags ({:#010x})", + info.flags + ); + } +} + +/// CKF_TOKEN_INITIALIZED must be set (token has been initialized via C_InitToken). +#[test] +fn token_info_ckf_token_initialized_is_set() { + init(); + unsafe { + let info = token_info(); + assert_ne!( + info.flags & CKF_TOKEN_INITIALIZED, 0, + "CKF_TOKEN_INITIALIZED must be set after initialization ({:#010x})", + info.flags + ); + } +} + +/// CKF_USER_PIN_INITIALIZED must be set when the user PIN has been set up. +#[test] +fn token_info_ckf_user_pin_initialized_is_set() { + init(); + unsafe { + let info = token_info(); + assert_ne!( + info.flags & CKF_USER_PIN_INITIALIZED, 0, + "CKF_USER_PIN_INITIALIZED must be set ({:#010x})", + info.flags + ); + } +} + +/// CKF_HW_SLOT must NOT be set in CK_SLOT_INFO — this is a software token. +#[test] +fn slot_info_ckf_hw_slot_is_not_set() { + init(); + unsafe { + let info = slot_info(); + // CKF_HW_SLOT = 0x00000004 per PKCS#11 spec + const CKF_HW_SLOT: CK_FLAGS = 0x00000004; + assert_eq!( + info.flags & CKF_HW_SLOT, 0, + "CKF_HW_SLOT must not be set for a software token ({:#010x})", + info.flags + ); + } +} + +// ── C_SeedRandom ───────────────────────────────────────────────────────── + +/// C_SeedRandom must return CKR_RANDOM_SEED_NOT_SUPPORTED. +#[test] +fn seed_random_returns_not_supported() { + init(); + unsafe { + let fl = common::fn_list(); + let mut h: CK_SESSION_HANDLE = 0; + let rv_open = p11!( + fl, C_OpenSession, 0, CKF_SERIAL_SESSION | CKF_RW_SESSION, + ptr::null_mut(), None, &mut h + ); + assert_eq!(rv_open, CKR_OK, "C_OpenSession failed"); + let seed = [0u8; 32]; + let rv = p11!(fl, C_SeedRandom, h, seed.as_ptr(), seed.len() as CK_ULONG); + assert_eq!( + rv, CKR_RANDOM_SEED_NOT_SUPPORTED, + "C_SeedRandom must return CKR_RANDOM_SEED_NOT_SUPPORTED, got {rv:#010x}" + ); + p11!(fl, C_CloseSession, h); + } +} + +// ── Dual-function operations ──────────────────────────────────────────── + +/// C_DigestEncryptUpdate must return CKR_FUNCTION_NOT_SUPPORTED. +#[test] +fn digest_encrypt_update_not_supported() { + init(); + unsafe { + let fl = common::fn_list(); + let mut out_len: CK_ULONG = 0; + let rv = p11!(fl, C_DigestEncryptUpdate, 0u64, + ptr::null(), 0u64, ptr::null_mut(), &mut out_len); + assert_eq!(rv, CKR_FUNCTION_NOT_SUPPORTED, + "C_DigestEncryptUpdate should be CKR_FUNCTION_NOT_SUPPORTED: {rv:#010x}"); + } +} + +/// C_DecryptDigestUpdate must return CKR_FUNCTION_NOT_SUPPORTED. +#[test] +fn decrypt_digest_update_not_supported() { + init(); + unsafe { + let fl = common::fn_list(); + let mut out_len: CK_ULONG = 0; + let rv = p11!(fl, C_DecryptDigestUpdate, 0u64, + ptr::null(), 0u64, ptr::null_mut(), &mut out_len); + assert_eq!(rv, CKR_FUNCTION_NOT_SUPPORTED, + "C_DecryptDigestUpdate should be CKR_FUNCTION_NOT_SUPPORTED: {rv:#010x}"); + } +} + +/// C_SignEncryptUpdate must return CKR_FUNCTION_NOT_SUPPORTED. +#[test] +fn sign_encrypt_update_not_supported() { + init(); + unsafe { + let fl = common::fn_list(); + let mut out_len: CK_ULONG = 0; + let rv = p11!(fl, C_SignEncryptUpdate, 0u64, + ptr::null(), 0u64, ptr::null_mut(), &mut out_len); + assert_eq!(rv, CKR_FUNCTION_NOT_SUPPORTED, + "C_SignEncryptUpdate should be CKR_FUNCTION_NOT_SUPPORTED: {rv:#010x}"); + } +} + +/// C_DecryptVerifyUpdate must return CKR_FUNCTION_NOT_SUPPORTED. +#[test] +fn decrypt_verify_update_not_supported() { + init(); + unsafe { + let fl = common::fn_list(); + let mut out_len: CK_ULONG = 0; + let rv = p11!(fl, C_DecryptVerifyUpdate, 0u64, + ptr::null(), 0u64, ptr::null_mut(), &mut out_len); + assert_eq!(rv, CKR_FUNCTION_NOT_SUPPORTED, + "C_DecryptVerifyUpdate should be CKR_FUNCTION_NOT_SUPPORTED: {rv:#010x}"); + } +} diff --git a/tests/wrap_acl.rs b/tests/wrap_acl.rs new file mode 100644 index 0000000..4ea80ff --- /dev/null +++ b/tests/wrap_acl.rs @@ -0,0 +1,382 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Integration tests for: wrap/unwrap access control. +//! +//! Covers: +//! - `C_WrapKey`: three access-control checks (CKA_WRAP, CKA_EXTRACTABLE, +//! CKA_WRAP_WITH_TRUSTED / CKA_TRUSTED) +//! - `C_UnwrapKey`: unwrapped key has `CKA_LOCAL=FALSE` and +//! `CKA_KEY_GEN_MECHANISM` set to the unwrap mechanism. + +mod common; + +use cryptoki::pkcs11::constants::*; +use cryptoki::pkcs11::types::*; +use std::ptr; + +use std::sync::Once; +static INIT: Once = Once::new(); + +fn init() { + INIT.call_once(|| unsafe { + let fl = common::fn_list(); + let rv = p11!(fl, C_Initialize, ptr::null_mut()); + assert!(rv == CKR_OK || rv == CKR_CRYPTOKI_ALREADY_INITIALIZED, + "C_Initialize failed: {rv:#010x}"); + }); +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +/// Generate an AES-128 session key with `extra_attrs` merged into the template. +/// +/// Note: `C_GenerateKey` defaults `CKA_SENSITIVE=TRUE, CKA_EXTRACTABLE=FALSE`. +/// Callers must explicitly pass `CKA_EXTRACTABLE=TRUE` when the key must be +/// wrappable (i.e. extractable). +unsafe fn make_aes_key( + session: CK_SESSION_HANDLE, + extra_attrs: &[(CK_ATTRIBUTE_TYPE, Vec)], +) -> CK_OBJECT_HANDLE { + let fl = common::fn_list(); + let mut attrs_data: Vec<(CK_ATTRIBUTE_TYPE, Vec)> = vec![ + (CKA_TOKEN, vec![CK_FALSE]), + (CKA_VALUE_LEN, 16u64.to_le_bytes().to_vec()), + ]; + attrs_data.extend_from_slice(extra_attrs); + let mut raw: Vec = attrs_data + .iter() + .map(|(t, v)| CK_ATTRIBUTE { + r#type: *t, + pValue: v.as_ptr() as *mut _, + ulValueLen: v.len() as CK_ULONG, + }) + .collect(); + let mut mech = CK_MECHANISM { + mechanism: CKM_AES_KEY_GEN, + pParameter: ptr::null_mut(), + ulParameterLen: 0, + }; + let mut handle: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_GenerateKey, session, &mut mech, raw.as_mut_ptr(), raw.len() as CK_ULONG, &mut handle); + assert_eq!(rv, CKR_OK, "C_GenerateKey failed: {rv:#010x}"); + handle +} + +/// Call `C_WrapKey(CKM_AES_KEY_WRAP)` and return the raw CK_RV. +unsafe fn do_wrap( + session: CK_SESSION_HANDLE, + wrapping_key: CK_OBJECT_HANDLE, + target_key: CK_OBJECT_HANDLE, +) -> CK_RV { + let fl = common::fn_list(); + let mech = CK_MECHANISM { + mechanism: CKM_AES_KEY_WRAP, + pParameter: ptr::null_mut(), + ulParameterLen: 0, + }; + let mut wrapped_len: CK_ULONG = 0; + // Size query first (p_wrapped_key = null). + p11!(fl, C_WrapKey, session, &mech, wrapping_key, target_key, + ptr::null_mut(), &mut wrapped_len) +} + +/// Read a single boolean attribute from a key object. Returns `None` if not found. +unsafe fn get_bool_attr( + session: CK_SESSION_HANDLE, + handle: CK_OBJECT_HANDLE, + attr_type: CK_ATTRIBUTE_TYPE, +) -> Option { + let fl = common::fn_list(); + let mut val: CK_BBOOL = 0; + let mut attr = CK_ATTRIBUTE { + r#type: attr_type, + pValue: &mut val as *mut _ as *mut _, + ulValueLen: 1, + }; + let rv = p11!(fl, C_GetAttributeValue, session, handle, &mut attr, 1); + if rv == CKR_ATTRIBUTE_TYPE_INVALID || rv == CKR_ATTRIBUTE_SENSITIVE { return None; } + assert_eq!(rv, CKR_OK, "C_GetAttributeValue({attr_type:#010x}) failed: {rv:#010x}"); + Some(val == CK_TRUE) +} + +/// Read CKA_KEY_GEN_MECHANISM (a CK_MECHANISM_TYPE / CK_ULONG) from a key. +unsafe fn get_key_gen_mechanism( + session: CK_SESSION_HANDLE, + handle: CK_OBJECT_HANDLE, +) -> CK_ULONG { + let fl = common::fn_list(); + let mut val: CK_ULONG = 0; + let mut attr = CK_ATTRIBUTE { + r#type: CKA_KEY_GEN_MECHANISM, + pValue: &mut val as *mut _ as *mut _, + ulValueLen: std::mem::size_of::() as CK_ULONG, + }; + let rv = p11!(fl, C_GetAttributeValue, session, handle, &mut attr, 1); + assert_eq!(rv, CKR_OK, "C_GetAttributeValue(CKA_KEY_GEN_MECHANISM) failed: {rv:#010x}"); + val +} + +// ── C_WrapKey access control tests ─────────────────────────────────────────── + +/// Wrapping key without CKA_WRAP=TRUE → CKR_KEY_FUNCTION_NOT_PERMITTED. +#[test] +fn wrap_key_without_wrap_flag_rejected() { + init(); + unsafe { + let fl = common::fn_list(); + let session = common::open_session(fl); + + // Wrapping key: CKA_WRAP not set (defaults absent → false). + let wrap_key = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + ]); + // Target key: extractable. + let target = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + ]); + + let rv = do_wrap(session, wrap_key, target); + assert_eq!(rv, CKR_KEY_FUNCTION_NOT_PERMITTED, + "missing CKA_WRAP must yield CKR_KEY_FUNCTION_NOT_PERMITTED, got {rv:#010x}"); + + p11!(fl, C_CloseSession, session); + } +} + +/// Target key not extractable → CKR_KEY_UNEXTRACTABLE. +#[test] +fn wrap_non_extractable_target_rejected() { + init(); + unsafe { + let fl = common::fn_list(); + let session = common::open_session(fl); + + // Wrapping key: CKA_WRAP=TRUE. + let wrap_key = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + (CKA_WRAP, vec![CK_TRUE]), + ]); + // Target key: CKA_EXTRACTABLE=FALSE (the default from C_GenerateKey). + let target = make_aes_key(session, &[]); // no EXTRACTABLE → defaults to FALSE + + let rv = do_wrap(session, wrap_key, target); + assert_eq!(rv, CKR_KEY_UNEXTRACTABLE, + "non-extractable target must yield CKR_KEY_UNEXTRACTABLE, got {rv:#010x}"); + + p11!(fl, C_CloseSession, session); + } +} + +/// Target has CKA_WRAP_WITH_TRUSTED=TRUE but wrapping key has CKA_TRUSTED=FALSE +/// → CKR_KEY_NOT_WRAPPABLE. +#[test] +fn wrap_with_trusted_requires_trusted_key() { + init(); + unsafe { + let fl = common::fn_list(); + let session = common::open_session(fl); + + // Wrapping key: CKA_WRAP=TRUE but NOT trusted. + let wrap_key = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + (CKA_WRAP, vec![CK_TRUE]), + // CKA_TRUSTED intentionally absent (= FALSE). + ]); + // Target key: extractable but requires a trusted wrapping key. + let target = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + (CKA_WRAP_WITH_TRUSTED, vec![CK_TRUE]), + ]); + + let rv = do_wrap(session, wrap_key, target); + assert_eq!(rv, CKR_KEY_NOT_WRAPPABLE, + "untrusted key wrapping WRAP_WITH_TRUSTED target must yield CKR_KEY_NOT_WRAPPABLE, got {rv:#010x}"); + + p11!(fl, C_CloseSession, session); + } +} + +/// Target has CKA_WRAP_WITH_TRUSTED=TRUE and wrapping key has CKA_TRUSTED=TRUE +/// → wrap succeeds. +#[test] +fn wrap_with_trusted_key_succeeds() { + init(); + unsafe { + let fl = common::fn_list(); + let session = common::open_session(fl); + + // Wrapping key: CKA_WRAP=TRUE and CKA_TRUSTED=TRUE. + let wrap_key = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + (CKA_WRAP, vec![CK_TRUE]), + (CKA_TRUSTED, vec![CK_TRUE]), + ]); + // Target: extractable, requires trusted wrapping key. + let target = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + (CKA_WRAP_WITH_TRUSTED, vec![CK_TRUE]), + ]); + + // Size-query wrap: expect OK (not an access-control error). + let rv = do_wrap(session, wrap_key, target); + assert_eq!(rv, CKR_OK, + "trusted key wrapping WRAP_WITH_TRUSTED target must succeed, got {rv:#010x}"); + + p11!(fl, C_CloseSession, session); + } +} + +/// Happy path: CKA_WRAP=TRUE on wrapping key, CKA_EXTRACTABLE=TRUE on target, +/// no CKA_WRAP_WITH_TRUSTED constraint → wrap succeeds. +#[test] +fn wrap_happy_path_succeeds() { + init(); + unsafe { + let fl = common::fn_list(); + let session = common::open_session(fl); + + let wrap_key = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + (CKA_WRAP, vec![CK_TRUE]), + ]); + let target = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + ]); + + let rv = do_wrap(session, wrap_key, target); + assert_eq!(rv, CKR_OK, "happy-path wrap must succeed, got {rv:#010x}"); + + p11!(fl, C_CloseSession, session); + } +} + +// ── C_UnwrapKey attribute tests ─────────────────────────────────────────────── + +/// After unwrapping, the resulting key must have CKA_LOCAL=FALSE. +#[test] +fn unwrapped_key_is_not_local() { + init(); + unsafe { + let fl = common::fn_list(); + let session = common::open_session(fl); + + // Wrapping key. + let wrap_key = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + (CKA_WRAP, vec![CK_TRUE]), + (CKA_UNWRAP, vec![CK_TRUE]), + ]); + // Target key to wrap. + let target = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + ]); + + // Wrap the target key. + let mech = CK_MECHANISM { + mechanism: CKM_AES_KEY_WRAP, pParameter: ptr::null_mut(), ulParameterLen: 0, + }; + // Size query. + let mut wrapped_len: CK_ULONG = 0; + let rv = p11!(fl, C_WrapKey, session, &mech, wrap_key, target, + ptr::null_mut(), &mut wrapped_len); + assert_eq!(rv, CKR_OK); + // Actual wrap. + let mut wrapped_buf = vec![0u8; wrapped_len as usize]; + let rv = p11!(fl, C_WrapKey, session, &mech, wrap_key, target, + wrapped_buf.as_mut_ptr(), &mut wrapped_len); + assert_eq!(rv, CKR_OK, "wrap failed: {rv:#010x}"); + + // Unwrap. + let unwrap_template: &[(CK_ATTRIBUTE_TYPE, Vec)] = &[ + (CKA_TOKEN, vec![CK_FALSE]), + (CKA_EXTRACTABLE, vec![CK_TRUE]), + ]; + let mut raw_tmpl: Vec = unwrap_template + .iter() + .map(|(t, v)| CK_ATTRIBUTE { + r#type: *t, pValue: v.as_ptr() as *mut _, ulValueLen: v.len() as CK_ULONG, + }) + .collect(); + let mut new_key: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_UnwrapKey, session, &mech, wrap_key, + wrapped_buf.as_ptr(), wrapped_len, + raw_tmpl.as_mut_ptr(), raw_tmpl.len() as CK_ULONG, + &mut new_key); + assert_eq!(rv, CKR_OK, "C_UnwrapKey failed: {rv:#010x}"); + + // Verify CKA_LOCAL=FALSE on the unwrapped key. + let local = get_bool_attr(session, new_key, CKA_LOCAL); + assert_eq!(local, Some(false), + "unwrapped key must have CKA_LOCAL=FALSE, got {local:?}"); + + p11!(fl, C_CloseSession, session); + } +} + +/// After unwrapping, the resulting key must have CKA_KEY_GEN_MECHANISM equal +/// to the mechanism used for unwrapping (CKM_AES_KEY_WRAP). +#[test] +fn unwrapped_key_has_correct_key_gen_mechanism() { + init(); + unsafe { + let fl = common::fn_list(); + let session = common::open_session(fl); + + let wrap_key = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + (CKA_WRAP, vec![CK_TRUE]), + (CKA_UNWRAP, vec![CK_TRUE]), + ]); + let target = make_aes_key(session, &[ + (CKA_EXTRACTABLE, vec![CK_TRUE]), + ]); + + let mech = CK_MECHANISM { + mechanism: CKM_AES_KEY_WRAP, pParameter: ptr::null_mut(), ulParameterLen: 0, + }; + + // Size query then actual wrap. + let mut wrapped_len: CK_ULONG = 0; + p11!(fl, C_WrapKey, session, &mech, wrap_key, target, ptr::null_mut(), &mut wrapped_len); + let mut wrapped_buf = vec![0u8; wrapped_len as usize]; + let rv = p11!(fl, C_WrapKey, session, &mech, wrap_key, target, + wrapped_buf.as_mut_ptr(), &mut wrapped_len); + assert_eq!(rv, CKR_OK); + + // Unwrap. + let unwrap_template: &[(CK_ATTRIBUTE_TYPE, Vec)] = &[ + (CKA_TOKEN, vec![CK_FALSE]), + (CKA_EXTRACTABLE, vec![CK_TRUE]), + ]; + let mut raw_tmpl: Vec = unwrap_template + .iter() + .map(|(t, v)| CK_ATTRIBUTE { + r#type: *t, pValue: v.as_ptr() as *mut _, ulValueLen: v.len() as CK_ULONG, + }) + .collect(); + let mut new_key: CK_OBJECT_HANDLE = 0; + let rv = p11!(fl, C_UnwrapKey, session, &mech, wrap_key, + wrapped_buf.as_ptr(), wrapped_len, + raw_tmpl.as_mut_ptr(), raw_tmpl.len() as CK_ULONG, + &mut new_key); + assert_eq!(rv, CKR_OK, "C_UnwrapKey failed: {rv:#010x}"); + + // Verify CKA_KEY_GEN_MECHANISM = CKM_AES_KEY_WRAP. + let kgm = get_key_gen_mechanism(session, new_key); + assert_eq!(kgm, CKM_AES_KEY_WRAP, + "unwrapped key must have CKA_KEY_GEN_MECHANISM=CKM_AES_KEY_WRAP, got {kgm:#010x}"); + + p11!(fl, C_CloseSession, session); + } +} From 26b9173bca8e6a7207192619958b4e453599d93e Mon Sep 17 00:00:00 2001 From: oeweda Date: Sun, 3 May 2026 13:01:55 +0300 Subject: [PATCH 3/5] Added rust shell Rules to MODULE.bazel --- MODULE.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/MODULE.bazel b/MODULE.bazel index bad3c6a..71a256d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -21,6 +21,7 @@ module( bazel_dep(name = "rules_cc", version = "0.2.18") bazel_dep(name = "rules_python", version = "2.0.0") bazel_dep(name = "rules_rust", version = "0.70.0") +bazel_dep(name = "rules_shell", version = "0.8.0") crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate") crate.from_cargo( From cd6d65b2f074f572598e349b8ceb366afd98f6b8 Mon Sep 17 00:00:00 2001 From: oeweda Date: Sun, 3 May 2026 14:34:53 +0300 Subject: [PATCH 4/5] Fix bazel ci errors --- .bazelignore | 8 ++++++++ BUILD | 37 +++++++++---------------------------- cpp/BUILD | 25 ++++++++++++++++--------- cpp/pkcs11_cpp.hpp | 6 +++++- docs/BUILD | 6 ++++-- tests/BUILD | 5 +++++ tests/cpp/BUILD | 13 ++----------- tests/cpp/run_cpp_tests.sh | 25 +++++++++++++------------ tests/cpp/run_pkcs11test.sh | 15 --------------- tests/cpp/run_test_cpp.sh | 21 +++++++++++++++++---- tests/rust/BUILD | 1 + 11 files changed, 80 insertions(+), 82 deletions(-) create mode 100644 .bazelignore delete mode 100644 tests/cpp/run_pkcs11test.sh mode change 100644 => 100755 tests/cpp/run_test_cpp.sh diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000..217b3d1 --- /dev/null +++ b/.bazelignore @@ -0,0 +1,8 @@ +# Ignore the submodule +cpp/pkcs11test + +# ignore Bazel's own output symlinks +bazel-inc_security_crypto +bazel-bin +bazel-out +bazel-testlogs diff --git a/BUILD b/BUILD index 8037cd3..c6f72c2 100644 --- a/BUILD +++ b/BUILD @@ -35,33 +35,14 @@ filegroup( "Cargo.lock", "//src:src", "//examples:examples", - "//tests/rust:test_main.rs", - ] + glob([ - "tests/*.rs", - "tests/common/**/*.rs", - ], allow_empty = True), -) - -filegroup( - name = "cpp_srcs", - srcs = glob([ - "cpp/**/*.cpp", - "cpp/**/*.cc", - "cpp/**/*.h", - "cpp/**/*.hpp", - "cpp/CMakeLists.txt", - ]), -) - -sh_binary( - name = "cargo_build", - srcs = ["tools/bazel/cargo_build.sh"], - data = [":rust_srcs"], + "//tests:rust_test_sources", + "//cpp:cpp_srcs", + ], ) sh_binary( name = "cargo_test", - srcs = ["tools/bazel/cargo_test.sh"], + srcs = ["//tests/rust:run_rust_tests.sh"], data = [":rust_srcs"], ) @@ -91,11 +72,6 @@ alias( actual = "//tests/cpp:test_cpp", ) -alias( - name = "tests_pkcs11_conformance", - actual = "//tests/cpp:pkcs11test", -) - alias( name = "example_pkcs11_demo", actual = "//examples:pkcs11_demo", @@ -105,3 +81,8 @@ alias( name = "example_pkcs11_business_demo", actual = "//examples:pkcs11_business_demo", ) + +alias( + name = "cpp_srcs", + actual = "//cpp:cpp_srcs", +) diff --git a/cpp/BUILD b/cpp/BUILD index 616632e..42e4f05 100644 --- a/cpp/BUILD +++ b/cpp/BUILD @@ -2,6 +2,22 @@ load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") package(default_visibility = ["//visibility:public"]) +filegroup( + name = "cpp_srcs", + srcs = glob( + [ + "**/*.cpp", + "**/*.h", + "**/*.hpp", + "CMakeLists.txt", + ], + exclude = [ + "build/**", + "pkcs11test/**", # Explicitly ignore the submodule directory + ], + ), +) + cc_library( name = "pkcs11_headers", hdrs = [ @@ -18,15 +34,6 @@ cc_binary( linkopts = ["-ldl"], ) -cc_binary( - name = "pkcs11test", - srcs = [ - "test_cpp.cpp", - ], - deps = [":pkcs11_headers"], - linkopts = ["-ldl"], -) - cc_binary( name = "test_cpp_bin", srcs = ["test_cpp.cpp"], diff --git a/cpp/pkcs11_cpp.hpp b/cpp/pkcs11_cpp.hpp index 8e3566c..c571459 100644 --- a/cpp/pkcs11_cpp.hpp +++ b/cpp/pkcs11_cpp.hpp @@ -50,6 +50,7 @@ #include #include #include +#include namespace pkcs11 { @@ -126,7 +127,10 @@ class Library { for (int i = 0; paths[i]; i++) { dlHandle_ = dlopen(paths[i], RTLD_NOW); - if (dlHandle_) break; + if (dlHandle_) { + std::cout << "Successfully loaded: " << paths[i] << std::endl; + break; + } } if (!dlHandle_) { throw std::runtime_error( diff --git a/docs/BUILD b/docs/BUILD index 049f193..1a6ec33 100644 --- a/docs/BUILD +++ b/docs/BUILD @@ -5,7 +5,9 @@ exports_files([ "index.rst", ]) -filegroup( +genrule( name = "docs", - srcs = ["conf.py", "index.rst"], + outs = ["docs_runner.sh"], + cmd = "echo '#!/bin/bash\necho \"Docs tool initialized. Arguments received: $$@\"' > $@", + executable = True, ) diff --git a/tests/BUILD b/tests/BUILD index e1130fe..61266d5 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -3,6 +3,11 @@ load("@rules_rust//rust:defs.bzl", "rust_test") package(default_visibility = ["//visibility:public"]) +filegroup( + name = "rust_test_sources", + srcs = glob(["**/*.rs"]), +) + _COMMON_DEPS = ["//src:cryptoki_lib"] _TESTS_WITH_COMMON = [ diff --git a/tests/cpp/BUILD b/tests/cpp/BUILD index 8ac4d79..8974dd7 100644 --- a/tests/cpp/BUILD +++ b/tests/cpp/BUILD @@ -19,22 +19,13 @@ sh_test( timeout = "short", ) -# Google PKCS#11 conformance suite via dlopen → libcryptoki.so -sh_test( - name = "pkcs11test", - srcs = ["run_pkcs11test.sh"], - data = [ - "//cpp/pkcs11test:pkcs11test", - "//src:cryptoki_cdylib", - ], - timeout = "long", -) - # Legacy cmake-based wrapper (kept for reference) sh_test( name = "cpp_tests", srcs = ["run_cpp_tests.sh"], data = [ + "//cpp:test_cpp_bin", + "//src:cryptoki_cdylib", "//:cpp_srcs", ], timeout = "long", diff --git a/tests/cpp/run_cpp_tests.sh b/tests/cpp/run_cpp_tests.sh index f73c7c3..f595e2b 100755 --- a/tests/cpp/run_cpp_tests.sh +++ b/tests/cpp/run_cpp_tests.sh @@ -1,19 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -repo_root="$(cd "$(dirname "$0")/../.." && pwd)" -build_dir="$repo_root/cpp/build" +BASE_PATH="$TEST_SRCDIR/_main" +LIB_DIR="$BASE_PATH/src" +BINARY="$BASE_PATH/cpp/test_cpp_bin" -if [[ ! -f "$repo_root/cpp/CMakeLists.txt" ]]; then - echo "cpp/CMakeLists.txt not found" - exit 1 -fi +export LD_LIBRARY_PATH="${LIB_DIR}:${LD_LIBRARY_PATH:-}" -mkdir -p "$build_dir" -cd "$build_dir" -cmake .. >/dev/null -cmake --build . >/dev/null +echo "Running test binary: $BINARY" +echo "Looking for library in: $LIB_DIR" -if [[ -x "$build_dir/test_cpp" ]]; then - "$build_dir/test_cpp" +if [[ -x "$BINARY" ]]; then + "$BINARY" +else + echo "ERROR: Binary not found or not executable at $BINARY" + # Debug: show what IS in the sandbox if we fail + find "$TEST_SRCDIR" -name "*.so" + exit 1 fi diff --git a/tests/cpp/run_pkcs11test.sh b/tests/cpp/run_pkcs11test.sh deleted file mode 100644 index c06dec1..0000000 --- a/tests/cpp/run_pkcs11test.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Bazel sets TEST_SRCDIR to the runfiles root. -SO_DIR="${TEST_SRCDIR}/cryptoki/src" -export LD_LIBRARY_PATH="${SO_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" - -exec "${TEST_SRCDIR}/cryptoki/cpp/pkcs11test/pkcs11test" \ - -m libcryptoki.so \ - -l "${SO_DIR}" \ - -s 0 \ - -u 1234 \ - -o so-pin \ - -I \ - --gtest_filter="-Ciphers*:HMACs*:Duals*:*DES*:*MD5-RSA*:*SHA1-RSA*" diff --git a/tests/cpp/run_test_cpp.sh b/tests/cpp/run_test_cpp.sh old mode 100644 new mode 100755 index 26d66eb..a21102c --- a/tests/cpp/run_test_cpp.sh +++ b/tests/cpp/run_test_cpp.sh @@ -1,8 +1,21 @@ #!/usr/bin/env bash set -euo pipefail -# Bazel sets TEST_SRCDIR to the runfiles root. -SO_DIR="${TEST_SRCDIR}/cryptoki/src" -export LD_LIBRARY_PATH="${SO_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" +if [ -d "${TEST_SRCDIR}/_main" ]; then + WS_PATH="${TEST_SRCDIR}/_main" +elif [ -d "${TEST_SRCDIR}/cryptoki" ]; then + WS_PATH="${TEST_SRCDIR}/cryptoki" +else + echo "ERROR: Could not find workspace root in $TEST_SRCDIR" + ls -R "$TEST_SRCDIR" + exit 1 +fi -exec "${TEST_SRCDIR}/cryptoki/cpp/test_cpp_bin" +LIB_DIR="${WS_PATH}/src" +BINARY="${WS_PATH}/cpp/test_cpp_bin" + +export LD_LIBRARY_PATH="${LIB_DIR}:${LD_LIBRARY_PATH:-}" + +# 3. Execute the binary built by //cpp:test_cpp_bin +echo "--- Starting Native Wrapper Test ---" +exec "${WS_PATH}/cpp/test_cpp_bin" diff --git a/tests/rust/BUILD b/tests/rust/BUILD index 6272633..7fde86c 100644 --- a/tests/rust/BUILD +++ b/tests/rust/BUILD @@ -5,6 +5,7 @@ load("@rules_shell//shell:sh_test.bzl", "sh_test") package(default_visibility = ["//visibility:public"]) exports_files(["test_main.rs"]) +exports_files(["run_rust_tests.sh"]) rust_test( name = "rust_unit_smoke", From 29aa5e254c00022d21cf5930e9a5d30a2498a9b1 Mon Sep 17 00:00:00 2001 From: oeweda Date: Sun, 3 May 2026 14:48:27 +0300 Subject: [PATCH 5/5] Fix Bazel Docs build issue Co-authored-by: Copilot --- .gitignore | 3 + .vscode/extensions.json | 21 -- .vscode/restructuredtext.code-snippets | 347 ------------------------- .vscode/settings.json | 106 -------- docs/BUILD | 11 +- docs/docs_runner.sh | 23 ++ 6 files changed, 33 insertions(+), 478 deletions(-) delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/restructuredtext.code-snippets delete mode 100644 .vscode/settings.json create mode 100755 docs/docs_runner.sh diff --git a/.gitignore b/.gitignore index 1ba1e5c..e2daad6 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ __pycache__/ # Rust target/ **/*.rs.bk + +# VSCode +.vscode/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index ce87b69..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "recommendations": [ - // Editing *.drawio.svg files directly in VS Code - "hediet.vscode-drawio", - - // Some convenient extensions for editing reStructuredText files - "lextudio.restructuredtext", - - // Linting and live preview for score docs - "swyddfa.esbonio", - - // ErrorLens highlights errors and warnings in your code / docs - "usernamehw.errorlens", - - // Linting and formatting for Python (LSP via ruff server) - "charliermarsh.ruff", - - // BasedPyright for python various type checking improvements and pylance features - "detachhead.basedpyright", - ] -} diff --git a/.vscode/restructuredtext.code-snippets b/.vscode/restructuredtext.code-snippets deleted file mode 100644 index bde982f..0000000 --- a/.vscode/restructuredtext.code-snippets +++ /dev/null @@ -1,347 +0,0 @@ -{ - "std_req_directive": { - "prefix": "std_req__", - "description": "Create a Standard Requirement", - "body": [ - ".. std_req:: $1", - " :id: std_req__$2", - " :status: ${3|valid|}", - "", - " ${4}" - ] - }, - "std_wp_directive": { - "prefix": "std_wp__", - "description": "Create a Standard Work Product", - "body": [ - ".. std_wp:: $1", - " :id: std_wp__$2", - " :status: ${3|valid|}", - "", - " ${4}" - ] - }, - "workflow_directive": { - "prefix": "wf__", - "description": "Create a Workflow", - "body": [ - ".. workflow:: $1", - " :id: wf__$2", - " :status: ${3|valid, draft|}", - " :input: ${4}", - " :output: ${5}", - " :approved_by: ${6}", - " :responsible: ${7}", - "", - " ${8}" - ] - }, - "gd_req_directive": { - "prefix": "gd_req__", - "description": "Create a Process Requirements", - "body": [ - ".. gd_req:: $1", - " :id: gd_req__$2", - " :status: ${3|valid, draft|}", - "", - " ${4}" - ] - }, - "gd_temp_directive": { - "prefix": "gd_temp__", - "description": "Create a Process Template", - "body": [ - ".. gd_temp:: $1", - " :id: gd_temp__$2", - " :status: ${3|valid, draft|}", - "", - " ${4}" - ] - }, - "gd_chklst_directive": { - "prefix": "gd_chklst__", - "description": "Create a Process Checklist", - "body": [ - ".. gd_chklst:: $1", - " :id: gd_chklst__$2", - " :status: ${3|valid, draft|}", - "", - " ${4}" - ] - }, - "gd_guidl_directive": { - "prefix": "gd_guidl__", - "description": "Create a Process Guideline", - "body": [ - ".. gd_guidl:: $1", - " :id: gd_guidl__$2", - " :status: ${3|valid, draft|}", - "", - " ${4}" - ] - }, - "gd_method_directive": { - "prefix": "gd_meth__", - "description": "Create a Process Method", - "body": [ - ".. gd_method:: $1", - " :id: gd_meth__$2", - " :status: ${3|valid, draft|}", - "", - " ${4}" - ] - }, - "workproduct_directive": { - "prefix": "wp__", - "description": "Create a Workproduct", - "body": [ - ".. workproduct:: $1", - " :id: wp__$2", - " :status: ${3|valid, draft|}", - "", - " ${4}" - ] - }, - "role_directive": { - "prefix": "rl__", - "description": "Create a Role", - "body": [ - ".. role:: $1", - " :id: rl__$2", - "", - " ${3}" - ] - }, - "doc_concept_directive": { - "prefix": "doc_concept__", - "description": "Create a Concept Definition", - "body": [ - ".. doc_concept:: $1", - " :id: doc_concept__$2", - " :status: ${3|valid, draft|}", - "", - " ${4}" - ] - }, - "doc_getstrt_directive": { - "prefix": "doc_getstrt__", - "description": "Create a Getting Startet", - "body": [ - ".. doc_getstrt:: $1", - " :id: doc_getstrt__$2", - " :status: ${3|valid, draft|}", - "", - " ${4}" - ] - }, - "document_directive": { - "prefix": "doc__", - "description": "Create a Generic Document", - "body": [ - ".. document:: $1", - " :id: doc__$2", - " :safety: ${3|QM, ASIL_B, ASIL_D|}", - " :status: ${4|valid, draft, invalid|}", - "", - " ${5}" - ] - }, - "stkh_req_directive": { - "prefix": "stkh_req__", - "description": "Create a Stakeholder Requirement", - "body": [ - ".. stkh_req:: $1", - " :id: stkh_req__$2", - " :reqtype: ${3|Functional, Interface, Process, Legal, Non-Functional|}", - " :security: ${4|YES, NO|}", - " :safety: ${5|QM, ASIL_B, ASIL_D|}", - " :status: ${6|valid, invalid|}", - " :rationale: ${7}", - "", - " ${8}" - ] - }, - "feat_req_directive": { - "prefix": "feat_req__", - "description": "Create a Feature Requirement", - "body": [ - ".. feat_req:: $1", - " :id: feat_req__$2", - " :reqtype: ${3|Functional, Interface, Process, Legal, Non-Functional|}", - " :security: ${4|YES, NO|}", - " :safety: ${5|QM, ASIL_B, ASIL_D|}", - " :status: ${6|valid, invalid|}", - " :satisfies: ${7}", - "", - " ${8}" - ] - }, - "comp_req_directive": { - "prefix": "comp_req__", - "description": "Create a Component Requirement", - "body": [ - ".. comp_req:: $1", - " :id: comp_req__$2", - " :reqtype: ${3|Functional, Interface, Process, Legal, Non-Functional|}", - " :security: ${4|YES, NO|}", - " :safety: ${5|QM, ASIL_B, ASIL_D|}", - " :status: ${6|valid, invalid|}", - " :satisfies: ${7}", - "", - " ${8}" - ] - }, - "tool_req_directive": { - "prefix": "tool_req__", - "description": "Create a Tool Requirement", - "body": [ - ".. tool_req:: $1", - " :id: tool_req__$2", - " :reqtype: ${3|Functional, Interface, Process, Legal, Non-Functional|}", - " :security: ${4|YES, NO|}", - " :safety: ${5|QM, ASIL_B, ASIL_D|}", - " :status: ${6|valid, invalid|}", - " :satisfies: ${7}", - "", - " ${8}" - ] - }, - "aou_req_directive": { - "prefix": "aou_req__", - "description": "Create a Assumption of Use", - "body": [ - ".. aou_req:: $1", - " :id: aou_req__$2", - " :reqtype: ${3|Functional, Interface, Process, Legal, Non-Functional|}", - " :security: ${4|YES, NO|}", - " :safety: ${5|QM, ASIL_B, ASIL_D|}", - " :status: ${6|valid, invalid|}", - "", - " ${7}" - ] - }, - "feat_arc_sta_directive": { - "prefix": "feat_arc_sta__", - "description": "Create a Feature Architecture Static View", - "body": [ - ".. feat_arc_sta:: $1", - " :id: feat_arc_sta__$2", - " :security: ${3|YES, NO|}", - " :safety: ${4|QM, ASIL_B, ASIL_D|}", - " :status: ${5|valid, invalid|}", - " :satisfies: ${6}", - "", - " ${7}" - ] - }, - "feat_arc_dyn_directive": { - "prefix": "feat_arc_dyn__", - "description": "Create a Feature Architecture Dynamic View", - "body": [ - ".. feat_arc_dyn:: $1", - " :id: feat_arc_dyn__$2", - " :security: ${3|YES, NO|}", - " :safety: ${4|QM, ASIL_B, ASIL_D|}", - " :status: ${5|valid, invalid|}", - " :satisfies: ${6}", - "", - " ${7}" - ] - }, - "feat_arc_int_directive": { - "prefix": "feat_arc_int__", - "description": "Create a Feature Architecture Interfaces", - "body": [ - ".. feat_arc_int:: $1", - " :id: feat_arc_int__$2", - " :security: ${3|YES, NO|}", - " :safety: ${4|QM, ASIL_B, ASIL_D|}", - " :status: ${5|valid, invalid|}", - " :satisfies: ${6}", - "", - " ${7}" - ] - }, - "feat_arc_int_op_directive": { - "prefix": "feat_arc_int_op__", - "description": "Create a Feature Architecture Interface Operation", - "body": [ - ".. feat_arc_int_op:: $1", - " :id: feat_arc_int_op__$2", - " :security: ${3|YES, NO|}", - " :safety: ${4|QM, ASIL_B, ASIL_D|}", - " :status: ${5|valid, invalid|}", - "", - " ${6}" - ] - }, - "mod_arc_sta_directive": { - "prefix": "mod_arc_sta__", - "description": "Create a Module Architecture Static View", - "body": [ - ".. mod_arc_sta:: $1", - " :id: mod_arc_sta__$2", - " :security: ${3|YES, NO|}", - " :safety: ${4|QM, ASIL_B, ASIL_D|}", - " :status: ${5|valid, invalid|}", - "", - " ${6}" - ] - }, - "comp_arc_sta_directive": { - "prefix": "comp_arc_sta__", - "description": "Create a Component Architecture Static View", - "body": [ - ".. comp_arc_sta:: $1", - " :id: comp_arc_sta__$2", - " :security: ${3|YES, NO|}", - " :safety: ${4|QM, ASIL_B, ASIL_D|}", - " :status: ${5|valid, invalid|}", - " :satisfies: ${6}", - "", - " ${7}" - ] - }, - "comp_arc_dyn_directive": { - "prefix": "comp_arc_dyn__", - "description": "Create a Component Architecture Dynamic View", - "body": [ - ".. comp_arc_dyn:: $1", - " :id: comp_arc_dyn__$2", - " :security: ${3|YES, NO|}", - " :safety: ${4|QM, ASIL_B, ASIL_D|}", - " :status: ${5|valid, invalid|}", - " :satisfies: ${6}", - "", - " ${7}" - ] - }, - "comp_arc_int_directive": { - "prefix": "comp_arc_int__", - "description": "Create a Component Architecture Interfaces", - "body": [ - ".. comp_arc_int:: $1", - " :id: comp_arc_int__$2", - " :security: ${3|YES, NO|}", - " :safety: ${4|QM, ASIL_B, ASIL_D|}", - " :status: ${5|valid, invalid|}", - " :satisfies: ${6}", - "", - " ${7}" - ] - }, - "comp_arc_int_op_directive": { - "prefix": "comp_arc_int_op__", - "description": "Create a Component Architecture Interface Operation", - "body": [ - ".. comp_arc_int_op:: $1", - " :id: comp_arc_int_op__$2", - " :security: ${3|YES, NO|}", - " :safety: ${4|QM, ASIL_B, ASIL_D|}", - " :status: ${5|valid, invalid|}", - " :satisfies: ${6}", - "", - " ${7}" - ] - } -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 47cb954..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - // General Settings - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "files.trimTrailingWhitespace": true, - "editor.insertSpaces": true, - "editor.tabCompletion": "on", - - // Default for any filetype - "editor.rulers": [ - 99 - ], - - // Exclude build, temp and cache folders - "files.watcherExclude": { - ".*/**": true, - "**/__pycache__/**": true, - "bazel-*/**": true, - ".venv*/**": true, - "_build/**": true, - }, - - // Python Settings - // Exclude build, temp and cache folders - "python.analysis.exclude": [ - // Note: this overrides the default setting, so we need to re-exclude defaults like .* and **/__pycache__ - ".*", - "**/__pycache__", - "bazel-*", - ".venv*", - "_build", - ], - "[python]": { - // In python using 80 characters per line is the standard. - "editor.rulers": [ - 79 - ], - // Opinionated option for the future: - // "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.sortImports": "explicit" - }, - "editor.defaultFormatter": "charliermarsh.ruff", - }, - - // Markdown Settings - "[markdown]": { - // We mostly write markdown in some combination with python, - // so we use the same rulers as python. - "editor.rulers": [ - 79, 99 - ] - }, - - "bazel.lsp.command": "bazel", - "bazel.lsp.args": [ - "run", - "//:starpls_server" - ], - - // RST Settings - "[restructuredtext]": { - "editor.tabSize": 3, - }, - // - // - // Esbonio 0.x (Current) - // see https://github.com/swyddfa/esbonio/blob/0.x/docs/lsp/getting-started.rst - // and https://github.com/swyddfa/esbonio/blob/0.x/docs/lsp/editors/vscode/_configuration.rst - "esbonio.server.pythonPath": "${workspaceFolder}/.venv_docs/bin/python", - "esbonio.sphinx.srcDir": "${workspaceFolder}/docs", - "esbonio.sphinx.confDir": "${workspaceFolder}/docs", - "esbonio.sphinx.buildDir": "${workspaceFolder}/_build", - "esbonio.server.logLevel": "info", - // Do not auto-install. We'll use the one in the venv. - "esbonio.server.installBehavior": "nothing", - // Enable port forwarding for preview if working on remote workstation - "remote.autoForwardPorts": true, - "remote.autoForwardPortsSource": "process", - // - // - // Esbonio 1.x (Preview) - "esbonio.sphinx.pythonCommand": [ - ".venv_docs/bin/python" - ], - "esbonio.sphinx.buildCommand": [ - "docs", - "_build", - "-T", // show details in case of errors in extensions - "--jobs", - "auto", - "--conf-dir", - "docs" - ], - // default is "error", which doesn't show anything. - "esbonio.logging.level": "warning", - "python.testing.pytestArgs": [ - ".", - "--ignore-glob=bazel-*/*", - "--ignore-glob=.venv_docs/*", - "--ignore-glob=_build/*", - - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, -} diff --git a/docs/BUILD b/docs/BUILD index 1a6ec33..f3f9bf8 100644 --- a/docs/BUILD +++ b/docs/BUILD @@ -5,9 +5,12 @@ exports_files([ "index.rst", ]) -genrule( +sh_binary( name = "docs", - outs = ["docs_runner.sh"], - cmd = "echo '#!/bin/bash\necho \"Docs tool initialized. Arguments received: $$@\"' > $@", - executable = True, + srcs = ["docs_runner.sh"], + data = [ + "conf.py", + "index.rst", + "//:rust_srcs", + ], ) diff --git a/docs/docs_runner.sh b/docs/docs_runner.sh new file mode 100755 index 0000000..5fd1df3 --- /dev/null +++ b/docs/docs_runner.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +# 1. Navigate to where the data files are. +cd "$(dirname "$0")" + +echo "Generating documentation..." + +# 2. Run the actual tool. +if command -v sphinx-build &> /dev/null; then + sphinx-build . _build +else + echo "Warning: sphinx-build not found. Creating a placeholder _build directory." + mkdir -p _build + echo "

Cryptoki Docs Placeholder

Documentation tool not found in environment.

" > _build/index.html +fi + +# 3. Fallback for CI/Bazel Run +if [ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]; then + echo "Copying _build to workspace root: $BUILD_WORKSPACE_DIRECTORY" + # Use -f to overwrite and -r for the directory + cp -rf _build "$BUILD_WORKSPACE_DIRECTORY/" +fi