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
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Byte-exact reference vectors and RDF Dataset Canonicalization fixtures (.nq/.nt/.json/...) MUST be
# LF on every platform. The canonicalizer emits LF N-Quads and the conformance suite asserts
# byte-equality (RdfCanonicalizationConformanceTests); without this, Windows `core.autocrlf` checks
# the fixtures out as CRLF, inflating their length and failing the windows-latest CI job. All
# fixtures are text and already stored as LF, so this only constrains checkout — it renormalizes
# nothing.
tests/fixtures/** text eol=lf
*.nq text eol=lf
*.nt text eol=lf
49 changes: 23 additions & 26 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ permissions:

# PRD §9: one CI job per acceptance criterion, ac-1 … ac-11, each exiting 0/non-zero, all
# required for merge to main. ac-11 (package-identity publish gate) lives in publish.yml.
# ac-5 (didcomm behavior-parity diff) has been retired — see the note where it used to sit, below.
#
# The build-test job is the comprehensive gate: it builds the whole solution with
# warnings-as-errors — which is itself the AC-6 banned-symbol scan (BannedApiAnalyzers) and the
Expand Down Expand Up @@ -85,9 +86,8 @@ jobs:
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
# All Jose conformance + oracle cross-verification + SD-JWT/VC + VcJose. (The ported
# didcomm parity suite is intermixed in these namespaces and runs here too; its dedicated
# assertion-equality gate is ac-5.)
# All Jose conformance + oracle cross-verification + SD-JWT/VC + VcJose, including the ported
# didcomm parity test suite (intermixed in these namespaces) as standard JOSE coverage.
- name: ac-3 Jose conformance + oracle + SD-JWT(/VC) + VcJose
run: dotnet test tests/DataProofsDotnet.Jose.Tests/DataProofsDotnet.Jose.Tests.csproj --configuration Release

Expand All @@ -102,27 +102,10 @@ jobs:
- name: ac-4 Cose suite
run: dotnet test tests/DataProofsDotnet.Cose.Tests/DataProofsDotnet.Cose.Tests.csproj --configuration Release

ac-5:
name: ac-5 didcomm JWS/JWE behavior parity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
# AC-5 = the ported parity suite passes AND parity-diff finds zero assertion differences.
# The parity tests live intermixed in the Jose test namespaces, so run the whole suite.
- name: ac-5 parity suite passes
run: dotnet test tests/DataProofsDotnet.Jose.Tests/DataProofsDotnet.Jose.Tests.csproj --configuration Release
# parity-diff needs the didcomm source at the SHA recorded in PARITY.md.
- name: Checkout didcomm-dotnet (parity source)
uses: actions/checkout@v4
with:
repository: moisesja/didcomm-dotnet
path: .parity-src/didcomm-dotnet
fetch-depth: 0
- name: ac-5 parity-diff (zero assertion differences)
run: dotnet run --project tasks/parity-diff --configuration Release -- --didcomm-repo .parity-src/didcomm-dotnet
# ac-5 (didcomm JWS/JWE behavior-parity diff) was retired: it pinned the ported JOSE tests to a
# didcomm-dotnet source SHA, which becomes meaningless once didcomm-dotnet is refactored. The
# ported parity TEST suite is kept and still runs under ac-3 (and build-test) as standard JOSE
# coverage; only the source-diff gate and its tooling were removed.

ac-6:
name: ac-6 dependency hygiene + NetCrypto-only crypto
Expand Down Expand Up @@ -202,23 +185,37 @@ jobs:
shell: bash
run: |
set -euo pipefail
version="0.1.0"
feed="$(pwd)/local-feed"
# Derive the version from the packed Core nupkg so the smoke install always matches what
# was actually packed (e.g. the -preview suffix from $(DataProofsVersion)); never hardcode.
version="$(basename "$(ls "$feed"/DataProofsDotnet.Core.*.nupkg | head -1)" .nupkg | sed 's/^DataProofsDotnet\.Core\.//')"
echo "smoke-testing packages at version: $version"
# Register the local pack feed once in the user NuGet config (shared by every iteration's
# restore). Adding it inside the loop would fail on the 2nd package with "source
# 'local-feed' already exists".
dotnet nuget add source "$feed" --name local-feed
declare -A smoke=(
[DataProofsDotnet.Core]=tasks/smoke/Core
[DataProofsDotnet.Jose]=tasks/smoke/Jose
[DataProofsDotnet.Cose]=tasks/smoke/Cose
[DataProofsDotnet.Rdfc]=tasks/smoke/Rdfc
[DataProofsDotnet.Extensions.DependencyInjection]=tasks/smoke/DependencyInjection
)
# Documented per-package prerequisite (tasks/smoke/README.md): the only extra package a
# clean-room install may add. The NetCrypto signer substrate flows transitively, so only
# the DI smoke needs one — the concrete Microsoft.Extensions.DependencyInjection container
# (the package surfaces only its Abstractions). Pinned to the abstractions' version.
declare -A prereq=(
[DataProofsDotnet.Extensions.DependencyInjection]="Microsoft.Extensions.DependencyInjection:10.0.8"
)
for id in "${!smoke[@]}"; do
echo "=== clean-room install + smoke: ${id} ==="
tmp="$(mktemp -d)"
( cd "$tmp" && dotnet new console -n SmokeApp >/dev/null )
cp "${smoke[$id]}/Program.cs" "$tmp/SmokeApp/Program.cs"
( cd "$tmp/SmokeApp" \
&& dotnet nuget add source "$feed" --name local-feed \
&& dotnet add package "$id" --version "$version" --source "$feed" \
&& { [ -z "${prereq[$id]:-}" ] || dotnet add package "${prereq[$id]%%:*}" --version "${prereq[$id]##*:}"; } \
&& dotnet run -c Release )
done

Expand Down
66 changes: 54 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable changes to this project will be 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).

## [Unreleased]
## [1.0.0] - 2026-06-14

### Added

Expand All @@ -24,15 +24,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`proofValue`). Unmodeled proof members (e.g. `capabilityChain`) ride through
`DataIntegrityProof.AdditionalProperties` into the JCS signing input. This unblocks
zcap-dotnet's proof-pipeline delegation and legacy-VC verification in credentials-dotnet.
- **Use legacy suites only for interop with existing corpora; prefer the 2022/2019 Data
Integrity suites for new proofs.**
- **Documented limitations** (verified by adversarial review): the JCS variant has no
representation for a W3C proof chain and **fails closed** if asked to secure/verify a
document that already carries a `proof` member (use the RDFC variant or a 2022/2019 suite
for chains); the RDFC variant binds only terms defined in the active JSON-LD `@context`
(members it does not define are dropped by RDF expansion — inherent to JSON-LD/RDFC, shared
with the conformant `rdfc-*` suites); and `EcdsaSecp256r1Signature2019` does not enforce
low-`s`, so ECDSA `proofValue`s are malleable and must not be used as unique identifiers.
- **Use legacy suites only for interop with existing corpora; prefer the 2022/2019 Data
Integrity suites for new proofs.**
- **Documented limitations** (verified by adversarial review): the JCS variant has no
representation for a W3C proof chain and **fails closed** if asked to secure/verify a
document that already carries a `proof` member (use the RDFC variant or a 2022/2019 suite
for chains); the RDFC variant binds only terms defined in the active JSON-LD `@context`
(members it does not define are dropped by RDF expansion — inherent to JSON-LD/RDFC, shared
with the conformant `rdfc-*` suites); and `EcdsaSecp256r1Signature2019` does not enforce
low-`s`, so ECDSA `proofValue`s are malleable and must not be used as unique identifiers.
- **JOSE explicit typing (RFC 8725 §3.11).** `JwtValidationOptions.ExpectedType` (opt-in) pins the
JWS protected `typ` header on `JwtHandler.Verify` (case-insensitive, `application/`-prefix
tolerant), rejecting cross-context token confusion; default behavior is unchanged. The verified
`typ` is now surfaced on `JwsParseResult.Typ`.

### Changed

- **`Base64Url.Decode` is now strict (behavioral change).** It rejects any input outside the
base64url-no-pad alphabet — including interior/surrounding ASCII whitespace, `=` padding, and the
standard-base64 `+`/`/` — by throwing `FormatException`, where the previous implementation
silently tolerated them. This matches the documented "valid base64url" contract and the JOSE
no-pad requirement, but an out-of-repo caller that previously passed padded or whitespace-bearing
input will now get a `FormatException`. **Upgraders:** scan your `Base64Url.Decode` call sites for
non-canonical input. (Also tracked under Security below — it closes an encoding-ambiguity gap.)
- **Bumped the `NetCrypto` dependency from 1.0.0 to 1.1.0** across all packages. The library
source is unaffected; consumers resolve NetCrypto ≥ 1.1.0 transitively. (1.1.0 also introduces a
`NetCrypto.Base64Url` type — when a consumer imports both `NetCrypto` and `DataProofsDotnet.Jose`,
reference `Base64Url` via a `using` alias or fully-qualified name to disambiguate it from
`DataProofsDotnet.Jose.Base64Url`.)

### Security

- **JOSE hardening pass (issue #6).** An adversarial multi-agent review of the entire
`DataProofsDotnet.Jose` surface (JWS/JWE/ECDH-1PU/JWK/SD-JWT/JWT/encoding), with every finding
put through independent majority-vote verification, produced these fixes (each pinned by a
regression test in `Hardening/HardeningRegressionTests.cs`):
- **SD-JWT reconstruction now fails closed on a deep recursive-disclosure chain.**
`SdJwtReconstructor` walked the disclosed payload by unbounded mutual recursion; a chained
recursive-disclosure presentation (RFC 9901 §6.3) rooted in an issuer-signed `_sd` digest could
drive it into an **uncatchable `StackOverflowException` that terminates the host process**,
defeating the verifier's fail-closed contract. Reconstruction is now depth-bounded (64, matching
the JSON parse depth) and raises a `MalformedJoseException` (surfaced as `DISCLOSURE_INVALID`).
- **A malformed/unsupported `cnf` (or issuer) key no longer crashes SD-JWT verification.**
`CompactJwt.Verify` mapped an unsupported curve / off-curve key point to an uncaught
`NotSupportedException`/crypto exception (reachable through an attacker-influenced `cnf` on the
Key Binding path); it now fails closed as `MalformedJoseException`, which both call sites handle.
- **JWS reports the verified signer only from integrity-protected material.** `JwsParseResult
.SignerKid` is now sourced solely from the protected header; a `kid` carried only in the
unauthenticated unprotected header is treated as a routing hint and never surfaced as the
verified identity. Verification of valid JWS (including `kid`-in-unprotected) is unchanged.
- **`Base64Url.Decode` is now strict** — it rejects interior/surrounding whitespace, `=` padding,
and standard-base64 `+`/`/`, matching the documented base64url-no-pad contract.

## [0.1.0-preview.2] - 2026-06-13

Expand All @@ -57,7 +99,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
present (`cryptosuite`-named suites still always win), closing the create→verify inconsistency
and honoring the FR-4 "registering a new suite requires no pipeline changes" contract. The
library still **creates** only conformant 2022/2019 proofs; this is verification-only and
non-breaking. Suite selection only *routes* — each suite still fully validates
non-breaking. Suite selection only _routes_ — each suite still fully validates
`type`/`cryptosuite`/key/encoding/signature. Regression tests in
`LegacyProofTypeVerificationTests` and `CryptosuiteRegistryTests`.

Expand All @@ -77,7 +119,7 @@ First preview release of all five packages (`Core`, `Jose`, `Cose`, `Rdfc`,

- **`bbs-2023` mandatory disclosure is now cryptographically enforced.** The mandatory-disclosure
group is bound into the BBS signature `header` (`SHA-256(proofConfig) ‖ SHA-256(mandatory
N-Quads)`) at sign and derive, and the header is recomputed at verify from the revealed
N-Quads)`) at sign and derive, and the header is recomputed at verify from the revealed
mandatory messages — so a holder that drops or alters a mandatory statement produces a header
that no longer matches the one the proof commits to, and verification fails. Closes the
adversarial-review finding that a holder could omit a mandatory claim and still verify. Requires
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dotnet test DataProofsDotnet.sln
src/ five publishable packages (Core, Jose, Cose, Rdfc, Extensions.DependencyInjection)
tests/ one test project per package + ApiSurface.Tests (hygiene gates) + vendored fixtures/
samples/ narrated console samples — every public API member must appear in at least one (FR-21)
tasks/ CI gate tooling (samples-coverage, parity-diff, dependency-hygiene, ...) and plan docs
tasks/ CI gate tooling (samples-coverage, dependency-hygiene, api-surface-scan, ...) and plan docs
docs/ supplementary documentation
```

Expand Down
18 changes: 0 additions & 18 deletions DataProofsDotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "package-identity", "package
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackageIdentity", "tasks\package-identity\PackageIdentity.csproj", "{E96D6952-1F47-493A-8C11-BE24278530CF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "parity-diff", "parity-diff", "{AA4E32FC-56B2-C72E-60FC-95F431031FA2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParityDiff", "tasks\parity-diff\ParityDiff.csproj", "{D35D6BFF-66E7-4956-A69F-F354C649CC41}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProofsDotnet.Legacy", "src\DataProofsDotnet.Legacy\DataProofsDotnet.Legacy.csproj", "{FB630922-2CA7-4368-9B02-6975250C4348}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProofsDotnet.Legacy.Tests", "tests\DataProofsDotnet.Legacy.Tests\DataProofsDotnet.Legacy.Tests.csproj", "{74CAD10E-C883-4924-8B13-AF048D361137}"
Expand Down Expand Up @@ -443,18 +439,6 @@ Global
{E96D6952-1F47-493A-8C11-BE24278530CF}.Release|x64.Build.0 = Release|Any CPU
{E96D6952-1F47-493A-8C11-BE24278530CF}.Release|x86.ActiveCfg = Release|Any CPU
{E96D6952-1F47-493A-8C11-BE24278530CF}.Release|x86.Build.0 = Release|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Debug|x64.ActiveCfg = Debug|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Debug|x64.Build.0 = Debug|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Debug|x86.ActiveCfg = Debug|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Debug|x86.Build.0 = Debug|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Release|Any CPU.Build.0 = Release|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Release|x64.ActiveCfg = Release|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Release|x64.Build.0 = Release|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Release|x86.ActiveCfg = Release|Any CPU
{D35D6BFF-66E7-4956-A69F-F354C649CC41}.Release|x86.Build.0 = Release|Any CPU
{FB630922-2CA7-4368-9B02-6975250C4348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB630922-2CA7-4368-9B02-6975250C4348}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB630922-2CA7-4368-9B02-6975250C4348}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -528,8 +512,6 @@ Global
{BCAF3445-C1E3-4260-A35B-37C620F7507E} = {51E1FF21-0147-325E-BF9F-837D1C0DC671}
{189960B5-5ED3-831C-6560-754D93EF961D} = {B554F44C-BC1A-140C-1D5D-67ADEC712783}
{E96D6952-1F47-493A-8C11-BE24278530CF} = {189960B5-5ED3-831C-6560-754D93EF961D}
{AA4E32FC-56B2-C72E-60FC-95F431031FA2} = {B554F44C-BC1A-140C-1D5D-67ADEC712783}
{D35D6BFF-66E7-4956-A69F-F354C649CC41} = {AA4E32FC-56B2-C72E-60FC-95F431031FA2}
{FB630922-2CA7-4368-9B02-6975250C4348} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{74CAD10E-C883-4924-8B13-AF048D361137} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{1A449C0C-3908-4917-AE93-AC230338942E} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
Expand Down
9 changes: 5 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!-- Preview line: the hyphenated SemVer suffix marks every build as a NuGet prerelease.
The first nuget-release ships as a preview (owner decision), not v1.0.0. Release CI
overrides this from the git tag (e.g. v0.1.0-preview.1 -> 0.1.0-preview.1). -->
<DataProofsVersion Condition="'$(DataProofsVersion)' == ''">0.1.0-preview.2</DataProofsVersion>
<!-- GA line: v1.0.0 (CHANGELOG [1.0.0]). A new, unpublished version, so the AC-10 clean-room
install resolves it (and its NetCrypto 1.1.0 dependency) only from the local pack feed —
no collision with the already-published 0.1.0-preview.* (which pin NetCrypto 1.0.0).
Release CI overrides this from the git tag (e.g. v1.0.0 -> 1.0.0). -->
<DataProofsVersion Condition="'$(DataProofsVersion)' == ''">1.0.0</DataProofsVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<Deterministic>true</Deterministic>
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ItemGroup>
<!-- Cryptography substrate: ALL cryptography (hashing, signing, AEAD, key wrap, KDF, BBS)
routes through NetCrypto — PRD §2.2; no other crypto backend may appear in src/. -->
<PackageVersion Include="NetCrypto" Version="1.0.0" />
<PackageVersion Include="NetCrypto" Version="1.1.0" />
<!-- Multiformats (multibase/multicodec/multihash/Multikey) + JCS — owned by NetCid stack-wide. -->
<PackageVersion Include="NetCid" Version="1.6.0" />
<!-- JWK boundary exception: only the JsonWebKey type may surface in public API (PRD §2.2, AC-7). -->
Expand Down
Loading
Loading