This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
CAS .NET SDK is a managed cryptographic toolkit published as the cas-dotnet-sdk NuGet package. It is a thin, high-level .NET API over a native Rust FFI layer (cas-core-lib), which itself wraps the RustCrypto and Dalek-Cryptography suites. The .NET code does almost no cryptography itself — it marshals data to/from the native library.
cas-dotnet-sdk/— the SDK class library (NuGet package source). Multi-targetsnet6.0–net10.0.cas-dotnet-sdk-tests/— xUnit test project, references the SDK directly.cas-core-lib/— git submodule containing the Rust source compiled tocas_core_lib.dll(Windows) /libcas_core_lib.so(Linux) /libcas_core_lib.dylib(macOS). Clone with--recursiveor rungit submodule update --init --recursive.docs/EXAMPLES.md— user-facing usage samples. Must be kept current with the public API (see "Keeping examples in sync").
The Rust native library must be built and present alongside the managed assemblies, or all P/Invoke calls fail at runtime. The SDK .csproj automates this via MSBuild targets (BuildRust / StageRustArtifacts) that run cargo build --release before each build — so a plain dotnet build builds Rust too. This requires a working Rust toolchain (cargo) on PATH.
# Full build (also compiles the Rust submodule via the BuildRust target)
dotnet build cas-dotnet-sdk.sln
# Skip the Rust build when the native lib is already staged (much faster)
dotnet build cas-dotnet-sdk.sln -p:BuildNativeRust=false
# Build the Rust lib manually
cd cas-core-lib && cargo build --release
# Run all tests for one framework (must specify -f; the project multi-targets)
dotnet test ./cas-dotnet-sdk-tests/cas-dotnet-sdk-tests.csproj -c Release -f net8.0
# Run a single test class or method
dotnet test ./cas-dotnet-sdk-tests/cas-dotnet-sdk-tests.csproj -f net8.0 --filter "FullyQualifiedName~SHAWrapperTests"
dotnet test ./cas-dotnet-sdk-tests/cas-dotnet-sdk-tests.csproj -f net8.0 --filter "DisplayName~Hash512"The same commands work for local development on macOS (Apple Silicon): a Rust toolchain on PATH builds libcas_core_lib.dylib, and the StageRustArtifacts MSBuild target stages it into artifacts/native/osx-arm64/ automatically — no extra flags needed.
Note: CI (.github/workflows/pr-tests-*.yml) builds cargo build --release separately, then copies the native lib into the test bin/Release/<tfm> directory before running dotnet test. If you build the Rust lib by hand and the MSBuild target is skipped, the native artifact must end up next to the test assembly the same way.
Most known-answer vectors are committed next to the test assembly: NIST CAVS .rsp files under AES/Data and SHA/Data, and the trimmed NIST ACVP nist_*.json files under PQC/Data (all copied to output via Content items in the test .csproj). The Project Wycheproof vectors are the exception: they are downloaded, not committed (matching cas-typescript-sdk). The FetchWycheproofVectors MSBuild target in cas-dotnet-sdk-tests.csproj resolves C2SP/wycheproof main to a commit SHA with git ls-remote (logged), and on every dotnet build/dotnet test downloads the testvectors_v1/ files listed as WycheproofVector items into obj/<config>/<tfm>/wycheproof/ — re-downloading only when that SHA changes — then copies them to the output as PQC/Data/wycheproof_*.json. The folder is per target framework on purpose: a plain dotnet test builds all TFMs in parallel, and a shared download folder makes them fail with "file is being used by another process". Pin a ref with -p:WycheproofRef=<sha-or-tag> (or WYCHEPROOF_REF). If the ref can't be resolved (offline, no git) and vectors are cached, they're reused with a warning; -p:FetchWycheproofVectors=false skips the network entirely. Wycheproof suites cover AES-GCM, Ed25519, X25519 and HMAC-SHA256 (cas-dotnet-sdk-tests/Wycheproof/) plus ML-KEM decaps and ML-DSA sign/verify (in MLKEMWrapperTests / MLDSAWrapperTests). To add a file, add one WycheproofVector item (local name + Upstream metadata) and load it with PqcVectorData.Load; skip (and count) only what the public API can't express, and let acceptable results go either way. When editing that target, qualify item metadata (%(WycheproofVector.Identity)): an unqualified %(Filename) inside the target rewrites the TargetPath of other Content items, silently copying the wrong file under the NIST file names, and PreserveNewest then never repairs the bad output copy. See cas-dotnet-sdk-tests/PQC/Data/README.md for the coverage table and skip rationale. A Wycheproof test that fails without an SDK change usually means upstream added a case not handled yet.
Every cryptographic capability is grouped into a top-level folder by category (Hashers, Symmetric, Asymmetric, Signatures, PasswordHashers, KeyExchange, Hybrid, Compression, Sponges, PQC). Each category has the same structure:
-
Public wrapper (e.g.
Hashers/SHAWrapper.cs) — the user-facingunsafeclass, implementing a category interface where one exists (e.g.IHasherBase,IPasswordHasherBase). Each method pins its inputs withfixed, calls the generated binding onNativeMethods, checks the result'serror_code, then copies the output into managed memory and frees the native allocation. -
Generated bindings (
generated/NativeMethods.g.cs) —[DllImport]declarations and C ABI result structs (e.g.CasStringResult,CasVerifyResult) emitted by csbindgen fromcas-core-lib/build.rsduring theBuildRusttarget. Never edit this file by hand. -
Types (
<Category>/Types/*.cs) — public result classes returned to callers (e.g.Ed25519KeyPairResult,HpkeEncryptResult), plus category enums.
CasErrorHandler.ThrowIfError— turns a non-zeroerror_codeinto aCasException(carryingCasErrorCodeand the raw code). Call it before reading any pointer from a result: the native layer leaves pointers null on failure.NativeByteBuffer.CopyAndFree/NativeString.ReadAndFree— copy a native buffer or C string into managed memory and free it through the FFI. Every pointer returned by the native layer must go through one of these, or memory leaks. This is the single most important invariant when adding new wrappers.NativeString.ToCString— encodes a managed string as a null-terminated UTF-8 buffer for pinning.NativePin.Of— substitutes a non-empty sentinel for zero-length arrays sofixedyields a non-null pointer.
Categories with interchangeable algorithms expose a *Factory (e.g. HasherFactory.Get(IHasherType.SHA), PasswordHasherFactory.Get(...)) returning the category interface. When adding a new algorithm to such a category, wire it into both the enum and the factory switch.
The password hashers expose HashPasswordWithParameters alongside the default HashPassword: Argon2 (memoryCost in MiB — cas-lib multiplies it by 1024, iterations, parallelism), SCrypt (cpuCost as log2(N), blockSize, parallelism) and BCrypt (cost, max 31).
- Add the Rust FFI function in
cas-core-lib(separate submodule/repo), returning a result struct with anerror_code, and rebuild — csbindgen regeneratesgenerated/NativeMethods.g.cs. - Implement the public method on the wrapper: pin inputs with
fixed, callNativeMethods,CasErrorHandler.ThrowIfError, then marshal out withNativeByteBuffer/NativeString. - Add any new public result class to
Types/. - If the category has a factory/enum, register the new type there.
- Add an xUnit test. NIST test vectors live as
.rspfiles under the test project'sAES/DataandSHA/Datafolders (copied to output viaContentitems in the test.csproj). - Add a usage example to
docs/EXAMPLES.md(see below).
docs/EXAMPLES.md is the user-facing reference and must cover every public method in cas-dotnet-sdk/. Whenever you add, rename, remove, or change the signature of a public method or result type, update docs/EXAMPLES.md in the same change, without being asked — this is part of the task, not a follow-up. A change to the public API is not done until its example is.
Before finishing any task that touches a public wrapper, run this coverage check from the repo root (Git Bash). It must print nothing; each MISSING line is a method without an example:
git grep -hoE "^\s+public [A-Za-z0-9<>\[\]]+ [A-Z][A-Za-z0-9]*\(" -- 'cas-dotnet-sdk/*.cs' ':!cas-dotnet-sdk/generated' ':!cas-dotnet-sdk/Helpers' \
| sed -E 's/.* ([A-Za-z0-9]+)\($/\1/' | sort -u \
| while read m; do grep -q "\.$m(" docs/EXAMPLES.md || echo "MISSING $m"; done- New method or algorithm: add an example under its category heading (create the heading if needed), with the
usinglines and a round trip where one applies (encrypt + decrypt, sign + verify, hash + verify). - Tunable parameters / cost: show the parameterized overload next to the default one and list each parameter with its unit, default, and limits (as the Password Hashers section does).
- Argument order: copy it from the wrapper's signature and the xUnit test, and run the snippet against the built native library when you can. Hash
Verifymethods differ:SHAWrapper.Verify*(data, hash)butBlake2Wrapper.Verify*(hash, data). - Renamed or removed APIs: fix or delete every example that calls them; grep
docs/EXAMPLES.mdfor the old name. - New result types: use the real property names from
Types/in the example. - Also give the public method XML doc comments (
<summary>, plus<param>for any tuning parameter). - Keep the doc example-only: no benchmark tables or performance claims.
Supported targets: Windows x64, Linux x64, and macOS Apple Silicon (osx-arm64). Intel macs (osx-x64) are intentionally not supported — the Intel runners were too slow for this project's CI. Since the csbindgen migration (#191), P/Invoke goes through the generated generated/NativeMethods.g.cs using the bare library name cas_core_lib; the .NET runtime resolves it per-OS to cas_core_lib.dll / libcas_core_lib.so / libcas_core_lib.dylib, so there are no per-platform [DllImport] files to keep in sync. The native lib is built (cargo build --release), staged into artifacts/native/<rid>/, and packed into runtimes/<rid>/native for each RID. Tests run against .NET 6–10 on all three OSes in CI on every PR: pr-tests-windows and pr-tests-linux fan out one job per TFM, while pr-tests-macos-arm64 (macos-latest, Apple Silicon) runs all five target frameworks in a single dotnet test invocation (no -f).
Note: this is desktop macOS / Darwin, not iOS. Real iOS (aarch64-apple-ios) would need a cross-compiled native lib and a simulator/device to run tests, neither of which the current CI provides.