IronWeave.Crypto
0.3.0
dotnet add package IronWeave.Crypto --version 0.3.0
NuGet\Install-Package IronWeave.Crypto -Version 0.3.0
<PackageReference Include="IronWeave.Crypto" Version="0.3.0" />
<PackageVersion Include="IronWeave.Crypto" Version="0.3.0" />
<PackageReference Include="IronWeave.Crypto" />
paket add IronWeave.Crypto --version 0.3.0
#r "nuget: IronWeave.Crypto, 0.3.0"
#:package IronWeave.Crypto@0.3.0
#addin nuget:?package=IronWeave.Crypto&version=0.3.0
#tool nuget:?package=IronWeave.Crypto&version=0.3.0
IronWeave.Crypto — C# Binding
A .NET wrapper for the iwcrypto native library, providing zero-knowledge cryptographic operations for confidential token transfers. All operations are exposed as static methods on a single class: IronWeave.Crypto.IwCrypto.
Installation
Install the NuGet package (includes native binaries for all 11 platforms):
dotnet add package IronWeave.Crypto
Requirements
- .NET 10
- Native
iwcryptolibrary (built from the Rust crate withcargo build --release) - Protocol Buffers compiler (
protoc) — only needed for test projects using proto types
Project Structure
bindings/csharp/
IronWeave.Crypto.csproj Class library
IwCrypto.cs All public API methods
tests/
IronWeave.Crypto.Tests.csproj xUnit test project
IwCryptoTests.cs FFI wrapper round-trip tests
CryptoInteropTests.cs Cross-language encryption interop (BouncyCastle)
SigningInteropTests.cs Cross-language signing interop (BouncyCastle)
ZkChainTests.cs Full blockchain scenario integration test
Adding to Your Project
Reference the class library project:
<ProjectReference Include="path/to/bindings/csharp/IronWeave.Crypto.csproj" />
Your project must enable unsafe blocks since the wrapper uses pointer-based P/Invoke internally:
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
The native library must be resolvable at runtime. For development, register a NativeLibrary.SetDllImportResolver to point at the Cargo build output:
using System.Runtime.InteropServices;
NativeLibrary.SetDllImportResolver(
typeof(IwCrypto).Assembly,
(libraryName, assembly, searchPath) =>
{
if (libraryName == "iwcrypto")
{
// Adjust path to your cargo build output
return NativeLibrary.Load("/path/to/target/release/libiwcrypto.dylib");
}
return IntPtr.Zero;
});
For production, place the native library where .NET can find it (application directory, system library path, or bundled in a NuGet runtime package).
.NET MAUI Deployment
The same iwcrypto native binaries work across all MAUI target platforms — no special MAUI-specific builds are needed. The differences are purely in packaging and linking.
iOS
MAUI uses static linking on iOS. Reference the .a file and use [LibraryImport("__Internal")] instead of [LibraryImport("iwcrypto")]:
<ItemGroup Condition="$(TargetFramework.Contains('ios'))">
<NativeReference Include="path/to/libiwcrypto.a">
<Kind>Static</Kind>
<ForceLoad>true</ForceLoad>
</NativeReference>
</ItemGroup>
The IwCrypto wrapper class uses [LibraryImport("iwcrypto")] by default. For iOS static linking, you'll need a conditional DllImportResolver or a build-time substitution to route to __Internal.
Android
MAUI uses the same .so files produced by the Android NDK (via cargo-ndk). Place them under the NuGet RID directories:
| NDK ABI | NuGet RID |
|---|---|
arm64-v8a/libiwcrypto.so |
runtimes/android-arm64/native/ |
armeabi-v7a/libiwcrypto.so |
runtimes/android-arm/native/ |
x86_64/libiwcrypto.so |
runtimes/android-x64/native/ |
Alternatively, add the .so files as AndroidNativeLibrary items:
<ItemGroup Condition="$(TargetFramework.Contains('android'))">
<AndroidNativeLibrary Include="libs/arm64-v8a/libiwcrypto.so" Abi="arm64-v8a" />
<AndroidNativeLibrary Include="libs/armeabi-v7a/libiwcrypto.so" Abi="armeabi-v7a" />
<AndroidNativeLibrary Include="libs/x86_64/libiwcrypto.so" Abi="x86_64" />
</ItemGroup>
Desktop (macOS / Windows / Linux)
Standard dynamic linking via [LibraryImport("iwcrypto")]. Place the native library (.dylib / .dll / .so) in a NuGet runtimes/{rid}/native/ directory or alongside the application binary.
CI-Built Binaries
The build-release.yml workflow produces a iwcrypto-nuget-runtimes.zip artifact containing the runtimes/ directory pre-organized by RID for all 11 targets. Extract it into your NuGet package or project to get all platforms at once.
API Reference
All methods are static on IronWeave.Crypto.IwCrypto. Methods that can fail on invalid input throw InvalidOperationException. Verification methods (VerifySignature, ValidateOperationProofs, ValidateSignedOperation) return bool instead.
Key Generation
using IronWeave.Crypto;
// Ed25519 keypair — public key (32 bytes) doubles as account address
var (privateKey, publicKey) = IwCrypto.GenerateEd25519Keypair();
// Convert Ed25519 keys to X25519 for direct ECDH use
var xPub = IwCrypto.Ed25519ToX25519Public(publicKey);
var (xPriv, xPub2) = IwCrypto.Ed25519ToX25519Keypair(privateKey);
Commitments
// Generate a random 32-byte blinding factor (canonical Ristretto scalar)
byte[] blinding = IwCrypto.GenerateRandomBlinding();
// Pedersen commitment: C = value * B + blinding * B_blinding
// Returns 32-byte compressed Ristretto point
byte[] commitment = IwCrypto.Commit(1000, blinding);
Signing & Verification
var (privateKey, publicKey) = IwCrypto.GenerateEd25519Keypair();
byte[] message = "operation data"u8.ToArray();
// Sign — returns 64-byte Ed25519 signature
byte[] signature = IwCrypto.SignMessage(privateKey, message);
// Verify — returns true/false (does not throw)
bool valid = IwCrypto.VerifySignature(publicKey, message, signature);
Building Operations
Each Build* method returns serialized protobuf bytes representing the ZK proof payload. These bytes are embedded in a CryptoOperation proto message along with addresses, UUIDs, and encrypted private data before signing.
byte[] opUuid = IwCrypto.GenerateUuidV7();
// Mint — token issuer mints to own account (public amount for supply auditing)
byte[] mintPayload = IwCrypto.BuildZkMint(
opUuid, tokenAddr, senderPrevOpUuid,
currentBalance, currentBlinding,
500, newBlinding);
// Burn — any account burns tokens (public amount for supply auditing)
byte[] burnPayload = IwCrypto.BuildZkBurn(
opUuid, tokenAddr, senderAddr, senderPrevOpUuid,
currentBalance, currentBlinding,
200, newBlinding);
// Zero-account transfer — send to an identity-commitment address (new or reset account)
byte[] firstPayload = IwCrypto.BuildZkTransfer(
opUuid, tokenAddr, senderAddr, senderPrevOpUuid,
senderStartingBalance, senderStartingBlinding, senderFinalBlinding,
receiverAddr, IwCrypto.NoPrevOpUuid,
IwCrypto.ZeroAccountBalance, IwCrypto.ZeroAccountBlinding, receiverFinalBlinding,
transferAmount);
// Transfer — between two accounts with known openings (same actor controls both)
byte[] transferPayload = IwCrypto.BuildZkTransfer(
opUuid, tokenAddr, senderAddr, senderPrevOpUuid,
senderStartingBalance, senderStartingBlinding, senderFinalBlinding,
receiverAddr, receiverPrevOpUuid,
receiverStartingBalance, receiverStartingBlinding, receiverFinalBlinding,
transferAmount);
// Zero-balance reset — prove balance is zero, reset commitment to identity
byte[] resetPayload = IwCrypto.BuildZeroBalanceReset(currentBlinding);
// Rerandomize — switch blinding factor without changing balance
byte[] rerandPayload = IwCrypto.BuildRerandomize(
currentBalance, currentBlinding, newBlinding);
BuildZkTransfer has a convenience overload that accepts Guid for UUID parameters instead of ReadOnlySpan<byte>.
Validation
// Pre-sign check (operation author) — validates ZK proofs only
bool proofsOk = IwCrypto.ValidateOperationProofs(
cryptoOpBytes, senderOnChainCommitment, receiverOnChainCommitment);
// Full validation (validator) — verifies Ed25519 signature + ZK proofs
bool signedOk = IwCrypto.ValidateSignedOperation(
signedCryptoOpBytes, senderOnChainCommitment, receiverOnChainCommitment);
Encryption & Decryption
Private transfer data (balance + blinding) is encrypted using ECDH (X25519) + AES-256-GCM. Ed25519 keys are converted internally when keyType is 0 (default).
The aad parameter (Associated Authenticated Data) binds additional context into the GCM authentication tag without encrypting it. For ZK operations, pass the party's final commitment (32 bytes) as AAD — this cryptographically ties the encrypted private data to the on-chain commitment. For user_data entries, omit aad (defaults to empty).
// Parse ZkTransfer to access final commitments for AAD
var zkTransfer = ZkTransfer.Parser.ParseFrom(payloadBytes);
// sender_private_data — self-encrypt, AAD = sender_final_commitment
var encSender = IwCrypto.EncryptPrivateData(
senderPtdBytes, opUuid, senderPriv, senderPub,
aad: zkTransfer.SenderFinalCommitment.ToByteArray());
// receiver_private_data — ECDH with receiver, AAD = receiver_final_commitment
var encReceiver = IwCrypto.EncryptPrivateData(
receiverPtdBytes, opUuid, senderPriv, receiverPub,
aad: zkTransfer.ReceiverFinalCommitment.ToByteArray());
// user_data entries — ECDH with receiver, no AAD
var encUserData = IwCrypto.EncryptPrivateData(
userDataBytes, opUuid, senderPriv, receiverPub);
// Receiver decrypts with matching AAD
var decrypted = IwCrypto.DecryptPrivateData(
encReceiver, opUuid, receiverPriv, senderPub,
aad: zkTransfer.ReceiverFinalCommitment.ToByteArray());
Key type parameter: 0 = Ed25519 (default, keys converted to X25519 internally), 1 = X25519 (already converted).
SECURITY: sender_private_data must use self-encryption. Because ECDH is symmetric (ECDH(A_priv, B_pub) == ECDH(B_priv, A_pub)), encrypting sender_private_data with (senderPriv, receiverPub) would let the receiver decrypt it and learn the sender's remaining balance. Always use (senderPriv, senderPub) for sender_private_data so only the sender can decrypt.
| Field | Encrypt with | Who can decrypt |
|---|---|---|
SenderPrivateData |
(senderPriv, senderPub) — self-encryption |
Sender only |
ReceiverPrivateData |
(senderPriv, receiverPub) — ECDH with receiver |
Sender and receiver |
UserData entries |
(senderPriv, receiverPub) — ECDH with receiver |
Sender and receiver |
Deterministic Blinding Derivation
Both sender and receiver can independently compute the same blinding factor from their ECDH shared secret, preventing fund-locking griefing:
// Sender derives
byte[] senderBlinding = IwCrypto.DeriveBlinding(
opUuid, senderPrivateKey, receiverPublicKey);
// Receiver derives the same value independently
byte[] receiverBlinding = IwCrypto.DeriveBlinding(
opUuid, receiverPrivateKey, senderPublicKey);
// senderBlinding == receiverBlinding
UUID Generation
// 16-byte UUIDv7 (big-endian, time-ordered)
byte[] opUuid = IwCrypto.GenerateUuidV7();
Working with Proto Types
Operation payloads must be wrapped in CryptoOperation and SignedCryptoOperation proto messages for signing and validation. Add proto codegen to your test or application project:
<PackageReference Include="Google.Protobuf" Version="3.*" />
<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="all" />
<Protobuf Include="path/to/proto/zk_transfer.proto" GrpcServices="None" />
Then build CryptoOperation messages using the generated types:
using Google.Protobuf;
using ZkTransfer;
// Build the ZK proof payload (mint to own account)
var payload = IwCrypto.BuildZkMint(
opUuid, tokenAddr, senderPrevOpUuid,
currentBalance, currentBlinding,
500, newBlinding);
// Parse Mint to access new_balance_commitment for AAD
var mint = ZkTransfer.Mint.Parser.ParseFrom(payload);
// Wrap in a CryptoOperation (single-party — no receiver)
var tx = new CryptoOperation
{
Uuid = new UUID { Value = ByteString.CopyFrom(opUuid) },
SenderAddr = new Address { Value = ByteString.CopyFrom(senderPublicKey) },
SenderPrevOpUuid = new UUID { Value = ByteString.CopyFrom(senderPrevOpUuid) },
TokenAddr = new Address { Value = ByteString.CopyFrom(tokenAddr) },
Mint = mint,
SenderPrivateData = ByteString.CopyFrom(
IwCrypto.EncryptPrivateData(senderPtdBytes, opUuid, senderPriv, senderPub,
aad: mint.NewBalanceCommitment.ToByteArray())),
};
// Validate proofs before signing
var opBytes = tx.ToByteArray();
bool valid = IwCrypto.ValidateOperationProofs(opBytes, senderCommitment, receiverCommitment);
// Sign and wrap in SignedCryptoOperation
var signature = IwCrypto.SignMessage(senderPrivateKey, opBytes);
var signed = new SignedCryptoOperation
{
Data = ByteString.CopyFrom(opBytes),
Signature = ByteString.CopyFrom(signature),
SignatureType = SignatureType.Ed25519,
};
byte[] signedBytes = signed.ToByteArray();
Error Handling
| Method category | Failure behavior |
|---|---|
Build*, Commit, Generate*, Sign*, Encrypt*, Decrypt*, DeriveBlinding, Ed25519ToX25519* |
Throws IwCryptoException (extends InvalidOperationException) |
VerifySignature |
Returns false |
ValidateOperationProofs, ValidateSignedOperation |
Returns false |
All methods map FFI return codes internally. IwCryptoException exposes structured fields for programmatic error handling:
| Property | Type | Description |
|---|---|---|
Function |
string |
The FFI function that failed (e.g. "iwzk_build_transfer") |
Code |
int |
Return code: 1=input error, 3=proof failed, 4=insufficient balance, 5=overflow, 6=bad signature, 7=encryption error |
Detail |
string? |
Human-readable detail from iwzk_last_error() |
Running Tests
Build the native library first, then run the C# tests:
# Build the native library
cargo build --release
# Run all C# tests (47 tests)
cd bindings/csharp/tests && dotnet test
The test project includes:
| Suite | Tests | Description |
|---|---|---|
IwCryptoTests |
18 | FFI wrapper round-trips for all operation types |
CryptoInteropTests |
20 | Rust encrypt / .NET decrypt and vice versa (BouncyCastle X25519 + AES-GCM) |
SigningInteropTests |
7 | Rust sign / .NET verify and vice versa (BouncyCastle Ed25519) |
ZkChainTests |
2 | Full blockchain scenario + blinding recovery |
Cross-language interop tests use BouncyCastle to independently implement the same X25519 ECDH + HKDF-SHA256 + AES-256-GCM and Ed25519 pipelines, verifying that the Rust native library and pure .NET produce byte-identical results.
License
Proprietary. All rights reserved.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.