diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5a98c05 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,89 @@ +# Changelog + +All notable changes to **NetCrypto** are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-06-13 + +First stable (GA) release. The public API is frozen: `PublicAPI.Shipped.txt` is the +authoritative contract and `PublicAPI.Unshipped.txt` is empty. From here, additive changes +bump the minor version and breaking changes the major. No `--prerelease` flag is required. + +This release also includes the malformed-input security hardening developed as `1.0.0-preview.3`, +which was never published as a standalone release. + +### Security +- Malformed key inputs now surface as a **parameter-named `ArgumentException`** instead of leaking + a backend exception type (`System.FormatException` from NSec, `Nethermind.Crypto.Bls+BlsException`, + or a platform `CryptographicException`). Up-front validation was added at every backend hand-off: + Ed25519 / X25519 (32-byte length), NIST EC private keys (per-curve scalar length **and** + `0 < D < n` range), and BLS12-381 (32-byte length + invalid-scalar mapping). +- `DefaultKeyGenerator.DeriveX25519PublicKeyFromEd25519` now rejects inputs that map to a + **low-order Curve25519 point** (e.g. the all-zero Ed25519 key), instead of minting a degenerate, + small-subgroup X25519 `PublicKeyReference`. +- The NFR-3 fuzz suite no longer carries a "known backend deviations" allow-list; any non-contract + exception fails the suite. + +### Changed +- Stabilized to GA `1.0.0` — the package is now a non-prerelease NuGet release. + +### Documentation +- Documented the supported **native platform RID matrix** (`osx-arm64`, `osx-x64`, `linux-x64`, + `linux-arm64`, `win-x64`) and that the published `.nupkg` ships `runtimes/{rid}/native/` + transitively, verified by release CI against the packed artifact and a BBS smoke test. (#4) +- Documented that the supported BBS keygen path is `DefaultKeyGenerator.Generate(KeyType.Bls12381G2)` + / `Bls12381G1`, and that raw FFI keygen is intentionally internal. (#6) +- Confirmed `EcPointValidator.EnsureOnCurve` as the public EC on-curve validation entry point, with + point decompression intentionally internal. (#7) + +## [1.0.0-preview.2] - 2026-06-13 + +### Added +- Exposed the BBS signature **`header`** parameter on `IBbsCryptoProvider` / + `DefaultBbsCryptoProvider` (`Sign`, `Verify`, `DeriveProof`, `VerifyProof`) as an optional + `ReadOnlySpan` (default empty). The header is fixed by the signer and committed by both + verification and any derived proof — letting a consumer bind application data (e.g. the W3C + `bbs-2023` mandatory-disclosure group) that a holder cannot drop or alter. (#2) + +### Changed +- **Breaking (pre-GA):** renamed the `nonce` parameter on `DeriveProof` / `VerifyProof` to + `presentationHeader` — it is the BBS presentation header (`ph`), distinct from the new signature + `header`. Positional callers are unaffected; named-argument (`nonce:`) callers must update. + +## [1.0.0-preview.1] - 2026-06-11 + +Initial preview. NetCrypto consolidates every cryptographic primitive for the NetCid/NetDid +library stack behind stable interfaces, so no domain library binds directly to a specific backend. + +### Added +- **Key model:** `KeyType` (Ed25519, X25519, P-256/384/521, secp256k1, BLS12-381 G1/G2), `KeyPair`, + `PublicKeyReference`, and multibase/multicodec encoding (`MultibasePublicKey`) via NetCid. +- **Signing & verification** (`ICryptoProvider` / `DefaultCryptoProvider`): EdDSA (Ed25519), ECDSA + on the NIST curves (DER and IEEE P1363), secp256k1 (64-byte compact, low-S), and BLS12-381 + (G1/G2 variants, hash-to-curve). +- **Recoverable secp256k1 ECDSA** (`Secp256k1Recoverable`) over a caller-supplied 32-byte digest, + returning the raw recovery id (no EVM `v`-encoding). +- **BBS selective-disclosure signatures** (`IBbsCryptoProvider` / `DefaultBbsCryptoProvider`, + BLS12-381-SHA-256, draft-irtf-cfrg-bbs-signatures-10): sign, verify, derive-proof, verify-proof; + `BbsCiphersuite`; `BbsUnavailableException`; and the supported **BBS-absent mode** (`IsAvailable`). +- **Key generation** (`IKeyGenerator` / `DefaultKeyGenerator`) for all key types, including + Ed25519→X25519 derivation. +- **Key agreement:** X25519 (with an HKDF-SHA256 convenience) and raw ECDH "Z" for X25519 and + P-256/384/521. +- **KDFs:** HKDF (SHA-256/384/512) and Concat KDF (NIST SP 800-56A). +- **AEADs:** AES-256-GCM (`A256GCM`), AES-256-CBC + HMAC-SHA-512 (`A256CBC-HS512`), + XChaCha20-Poly1305 (`XC20P`); and AES Key Wrap (`A256KW`). +- **Hashing:** SHA-256/384/512 and Keccak-256 (original padding, not SHA3-256). +- **JWK conversion** (`JwkConverter`) for all key types. +- **Signing & key-store abstractions:** `ISigner`, `KeyPairSigner`, `KeyStoreSigner`, `IKeyStore`, + `InMemoryKeyStore`. +- **EC point validation** (`EcPointValidator.EnsureOnCurve`) — the invalid-curve-attack defense. +- **Dependency injection:** `AddNetCrypto()` (swap-seam via `TryAdd`). +- **Native BBS distribution** for five RIDs (`osx-arm64`, `osx-x64`, `linux-x64`, `linux-arm64`, + `win-x64`), packed into the single NuGet package; the repository stays source-only. + +[1.0.0]: https://github.com/moisesja/crypto-dotnet/compare/v1.0.0-preview.2...v1.0.0 +[1.0.0-preview.2]: https://github.com/moisesja/crypto-dotnet/compare/v1.0.0-preview.1...v1.0.0-preview.2 +[1.0.0-preview.1]: https://github.com/moisesja/crypto-dotnet/releases/tag/v1.0.0-preview.1 diff --git a/Directory.Build.props b/Directory.Build.props index 25c8f44..9d49a6e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,9 +3,10 @@ net10.0 enable enable - - 1.0.0-preview.3 + + 1.0.0 true true true diff --git a/README.md b/README.md index 80b1e07..809324f 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,14 @@ stable interfaces, so that no domain library (`net-did`, `dataproofs-dotnet`, `credentials-dotnet`, `didcomm-dotnet`) binds directly to a specific crypto backend. ``` -dotnet add package NetCrypto --prerelease +dotnet add package NetCrypto ``` -> NetCrypto is currently published as a **preview** (`1.0.0-preview.*`); the `--prerelease` -> flag (or an explicit prerelease version) is required until a stable `1.0.0` is cut. +> **Stable release.** NetCrypto is published as a stable **`1.0.0`** (GA) — no `--prerelease` +> flag is required. The public API is frozen: `PublicAPI.Shipped.txt` is the authoritative +> contract and `PublicAPI.Unshipped.txt` is empty. Semantic versioning applies — additive +> changes bump the minor version, breaking changes the major; any pre-GA `1.0.0-preview.*` +> packages are superseded by `1.0.0`. See [CHANGELOG.md](CHANGELOG.md) for the release history. Target framework: **net10.0**. Depends on [`NetCid`](https://www.nuget.org/packages/NetCid) for multibase/multicodec encoding. @@ -88,8 +91,25 @@ Every primitive is tested against the test vectors of its governing specificatio BBS is the only non-managed primitive: the `zkryptium-ffi` Rust crate ([`native/zkryptium-ffi/`](native/zkryptium-ffi/)) is compiled per platform and shipped -inside this single NuGet package under `runtimes/{rid}/native/` for: -`linux-x64`, `linux-arm64`, `osx-arm64`, `osx-x64`, `win-x64`. +**inside this single NuGet package** under `runtimes/{rid}/native/`. A consumer that restores +NetCrypto gets the BBS native payload transitively — no per-consumer native assets are needed. + +### Supported native platform matrix + +| RID | Library file | BBS at runtime | +|---|---|---| +| `osx-arm64` | `libzkryptium_ffi.dylib` | ✅ built + smoke-executed in CI | +| `osx-x64` | `libzkryptium_ffi.dylib` | ✅ shipped (cross-compiled; Rosetta-capable hosts load it) | +| `linux-x64` | `libzkryptium_ffi.so` | ✅ built + smoke-executed in CI | +| `linux-arm64`| `libzkryptium_ffi.so` | ✅ shipped (cross-compiled) | +| `win-x64` | `zkryptium_ffi.dll` | ✅ built + smoke-executed in CI | + +On any of these RIDs `IBbsCryptoProvider.IsAvailable` returns `true` after a plain +`dotnet restore`. The tag-triggered release workflow **fails the build** if any RID's native +payload is missing from the produced `.nupkg` (the "Verify nupkg contains all five RIDs" step), +publishes a SHA-256 checksum per binary, and then **smoke-tests** the packed package — installing +it into a clean console app and running a BBS sign/verify round-trip — so the guarantee is +verified against the actual published artifact, not the source tree. **All managed primitives work with no native library present** (unlisted platforms, or environments that prohibit native code). This is a supported, CI-tested mode: @@ -103,6 +123,22 @@ if (!bbs.IsAvailable) } ``` +### BBS key generation + +The supported way to mint a BBS keypair is the **standard key generator** — the same one used +for every other key type: + +```csharp +var keyPair = new DefaultKeyGenerator().Generate(KeyType.Bls12381G2); // or Bls12381G1 +// keyPair.PrivateKey (32-byte BLS12-381 scalar), keyPair.PublicKey (96-byte G2 / 48-byte G1) +byte[] sig = new DefaultBbsCryptoProvider().Sign(keyPair.PrivateKey, messages); +``` + +`Bls12381G2` (96-byte public key) is the variant the BBS provider signs/verifies with. Raw FFI +keygen (`bbs_keygen`) is **intentionally internal** — there is no public raw-keygen helper, by +design, to keep the public surface minimal and backend-agnostic. See +[`samples/NetCrypto.Samples.Bbs`](samples/NetCrypto.Samples.Bbs) for an end-to-end example. + The repository itself is source-only — every shipped binary is cross-compiled by the tag-triggered release workflow from pinned sources, with SHA-256 checksums published as release assets. @@ -118,6 +154,10 @@ as release assets. any public signature (enforced by `Microsoft.CodeAnalysis.PublicApiAnalyzers` with a committed `PublicAPI.Shipped.txt`, plus a reflection test). Sanctioned exceptions: `NetCid` types and `Microsoft.IdentityModel.Tokens.JsonWebKey`. +- **EC point validation.** `EcPointValidator.EnsureOnCurve(KeyType, x, y)` is the public + on-curve validation entry point — the invalid-curve-attack defense (RFC 7518 §6.2.2). Point + *decompression* (`DecompressEcPoint` / `DecompressSecp256k1Point`) is an internal implementation + detail and is **not** part of the public surface; validating a key never requires it. - **Boundaries.** EVM `v`-encoding/RLP/transactions, JOSE/JWE/SD-JWT envelopes, Data Integrity proofs, ECDH-ES/1PU assembly, and concrete HSM/KMS stores are deliberately out of scope — they belong to the consumer layers. diff --git a/src/NetCrypto/DefaultBbsCryptoProvider.cs b/src/NetCrypto/DefaultBbsCryptoProvider.cs index b66b8e6..3727e0f 100644 --- a/src/NetCrypto/DefaultBbsCryptoProvider.cs +++ b/src/NetCrypto/DefaultBbsCryptoProvider.cs @@ -7,6 +7,12 @@ namespace NetCrypto; /// BBS signature operations using BLS12-381-SHA-256 (IETF draft-irtf-cfrg-bbs-signatures-10). /// Delegates to the zkryptium-ffi native library via P/Invoke. /// +/// +/// To mint a BBS keypair, use with +/// (96-byte public key — the variant signed/verified here) or +/// . Raw FFI keygen is intentionally internal: there is no public +/// raw secret/public-key minting helper, keeping the surface minimal and backend-agnostic. +/// public sealed class DefaultBbsCryptoProvider : IBbsCryptoProvider { private const int SecretKeySize = 32; diff --git a/src/NetCrypto/EcPointValidator.cs b/src/NetCrypto/EcPointValidator.cs index 18b5d3c..7efebba 100644 --- a/src/NetCrypto/EcPointValidator.cs +++ b/src/NetCrypto/EcPointValidator.cs @@ -15,6 +15,12 @@ namespace NetCrypto; /// key agreement. Curve25519/Ed25519 do not need this — all encoded points are on-curve — so /// this validator only applies to NIST curves (P-256, P-384, P-521) and secp256k1. All four have /// cofactor 1, so a point on the curve is automatically in the prime-order subgroup. +/// +/// is the public EC point-validation entry point. Point +/// decompression (recovering Y from a compressed SEC1 point) is an internal +/// implementation detail of the provider, not part of the public surface — on-curve validation +/// does not require it. +/// /// public static class EcPointValidator { diff --git a/src/NetCrypto/PublicAPI.Shipped.txt b/src/NetCrypto/PublicAPI.Shipped.txt index 71cf05b..40e3a3e 100644 --- a/src/NetCrypto/PublicAPI.Shipped.txt +++ b/src/NetCrypto/PublicAPI.Shipped.txt @@ -11,11 +11,11 @@ NetCrypto.ConcatKdf NetCrypto.DefaultBbsCryptoProvider NetCrypto.DefaultBbsCryptoProvider.Ciphersuite.get -> NetCrypto.BbsCiphersuite NetCrypto.DefaultBbsCryptoProvider.DefaultBbsCryptoProvider(NetCrypto.BbsCiphersuite ciphersuite = NetCrypto.BbsCiphersuite.Bls12381Sha256) -> void -NetCrypto.DefaultBbsCryptoProvider.DeriveProof(System.ReadOnlySpan publicKey, byte[]! signature, System.Collections.Generic.IReadOnlyList! messages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan nonce) -> byte[]! +NetCrypto.DefaultBbsCryptoProvider.DeriveProof(System.ReadOnlySpan publicKey, byte[]! signature, System.Collections.Generic.IReadOnlyList! messages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan presentationHeader, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> byte[]! NetCrypto.DefaultBbsCryptoProvider.IsAvailable.get -> bool -NetCrypto.DefaultBbsCryptoProvider.Sign(System.ReadOnlySpan privateKey, System.Collections.Generic.IReadOnlyList! messages) -> byte[]! -NetCrypto.DefaultBbsCryptoProvider.Verify(System.ReadOnlySpan publicKey, System.ReadOnlySpan signature, System.Collections.Generic.IReadOnlyList! messages) -> bool -NetCrypto.DefaultBbsCryptoProvider.VerifyProof(System.ReadOnlySpan publicKey, byte[]! proof, System.Collections.Generic.IReadOnlyList! revealedMessages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan nonce) -> bool +NetCrypto.DefaultBbsCryptoProvider.Sign(System.ReadOnlySpan privateKey, System.Collections.Generic.IReadOnlyList! messages, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> byte[]! +NetCrypto.DefaultBbsCryptoProvider.Verify(System.ReadOnlySpan publicKey, System.ReadOnlySpan signature, System.Collections.Generic.IReadOnlyList! messages, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> bool +NetCrypto.DefaultBbsCryptoProvider.VerifyProof(System.ReadOnlySpan publicKey, byte[]! proof, System.Collections.Generic.IReadOnlyList! revealedMessages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan presentationHeader, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> bool NetCrypto.DefaultCryptoProvider NetCrypto.DefaultCryptoProvider.DefaultCryptoProvider() -> void NetCrypto.DefaultCryptoProvider.DeriveSharedSecret(NetCrypto.KeyType keyType, System.ReadOnlySpan privateKey, System.ReadOnlySpan publicKey) -> byte[]! @@ -39,11 +39,11 @@ NetCrypto.Hash NetCrypto.Hkdf NetCrypto.IBbsCryptoProvider NetCrypto.IBbsCryptoProvider.Ciphersuite.get -> NetCrypto.BbsCiphersuite -NetCrypto.IBbsCryptoProvider.DeriveProof(System.ReadOnlySpan publicKey, byte[]! signature, System.Collections.Generic.IReadOnlyList! messages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan nonce) -> byte[]! +NetCrypto.IBbsCryptoProvider.DeriveProof(System.ReadOnlySpan publicKey, byte[]! signature, System.Collections.Generic.IReadOnlyList! messages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan presentationHeader, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> byte[]! NetCrypto.IBbsCryptoProvider.IsAvailable.get -> bool -NetCrypto.IBbsCryptoProvider.Sign(System.ReadOnlySpan privateKey, System.Collections.Generic.IReadOnlyList! messages) -> byte[]! -NetCrypto.IBbsCryptoProvider.Verify(System.ReadOnlySpan publicKey, System.ReadOnlySpan signature, System.Collections.Generic.IReadOnlyList! messages) -> bool -NetCrypto.IBbsCryptoProvider.VerifyProof(System.ReadOnlySpan publicKey, byte[]! proof, System.Collections.Generic.IReadOnlyList! revealedMessages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan nonce) -> bool +NetCrypto.IBbsCryptoProvider.Sign(System.ReadOnlySpan privateKey, System.Collections.Generic.IReadOnlyList! messages, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> byte[]! +NetCrypto.IBbsCryptoProvider.Verify(System.ReadOnlySpan publicKey, System.ReadOnlySpan signature, System.Collections.Generic.IReadOnlyList! messages, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> bool +NetCrypto.IBbsCryptoProvider.VerifyProof(System.ReadOnlySpan publicKey, byte[]! proof, System.Collections.Generic.IReadOnlyList! revealedMessages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan presentationHeader, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> bool NetCrypto.ICryptoProvider NetCrypto.ICryptoProvider.DeriveSharedSecret(NetCrypto.KeyType keyType, System.ReadOnlySpan privateKey, System.ReadOnlySpan publicKey) -> byte[]! NetCrypto.ICryptoProvider.KeyAgreement(System.ReadOnlySpan privateKey, System.ReadOnlySpan publicKey) -> byte[]! diff --git a/src/NetCrypto/PublicAPI.Unshipped.txt b/src/NetCrypto/PublicAPI.Unshipped.txt index ac42edf..7dc5c58 100644 --- a/src/NetCrypto/PublicAPI.Unshipped.txt +++ b/src/NetCrypto/PublicAPI.Unshipped.txt @@ -1,17 +1 @@ #nullable enable -*REMOVED*NetCrypto.DefaultBbsCryptoProvider.DeriveProof(System.ReadOnlySpan publicKey, byte[]! signature, System.Collections.Generic.IReadOnlyList! messages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan nonce) -> byte[]! -*REMOVED*NetCrypto.DefaultBbsCryptoProvider.Sign(System.ReadOnlySpan privateKey, System.Collections.Generic.IReadOnlyList! messages) -> byte[]! -*REMOVED*NetCrypto.DefaultBbsCryptoProvider.Verify(System.ReadOnlySpan publicKey, System.ReadOnlySpan signature, System.Collections.Generic.IReadOnlyList! messages) -> bool -*REMOVED*NetCrypto.DefaultBbsCryptoProvider.VerifyProof(System.ReadOnlySpan publicKey, byte[]! proof, System.Collections.Generic.IReadOnlyList! revealedMessages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan nonce) -> bool -*REMOVED*NetCrypto.IBbsCryptoProvider.DeriveProof(System.ReadOnlySpan publicKey, byte[]! signature, System.Collections.Generic.IReadOnlyList! messages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan nonce) -> byte[]! -*REMOVED*NetCrypto.IBbsCryptoProvider.Sign(System.ReadOnlySpan privateKey, System.Collections.Generic.IReadOnlyList! messages) -> byte[]! -*REMOVED*NetCrypto.IBbsCryptoProvider.Verify(System.ReadOnlySpan publicKey, System.ReadOnlySpan signature, System.Collections.Generic.IReadOnlyList! messages) -> bool -*REMOVED*NetCrypto.IBbsCryptoProvider.VerifyProof(System.ReadOnlySpan publicKey, byte[]! proof, System.Collections.Generic.IReadOnlyList! revealedMessages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan nonce) -> bool -NetCrypto.DefaultBbsCryptoProvider.DeriveProof(System.ReadOnlySpan publicKey, byte[]! signature, System.Collections.Generic.IReadOnlyList! messages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan presentationHeader, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> byte[]! -NetCrypto.DefaultBbsCryptoProvider.Sign(System.ReadOnlySpan privateKey, System.Collections.Generic.IReadOnlyList! messages, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> byte[]! -NetCrypto.DefaultBbsCryptoProvider.Verify(System.ReadOnlySpan publicKey, System.ReadOnlySpan signature, System.Collections.Generic.IReadOnlyList! messages, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> bool -NetCrypto.DefaultBbsCryptoProvider.VerifyProof(System.ReadOnlySpan publicKey, byte[]! proof, System.Collections.Generic.IReadOnlyList! revealedMessages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan presentationHeader, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> bool -NetCrypto.IBbsCryptoProvider.DeriveProof(System.ReadOnlySpan publicKey, byte[]! signature, System.Collections.Generic.IReadOnlyList! messages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan presentationHeader, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> byte[]! -NetCrypto.IBbsCryptoProvider.Sign(System.ReadOnlySpan privateKey, System.Collections.Generic.IReadOnlyList! messages, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> byte[]! -NetCrypto.IBbsCryptoProvider.Verify(System.ReadOnlySpan publicKey, System.ReadOnlySpan signature, System.Collections.Generic.IReadOnlyList! messages, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> bool -NetCrypto.IBbsCryptoProvider.VerifyProof(System.ReadOnlySpan publicKey, byte[]! proof, System.Collections.Generic.IReadOnlyList! revealedMessages, System.Collections.Generic.IReadOnlyList! revealedIndices, System.ReadOnlySpan presentationHeader, System.ReadOnlySpan header = default(System.ReadOnlySpan)) -> bool