Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<!-- Preview line: hyphenated SemVer suffix marks every build as a NuGet prerelease.
Release CI overrides this from the git tag (e.g. v1.0.0-preview.1 -> 1.0.0-preview.1). -->
<NetCryptoVersion Condition="'$(NetCryptoVersion)' == ''">1.0.0-preview.1</NetCryptoVersion>
<NetCryptoVersion Condition="'$(NetCryptoVersion)' == ''">1.0.0-preview.2</NetCryptoVersion>
<Deterministic>true</Deterministic>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSourceRevisionInInformationalVersion>true</IncludeSourceRevisionInInformationalVersion>
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ Every primitive is tested against the test vectors of its governing specificatio
> **BBS terminology.** "BBS" is the CFRG name for the scheme historically called BBS+.
> Conformance is pinned to draft-10 via zkryptium 0.6; the `BbsCiphersuite` parameter
> (only `Bls12381Sha256` in v1) and the FFI isolation contain future draft churn.
>
> **BBS header vs presentation header.** `Sign`/`Verify`/`DeriveProof`/`VerifyProof` take an
> optional `header` (default empty): data the *signer* binds at sign time and that every
> derived proof commits — the holder cannot drop or alter it (e.g. the W3C `bbs-2023`
> cryptosuite binds its mandatory-disclosure group here). It is distinct from the
> `presentationHeader` (`ph`) on `DeriveProof`/`VerifyProof`, which the *holder* chooses at
> derive time (typically the verifier's challenge).

## Native BBS library and the supported "BBS-absent" mode

Expand Down
13 changes: 9 additions & 4 deletions native/zkryptium-ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,15 @@ All functions return `0` on success, `-1` on error.
|----------|-------------|
| `bbs_keygen` | Generate BBS keypair from IKM plus optional `key_info` (SK: 32 bytes, PK: 96 bytes). Pass null/0 for `key_info` to use the spec default (empty string); the key-generation DST is always the spec default. |
| `bbs_sk_to_pk` | Derive public key from secret key |
| `bbs_sign` | Sign an ordered set of messages |
| `bbs_verify` | Verify a BBS signature against the full message set |
| `bbs_proof_gen` | Derive a selective-disclosure zero-knowledge proof |
| `bbs_proof_verify` | Verify a selective-disclosure proof |
| `bbs_sign` | Sign an ordered set of messages, with an optional signer-bound `header` |
| `bbs_verify` | Verify a BBS signature against the full message set and the same `header` |
| `bbs_proof_gen` | Derive a selective-disclosure zero-knowledge proof (binds both `header` and presentation header `ph`) |
| `bbs_proof_verify` | Verify a selective-disclosure proof against the committed `header` and `ph` |

The signature `header` (passed as `header_ptr`/`header_len`) is fixed by the signer and committed
by both verification and any derived proof; the presentation header `ph` (`ph_ptr`/`ph_len`) is
chosen by the holder at proof-derivation time. Both are optional — pass null/0 for the spec default
of an empty octet string.

## Message encoding

Expand Down
2 changes: 2 additions & 0 deletions netcrypto-prd.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,15 @@ Migrate `IBbsCryptoProvider`, `DefaultBbsCryptoProvider`, `ZkryptiumNative` (int
3. Add to `IBbsCryptoProvider`: `BbsCiphersuite Ciphersuite { get; }` and `bool IsAvailable { get; }`.
4. `DefaultBbsCryptoProvider` gains constructor parameter `BbsCiphersuite ciphersuite = BbsCiphersuite.Bls12381Sha256`; any other value throws `NotSupportedException` naming the unsupported suite.
5. Define `public sealed class BbsUnavailableException : System.Security.Cryptography.CryptographicException`, carrying the original native-load error as `InnerException`. All five BBS operations throw it (instead of the current generic throw) when the native library failed to load. `IsAvailable` returns the probe result and never throws.
6. Expose the BBS signature **`header`** on `IBbsCryptoProvider`/`DefaultBbsCryptoProvider` (issue #2). Add an optional `ReadOnlySpan<byte> header = default` (last parameter) to `Sign`, `Verify`, `DeriveProof`, and `VerifyProof`, threaded into the already-present FFI header arguments (the native `zkryptium-ffi` layer already accepts and consumes it; **no Rust change**). The header is fixed by the signer at sign time and committed by both verification and any derived proof — it lets a consumer bind application data the holder cannot drop or alter (the W3C `bbs-2023` cryptosuite binds its mandatory-disclosure group here). Rename the existing `nonce` parameter on `DeriveProof`/`VerifyProof` to `presentationHeader`: it is the BBS presentation header (`ph`), a value distinct from `header` and chosen by the holder at derive time. Default empty preserves the prior behavior for callers that omit it.

**Acceptance criteria:**
- [ ] BBS round-trip test passes on a platform with the native library: keygen → sign(3 messages) → verify(true) → DeriveProof(reveal indices {0,2}) → VerifyProof(true); tamper any revealed message → VerifyProof(false).
- [ ] Keygen fixture test: deterministic IKM from draft-irtf-cfrg-bbs-signatures-10 BLS12-381-SHA-256 test fixtures produces the fixture's expected SK/PK through the FFI (proves the wrapped zkryptium build matches draft-10; cite the fixture used in the test).
- [ ] Size invariants asserted: SK 32, PK 96, signature 80 bytes.
- [ ] With the native library absent (test by running the managed test assembly with no `runtimes/` payload — CI job, FR-22): `IsAvailable == false`; each of the five operations throws `BbsUnavailableException` with non-null `InnerException`; every non-BBS test in the suite still passes.
- [ ] `new DefaultBbsCryptoProvider((BbsCiphersuite)1)` throws `NotSupportedException`.
- [ ] Header binding (issue #2): `Sign(sk, msgs, header)` + `Verify(pk, sig, msgs, header)` round-trips for a non-empty header, and `Verify` returns `false` for a different header or the default empty header; `DeriveProof(..., presentationHeader, header)` + `VerifyProof(..., presentationHeader, header)` round-trips, and `VerifyProof` returns `false` when the `header` differs from the one bound at derive time (the header is committed by the proof); the `presentationHeader` and `header` are independently committed. The default empty-header behavior is unchanged for callers that omit it.

### FR-6 — Key generation

Expand Down
39 changes: 39 additions & 0 deletions samples/NetCrypto.Samples.Bbs/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,45 @@
Check(!tamperedValid, "VerifyProof returns false for a tampered revealed message");
Console.WriteLine();

// -------------------------------------------------------
// 8. Header — bind issuer data the holder CANNOT drop
// -------------------------------------------------------
// The presentation header (the `nonce` above) is chosen by the HOLDER
// at derive time. The signature `header` is different: it is fixed by
// the ISSUER at sign time and committed by the signature AND every
// derived proof. Anything bound into the header — e.g. the W3C
// bbs-2023 cryptosuite binds its mandatory-disclosure group here —
// cannot be altered or dropped without verification failing.
//
// header (issuer, sign time) != presentationHeader (holder, derive time)
Console.WriteLine("=== 8. Signature header (issuer-bound) ===");

var header = "bbs-2023:mandatory=[issuer,expiry]"u8.ToArray();

// Issuer signs WITH the header bound in.
var boundSig = bbs.Sign(keyPair.PrivateKey, messages, header);

// Verifying with the same header succeeds; a different header fails;
// and omitting the header (the default) fails — proving the header is
// genuinely committed, not ignored.
Check(bbs.Verify(keyPair.PublicKey, boundSig, messages, header),
"Verify succeeds with the header the issuer bound");
Check(!bbs.Verify(keyPair.PublicKey, boundSig, messages, "different-header"u8.ToArray()),
"Verify fails when the header differs from the one signed");
Check(!bbs.Verify(keyPair.PublicKey, boundSig, messages),
"Verify fails when the header is omitted (default empty) for a header-bound signature");

// The header flows through selective disclosure too: derive a proof
// under the header, then a verifier checks it with the SAME header.
var boundProof = bbs.DeriveProof(
keyPair.PublicKey, boundSig, messages, revealedIndices, nonce, header);
Check(bbs.VerifyProof(keyPair.PublicKey, boundProof, revealedMessages, revealedIndices, nonce, header),
"VerifyProof succeeds with the matching presentation header and header");
Check(!bbs.VerifyProof(keyPair.PublicKey, boundProof, revealedMessages, revealedIndices, nonce, "wrong-header"u8.ToArray()),
"VerifyProof fails when the header differs from the one bound at derive time");
Console.WriteLine(" Header is committed by both Verify and the derived proof.");
Console.WriteLine();

Console.WriteLine("Done! All BBS examples completed successfully.");
return 0;

Expand Down
24 changes: 14 additions & 10 deletions src/NetCrypto/DefaultBbsCryptoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ private static void EnsureNativeAvailable()
}

/// <inheritdoc />
public byte[] Sign(ReadOnlySpan<byte> privateKey, IReadOnlyList<byte[]> messages)
public byte[] Sign(ReadOnlySpan<byte> privateKey, IReadOnlyList<byte[]> messages,
ReadOnlySpan<byte> header = default)
{
ArgumentNullException.ThrowIfNull(messages);
EnsureNativeAvailable();
Expand All @@ -109,7 +110,7 @@ public byte[] Sign(ReadOnlySpan<byte> privateKey, IReadOnlyList<byte[]> messages

rc = ZkryptiumNative.bbs_sign(
privateKey, pk,
ReadOnlySpan<byte>.Empty, 0,
header, (nuint)header.Length,
encodedMessages, (nuint)encodedMessages.Length,
signature);

Expand All @@ -120,7 +121,8 @@ public byte[] Sign(ReadOnlySpan<byte> privateKey, IReadOnlyList<byte[]> messages
}

/// <inheritdoc />
public bool Verify(ReadOnlySpan<byte> publicKey, ReadOnlySpan<byte> signature, IReadOnlyList<byte[]> messages)
public bool Verify(ReadOnlySpan<byte> publicKey, ReadOnlySpan<byte> signature, IReadOnlyList<byte[]> messages,
ReadOnlySpan<byte> header = default)
{
ArgumentNullException.ThrowIfNull(messages);
EnsureNativeAvailable();
Expand All @@ -134,7 +136,7 @@ public bool Verify(ReadOnlySpan<byte> publicKey, ReadOnlySpan<byte> signature, I

var rc = ZkryptiumNative.bbs_verify(
publicKey,
ReadOnlySpan<byte>.Empty, 0,
header, (nuint)header.Length,
encodedMessages, (nuint)encodedMessages.Length,
signature);

Expand All @@ -147,7 +149,8 @@ public byte[] DeriveProof(
byte[] signature,
IReadOnlyList<byte[]> messages,
IReadOnlyList<int> revealedIndices,
ReadOnlySpan<byte> nonce)
ReadOnlySpan<byte> presentationHeader,
ReadOnlySpan<byte> header = default)
{
ArgumentNullException.ThrowIfNull(signature);
ArgumentNullException.ThrowIfNull(messages);
Expand Down Expand Up @@ -188,8 +191,8 @@ public byte[] DeriveProof(
var rc = ZkryptiumNative.bbs_proof_gen(
publicKey,
signature,
ReadOnlySpan<byte>.Empty, 0,
nonce, (nuint)nonce.Length,
header, (nuint)header.Length,
presentationHeader, (nuint)presentationHeader.Length,
encodedMessages, (nuint)encodedMessages.Length,
encodedIndices, (nuint)encodedIndices.Length,
proofBuf, (nuint)proofBuf.Length,
Expand All @@ -207,7 +210,8 @@ public bool VerifyProof(
byte[] proof,
IReadOnlyList<byte[]> revealedMessages,
IReadOnlyList<int> revealedIndices,
ReadOnlySpan<byte> nonce)
ReadOnlySpan<byte> presentationHeader,
ReadOnlySpan<byte> header = default)
{
ArgumentNullException.ThrowIfNull(proof);
ArgumentNullException.ThrowIfNull(revealedMessages);
Expand All @@ -223,8 +227,8 @@ public bool VerifyProof(
var rc = ZkryptiumNative.bbs_proof_verify(
publicKey,
proof, (nuint)proof.Length,
ReadOnlySpan<byte>.Empty, 0,
nonce, (nuint)nonce.Length,
header, (nuint)header.Length,
presentationHeader, (nuint)presentationHeader.Length,
encodedMessages, (nuint)encodedMessages.Length,
encodedIndices, (nuint)encodedIndices.Length);

Expand Down
70 changes: 63 additions & 7 deletions src/NetCrypto/ICryptoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,28 +86,84 @@ public interface IBbsCryptoProvider
/// </summary>
bool IsAvailable { get; }

/// <summary>Sign an ordered set of messages using a BLS12-381 G2 private key.</summary>
byte[] Sign(ReadOnlySpan<byte> privateKey, IReadOnlyList<byte[]> messages);
/// <summary>
/// Sign an ordered set of messages using a BLS12-381 G2 private key.
/// </summary>
/// <param name="privateKey">32-byte BLS12-381 secret scalar.</param>
/// <param name="messages">The ordered set of messages to sign.</param>
/// <param name="header">
/// Optional BBS signature <c>header</c> (draft-irtf-cfrg-bbs-signatures). Fixed by the
/// signer at sign time and bound into the signature: <see cref="Verify"/> and any derived
/// proof only succeed when the same <paramref name="header"/> is supplied. Application data
/// committed here cannot be dropped or altered by the holder — e.g. the W3C <c>bbs-2023</c>
/// cryptosuite binds its mandatory-disclosure group into the header. Distinct from the
/// <c>presentationHeader</c> on <see cref="DeriveProof"/>/<see cref="VerifyProof"/>, which the
/// holder chooses at derive time. Defaults to empty (no header bound).
/// </param>
byte[] Sign(ReadOnlySpan<byte> privateKey, IReadOnlyList<byte[]> messages,
ReadOnlySpan<byte> header = default);

/// <summary>Verify a BBS signature against the full set of messages.</summary>
bool Verify(ReadOnlySpan<byte> publicKey, ReadOnlySpan<byte> signature, IReadOnlyList<byte[]> messages);
/// <summary>
/// Verify a BBS signature against the full set of messages.
/// </summary>
/// <param name="publicKey">96-byte BLS12-381 G2 public key.</param>
/// <param name="signature">The 80-byte BBS signature to verify.</param>
/// <param name="messages">The full ordered set of messages that was signed.</param>
/// <param name="header">
/// The same BBS signature <c>header</c> that was supplied to <see cref="Sign"/>. Verification
/// fails (returns <c>false</c>) if it differs from the header bound at sign time. Defaults to
/// empty.
/// </param>
bool Verify(ReadOnlySpan<byte> publicKey, ReadOnlySpan<byte> signature, IReadOnlyList<byte[]> messages,
ReadOnlySpan<byte> header = default);

/// <summary>
/// Derive a zero-knowledge proof that selectively discloses only the messages
/// at the specified indices, without revealing the original signature.
/// </summary>
/// <param name="publicKey">96-byte BLS12-381 G2 public key of the signer.</param>
/// <param name="signature">The 80-byte BBS signature over <paramref name="messages"/>.</param>
/// <param name="messages">The full ordered set of signed messages.</param>
/// <param name="revealedIndices">Indices of the messages to disclose; must be distinct and in range.</param>
/// <param name="presentationHeader">
/// The BBS presentation header (<c>ph</c>) — chosen by the holder at derive time, typically the
/// verifier's challenge/nonce so a captured proof cannot be replayed. Bound into the proof and
/// must be supplied unchanged to <see cref="VerifyProof"/>. Distinct from
/// <paramref name="header"/>.
/// </param>
/// <param name="header">
/// The same BBS signature <c>header</c> that was bound at <see cref="Sign"/> time. It is
/// committed by the derived proof, so <see cref="VerifyProof"/> fails unless the same value is
/// supplied there. Defaults to empty.
/// </param>
byte[] DeriveProof(
ReadOnlySpan<byte> publicKey,
byte[] signature,
IReadOnlyList<byte[]> messages,
IReadOnlyList<int> revealedIndices,
ReadOnlySpan<byte> nonce);
ReadOnlySpan<byte> presentationHeader,
ReadOnlySpan<byte> header = default);

/// <summary>Verify a derived proof against the revealed messages.</summary>
/// <summary>
/// Verify a derived proof against the revealed messages.
/// </summary>
/// <param name="publicKey">96-byte BLS12-381 G2 public key of the signer.</param>
/// <param name="proof">The proof bytes produced by <see cref="DeriveProof"/>.</param>
/// <param name="revealedMessages">Only the disclosed messages, in the order of their indices.</param>
/// <param name="revealedIndices">The indices the proof discloses, matching <paramref name="revealedMessages"/>.</param>
/// <param name="presentationHeader">
/// The BBS presentation header (<c>ph</c>) that was supplied to <see cref="DeriveProof"/>.
/// Verification fails if it differs.
/// </param>
/// <param name="header">
/// The BBS signature <c>header</c> bound at <see cref="Sign"/>/<see cref="DeriveProof"/> time.
/// Verification fails if it differs from the header committed by the proof. Defaults to empty.
/// </param>
bool VerifyProof(
ReadOnlySpan<byte> publicKey,
byte[] proof,
IReadOnlyList<byte[]> revealedMessages,
IReadOnlyList<int> revealedIndices,
ReadOnlySpan<byte> nonce);
ReadOnlySpan<byte> presentationHeader,
ReadOnlySpan<byte> header = default);
}
Loading
Loading