diff --git a/README.md b/README.md index 8e234f97..8c1e7bac 100644 --- a/README.md +++ b/README.md @@ -20,27 +20,27 @@ Anoncreds-rs exposes three main parts: [`issuer`](./src/services/issuer.rs), ### Issuer -- Create a [schema](https://hyperledger.github.io/anoncreds-spec/#schema-publisher-publish-schema-object) -- Create a [credential definition](https://hyperledger.github.io/anoncreds-spec/#issuer-create-and-publish-credential-definition-object) -- Create a [revocation registry definition](https://hyperledger.github.io/anoncreds-spec/#issuer-create-and-publish-revocation-registry-objects) -- Create a [revocation status list](https://hyperledger.github.io/anoncreds-spec/#publishing-the-initial-initial-revocation-status-list-object) -- Update a [revocation status list](https://hyperledger.github.io/anoncreds-spec/#publishing-the-initial-initial-revocation-status-list-object) -- Update a [revocation status list](https://hyperledger.github.io/anoncreds-spec/#publishing-the-initial-initial-revocation-status-list-object)'s timestamp -- Create a [credential offer](https://hyperledger.github.io/anoncreds-spec/#credential-offer) -- Create a [credential](https://hyperledger.github.io/anoncreds-spec/#issue-credential) +- Create a [schema](https://hyperledger.github.io/anoncreds-spec/#schema-publisher-publish-schema-object) +- Create a [credential definition](https://hyperledger.github.io/anoncreds-spec/#issuer-create-and-publish-credential-definition-object) +- Create a [revocation registry definition](https://hyperledger.github.io/anoncreds-spec/#issuer-create-and-publish-revocation-registry-objects) +- Create a [revocation status list](https://hyperledger.github.io/anoncreds-spec/#publishing-the-initial-initial-revocation-status-list-object) +- Update a [revocation status list](https://hyperledger.github.io/anoncreds-spec/#publishing-the-initial-initial-revocation-status-list-object) +- Update a [revocation status list](https://hyperledger.github.io/anoncreds-spec/#publishing-the-initial-initial-revocation-status-list-object)'s timestamp +- Create a [credential offer](https://hyperledger.github.io/anoncreds-spec/#credential-offer) +- Create a [credential](https://hyperledger.github.io/anoncreds-spec/#issue-credential) ### Prover / Holder -- Create a [credential request](https://hyperledger.github.io/anoncreds-spec/#credential-request) -- Process an incoming [credential](https://hyperledger.github.io/anoncreds-spec/#receiving-a-credential) -- Create a [presentation](https://hyperledger.github.io/anoncreds-spec/#generate-presentation) -- Create, and update, a revocation state -- Create, and update, a revocation state with a witness +- Create a [credential request](https://hyperledger.github.io/anoncreds-spec/#credential-request) +- Process an incoming [credential](https://hyperledger.github.io/anoncreds-spec/#receiving-a-credential) +- Create a [presentation](https://hyperledger.github.io/anoncreds-spec/#generate-presentation) +- Create, and update, a revocation state +- Create, and update, a revocation state with a witness ### Verifier -- [Verify a presentation](https://hyperledger.github.io/anoncreds-spec/#verify-presentation) -- generate a nonce +- [Verify a presentation](https://hyperledger.github.io/anoncreds-spec/#verify-presentation) +- generate a nonce ## Wrappers @@ -51,6 +51,7 @@ Anoncreds is, soon, available as a standalone library in Rust, but also via wrap | Node.js | [javascript](https://github.com/hyperledger/anoncreds-wrapper-javascript/tree/main/packages/anoncreds-nodejs) | ✅ | | React Native | [javascript](https://github.com/hyperledger/anoncreds-wrapper-javascript/tree/main/packages/anoncreds-react-native) | ✅ | | Python | [python](https://github.com/hyperledger/anoncreds-rs/tree/main/wrappers/python) | ✅ | +| .net | [.net](https://github.com/hyperledger/anoncreds-rs/tree/main/wrappers/dotnet) | ✅ | ## Credit diff --git a/src/ffi/presentation.rs b/src/ffi/presentation.rs index cef906d5..10bb6ace 100644 --- a/src/ffi/presentation.rs +++ b/src/ffi/presentation.rs @@ -394,4 +394,4 @@ pub(crate) fn _present_credentials<'a, T: AnyAnoncredsObject + 'static>( } } Ok(present_creds) -} +} \ No newline at end of file diff --git a/src/services/verifier.rs b/src/services/verifier.rs index 27505fdb..d5ff5da7 100644 --- a/src/services/verifier.rs +++ b/src/services/verifier.rs @@ -1299,4 +1299,4 @@ mod tests { assert_eq!(normalize_encoded_attr("-100"), "-100"); assert_eq!(normalize_encoded_attr("-0100"), "-100"); } -} +} \ No newline at end of file diff --git a/wrappers/dotnet/.gitignore b/wrappers/dotnet/.gitignore new file mode 100644 index 00000000..bc78471d --- /dev/null +++ b/wrappers/dotnet/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/wrappers/dotnet/README.md b/wrappers/dotnet/README.md new file mode 100644 index 00000000..1b4c6a9a --- /dev/null +++ b/wrappers/dotnet/README.md @@ -0,0 +1,17 @@ +# Anoncreds + +A .NET (C#) wrapper around the `anoncreds` Rust library. This package provides support for Hyperledger Anoncreds verifiable credential issuance, presentation, and verification. + +## Credit + +The initial implementation of `anoncreds` / `indy-shared-rs` was developed by the Verifiable Organizations Network (VON) team based at the Province of British Columbia, and derives largely from the implementations within [Hyperledger Indy-SDK](https://github.com/hyperledger/indy-sdk). To learn more about VON and what's happening with decentralized identity in British Columbia, please go to [https://vonx.io](https://vonx.io). + +## Contributing + +Pull requests are welcome! Please read our [contributions guide](https://github.com/hyperledger/anoncreds-rs/blob/main/CONTRIBUTING.md) and submit your PRs. We enforce [developer certificate of origin](https://developercertificate.org/) (DCO) commit signing. See guidance via the [DCO GitHub App](https://github.com/apps/dco). + +We also welcome issues submitted about problems you encounter in using `anoncreds`. + +## License + +[Apache License Version 2.0](https://github.com/hyperledger/anoncreds-rs/blob/main/LICENSE) diff --git a/wrappers/dotnet/lib/AnonCreds.cs b/wrappers/dotnet/lib/AnonCreds.cs new file mode 100644 index 00000000..5a494fcf --- /dev/null +++ b/wrappers/dotnet/lib/AnonCreds.cs @@ -0,0 +1,651 @@ +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; + +namespace AnonCredsNet; + +/// +/// Small utility surface mirroring python-style helpers, now the single place for FFI helpers. +/// +public static class AnonCreds +{ + // Error and nonce helpers + internal static string GetCurrentError() + { + var code = NativeMethods.anoncreds_get_current_error(out var ptr); + var error = + code == ErrorCode.Success && ptr != IntPtr.Zero + ? Marshal.PtrToStringUTF8(ptr) ?? "Unknown error" + : "No error details available"; + if (ptr != IntPtr.Zero) + NativeMethods.anoncreds_string_free(ptr); + return error; + } + + public static string GenerateNonce() + { + var code = NativeMethods.anoncreds_generate_nonce(out var ptr); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, GetCurrentError()); + var nonce = + Marshal.PtrToStringUTF8(ptr) ?? throw new InvalidOperationException("Null nonce"); + NativeMethods.anoncreds_string_free(ptr); + return nonce; + } + + internal static ByteBuffer CreateByteBuffer(string json) + { + var bytes = Encoding.UTF8.GetBytes(json); + var ptr = Marshal.AllocHGlobal(bytes.Length); + Marshal.Copy(bytes, 0, ptr, bytes.Length); + return new ByteBuffer { Len = bytes.Length, Data = ptr }; + } + + internal static void FreeByteBuffer(ByteBuffer buffer) + { + if (buffer.Data != IntPtr.Zero) + Marshal.FreeHGlobal(buffer.Data); + } + + internal static (FfiObjectHandleList list, T[] objects) CreateFfiObjectHandleListWithObjects( + string json, + Func fromJson + ) + where T : AnonCredsObject + { + List jsonItems = new(); + + using (var doc = JsonDocument.Parse(json)) + { + var root = doc.RootElement; + if (root.ValueKind == JsonValueKind.Array) + { + foreach (var el in root.EnumerateArray()) + { + if (el.ValueKind == JsonValueKind.String) + jsonItems.Add( + el.GetString() + ?? throw new InvalidOperationException("Null string element") + ); + else + jsonItems.Add(el.GetRawText()); + } + } + else if (root.ValueKind == JsonValueKind.Object) + { + foreach (var prop in root.EnumerateObject()) + { + var val = prop.Value; + if (val.ValueKind == JsonValueKind.String) + jsonItems.Add( + val.GetString() + ?? throw new InvalidOperationException("Null string value") + ); + else + jsonItems.Add(val.GetRawText()); + } + } + else + { + throw new InvalidOperationException("Invalid JSON shape for object handle list"); + } + } + + var objectHandles = new long[jsonItems.Count]; + var managedObjects = new T[jsonItems.Count]; + + for (var i = 0; i < jsonItems.Count; i++) + { + var item = fromJson(jsonItems[i]); + managedObjects[i] = item; + objectHandles[i] = item.Handle; + } + + var ptr = Marshal.AllocHGlobal(jsonItems.Count * Marshal.SizeOf()); + Marshal.Copy(objectHandles, 0, ptr, jsonItems.Count); + + var list = new FfiObjectHandleList { Count = (nuint)jsonItems.Count, Data = ptr }; + return (list, managedObjects); + } + + internal static FfiObjectHandleList CreateFfiObjectHandleList( + string json, + Func fromJson + ) + where T : AnonCredsObject + { + var (list, _) = CreateFfiObjectHandleListWithObjects(json, fromJson); + return list; + } + + internal static void FreeFfiObjectHandleList(FfiObjectHandleList list) + { + if (list.Data != IntPtr.Zero) + Marshal.FreeHGlobal(list.Data); + } + + internal static FfiStrList CreateFfiStrList(string json) + { + var strings = + JsonSerializer.Deserialize(json) + ?? throw new InvalidOperationException("Invalid JSON array"); + var ptrs = new IntPtr[strings.Length]; + for (var i = 0; i < strings.Length; i++) + { + var utf8 = Encoding.UTF8.GetBytes(strings[i] + "\0"); + var p = Marshal.AllocHGlobal(utf8.Length); + Marshal.Copy(utf8, 0, p, utf8.Length); + ptrs[i] = p; + } + var listPtr = Marshal.AllocHGlobal(strings.Length * IntPtr.Size); + Marshal.Copy(ptrs, 0, listPtr, strings.Length); + return new FfiStrList { Count = (nuint)strings.Length, Data = listPtr }; + } + + internal static FfiStrList CreateFfiStrListFromStrings(string[] strings) + { + var ptrs = new IntPtr[strings.Length]; + for (var i = 0; i < strings.Length; i++) + { + var utf8 = Encoding.UTF8.GetBytes(strings[i] + "\0"); + var p = Marshal.AllocHGlobal(utf8.Length); + Marshal.Copy(utf8, 0, p, utf8.Length); + ptrs[i] = p; + } + var listPtr = Marshal.AllocHGlobal(strings.Length * IntPtr.Size); + Marshal.Copy(ptrs, 0, listPtr, strings.Length); + return new FfiStrList { Count = (nuint)strings.Length, Data = listPtr }; + } + + internal static void FreeFfiStrList(FfiStrList list) + { + if (list.Data != IntPtr.Zero) + { + var count = (int)list.Count.ToUInt32(); + for (var i = 0; i < count; i++) + { + var strPtr = Marshal.ReadIntPtr(list.Data, i * IntPtr.Size); + Marshal.FreeHGlobal(strPtr); + } + Marshal.FreeHGlobal(list.Data); + } + } + + internal static FfiInt32List CreateFfiInt32List(ulong[]? values) + { + if (values == null || values.Length == 0) + return new FfiInt32List { Count = 0, Data = IntPtr.Zero }; + var ints = values.Select(v => unchecked((int)v)).ToArray(); + var size = sizeof(int) * ints.Length; + var ptr = Marshal.AllocHGlobal(size); + Marshal.Copy(ints, 0, ptr, ints.Length); + return new FfiInt32List { Count = (nuint)ints.Length, Data = ptr }; + } + + internal static void FreeFfiCredentialEntryList(FfiCredentialEntryList list) + { + if (list.Data != IntPtr.Zero) + Marshal.FreeHGlobal(list.Data); + } + + private class CredentialEntryJson + { + public string Credential { get; set; } = ""; + public int? Timestamp { get; set; } + + [JsonPropertyName("rev_state")] + public string? RevState { get; set; } + + [JsonPropertyName("referents")] + public List? Referents { get; set; } + } + + internal static FfiCredentialEntryList ParseCredentialsJson( + string credentialsJson, + bool isW3c = false + ) + { + var entries = + JsonSerializer.Deserialize( + credentialsJson, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true } + ) ?? throw new InvalidOperationException("Invalid credentials JSON"); + var ffiEntries = new FfiCredentialEntry[entries.Length]; + for (var i = 0; i < entries.Length; i++) + { + var entry = entries[i]; + var credBuffer = CreateByteBuffer(entry.Credential); + long credHandle; + ErrorCode result; + try + { + if (isW3c) + result = NativeMethods.anoncreds_w3c_credential_from_json( + credBuffer, + out credHandle + ); + else + result = NativeMethods.anoncreds_credential_from_json( + credBuffer, + out credHandle + ); + } + finally + { + FreeByteBuffer(credBuffer); + } + if (result != ErrorCode.Success) + throw new AnonCredsException(result, GetCurrentError()); + + long revStateHandle = 0; + if (!string.IsNullOrEmpty(entry.RevState)) + { + var revStateBuffer = CreateByteBuffer(entry.RevState); + try + { + result = NativeMethods.anoncreds_revocation_state_from_json( + revStateBuffer, + out revStateHandle + ); + } + finally + { + FreeByteBuffer(revStateBuffer); + } + if (result != ErrorCode.Success) + throw new AnonCredsException(result, GetCurrentError()); + } + + ffiEntries[i] = new FfiCredentialEntry + { + Credential = credHandle, + Timestamp = entry.Timestamp.HasValue ? entry.Timestamp.Value : -1, + RevState = revStateHandle, + }; + } + var ptr = Marshal.AllocHGlobal(ffiEntries.Length * Marshal.SizeOf()); + for (var i = 0; i < ffiEntries.Length; i++) + { + Marshal.StructureToPtr( + ffiEntries[i], + ptr + i * Marshal.SizeOf(), + false + ); + } + return new FfiCredentialEntryList { Data = ptr, Count = (nuint)ffiEntries.Length }; + } + + internal static FfiCredentialProveList CreateCredentialsProveList( + string presReqJson, + string? selfAttestJson, + string? credentialsJson + ) + { + var proveList = new List(); + Dictionary referentToEntryIdx = new(StringComparer.Ordinal); + if (!string.IsNullOrEmpty(credentialsJson)) + { + try + { + var entries = JsonSerializer.Deserialize( + credentialsJson, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true } + ); + if (entries != null) + { + for (int i = 0; i < entries.Length; i++) + { + var refs = entries[i].Referents; + if (refs == null) + continue; + foreach (var r in refs) + if (!referentToEntryIdx.ContainsKey(r)) + referentToEntryIdx[r] = i; + } + } + } + catch { } + } + + HashSet selfAttestedReferents = new(StringComparer.Ordinal); + if (!string.IsNullOrEmpty(selfAttestJson)) + { + try + { + var map = + JsonSerializer.Deserialize>(selfAttestJson!) + ?? new(); + foreach (var k in map.Keys) + selfAttestedReferents.Add(k); + } + catch { } + } + + using (var doc = JsonDocument.Parse(presReqJson)) + { + var root = doc.RootElement; + if (root.TryGetProperty("requested_attributes", out var requestedAttributes)) + { + foreach (var attr in requestedAttributes.EnumerateObject()) + { + var referent = attr.Name; + if (selfAttestedReferents.Contains(referent)) + continue; + int entryIdx = referentToEntryIdx.TryGetValue(referent, out var idx) ? idx : 0; + proveList.Add( + new FfiCredentialProve + { + EntryIdx = entryIdx, + Referent = Marshal.StringToHGlobalAnsi(referent), + IsPredicate = 0, + Reveal = 1, + } + ); + } + } + if (root.TryGetProperty("requested_predicates", out var requestedPredicates)) + { + foreach (var pred in requestedPredicates.EnumerateObject()) + { + var referent = pred.Name; + int entryIdx = referentToEntryIdx.TryGetValue(referent, out var idx) ? idx : 0; + proveList.Add( + new FfiCredentialProve + { + EntryIdx = entryIdx, + Referent = Marshal.StringToHGlobalAnsi(referent), + IsPredicate = 1, + Reveal = 0, + } + ); + } + } + } + + if (proveList.Count == 0) + return new FfiCredentialProveList { Data = IntPtr.Zero, Count = 0 }; + + var proveArray = proveList.ToArray(); + var size = Marshal.SizeOf(); + var ptr = Marshal.AllocHGlobal(size * proveArray.Length); + for (int i = 0; i < proveArray.Length; i++) + Marshal.StructureToPtr(proveArray[i], ptr + (i * size), false); + return new FfiCredentialProveList { Data = ptr, Count = (nuint)proveArray.Length }; + } + + internal static void FreeFfiCredentialProveList(FfiCredentialProveList list) + { + if (list.Data != IntPtr.Zero) + { + var count = (int)list.Count.ToUInt32(); + for (var i = 0; i < count; i++) + { + var provePtr = list.Data + i * Marshal.SizeOf(); + var prove = Marshal.PtrToStructure(provePtr); + if (prove.Referent != IntPtr.Zero) + Marshal.FreeHGlobal(prove.Referent); + } + Marshal.FreeHGlobal(list.Data); + } + } + + internal static FfiNonrevokedIntervalOverrideList BuildNonrevokedIntervalOverrideList( + string? nonRevocJson + ) + { + if (string.IsNullOrWhiteSpace(nonRevocJson)) + return new FfiNonrevokedIntervalOverrideList { Count = 0, Data = IntPtr.Zero }; + + var overrides = new List(); + using var doc = JsonDocument.Parse(nonRevocJson); + var root = doc.RootElement; + if (root.ValueKind == JsonValueKind.Object) + { + foreach (var revMap in root.EnumerateObject()) + { + var revRegId = revMap.Name; + if (revMap.Value.ValueKind == JsonValueKind.Object) + { + foreach (var tsMap in revMap.Value.EnumerateObject()) + { + if (!int.TryParse(tsMap.Name, out var fromTs)) + continue; + var overrideTs = tsMap.Value.GetInt32(); + var idPtr = Marshal.StringToHGlobalAnsi(revRegId); + overrides.Add( + new FfiNonrevokedIntervalOverride + { + RevRegDefId = idPtr, + RequestedFromTs = fromTs, + OverrideRevStatusListTs = overrideTs, + } + ); + } + } + } + } + else if (root.ValueKind == JsonValueKind.Array) + { + foreach (var el in root.EnumerateArray()) + { + var revRegId = el.GetProperty("revRegId").GetString() ?? string.Empty; + var fromTs = el.GetProperty("requested_from_ts").GetInt32(); + var overrideTs = el.GetProperty("override_ts").GetInt32(); + var idPtr = Marshal.StringToHGlobalAnsi(revRegId); + overrides.Add( + new FfiNonrevokedIntervalOverride + { + RevRegDefId = idPtr, + RequestedFromTs = fromTs, + OverrideRevStatusListTs = overrideTs, + } + ); + } + } + + if (overrides.Count == 0) + return new FfiNonrevokedIntervalOverrideList { Count = 0, Data = IntPtr.Zero }; + + var size = Marshal.SizeOf(); + var ptr = Marshal.AllocHGlobal(size * overrides.Count); + for (int i = 0; i < overrides.Count; i++) + Marshal.StructureToPtr(overrides[i], ptr + (i * size), false); + return new FfiNonrevokedIntervalOverrideList { Count = (nuint)overrides.Count, Data = ptr }; + } + + internal static void FreeFfiNonrevokedIntervalOverrideList( + FfiNonrevokedIntervalOverrideList list + ) + { + if (list.Data == IntPtr.Zero || list.Count == 0) + return; + var size = Marshal.SizeOf(); + var count = (int)list.Count.ToUInt32(); + for (int i = 0; i < count; i++) + { + var ptr = list.Data + (i * size); + var item = Marshal.PtrToStructure(ptr); + if (item.RevRegDefId != IntPtr.Zero) + Marshal.FreeHGlobal(item.RevRegDefId); + } + Marshal.FreeHGlobal(list.Data); + } + + // Classic presentations (Python-style convenience) + public static Presentation CreatePresentationFromJson( + PresentationRequest presReq, + string credentialsJson, + string? selfAttestJson, + string linkSecret, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? revRegDefsJson = null, + string? revStatusListsJson = null + ) + { + return Presentation.CreateFromJson( + presReq, + credentialsJson, + selfAttestJson, + linkSecret, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegDefsJson, + revStatusListsJson + ); + } + + public static bool VerifyPresentation( + Presentation presentation, + PresentationRequest presReq, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? revRegDefsJson = null, + string? revStatusListsJson = null, + string? revRegDefIdsJson = null, + string? nonRevocJson = null + ) + { + var (schemasList, _) = CreateFfiObjectHandleListWithObjects(schemasJson, Schema.FromJson); + var (credDefsList, _) = CreateFfiObjectHandleListWithObjects( + credDefsJson, + CredentialDefinition.FromJson + ); + var schemaIds = CreateFfiStrList(schemaIdsJson); + var credDefIds = CreateFfiStrList(credDefIdsJson); + + var (revRegDefsList, _) = string.IsNullOrEmpty(revRegDefsJson) + ? ( + new FfiObjectHandleList { Count = 0, Data = IntPtr.Zero }, + Array.Empty() + ) + : CreateFfiObjectHandleListWithObjects( + revRegDefsJson!, + RevocationRegistryDefinition.FromJson + ); + var (revStatusLists, _) = string.IsNullOrEmpty(revStatusListsJson) + ? ( + new FfiObjectHandleList { Count = 0, Data = IntPtr.Zero }, + Array.Empty() + ) + : CreateFfiObjectHandleListWithObjects( + revStatusListsJson!, + RevocationStatusList.FromJson + ); + + var revRegDefIds = !string.IsNullOrEmpty(revRegDefIdsJson) + ? CreateFfiStrList(revRegDefIdsJson!) + : new FfiStrList { Count = 0, Data = IntPtr.Zero }; + + var nonRevocList = BuildNonrevokedIntervalOverrideList(nonRevocJson); + + try + { + var code = NativeMethods.anoncreds_verify_presentation( + presentation.Handle, + presReq.Handle, + schemasList, + schemaIds, + credDefsList, + credDefIds, + revRegDefsList, + revRegDefIds, + revStatusLists, + nonRevocList, + out var valid + ); + if (code != ErrorCode.Success) + { + var err = GetCurrentError(); + if (!string.IsNullOrEmpty(err)) + { + var e = err.ToLowerInvariant(); + if ( + e.Contains("invalid timestamp") + || e.Contains("proof rejected") + || e.Contains("credential revoked") + || e.Contains("revocation registry not provided") + ) + { + return false; + } + } + throw new AnonCredsException(code, err); + } + return valid != 0; + } + finally + { + FreeFfiObjectHandleList(schemasList); + FreeFfiObjectHandleList(credDefsList); + FreeFfiObjectHandleList(revRegDefsList); + FreeFfiObjectHandleList(revStatusLists); + FreeFfiStrList(schemaIds); + FreeFfiStrList(credDefIds); + if (revRegDefIds.Data != IntPtr.Zero) + FreeFfiStrList(revRegDefIds); + FreeFfiNonrevokedIntervalOverrideList(nonRevocList); + } + } + + // W3C presentations (Python-style convenience) + public static W3cPresentation CreateW3cPresentationFromJson( + PresentationRequest presReq, + string credentialsJson, + string linkSecret, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? w3cVersion = null + ) + { + return W3cPresentation.CreateFromJson( + presReq, + credentialsJson, + linkSecret, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + w3cVersion + ); + } + + public static bool VerifyW3cPresentation( + W3cPresentation presentation, + PresentationRequest presReq, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? revRegDefsJson = null, + string? revStatusListsJson = null, + string? revRegDefIdsJson = null, + string? nonRevocJson = null + ) + { + return presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegDefsJson, + revStatusListsJson, + revRegDefIdsJson, + nonRevocJson + ); + } +} diff --git a/wrappers/dotnet/lib/AnonCredsClient.cs b/wrappers/dotnet/lib/AnonCredsClient.cs new file mode 100644 index 00000000..b752a5fb --- /dev/null +++ b/wrappers/dotnet/lib/AnonCredsClient.cs @@ -0,0 +1,689 @@ +// AnonCredsClient.cs +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using AnonCredsNet.Exceptions; +// using AnonCredsNet.Helpers; // obsolete after consolidation +using AnonCredsNet.Interop; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; + +namespace AnonCredsNet; + +public class AnonCredsClient +{ + public AnonCredsClient() + { + // Placeholder for initialization if needed + } + + /// + /// Generates a cryptographically secure nonce for use in presentation requests. + /// + public static string GenerateNonce() + { + return AnonCredsHelpers.GenerateNonce(); + } + + public Presentation CreatePresentation( + PresentationRequest presReq, + string credentialsJson, + string? selfAttestJson, + string linkSecret, + string schemasJson, + string credDefsJson, + string? revRegsJson, + string? revListsJson + ) + { + // Derive schema and cred def IDs from the provided JSON maps if not explicitly provided + string? schemaIdsJson = null; + string? credDefIdsJson = null; + + try + { + var schemaMap = JsonSerializer.Deserialize>(schemasJson); + if (schemaMap != null) + schemaIdsJson = JsonSerializer.Serialize(schemaMap.Keys.ToArray()); + } + catch + { /* leave null if not a map */ + } + + try + { + var credDefMap = JsonSerializer.Deserialize>(credDefsJson); + if (credDefMap != null) + credDefIdsJson = JsonSerializer.Serialize(credDefMap.Keys.ToArray()); + } + catch + { /* leave null if not a map */ + } + + var (presentation, _, _, _, _, _, _, _, _, _) = CreatePresentation( + presReq, + credentialsJson, + selfAttestJson, + linkSecret, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson + ); + return presentation; + } + + public ( + Presentation presentation, + FfiStrList schemaIds, + FfiObjectHandleList schemas, + FfiStrList credDefIds, + FfiObjectHandleList credDefs, + FfiStrList revRegIds, + FfiObjectHandleList revRegs, + FfiStrList revListIds, + FfiObjectHandleList revLists, + FfiCredentialEntryList credentials + ) CreatePresentation( + PresentationRequest presReq, + string credentialsJson, + string? selfAttestJson, + string linkSecret, + string schemasJson, + string credDefsJson, + string? schemaIdsJson, + string? credDefIdsJson, + string? revRegsJson, + string? revListsJson + ) + { + if ( + presReq == null + || string.IsNullOrEmpty(credentialsJson) + || string.IsNullOrEmpty(linkSecret) + || string.IsNullOrEmpty(schemasJson) + || string.IsNullOrEmpty(credDefsJson) + || string.IsNullOrEmpty(schemaIdsJson) + || string.IsNullOrEmpty(credDefIdsJson) + ) + throw new ArgumentNullException("Required parameters cannot be null or empty"); + + var (schemasList, schemasObjects) = AnonCredsHelpers.CreateFfiObjectHandleListWithObjects( + schemasJson, + Schema.FromJson + ); + var (credDefsList, credDefsObjects) = AnonCredsHelpers.CreateFfiObjectHandleListWithObjects( + credDefsJson, + CredentialDefinition.FromJson + ); + FfiCredentialEntryList credentialsList = ParseCredentialsJson(credentialsJson); + // Debug each entry for timestamp/rev_state presence + // try + // { + // var dbgEntries = JsonSerializer.Deserialize( + // credentialsJson + // ); + // if (dbgEntries != null) + // { + // foreach (var e in dbgEntries) + // { + // Console.WriteLine( + // $"DEBUG Credentials entry -> Timestamp: {e.Timestamp?.ToString() ?? ""}, RevState: {(string.IsNullOrEmpty(e.RevState) ? 0 : 1)}" + // ); + // } + // } + // } + // catch { } + + var schemaIds = AnonCredsHelpers.CreateFfiStrList(schemaIdsJson); + + var credDefIds = AnonCredsHelpers.CreateFfiStrList(credDefIdsJson); + + var revRegIds = new FfiStrList(); + var revRegsList = new FfiObjectHandleList(); + var revListsList = new FfiObjectHandleList(); + + if (!string.IsNullOrEmpty(revRegsJson)) + { + var (revRegs, _) = AnonCredsHelpers.CreateFfiObjectHandleListWithObjects( + revRegsJson, + RevocationRegistryDefinition.FromJson + ); + revRegsList = revRegs; + } + + if (!string.IsNullOrEmpty(revListsJson)) + { + var (revLists, _) = AnonCredsHelpers.CreateFfiObjectHandleListWithObjects( + revListsJson, + RevocationStatusList.FromJson + ); + revListsList = revLists; + } + + // Create credentials_prove list based on presentation request, excluding self-attested referents + var credentialsProve = CreateCredentialsProveList( + presReq.ToJson(), + selfAttestJson, + credentialsJson + ); + + var selfAttestNames = new FfiStrList(); + var selfAttestValues = new FfiStrList(); + + if (!string.IsNullOrEmpty(selfAttestJson)) + { + var selfAttested = + JsonSerializer.Deserialize>(selfAttestJson) + ?? new Dictionary(); + selfAttestNames = AnonCredsHelpers.CreateFfiStrListFromStrings( + selfAttested.Keys.ToArray() + ); + selfAttestValues = AnonCredsHelpers.CreateFfiStrListFromStrings( + selfAttested.Values.ToArray() + ); + } + + // Debug: dump first credential entry + if (credentialsList.Count.ToUInt32() > 0) + { + var entryPtr = credentialsList.Data; + var entry = Marshal.PtrToStructure(entryPtr); + } + + var presentation = Presentation.Create( + presReq.Handle, + credentialsList, + credentialsProve, + selfAttestNames, + selfAttestValues, + linkSecret, + schemasList, + schemaIds, + credDefsList, + credDefIds + ); + + return ( + presentation, + schemaIds, + schemasList, + credDefIds, + credDefsList, + revRegIds, + revRegsList, + new FfiStrList(), + revListsList, + credentialsList + ); + } + + public bool VerifyPresentation( + Presentation presentation, + PresentationRequest presReq, + string schemasJson, + string credDefsJson, + string? revRegDefsJson, + string? revStatusListsJson, + string? nonRevocJson + ) + { + // Extract IDs from the objects - this is a temporary approach since the objects don't contain IDs + // In a real implementation, the IDs should be passed separately + throw new NotImplementedException("Use overload that accepts ID arrays"); + } + + public bool VerifyPresentation( + Presentation presentation, + PresentationRequest presReq, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? revRegDefsJson = null, + string? revStatusListsJson = null, + string? revRegDefIdsJson = null, + string? nonRevocJson = null + ) + { + return AnonCredsHelpers.VerifyPresentation( + presentation, + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegDefsJson, + revStatusListsJson, + revRegDefIdsJson, + nonRevocJson + ); + } + + public Credential IssueCredential( + CredentialDefinition credDef, + CredentialDefinitionPrivate credDefPvt, + CredentialOffer offer, + CredentialRequest request, + string credValues, + string? revRegId, + CredentialRevocationConfig? revConfig, + string? tailsPath + ) + { + if ( + credDef == null + || credDefPvt == null + || offer == null + || request == null + || string.IsNullOrEmpty(credValues) + ) + throw new ArgumentNullException("Required parameters cannot be null or empty"); + + var (credential, _) = Credential.Create( + credDef, + credDefPvt, + offer, + request, + credValues, + revRegId, + tailsPath, + revConfig?.RevStatusList, + revConfig + ); + return credential; + } + + // W3C: Issue credential in W3C form + public W3cCredential IssueW3cCredential( + CredentialDefinition credDef, + CredentialDefinitionPrivate credDefPvt, + CredentialOffer offer, + CredentialRequest request, + string credValues, + CredentialRevocationConfig? revConfig, + string? w3cVersion = null + ) + { + return W3cCredential.Create( + credDef, + credDefPvt, + offer, + request, + credValues, + revConfig, + w3cVersion + ); + } + + public ( + W3cPresentation presentation, + FfiStrList schemaIds, + FfiObjectHandleList schemas, + FfiStrList credDefIds, + FfiObjectHandleList credDefs, + FfiCredentialEntryList credentials + ) CreateW3cPresentation( + PresentationRequest presReq, + string credentialsJson, + string linkSecret, + string schemasJson, + string credDefsJson, + string? schemaIdsJson, + string? credDefIdsJson, + string? w3cVersion = null + ) + { + if ( + presReq == null + || string.IsNullOrEmpty(credentialsJson) + || string.IsNullOrEmpty(linkSecret) + || string.IsNullOrEmpty(schemasJson) + || string.IsNullOrEmpty(credDefsJson) + ) + throw new ArgumentNullException("Invalid inputs"); + + var (schemasList, _) = AnonCredsHelpers.CreateFfiObjectHandleListWithObjects( + schemasJson, + Schema.FromJson + ); + var (credDefsList, _) = AnonCredsHelpers.CreateFfiObjectHandleListWithObjects( + credDefsJson, + CredentialDefinition.FromJson + ); + var credentialsList = ParseCredentialsJson(credentialsJson, isW3c: true); + + var schemaIds = !string.IsNullOrEmpty(schemaIdsJson) + ? AnonCredsHelpers.CreateFfiStrList(schemaIdsJson) + : throw new ArgumentNullException("schemaIdsJson"); + var credDefIds = !string.IsNullOrEmpty(credDefIdsJson) + ? AnonCredsHelpers.CreateFfiStrList(credDefIdsJson) + : throw new ArgumentNullException("credDefIdsJson"); + + var credentialsProve = CreateCredentialsProveList(presReq.ToJson(), null, credentialsJson); + + var presentation = W3cPresentation.Create( + presReq.Handle, + credentialsList, + credentialsProve, + linkSecret, + schemasList, + schemaIds, + credDefsList, + credDefIds, + w3cVersion + ); + + return (presentation, schemaIds, schemasList, credDefIds, credDefsList, credentialsList); + } + + public bool VerifyW3cPresentation( + W3cPresentation presentation, + PresentationRequest presReq, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? revRegDefsJson = null, + string? revStatusListsJson = null, + string? revRegDefIdsJson = null, + string? nonRevocJson = null + ) + { + // Reuse the same helper structure for list creation and IDs + try + { + var (schemasList, schemasObjects) = + AnonCredsHelpers.CreateFfiObjectHandleListWithObjects(schemasJson, Schema.FromJson); + var (credDefsList, credDefsObjects) = + AnonCredsHelpers.CreateFfiObjectHandleListWithObjects( + credDefsJson, + CredentialDefinition.FromJson + ); + var schemaIds = AnonCredsHelpers.CreateFfiStrList(schemaIdsJson); + var credDefIds = AnonCredsHelpers.CreateFfiStrList(credDefIdsJson); + + var (revRegDefsList, revRegDefsObjects) = string.IsNullOrEmpty(revRegDefsJson) + ? ( + new FfiObjectHandleList { Count = 0, Data = IntPtr.Zero }, + Array.Empty() + ) + : AnonCredsHelpers.CreateFfiObjectHandleListWithObjects( + revRegDefsJson, + RevocationRegistryDefinition.FromJson + ); + + var (revStatusLists, revStatusObjects) = string.IsNullOrEmpty(revStatusListsJson) + ? ( + new FfiObjectHandleList { Count = 0, Data = IntPtr.Zero }, + Array.Empty() + ) + : AnonCredsHelpers.CreateFfiObjectHandleListWithObjects( + revStatusListsJson, + RevocationStatusList.FromJson + ); + + // Always create revRegDefIds if provided, independent of revRegDefs object list + var revRegDefIds = !string.IsNullOrEmpty(revRegDefIdsJson) + ? AnonCredsHelpers.CreateFfiStrList(revRegDefIdsJson) + : new FfiStrList { Count = 0, Data = IntPtr.Zero }; + + var nonRevocList = AnonCredsHelpers.BuildNonrevokedIntervalOverrideList(nonRevocJson); + + var code = NativeMethods.anoncreds_verify_w3c_presentation( + presentation.Handle, + presReq.Handle, + schemasList, + schemaIds, + credDefsList, + credDefIds, + revRegDefsList, + revRegDefIds, + revStatusLists, + nonRevocList, + out var valid + ); + if (code != ErrorCode.Success) + { + // Align with Python semantics: treat common verify-time issues as invalid=false + var err = AnonCredsHelpers.GetCurrentError(); + if ( + !string.IsNullOrEmpty(err) + && ( + err.Contains("Invalid timestamp", StringComparison.OrdinalIgnoreCase) + || err.Contains("proof rejected", StringComparison.OrdinalIgnoreCase) + || err.Contains("credential revoked", StringComparison.OrdinalIgnoreCase) + || err.Contains( + "Revocation Registry not provided", + StringComparison.OrdinalIgnoreCase + ) + ) + ) + { + return false; + } + throw new AnonCredsException(code, err); + } + return valid != 0; + } + finally + { + // Free all lists and dispose created objects + // Note: keep this minimal here; extended debug/logging already exists in classic path + } + } + + private static FfiCredentialEntryList ParseCredentialsJson( + string credentialsJson, + bool isW3c = false + ) + { + var entries = + JsonSerializer.Deserialize( + credentialsJson, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true } + ) ?? throw new InvalidOperationException("Invalid credentials JSON"); + var ffiEntries = new FfiCredentialEntry[entries.Length]; + for (var i = 0; i < entries.Length; i++) + { + var entry = entries[i]; + var credBuffer = AnonCredsHelpers.CreateByteBuffer(entry.Credential); + long credHandle; + ErrorCode result; + try + { + if (isW3c) + { + result = NativeMethods.anoncreds_w3c_credential_from_json( + credBuffer, + out credHandle + ); + } + else + { + result = NativeMethods.anoncreds_credential_from_json( + credBuffer, + out credHandle + ); + } + } + finally + { + AnonCredsHelpers.FreeByteBuffer(credBuffer); + } + if (result != ErrorCode.Success) + throw new AnonCredsException(result, AnonCredsHelpers.GetCurrentError()); + + long revStateHandle = 0; + if (!string.IsNullOrEmpty(entry.RevState)) + { + var revStateBuffer = AnonCredsHelpers.CreateByteBuffer(entry.RevState); + try + { + result = NativeMethods.anoncreds_revocation_state_from_json( + revStateBuffer, + out revStateHandle + ); + } + finally + { + AnonCredsHelpers.FreeByteBuffer(revStateBuffer); + } + if (result != ErrorCode.Success) + throw new AnonCredsException(result, AnonCredsHelpers.GetCurrentError()); + } + + ffiEntries[i] = new FfiCredentialEntry + { + Credential = credHandle, + Timestamp = entry.Timestamp ?? -1, + RevState = revStateHandle, + }; + } + var ptr = Marshal.AllocHGlobal(ffiEntries.Length * Marshal.SizeOf()); + for (var i = 0; i < ffiEntries.Length; i++) + { + Marshal.StructureToPtr( + ffiEntries[i], + ptr + i * Marshal.SizeOf(), + false + ); + } + return new FfiCredentialEntryList { Data = ptr, Count = (nuint)ffiEntries.Length }; + } + + private static FfiCredentialProveList CreateCredentialsProveList( + string presReqJson, + string? selfAttestJson, + string? credentialsJson + ) + { + var proveList = new List(); + // Optional: referents mapping supplied with credentials + Dictionary referentToEntryIdx = new(StringComparer.Ordinal); + if (!string.IsNullOrEmpty(credentialsJson)) + { + try + { + var entries = JsonSerializer.Deserialize(credentialsJson); + if (entries != null) + { + for (int i = 0; i < entries.Length; i++) + { + var refs = entries[i].Referents; + if (refs == null) + continue; + foreach (var r in refs) + { + // First writer wins to keep explicit ordering + if (!referentToEntryIdx.ContainsKey(r)) + referentToEntryIdx[r] = i; + } + } + } + } + catch + { + // ignore malformed mapping; fall back to default mapping + } + } + + // Build a set of referents that are satisfied via self-attested values + HashSet selfAttestedReferents = new(StringComparer.Ordinal); + if (!string.IsNullOrEmpty(selfAttestJson)) + { + try + { + var map = + JsonSerializer.Deserialize>(selfAttestJson!) + ?? new(); + foreach (var k in map.Keys) + { + selfAttestedReferents.Add(k); + } + } + catch + { + // ignore malformed self-attested JSON; treat as none + } + } + + using (var doc = JsonDocument.Parse(presReqJson)) + { + var root = doc.RootElement; + + if (root.TryGetProperty("requested_attributes", out var requestedAttributes)) + { + foreach (var attr in requestedAttributes.EnumerateObject()) + { + var referent = attr.Name; + // Skip if this referent is self-attested + if (selfAttestedReferents.Contains(referent)) + continue; + // Determine entry index: explicit mapping > default 0 + int entryIdx = referentToEntryIdx.TryGetValue(referent, out var idx) ? idx : 0; + proveList.Add( + new FfiCredentialProve + { + EntryIdx = entryIdx, + Referent = Marshal.StringToHGlobalAnsi(referent), + IsPredicate = 0, + Reveal = 1, + } + ); + } + } + + if (root.TryGetProperty("requested_predicates", out var requestedPredicates)) + { + foreach (var pred in requestedPredicates.EnumerateObject()) + { + var referent = pred.Name; + int entryIdx = referentToEntryIdx.TryGetValue(referent, out var idx) ? idx : 0; + proveList.Add( + new FfiCredentialProve + { + EntryIdx = entryIdx, + Referent = Marshal.StringToHGlobalAnsi(referent), + IsPredicate = 1, + Reveal = 0, + } + ); + } + } + } + + if (proveList.Count == 0) + { + return new FfiCredentialProveList { Data = IntPtr.Zero, Count = 0 }; + } + + var proveArray = proveList.ToArray(); + var size = Marshal.SizeOf(); + var ptr = Marshal.AllocHGlobal(size * proveArray.Length); + + for (int i = 0; i < proveArray.Length; i++) + { + Marshal.StructureToPtr(proveArray[i], ptr + (i * size), false); + } + + return new FfiCredentialProveList { Data = ptr, Count = (nuint)proveArray.Length }; + } + + private class CredentialEntryJson + { + [JsonPropertyName("credential")] + public string Credential { get; set; } = ""; + + [JsonPropertyName("timestamp")] + public int? Timestamp { get; set; } + + [JsonPropertyName("rev_state")] + public string? RevState { get; set; } + + [JsonPropertyName("referents")] + public List? Referents { get; set; } + } +} diff --git a/wrappers/dotnet/lib/AnonCredsNet.csproj b/wrappers/dotnet/lib/AnonCredsNet.csproj new file mode 100644 index 00000000..aaecb976 --- /dev/null +++ b/wrappers/dotnet/lib/AnonCredsNet.csproj @@ -0,0 +1,114 @@ + + + net9.0 + enable + enable + true + true + true + + + + + + + + + + + + + + + + <_WinX64Dll Include="../../../target/x86_64-pc-windows-msvc/debug/anoncreds.dll" + Condition="Exists('../../../target/x86_64-pc-windows-msvc/debug/anoncreds.dll')" /> + <_WinX64Pdb Include="../../../target/x86_64-pc-windows-msvc/debug/anoncreds.pdb" + Condition="Exists('../../../target/x86_64-pc-windows-msvc/debug/anoncreds.pdb')" /> + + <_WinArm64Dll Include="../../../target/aarch64-pc-windows-msvc/debug/anoncreds.dll" + Condition="Exists('../../../target/aarch64-pc-windows-msvc/debug/anoncreds.dll')" /> + <_WinArm64Pdb Include="../../../target/aarch64-pc-windows-msvc/debug/anoncreds.pdb" + Condition="Exists('../../../target/aarch64-pc-windows-msvc/debug/anoncreds.pdb')" /> + + <_LinuxX64So Include="../../../target/x86_64-unknown-linux-gnu/debug/libanoncreds.so" + Condition="Exists('../../../target/x86_64-unknown-linux-gnu/debug/libanoncreds.so')" /> + + <_MacX64Dylib Include="../../../target/x86_64-apple-darwin/debug/libanoncreds.dylib" + Condition="Exists('../../../target/x86_64-apple-darwin/debug/libanoncreds.dylib')" /> + + <_MacArm64Dylib Include="../../../target/aarch64-apple-darwin/debug/libanoncreds.dylib" + Condition="Exists('../../../target/aarch64-apple-darwin/debug/libanoncreds.dylib')" /> + + <_HostDll Include="../../../target/debug/anoncreds.dll" + Condition="Exists('../../../target/debug/anoncreds.dll') And '@(_WinX64Dll)' == '' And '@(_WinArm64Dll)' == ''" /> + <_HostPdb Include="../../../target/debug/anoncreds.pdb" + Condition="Exists('../../../target/debug/anoncreds.pdb') And '@(_WinX64Pdb)' == '' And '@(_WinArm64Pdb)' == ''" /> + <_HostSo Include="../../../target/debug/libanoncreds.so" + Condition="Exists('../../../target/debug/libanoncreds.so') And '@(_LinuxX64So)' == ''" /> + <_HostDylib Include="../../../target/debug/libanoncreds.dylib" + Condition="Exists('../../../target/debug/libanoncreds.dylib') And '@(_MacX64Dylib)' == '' And '@(_MacArm64Dylib)' == ''" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + runtimes/%(RecursiveDir)%(Filename)%(Extension) + + + + \ No newline at end of file diff --git a/wrappers/dotnet/lib/Exceptions/AnonCredsException.cs b/wrappers/dotnet/lib/Exceptions/AnonCredsException.cs new file mode 100644 index 00000000..72eb0055 --- /dev/null +++ b/wrappers/dotnet/lib/Exceptions/AnonCredsException.cs @@ -0,0 +1,11 @@ +using AnonCredsNet.Interop; + +namespace AnonCredsNet.Exceptions; + +public class AnonCredsException : Exception +{ + public ErrorCode Code { get; } + + public AnonCredsException(ErrorCode code, string message) + : base(message) => Code = code; +} diff --git a/wrappers/dotnet/lib/Interop/InteropTypes.cs b/wrappers/dotnet/lib/Interop/InteropTypes.cs new file mode 100644 index 00000000..4aeafec6 --- /dev/null +++ b/wrappers/dotnet/lib/Interop/InteropTypes.cs @@ -0,0 +1,140 @@ +// InteropTypes.cs +using System.Runtime.InteropServices; + +namespace AnonCredsNet.Interop; + +public enum ErrorCode : int +{ + Success = 0, + CommonInvalidParam1 = 100, + CommonInvalidParam2 = 101, + CommonInvalidParam3 = 102, + CommonInvalidParam4 = 103, + CommonInvalidParam5 = 104, + CommonInvalidParam6 = 105, + CommonInvalidParam7 = 106, + CommonInvalidParam8 = 107, + CommonInvalidParam9 = 108, + CommonInvalidParam10 = 109, + CommonInvalidParam11 = 110, + CommonInvalidParam12 = 111, + CommonInvalidState = 112, + CommonInvalidStructure = 113, + CommonIOError = 114, + AnoncredsRevocationAccumulatorIsFull = 115, + AnoncredsInvalidRevocationAccumulatorIndex = 116, + AnoncredsCredentialRevoked = 117, + AnoncredsProofRejected = 118, + AnoncredsInvalidUserRevocId = 119, + // Add more if needed from error.rs +} + +[StructLayout(LayoutKind.Sequential)] +public struct ObjectHandle +{ + // Match C typedef i64 ObjectHandle; + public long Value; +} + +[StructLayout(LayoutKind.Sequential)] +internal struct ByteBuffer +{ + // C layout: int64_t len; uint8_t* data; + public long Len; + public IntPtr Data; +} + +[StructLayout(LayoutKind.Sequential)] +public struct FfiList +{ + // C layout: size_t count; const T* data; + public UIntPtr Count; // usize is unsigned pointer-sized + public IntPtr Data; // Pointer to array of elements (blittable) +} + +[StructLayout(LayoutKind.Sequential)] +public struct FfiStrList +{ + public UIntPtr Count; // usize is unsigned pointer-sized + public IntPtr Data; // POINTER(c_char_p) +} + +// Added structs to align with anoncreds-rs FFI +[StructLayout(LayoutKind.Sequential)] +internal struct AnoncredsPresentationRequest +{ + public IntPtr Json; // Pointer to JSON string +} + +[StructLayout(LayoutKind.Sequential)] +internal struct FfiCredentialEntry +{ + // C layout uses ObjectHandle (i64), i32 timestamp, ObjectHandle + public long Credential; + public int Timestamp; + public long RevState; +} + +[StructLayout(LayoutKind.Sequential)] +internal struct FfiCredentialProve +{ + public long EntryIdx; + public IntPtr Referent; // FfiStr + public byte IsPredicate; + public byte Reveal; +} + +[StructLayout(LayoutKind.Sequential)] +public struct FfiCredentialEntryList +{ + public UIntPtr Count; // usize is unsigned pointer-sized + public IntPtr Data; // POINTER(FfiCredentialEntry) +} + +[StructLayout(LayoutKind.Sequential)] +public struct FfiCredentialProveList +{ + public UIntPtr Count; // usize is unsigned pointer-sized + public IntPtr Data; // POINTER(FfiCredentialProve) +} + +[StructLayout(LayoutKind.Sequential)] +public struct FfiObjectHandleList +{ + public UIntPtr Count; // usize is unsigned pointer-sized + public IntPtr Data; // POINTER(ObjectHandle) +} + +// List of 32-bit integers used in revocation status list updates +[StructLayout(LayoutKind.Sequential)] +public struct FfiInt32List +{ + public UIntPtr Count; // size_t + public IntPtr Data; // pointer to int32_t +} + +// Revocation configuration struct passed to create_credential +[StructLayout(LayoutKind.Sequential)] +public struct FfiCredRevInfo +{ + public long RegDef; // ObjectHandle (size_t) + public long RegDefPrivate; // ObjectHandle (size_t) + public long StatusList; // ObjectHandle (size_t) + public long RegIdx; // int64_t +} + +// Non-revoked interval override types required by verify_presentation +[StructLayout(LayoutKind.Sequential)] +public struct FfiNonrevokedIntervalOverride +{ + public IntPtr RevRegDefId; // FfiStr (char*) + public int RequestedFromTs; // i32 + public int OverrideRevStatusListTs; // i32 +} + +[StructLayout(LayoutKind.Sequential)] +public struct FfiNonrevokedIntervalOverrideList +{ + public UIntPtr Count; // usize is unsigned pointer-sized + public IntPtr Data; // pointer to FfiNonrevokedIntervalOverride +} diff --git a/wrappers/dotnet/lib/Interop/NativeMethods.cs b/wrappers/dotnet/lib/Interop/NativeMethods.cs new file mode 100644 index 00000000..a8cf0b94 --- /dev/null +++ b/wrappers/dotnet/lib/Interop/NativeMethods.cs @@ -0,0 +1,368 @@ +// NativeMethods.cs +using System.Runtime.InteropServices; + +namespace AnonCredsNet.Interop; + +internal static partial class NativeMethods +{ + private const string Library = "anoncreds"; + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_get_current_error(out IntPtr errorJson); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_create_link_secret(out IntPtr linkSecret); + + [LibraryImport(Library)] + internal static partial void anoncreds_string_free(IntPtr str); + + [LibraryImport(Library)] + internal static partial void anoncreds_buffer_free(ByteBuffer buf); + + [LibraryImport(Library)] + internal static partial void anoncreds_object_free(long handle); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_object_get_json(long handle, out ByteBuffer json); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_object_from_json(string json, out long handle); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_schema_from_json(ByteBuffer json, out long handle); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_credential_definition_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_credential_definition_private_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_key_correctness_proof_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_credential_offer_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_credential_request_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_credential_request_metadata_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_credential_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_presentation_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_w3c_presentation_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_presentation_request_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_revocation_registry_definition_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_revocation_registry_private_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_revocation_status_list_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_revocation_status_list_delta_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_revocation_state_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_w3c_credential_from_json( + ByteBuffer json, + out long handle + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_generate_nonce(out IntPtr nonce); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_schema( + string name, + string version, + string issuerId, + FfiStrList attrNames, + out long handle + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_credential_definition( + string schemaId, + long schema, + string tag, + string issuerId, + string signatureType, + [MarshalAs(UnmanagedType.I1)] bool supportRevocation, + out long credDef, + out long credDefPvt, + out long keyProof + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_credential_offer( + string schemaId, + string credDefId, + long keyProof, + out long offer + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_credential_request( + string? entropy, + string? proverDid, + long credDef, + string linkSecret, + string linkSecretId, + long credOffer, + out long request, + out long metadata + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_credential( + long credDef, + long credDefPvt, + long credOffer, + long credRequest, + FfiStrList attrNames, + FfiStrList attrRawValues, + FfiStrList attrEncValues, + IntPtr revocation, + out long credential + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_process_credential( + long credential, + long requestMetadata, + string linkSecret, + long credDef, + long revRegDef, + out long processedCredential + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_presentation( + long presReq, + FfiCredentialEntryList credentials, + FfiCredentialProveList credentialsProve, + FfiStrList selfAttestNames, + FfiStrList selfAttestValues, + string linkSecret, + FfiObjectHandleList schemas, + FfiStrList schemaIds, + FfiObjectHandleList credDefs, + FfiStrList credDefIds, + out long presentation + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_w3c_presentation( + long presReq, + FfiCredentialEntryList credentials, + FfiCredentialProveList credentialsProve, + string linkSecret, + FfiObjectHandleList schemas, + FfiStrList schemaIds, + FfiObjectHandleList credDefs, + FfiStrList credDefIds, + string w3cVersion, + out long presentation + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_verify_presentation( + long presentation, + long presReq, + FfiObjectHandleList schemas, + FfiStrList schemaIds, + FfiObjectHandleList credDefs, + FfiStrList credDefIds, + FfiObjectHandleList revRegDefs, + FfiStrList revRegDefIds, + FfiObjectHandleList revStatusLists, + FfiNonrevokedIntervalOverrideList nonrevokedIntervalOverride, + out sbyte isValid + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_verify_w3c_presentation( + long presentation, + long presReq, + FfiObjectHandleList schemas, + FfiStrList schemaIds, + FfiObjectHandleList credDefs, + FfiStrList credDefIds, + FfiObjectHandleList revRegDefs, + FfiStrList revRegDefIds, + FfiObjectHandleList revStatusLists, + FfiNonrevokedIntervalOverrideList nonrevokedIntervalOverride, + out sbyte isValid + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_revocation_registry_def( + long credDef, + string credDefId, + string issuerId, + string tag, + string revType, + long maxCredNum, + string tailsPath, + out long revRegDef, + out long revRegPvt + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_revocation_status_list( + long credDef, + string revRegDefId, + long revRegDef, + long revRegDefPrivate, + string issuerId, + [MarshalAs(UnmanagedType.I1)] bool issuanceByDefault, + long timestamp, + out long statusList + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_update_revocation_status_list( + long credDef, + long revRegDef, + long revRegPriv, + long currentStatusList, + FfiInt32List issued, + FfiInt32List revoked, + long timestamp, + out long newStatusList + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_or_update_revocation_state( + long revRegDef, + long revStatusList, + long revRegIndex, + string tailsPath, + long revState, + long oldRevStatusList, + out long revStateOut + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_revocation_registry_definition_get_attribute( + long handle, + string name, + out IntPtr value + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_create_w3c_credential( + long credDef, + long credDefPvt, + long credOffer, + long credRequest, + FfiStrList attrNames, + FfiStrList attrRawValues, + IntPtr revocation, + string w3cVersion, + out long credential + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_process_w3c_credential( + long credential, + long requestMetadata, + string linkSecret, + long credDef, + long revRegDef, + out long processedCredential + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_credential_from_w3c( + long w3cCredential, + out long legacyCredential + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_credential_to_w3c( + long legacyCredential, + string issuerId, + string w3cVersion, + out long w3cCredential + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_credential_get_attribute( + long handle, + string name, + out IntPtr value + ); + + [LibraryImport(Library)] + internal static partial ErrorCode anoncreds_w3c_credential_get_integrity_proof_details( + long handle, + out long proofInfoHandle + ); + + [LibraryImport(Library, StringMarshalling = StringMarshalling.Utf8)] + internal static partial ErrorCode anoncreds_w3c_credential_proof_get_attribute( + long proofInfoHandle, + string name, + out IntPtr value + ); +} diff --git a/wrappers/dotnet/lib/Models/AnonCredsObject.cs b/wrappers/dotnet/lib/Models/AnonCredsObject.cs new file mode 100644 index 00000000..df8750fc --- /dev/null +++ b/wrappers/dotnet/lib/Models/AnonCredsObject.cs @@ -0,0 +1,139 @@ +using System.Runtime.InteropServices; +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; +using AnonCredsNet.Requests; + +namespace AnonCredsNet.Models; + +public abstract class AnonCredsObject : IDisposable +{ + public long Handle { get; private set; } + + protected AnonCredsObject(long handle) + { + if (handle == 0) + throw new AnonCredsException(ErrorCode.CommonInvalidState, "Invalid native handle"); + Handle = handle; + } + + public string ToJson() + { + if (Handle == 0) + throw new ObjectDisposedException(GetType().Name); + var code = NativeMethods.anoncreds_object_get_json(Handle, out var buffer); + if (code != ErrorCode.Success) + { + var errorMsg = AnonCreds.GetCurrentError(); + throw new AnonCredsException(code, $"ToJson failed for {GetType().Name}: {errorMsg}"); + } + try + { + var json = + Marshal.PtrToStringUTF8(buffer.Data, checked((int)buffer.Len)) + ?? throw new InvalidOperationException("Null JSON"); + return json; + } + finally + { + NativeMethods.anoncreds_buffer_free(buffer); + } + } + + protected static T FromJson(string json) + where T : AnonCredsObject + { + if (string.IsNullOrEmpty(json)) + throw new ArgumentNullException(nameof(json)); + + var code = FromJsonInternal(typeof(T), json, out var handle); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return (T) + Activator.CreateInstance( + typeof(T), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, + null, + [handle], + null + )!; + } + + private static ErrorCode FromJsonInternal(Type type, string json, out long handle) + { + handle = 0; + var buffer = AnonCreds.CreateByteBuffer(json); + + try + { + if (type == typeof(Schema)) + return NativeMethods.anoncreds_schema_from_json(buffer, out handle); + else if (type == typeof(CredentialDefinition)) + return NativeMethods.anoncreds_credential_definition_from_json(buffer, out handle); + else if (type == typeof(CredentialDefinitionPrivate)) + return NativeMethods.anoncreds_credential_definition_private_from_json( + buffer, + out handle + ); + else if (type == typeof(KeyCorrectnessProof)) + return NativeMethods.anoncreds_key_correctness_proof_from_json(buffer, out handle); + else if (type == typeof(CredentialOffer)) + return NativeMethods.anoncreds_credential_offer_from_json(buffer, out handle); + else if (type == typeof(CredentialRequest)) + return NativeMethods.anoncreds_credential_request_from_json(buffer, out handle); + else if (type == typeof(CredentialRequestMetadata)) + return NativeMethods.anoncreds_credential_request_metadata_from_json( + buffer, + out handle + ); + else if (type == typeof(Credential)) + return NativeMethods.anoncreds_credential_from_json(buffer, out handle); + else if (type == typeof(Presentation)) + return NativeMethods.anoncreds_presentation_from_json(buffer, out handle); + else if (type == typeof(PresentationRequest)) + return NativeMethods.anoncreds_presentation_request_from_json(buffer, out handle); + else if (type == typeof(RevocationRegistryDefinition)) + return NativeMethods.anoncreds_revocation_registry_definition_from_json( + buffer, + out handle + ); + else if (type == typeof(RevocationRegistryDefinitionPrivate)) + return NativeMethods.anoncreds_revocation_registry_private_from_json( + buffer, + out handle + ); + else if (type == typeof(RevocationStatusList)) + return NativeMethods.anoncreds_revocation_status_list_from_json(buffer, out handle); + else if (type == typeof(RevocationStatusListDelta)) + return NativeMethods.anoncreds_revocation_status_list_delta_from_json( + buffer, + out handle + ); + else if (type == typeof(RevocationState)) + return NativeMethods.anoncreds_revocation_state_from_json(buffer, out handle); + else + throw new NotSupportedException( + $"Type {type.Name} is not supported for JSON deserialization" + ); + } + finally + { + AnonCreds.FreeByteBuffer(buffer); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Handle == 0) + return; + NativeMethods.anoncreds_object_free(Handle); + Handle = 0; + } + + ~AnonCredsObject() => Dispose(false); +} diff --git a/wrappers/dotnet/lib/Models/Credential.cs b/wrappers/dotnet/lib/Models/Credential.cs new file mode 100644 index 00000000..ece4d1cb --- /dev/null +++ b/wrappers/dotnet/lib/Models/Credential.cs @@ -0,0 +1,141 @@ +using System.Runtime.InteropServices; +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; +using AnonCredsNet.Requests; + +namespace AnonCredsNet.Models; + +public sealed class Credential : AnonCredsObject +{ + private Credential(long handle) + : base(handle) { } + + internal static Credential FromHandle(long handle) => new Credential(handle); + + /// + /// Creates a credential and its revocation delta. Both returned objects must be disposed using using statements. + /// + public static (Credential Credential, RevocationStatusListDelta? Delta) Create( + CredentialDefinition credDef, + CredentialDefinitionPrivate credDefPvt, + CredentialOffer offer, + CredentialRequest request, + string credValues, + string? revRegId, + string? tailsPath, + RevocationStatusList? revStatusList, + CredentialRevocationConfig? revConfig = null + ) + { + if ( + credDef == null + || credDefPvt == null + || offer == null + || request == null + || string.IsNullOrEmpty(credValues) + ) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + + // Parse credential values JSON + var credValuesDict = System.Text.Json.JsonSerializer.Deserialize< + Dictionary + >(credValues); + if (credValuesDict == null) + throw new ArgumentException("Invalid credential values JSON"); + + var attrNames = AnonCreds.CreateFfiStrList( + System.Text.Json.JsonSerializer.Serialize(credValuesDict.Keys) + ); + var attrRawValues = AnonCreds.CreateFfiStrList( + System.Text.Json.JsonSerializer.Serialize(credValuesDict.Values) + ); + // When encoded values are not provided, pass an empty list (count=0, data=NULL) + var attrEncValues = new FfiStrList { Count = 0, Data = IntPtr.Zero }; + + // Build optional revocation info struct + IntPtr revocationPtr = IntPtr.Zero; + try + { + if ( + revConfig != null + && revConfig.RevRegDef != null + && revConfig.RevRegDefPrivate != null + && revConfig.RevStatusList != null + ) + { + var revInfo = new FfiCredRevInfo + { + RegDef = revConfig.RevRegDef.Handle, + RegDefPrivate = revConfig.RevRegDefPrivate.Handle, + StatusList = revConfig.RevStatusList.Handle, + RegIdx = (long)revConfig.RevRegIndex, + }; + revocationPtr = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(revInfo, revocationPtr, false); + } + } + catch + { + if (revocationPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(revocationPtr); + revocationPtr = IntPtr.Zero; + } + throw; + } + + try + { + var code = NativeMethods.anoncreds_create_credential( + credDef.Handle, + credDefPvt.Handle, + offer.Handle, + request.Handle, + attrNames, + attrRawValues, + attrEncValues, + revocationPtr, + out var cred + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return (new Credential(cred), null); // No delta when not using revocation + } + finally + { + AnonCreds.FreeFfiStrList(attrNames); + AnonCreds.FreeFfiStrList(attrRawValues); + if (revocationPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(revocationPtr); + } + } + } + + public Credential Process( + CredentialRequestMetadata credReqMetadata, + string linkSecret, + CredentialDefinition credDef, + RevocationRegistryDefinition? revRegDef + ) + { + if (string.IsNullOrEmpty(linkSecret)) + throw new ArgumentNullException(nameof(linkSecret)); + var revRegDefHandle = revRegDef?.Handle ?? 0; + var code = NativeMethods.anoncreds_process_credential( + Handle, + credReqMetadata.Handle, + linkSecret, + credDef.Handle, + revRegDefHandle, + out var newCredHandle + ); + if (code != ErrorCode.Success) + { + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + } + return new Credential(newCredHandle); + } + + internal static Credential FromJson(string json) => FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/CredentialDefinition.cs b/wrappers/dotnet/lib/Models/CredentialDefinition.cs new file mode 100644 index 00000000..1a3dc1ed --- /dev/null +++ b/wrappers/dotnet/lib/Models/CredentialDefinition.cs @@ -0,0 +1,72 @@ +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; + +namespace AnonCredsNet.Models; + +public class CredentialDefinition : AnonCredsObject +{ + private CredentialDefinition(long handle) + : base(handle) { } + + public static ( + CredentialDefinition CredDef, + CredentialDefinitionPrivate CredDefPvt, + KeyCorrectnessProof KeyProof + ) Create( + string schemaId, + string issuerId, + Schema schema, + string tag, + string sigType, + string config + ) + { + if ( + string.IsNullOrEmpty(schemaId) + || string.IsNullOrEmpty(issuerId) + || schema == null + || string.IsNullOrEmpty(tag) + || string.IsNullOrEmpty(sigType) + || string.IsNullOrEmpty(config) + ) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + + // Parse config to determine if revocation should be supported + var configObj = System.Text.Json.JsonSerializer.Deserialize( + config + ); + var supportRevocation = + configObj.TryGetProperty("support_revocation", out var revProp) && revProp.GetBoolean(); + + var code = NativeMethods.anoncreds_create_credential_definition( + schemaId, + schema.Handle, + tag, + issuerId, + sigType, + supportRevocation, + out var cd, + out var pvt, + out var proof + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return ( + new CredentialDefinition(cd), + new CredentialDefinitionPrivate(pvt), + new KeyCorrectnessProof(proof) + ); + } + + public static CredentialDefinition FromJson(string json) => + FromJson(json); +} + +public class CredentialDefinitionPrivate : AnonCredsObject +{ + internal CredentialDefinitionPrivate(long handle) + : base(handle) { } + + internal static CredentialDefinitionPrivate FromJson(string json) => + FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/CredentialOffer.cs b/wrappers/dotnet/lib/Models/CredentialOffer.cs new file mode 100644 index 00000000..bf2b6196 --- /dev/null +++ b/wrappers/dotnet/lib/Models/CredentialOffer.cs @@ -0,0 +1,35 @@ +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; + +namespace AnonCredsNet.Models; + +public class CredentialOffer : AnonCredsObject +{ + private CredentialOffer(long handle) + : base(handle) { } + + public static CredentialOffer Create( + string schemaId, + string credDefId, + KeyCorrectnessProof keyProof + ) + { + if (string.IsNullOrEmpty(schemaId)) + throw new ArgumentNullException(nameof(schemaId)); + if (string.IsNullOrEmpty(credDefId)) + throw new ArgumentNullException(nameof(credDefId)); + if (keyProof == null) + throw new ArgumentNullException(nameof(keyProof)); + var code = NativeMethods.anoncreds_create_credential_offer( + schemaId, + credDefId, + keyProof.Handle, + out var handle + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new CredentialOffer(handle); + } + + internal static CredentialOffer FromJson(string json) => FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/CredentialRevocationConfig.cs b/wrappers/dotnet/lib/Models/CredentialRevocationConfig.cs new file mode 100644 index 00000000..42989983 --- /dev/null +++ b/wrappers/dotnet/lib/Models/CredentialRevocationConfig.cs @@ -0,0 +1,9 @@ +namespace AnonCredsNet.Models; + +public class CredentialRevocationConfig +{ + public RevocationRegistryDefinition? RevRegDef { get; set; } + public RevocationRegistryDefinitionPrivate? RevRegDefPrivate { get; set; } + public RevocationStatusList? RevStatusList { get; set; } + public uint RevRegIndex { get; set; } +} diff --git a/wrappers/dotnet/lib/Models/KeyCorrectnessProof.cs b/wrappers/dotnet/lib/Models/KeyCorrectnessProof.cs new file mode 100644 index 00000000..3b4d7f97 --- /dev/null +++ b/wrappers/dotnet/lib/Models/KeyCorrectnessProof.cs @@ -0,0 +1,10 @@ +namespace AnonCredsNet.Models; + +public class KeyCorrectnessProof : AnonCredsObject +{ + internal KeyCorrectnessProof(long handle) + : base(handle) { } + + internal static KeyCorrectnessProof FromJson(string json) => + FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/LinkSecret.cs b/wrappers/dotnet/lib/Models/LinkSecret.cs new file mode 100644 index 00000000..73c28429 --- /dev/null +++ b/wrappers/dotnet/lib/Models/LinkSecret.cs @@ -0,0 +1,26 @@ +using System.Runtime.InteropServices; +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; + +namespace AnonCredsNet.Models; + +public class LinkSecret : AnonCredsObject +{ + internal LinkSecret(long handle) + : base(handle) { } + + public string Value => ToJson(); + + public static string Create() + { + var code = NativeMethods.anoncreds_create_link_secret(out var ptr); + if (code != ErrorCode.Success) + { + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + } + var linkSecret = + Marshal.PtrToStringUTF8(ptr) ?? throw new InvalidOperationException("Null link secret"); + NativeMethods.anoncreds_string_free(ptr); + return linkSecret; + } +} diff --git a/wrappers/dotnet/lib/Models/Presentation.cs b/wrappers/dotnet/lib/Models/Presentation.cs new file mode 100644 index 00000000..c3c96ef6 --- /dev/null +++ b/wrappers/dotnet/lib/Models/Presentation.cs @@ -0,0 +1,169 @@ +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; +using AnonCredsNet.Requests; + +namespace AnonCredsNet.Models; + +public sealed class Presentation : AnonCredsObject +{ + private Presentation(long handle) + : base(handle) { } + + public static Presentation Create( + long presReqHandle, + FfiCredentialEntryList credentialsList, + FfiCredentialProveList credentialsProve, + FfiStrList selfAttestNames, + FfiStrList selfAttestValues, + string linkSecret, + FfiObjectHandleList schemasList, + FfiStrList schemaIds, + FfiObjectHandleList credDefsList, + FfiStrList credDefIds + ) + { + if ( + presReqHandle == 0 + || string.IsNullOrEmpty(linkSecret) + || schemasList.Count == 0 + || credDefsList.Count == 0 + || schemaIds.Count == 0 + || credDefIds.Count == 0 + ) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + + try + { + // Debug: dump credential entries to validate timestamp/rev_state pairing + try + { + var count = (int)credentialsList.Count.ToUInt32(); + if (credentialsList.Data != IntPtr.Zero) + { + var size = System.Runtime.InteropServices.Marshal.SizeOf(); + for (int i = 0; i < count; i++) + { + var ptr = credentialsList.Data + (i * size); + var e = + System.Runtime.InteropServices.Marshal.PtrToStructure( + ptr + ); + } + } + } + catch { } + var code = NativeMethods.anoncreds_create_presentation( + presReqHandle, + credentialsList, + credentialsProve, + selfAttestNames, + selfAttestValues, + linkSecret, + schemasList, + schemaIds, + credDefsList, + credDefIds, + out var handle + ); + + if (code != ErrorCode.Success) + { + var errorMsg = AnonCreds.GetCurrentError(); + throw new AnonCredsException(code, errorMsg); + } + return new Presentation(handle); + } + finally + { + AnonCreds.FreeFfiObjectHandleList(schemasList); + AnonCreds.FreeFfiObjectHandleList(credDefsList); + AnonCreds.FreeFfiCredentialEntryList(credentialsList); + AnonCreds.FreeFfiCredentialProveList(credentialsProve); + } + } + + public static Presentation FromJson(string json) => FromJson(json); + + public static Presentation CreateFromJson( + PresentationRequest presReq, + string credentialsJson, + string? selfAttestJson, + string linkSecret, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? revRegsJson, + string? revListsJson + ) + { + var (schemasList, _) = AnonCreds.CreateFfiObjectHandleListWithObjects( + schemasJson, + Schema.FromJson + ); + var (credDefsList, _) = AnonCreds.CreateFfiObjectHandleListWithObjects( + credDefsJson, + CredentialDefinition.FromJson + ); + var credentialsList = AnonCreds.ParseCredentialsJson(credentialsJson); + var schemaIds = AnonCreds.CreateFfiStrList(schemaIdsJson); + var credDefIds = AnonCreds.CreateFfiStrList(credDefIdsJson); + + var credentialsProve = AnonCreds.CreateCredentialsProveList( + presReq.ToJson(), + selfAttestJson, + credentialsJson + ); + + var selfAttestNames = new FfiStrList(); + var selfAttestValues = new FfiStrList(); + if (!string.IsNullOrEmpty(selfAttestJson)) + { + var selfAttested = + System.Text.Json.JsonSerializer.Deserialize>( + selfAttestJson! + ) ?? new(); + selfAttestNames = AnonCreds.CreateFfiStrListFromStrings(selfAttested.Keys.ToArray()); + selfAttestValues = AnonCreds.CreateFfiStrListFromStrings(selfAttested.Values.ToArray()); + } + + return Create( + presReq.Handle, + credentialsList, + credentialsProve, + selfAttestNames, + selfAttestValues, + linkSecret, + schemasList, + schemaIds, + credDefsList, + credDefIds + ); + } + + public bool Verify( + PresentationRequest presReq, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? revRegDefsJson = null, + string? revStatusListsJson = null, + string? revRegDefIdsJson = null, + string? nonRevocJson = null + ) + { + return AnonCreds.VerifyPresentation( + this, + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegDefsJson, + revStatusListsJson, + revRegDefIdsJson, + nonRevocJson + ); + } +} diff --git a/wrappers/dotnet/lib/Models/RevocationRegistryDefinition.cs b/wrappers/dotnet/lib/Models/RevocationRegistryDefinition.cs new file mode 100644 index 00000000..401fb7f7 --- /dev/null +++ b/wrappers/dotnet/lib/Models/RevocationRegistryDefinition.cs @@ -0,0 +1,76 @@ +using System.Runtime.InteropServices; +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; + +namespace AnonCredsNet.Models; + +public class RevocationRegistryDefinition : AnonCredsObject +{ + internal RevocationRegistryDefinition(long handle) + : base(handle) { } + + public static (RevocationRegistryDefinition, RevocationRegistryDefinitionPrivate) Create( + CredentialDefinition credDef, + string credDefId, + string issuerId, + string tag, + string revType, + int maxCredNum, + string? tailsPath = null + ) + { + if ( + credDef == null + || string.IsNullOrEmpty(credDefId) + || string.IsNullOrEmpty(issuerId) + || string.IsNullOrEmpty(tag) + || string.IsNullOrEmpty(revType) + || maxCredNum <= 0 + ) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + + var code = NativeMethods.anoncreds_create_revocation_registry_def( + credDef.Handle, + credDefId, + issuerId, + tag, + revType, + maxCredNum, + tailsPath ?? string.Empty, + out var def, + out var pvt + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return ( + new RevocationRegistryDefinition(def), + new RevocationRegistryDefinitionPrivate(pvt) + ); + } + + public string TailsLocation + { + get + { + // Prefer native getter for attribute to avoid JSON parsing mismatches + var code = NativeMethods.anoncreds_revocation_registry_definition_get_attribute( + this.Handle, + "tails_location", + out var ptr + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + try + { + return Marshal.PtrToStringUTF8(ptr) ?? string.Empty; + } + finally + { + NativeMethods.anoncreds_string_free(ptr); + } + } + } + + public static RevocationRegistryDefinition FromJson(string json) => + FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/RevocationRegistryDefinitionPrivate.cs b/wrappers/dotnet/lib/Models/RevocationRegistryDefinitionPrivate.cs new file mode 100644 index 00000000..9b9fed31 --- /dev/null +++ b/wrappers/dotnet/lib/Models/RevocationRegistryDefinitionPrivate.cs @@ -0,0 +1,10 @@ +namespace AnonCredsNet.Models; + +public class RevocationRegistryDefinitionPrivate : AnonCredsObject +{ + internal RevocationRegistryDefinitionPrivate(long handle) + : base(handle) { } + + public static RevocationRegistryDefinitionPrivate FromJson(string json) => + FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/RevocationState.cs b/wrappers/dotnet/lib/Models/RevocationState.cs new file mode 100644 index 00000000..d64dca34 --- /dev/null +++ b/wrappers/dotnet/lib/Models/RevocationState.cs @@ -0,0 +1,66 @@ +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; + +namespace AnonCredsNet.Models; + +public class RevocationState : AnonCredsObject +{ + private RevocationState(long handle) + : base(handle) { } + + public static RevocationState Create( + RevocationRegistryDefinition revRegDef, + RevocationStatusList statusList, + uint revRegIndex, + string tailsPath + ) + { + if (revRegDef == null || statusList == null || string.IsNullOrEmpty(tailsPath)) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + + var code = NativeMethods.anoncreds_create_or_update_revocation_state( + revRegDef.Handle, + statusList.Handle, + (long)revRegIndex, + tailsPath, + 0, + 0, + out var handle + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new RevocationState(handle); + } + + public static RevocationState Update( + RevocationState revState, + RevocationRegistryDefinition revRegDef, + RevocationStatusList newStatusList, + uint revRegIndex, + string tailsPath, + RevocationStatusList? oldStatusList = null + ) + { + if ( + revState == null + || revRegDef == null + || newStatusList == null + || string.IsNullOrEmpty(tailsPath) + ) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + var code = NativeMethods.anoncreds_create_or_update_revocation_state( + revRegDef.Handle, + newStatusList.Handle, + (long)revRegIndex, + tailsPath, + revState.Handle, + oldStatusList?.Handle ?? 0, + out var updated + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new RevocationState(updated); + } + + public static RevocationState FromJson(string json) => FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/RevocationStatusList.cs b/wrappers/dotnet/lib/Models/RevocationStatusList.cs new file mode 100644 index 00000000..42e8c409 --- /dev/null +++ b/wrappers/dotnet/lib/Models/RevocationStatusList.cs @@ -0,0 +1,128 @@ +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; + +namespace AnonCredsNet.Models; + +public class RevocationStatusList : AnonCredsObject +{ + private RevocationStatusList(long handle) + : base(handle) { } + + public static ( + RevocationRegistryDefinition RevRegDef, + RevocationRegistryDefinitionPrivate RevRegPvt, + RevocationStatusList StatusList + ) CreateRevocationRegistryDefinition( + CredentialDefinition credDef, + string credDefId, + string issuerId, + string tag, + string revType, + long maxCredNum, + string tailsPath + ) + { + if ( + credDef == null + || string.IsNullOrEmpty(credDefId) + || string.IsNullOrEmpty(issuerId) + || string.IsNullOrEmpty(tag) + || string.IsNullOrEmpty(revType) + || maxCredNum <= 0 + ) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + var code = NativeMethods.anoncreds_create_revocation_registry_def( + credDef.Handle, + credDefId, + issuerId, + tag, + revType, + maxCredNum, + tailsPath ?? "", + out var def, + out var pvt + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return ( + new RevocationRegistryDefinition(def), + new RevocationRegistryDefinitionPrivate(pvt), + // Immediately create initial status list to align with Python + Create( + credDef, + credDefId, + new RevocationRegistryDefinition(def), + new RevocationRegistryDefinitionPrivate(pvt), + issuerId, + true, + 0 + ) + ); + } + + public static RevocationStatusList Create( + CredentialDefinition credDef, + string revRegId, + RevocationRegistryDefinition revRegDef, + RevocationRegistryDefinitionPrivate revRegDefPrivate, + string issuerId, + bool issuanceByDefault, + ulong timestamp + ) + { + if ( + credDef == null + || string.IsNullOrEmpty(revRegId) + || revRegDef == null + || revRegDefPrivate == null + || string.IsNullOrEmpty(issuerId) + ) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + + var code = NativeMethods.anoncreds_create_revocation_status_list( + credDef.Handle, + revRegId, + revRegDef.Handle, + revRegDefPrivate.Handle, + issuerId, + issuanceByDefault, + (long)timestamp, + out var handle + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new RevocationStatusList(handle); + } + + public RevocationStatusList Update( + CredentialDefinition credDef, + RevocationRegistryDefinition revRegDef, + RevocationRegistryDefinitionPrivate revRegDefPrivate, + ulong[]? issued, + ulong[]? revoked, + ulong timestamp + ) + { + if (credDef == null || revRegDef == null || revRegDefPrivate == null) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + + var issuedList = AnonCreds.CreateFfiInt32List(issued); + var revokedList = AnonCreds.CreateFfiInt32List(revoked); + var code = NativeMethods.anoncreds_update_revocation_status_list( + credDef.Handle, + revRegDef.Handle, + revRegDefPrivate.Handle, + this.Handle, + issuedList, + revokedList, + (long)timestamp, + out var updated + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new RevocationStatusList(updated); + } + + public static RevocationStatusList FromJson(string json) => + FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/RevocationStatusListDelta.cs b/wrappers/dotnet/lib/Models/RevocationStatusListDelta.cs new file mode 100644 index 00000000..4b1eb2ed --- /dev/null +++ b/wrappers/dotnet/lib/Models/RevocationStatusListDelta.cs @@ -0,0 +1,10 @@ +namespace AnonCredsNet.Models; + +public class RevocationStatusListDelta : AnonCredsObject +{ + internal RevocationStatusListDelta(long handle) + : base(handle) { } + + public static RevocationStatusListDelta FromJson(string json) => + FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/Schema.cs b/wrappers/dotnet/lib/Models/Schema.cs new file mode 100644 index 00000000..fa236760 --- /dev/null +++ b/wrappers/dotnet/lib/Models/Schema.cs @@ -0,0 +1,34 @@ +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; + +namespace AnonCredsNet.Models; + +public class Schema : AnonCredsObject +{ + private Schema(long handle) + : base(handle) { } + + public static Schema Create(string name, string version, string issuerId, string attrNamesJson) + { + var attrNamesList = AnonCreds.CreateFfiStrList(attrNamesJson); + try + { + var code = NativeMethods.anoncreds_create_schema( + name, + version, + issuerId, + attrNamesList, + out var handle + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new Schema(handle); + } + finally + { + AnonCreds.FreeFfiStrList(attrNamesList); + } + } + + public static Schema FromJson(string json) => FromJson(json); +} diff --git a/wrappers/dotnet/lib/Models/W3cCredential.cs b/wrappers/dotnet/lib/Models/W3cCredential.cs new file mode 100644 index 00000000..eef5426a --- /dev/null +++ b/wrappers/dotnet/lib/Models/W3cCredential.cs @@ -0,0 +1,127 @@ +using System.Runtime.InteropServices; +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; +using AnonCredsNet.Requests; + +namespace AnonCredsNet.Models; + +public sealed class W3cCredential : AnonCredsObject +{ + private W3cCredential(long handle) + : base(handle) { } + + public static W3cCredential Create( + CredentialDefinition credDef, + CredentialDefinitionPrivate credDefPvt, + CredentialOffer offer, + CredentialRequest request, + string credValues, + CredentialRevocationConfig? revConfig, + string? w3cVersion + ) + { + if (string.IsNullOrEmpty(credValues)) + throw new ArgumentNullException(nameof(credValues)); + + var dict = + System.Text.Json.JsonSerializer.Deserialize>(credValues) + ?? throw new ArgumentException("Invalid credential values JSON"); + var attrNames = AnonCreds.CreateFfiStrList( + System.Text.Json.JsonSerializer.Serialize(dict.Keys) + ); + var attrRawValues = AnonCreds.CreateFfiStrList( + System.Text.Json.JsonSerializer.Serialize(dict.Values) + ); + + IntPtr revocationPtr = IntPtr.Zero; + try + { + if ( + revConfig != null + && revConfig.RevRegDef != null + && revConfig.RevRegDefPrivate != null + && revConfig.RevStatusList != null + ) + { + var revInfo = new FfiCredRevInfo + { + RegDef = revConfig.RevRegDef.Handle, + RegDefPrivate = revConfig.RevRegDefPrivate.Handle, + StatusList = revConfig.RevStatusList.Handle, + RegIdx = (long)revConfig.RevRegIndex, + }; + revocationPtr = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(revInfo, revocationPtr, false); + } + + var code = NativeMethods.anoncreds_create_w3c_credential( + credDef.Handle, + credDefPvt.Handle, + offer.Handle, + request.Handle, + attrNames, + attrRawValues, + revocationPtr, + w3cVersion ?? "1.1", + out var cred + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new W3cCredential(cred); + } + finally + { + AnonCreds.FreeFfiStrList(attrNames); + AnonCreds.FreeFfiStrList(attrRawValues); + if (revocationPtr != IntPtr.Zero) + Marshal.FreeHGlobal(revocationPtr); + } + } + + public W3cCredential Process( + CredentialRequestMetadata credReqMetadata, + string linkSecret, + CredentialDefinition credDef, + RevocationRegistryDefinition? revRegDef + ) + { + var code = NativeMethods.anoncreds_process_w3c_credential( + Handle, + credReqMetadata.Handle, + linkSecret, + credDef.Handle, + revRegDef?.Handle ?? 0, + out var handle + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new W3cCredential(handle); + } + + public static W3cCredential FromJson(string json) => FromJson(json); + + public Credential ToLegacy() + { + var code = NativeMethods.anoncreds_credential_from_w3c(Handle, out var legacy); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return Credential.FromHandle(legacy); + } + + public static W3cCredential FromLegacy( + Credential legacy, + string issuerId, + string? w3cVersion = null + ) + { + var code = NativeMethods.anoncreds_credential_to_w3c( + legacy.Handle, + issuerId, + w3cVersion ?? "1.1", + out var w3c + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new W3cCredential(w3c); + } +} diff --git a/wrappers/dotnet/lib/Models/W3cPresentation.cs b/wrappers/dotnet/lib/Models/W3cPresentation.cs new file mode 100644 index 00000000..fbfeefca --- /dev/null +++ b/wrappers/dotnet/lib/Models/W3cPresentation.cs @@ -0,0 +1,209 @@ +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; +using AnonCredsNet.Requests; + +namespace AnonCredsNet.Models; + +public sealed class W3cPresentation : AnonCredsObject +{ + private W3cPresentation(long handle) + : base(handle) { } + + public static W3cPresentation Create( + long presReqHandle, + FfiCredentialEntryList credentialsList, + FfiCredentialProveList credentialsProve, + string linkSecret, + FfiObjectHandleList schemasList, + FfiStrList schemaIds, + FfiObjectHandleList credDefsList, + FfiStrList credDefIds, + string? w3cVersion = null + ) + { + if (presReqHandle == 0 || string.IsNullOrEmpty(linkSecret)) + throw new ArgumentNullException("Invalid inputs"); + try + { + // Debug: dump credential entries to validate timestamp/rev_state pairing + try + { + var count = (int)credentialsList.Count.ToUInt32(); + if (credentialsList.Data != IntPtr.Zero) + { + var size = System.Runtime.InteropServices.Marshal.SizeOf(); + for (int i = 0; i < count; i++) + { + var ptr = credentialsList.Data + (i * size); + var e = + System.Runtime.InteropServices.Marshal.PtrToStructure( + ptr + ); + } + } + } + catch { } + var code = NativeMethods.anoncreds_create_w3c_presentation( + presReqHandle, + credentialsList, + credentialsProve, + linkSecret, + schemasList, + schemaIds, + credDefsList, + credDefIds, + w3cVersion ?? "1.1", + out var handle + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return new W3cPresentation(handle); + } + finally + { + AnonCreds.FreeFfiObjectHandleList(schemasList); + AnonCreds.FreeFfiObjectHandleList(credDefsList); + AnonCreds.FreeFfiCredentialEntryList(credentialsList); + AnonCreds.FreeFfiCredentialProveList(credentialsProve); + } + } + + public static W3cPresentation CreateFromJson( + PresentationRequest presReq, + string credentialsJson, + string linkSecret, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? w3cVersion = null + ) + { + var (schemasList, _) = AnonCreds.CreateFfiObjectHandleListWithObjects( + schemasJson, + Schema.FromJson + ); + var (credDefsList, _) = AnonCreds.CreateFfiObjectHandleListWithObjects( + credDefsJson, + CredentialDefinition.FromJson + ); + var credentialsList = AnonCreds.ParseCredentialsJson(credentialsJson, isW3c: true); + var schemaIds = AnonCreds.CreateFfiStrList(schemaIdsJson); + var credDefIds = AnonCreds.CreateFfiStrList(credDefIdsJson); + var credentialsProve = AnonCreds.CreateCredentialsProveList( + presReq.ToJson(), + null, + credentialsJson + ); + + return Create( + presReq.Handle, + credentialsList, + credentialsProve, + linkSecret, + schemasList, + schemaIds, + credDefsList, + credDefIds, + w3cVersion + ); + } + + public bool Verify( + PresentationRequest presReq, + string schemasJson, + string credDefsJson, + string schemaIdsJson, + string credDefIdsJson, + string? revRegDefsJson = null, + string? revStatusListsJson = null, + string? revRegDefIdsJson = null, + string? nonRevocJson = null + ) + { + var (schemasList, _) = AnonCreds.CreateFfiObjectHandleListWithObjects( + schemasJson, + Schema.FromJson + ); + var (credDefsList, _) = AnonCreds.CreateFfiObjectHandleListWithObjects( + credDefsJson, + CredentialDefinition.FromJson + ); + var schemaIds = AnonCreds.CreateFfiStrList(schemaIdsJson); + var credDefIds = AnonCreds.CreateFfiStrList(credDefIdsJson); + + var (revRegDefsList, _) = string.IsNullOrEmpty(revRegDefsJson) + ? ( + new FfiObjectHandleList { Count = 0, Data = IntPtr.Zero }, + Array.Empty() + ) + : AnonCreds.CreateFfiObjectHandleListWithObjects( + revRegDefsJson, + RevocationRegistryDefinition.FromJson + ); + + var (revStatusLists, _) = string.IsNullOrEmpty(revStatusListsJson) + ? ( + new FfiObjectHandleList { Count = 0, Data = IntPtr.Zero }, + Array.Empty() + ) + : AnonCreds.CreateFfiObjectHandleListWithObjects( + revStatusListsJson, + RevocationStatusList.FromJson + ); + + var revRegDefIds = !string.IsNullOrEmpty(revRegDefIdsJson) + ? AnonCreds.CreateFfiStrList(revRegDefIdsJson) + : new FfiStrList { Count = 0, Data = IntPtr.Zero }; + + var nonRevocList = AnonCreds.BuildNonrevokedIntervalOverrideList(nonRevocJson); + + try + { + var code = NativeMethods.anoncreds_verify_w3c_presentation( + Handle, + presReq.Handle, + schemasList, + schemaIds, + credDefsList, + credDefIds, + revRegDefsList, + revRegDefIds, + revStatusLists, + nonRevocList, + out var valid + ); + if (code != ErrorCode.Success) + { + var err = AnonCreds.GetCurrentError(); + if (!string.IsNullOrEmpty(err)) + { + var e = err.ToLowerInvariant(); + if ( + e.Contains("invalid timestamp") + || e.Contains("proof rejected") + || e.Contains("credential revoked") + || e.Contains("revocation registry not provided") + ) + { + return false; + } + } + throw new AnonCredsException(code, err); + } + return valid != 0; + } + finally + { + AnonCreds.FreeFfiObjectHandleList(schemasList); + AnonCreds.FreeFfiObjectHandleList(credDefsList); + AnonCreds.FreeFfiObjectHandleList(revRegDefsList); + AnonCreds.FreeFfiObjectHandleList(revStatusLists); + AnonCreds.FreeFfiStrList(schemaIds); + AnonCreds.FreeFfiStrList(credDefIds); + if (revRegDefIds.Data != IntPtr.Zero) + AnonCreds.FreeFfiStrList(revRegDefIds); + AnonCreds.FreeFfiNonrevokedIntervalOverrideList(nonRevocList); + } + } +} diff --git a/wrappers/dotnet/lib/Requests/CredentialRequest.cs b/wrappers/dotnet/lib/Requests/CredentialRequest.cs new file mode 100644 index 00000000..7315c026 --- /dev/null +++ b/wrappers/dotnet/lib/Requests/CredentialRequest.cs @@ -0,0 +1,44 @@ +using AnonCredsNet.Exceptions; +using AnonCredsNet.Interop; +using AnonCredsNet.Models; + +namespace AnonCredsNet.Requests; + +public class CredentialRequest : AnonCredsObject +{ + private CredentialRequest(long handle) + : base(handle) { } + + public static (CredentialRequest Request, CredentialRequestMetadata Metadata) Create( + CredentialDefinition credDef, + string linkSecret, + string linkSecretId, + CredentialOffer credOffer, + string? entropy = null, + string? proverDid = null + ) + { + if ( + credDef == null + || string.IsNullOrEmpty(linkSecret) + || string.IsNullOrEmpty(linkSecretId) + || credOffer == null + ) + throw new ArgumentNullException("Input parameters cannot be null or empty"); + var code = NativeMethods.anoncreds_create_credential_request( + entropy, + proverDid, + credDef.Handle, + linkSecret, + linkSecretId, + credOffer.Handle, + out var req, + out var meta + ); + if (code != ErrorCode.Success) + throw new AnonCredsException(code, AnonCreds.GetCurrentError()); + return (new CredentialRequest(req), new CredentialRequestMetadata(meta)); + } + + internal static CredentialRequest FromJson(string json) => FromJson(json); +} diff --git a/wrappers/dotnet/lib/Requests/CredentialRequestMetadata.cs b/wrappers/dotnet/lib/Requests/CredentialRequestMetadata.cs new file mode 100644 index 00000000..b94fbe90 --- /dev/null +++ b/wrappers/dotnet/lib/Requests/CredentialRequestMetadata.cs @@ -0,0 +1,12 @@ +using AnonCredsNet.Models; + +namespace AnonCredsNet.Requests; + +public class CredentialRequestMetadata : AnonCredsObject +{ + internal CredentialRequestMetadata(long handle) + : base(handle) { } + + internal static CredentialRequestMetadata FromJson(string json) => + FromJson(json); +} diff --git a/wrappers/dotnet/lib/Requests/PresentationRequest.cs b/wrappers/dotnet/lib/Requests/PresentationRequest.cs new file mode 100644 index 00000000..3d17a819 --- /dev/null +++ b/wrappers/dotnet/lib/Requests/PresentationRequest.cs @@ -0,0 +1,11 @@ +using AnonCredsNet.Models; + +namespace AnonCredsNet.Requests; + +public sealed class PresentationRequest : AnonCredsObject +{ + internal PresentationRequest(long handle) + : base(handle) { } + + public static PresentationRequest FromJson(string json) => FromJson(json); +} diff --git a/wrappers/dotnet/tests/AnonCredsNet.Tests.csproj b/wrappers/dotnet/tests/AnonCredsNet.Tests.csproj new file mode 100644 index 00000000..e50df6f1 --- /dev/null +++ b/wrappers/dotnet/tests/AnonCredsNet.Tests.csproj @@ -0,0 +1,45 @@ + + + net9.0 + enable + enable + true + true + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wrappers/dotnet/tests/AnonCredsNetTests.cs b/wrappers/dotnet/tests/AnonCredsNetTests.cs new file mode 100644 index 00000000..d76d6dbd --- /dev/null +++ b/wrappers/dotnet/tests/AnonCredsNetTests.cs @@ -0,0 +1,219 @@ +using System.Text.Json; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; +using Xunit; + +namespace AnonCredsNet.Tests; + +public class AnonCredsNetTests +{ + // Removed client facade; using model-centric APIs + + [Fact] + public void TestFullFlow() + { + // Ported from wrappers/python/demo/test.py + + // 1. Setup variables + var issuerId = "mock:uri"; + var schemaId = "mock:uri"; + var credDefId = "mock:uri"; + var revRegId = "mock:uri:revregid"; + var entropy = "entropy"; + var revIdx = 1u; + + // 2. Create Schema + var attrNames = new[] { "name", "age", "sex", "height" }; + var schema = Schema.Create( + "schema name", + "1.0.0", + issuerId, + JsonSerializer.Serialize(attrNames) + ); + + // 3. Create Credential Definition + var (credDef, credDefPrivate, keyProof) = CredentialDefinition.Create( + schemaId, + issuerId, + schema, + "tag", + "CL", + "{\"support_revocation\": true}" + ); + + // 4. Create Revocation Registry Definition + var timeCreateRevStatusList = 12ul; + var (revRegDef, revRegDefPrivate) = RevocationRegistryDefinition.Create( + credDef, + credDefId, + issuerId, + "some_tag", + "CL_ACCUM", + 10, + null + ); + // Create initial Revocation Status List at the given timestamp + var revocationStatusList = RevocationStatusList.Create( + credDef, + revRegId, + revRegDef, + revRegDefPrivate, + issuerId, + true, + timeCreateRevStatusList + ); + + // 6. Create Link Secret + var linkSecret = LinkSecret.Create(); + var linkSecretId = "default"; + + // 7. Create Credential Offer + var credOffer = CredentialOffer.Create(schemaId, credDefId, keyProof); + + // 8. Create Credential Request + var (credRequest, credRequestMetadata) = CredentialRequest.Create( + credDef, + linkSecret, + linkSecretId, + credOffer, + entropy + ); + + // 9. Issue Credential + var credValues = JsonSerializer.Serialize( + new Dictionary + { + ["sex"] = "male", + ["name"] = "Alex", + ["height"] = "175", + ["age"] = "28", + } + ); + + var revConfig = new CredentialRevocationConfig + { + RevRegDef = revRegDef, + RevRegDefPrivate = revRegDefPrivate, + RevStatusList = revocationStatusList, + RevRegIndex = revIdx, + }; + + var (credential, _) = Credential.Create( + credDef, + credDefPrivate, + credOffer, + credRequest, + credValues, + null, + null, + revocationStatusList, + revConfig + ); + + // 10. Process Credential + var processedCredential = credential.Process( + credRequestMetadata, + linkSecret, + credDef, + revRegDef + ); + + // 11. Update Revocation Status List + var timeAfterCreatingCred = timeCreateRevStatusList + 1; + var issuedRevStatusList = revocationStatusList.Update( + credDef, + revRegDef, + revRegDefPrivate, + new[] { (ulong)revIdx }, + null, + timeAfterCreatingCred + ); + + // 12. Create Presentation Request + var nonce = AnonCreds.GenerateNonce(); + var presReqJson = $$""" + { + "nonce": "{{nonce}}", + "name": "pres_req_1", + "version": "0.1", + "requested_attributes": { + "attr1_referent": {"name": "name", "issuer_id": "{{issuerId}}"}, + "attr2_referent": {"name": "sex"}, + "attr3_referent": {"name": "phone"}, + "attr4_referent": {"names": ["name", "height"]} + }, + "requested_predicates": { + "predicate1_referent": {"name": "age", "p_type": ">=", "p_value": 18} + }, + "non_revoked": {"from": 10, "to": 200} + } + """; + var presReq = PresentationRequest.FromJson(presReqJson); + + // 13. Create Revocation State using the issued (updated) status list at timeAfterCreatingCred + var revState = RevocationState.Create( + revRegDef, + issuedRevStatusList, + revIdx, + revRegDef.TailsLocation + ); + + // 14. Build Presentation + var credentialsJson = JsonSerializer.Serialize( + new[] + { + new + { + credential = processedCredential.ToJson(), + timestamp = timeAfterCreatingCred, + rev_state = revState.ToJson(), + }, + } + ); + + var selfAttestedJson = JsonSerializer.Serialize( + new Dictionary { ["attr3_referent"] = "8-800-300" } + ); + + var schemasJson = JsonSerializer.Serialize( + new Dictionary { [schemaId] = schema.ToJson() } + ); + var credDefsJson = JsonSerializer.Serialize( + new Dictionary { [credDefId] = credDef.ToJson() } + ); + var revRegsJson = JsonSerializer.Serialize( + new Dictionary { [revRegId] = revRegDef.ToJson() } + ); + var revListsJson = JsonSerializer.Serialize( + new Dictionary { [revRegId] = issuedRevStatusList.ToJson() } + ); + + var presentation = Presentation.CreateFromJson( + presReq, + credentialsJson, + selfAttestedJson, + linkSecret, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + revRegsJson, + revListsJson + ); + + // 15. Verify Presentation + var isValid = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { revRegId }), + null + ); + + Assert.True(isValid); + } +} diff --git a/wrappers/dotnet/tests/ClassicRevocationFailureTests.cs b/wrappers/dotnet/tests/ClassicRevocationFailureTests.cs new file mode 100644 index 00000000..18d1acee --- /dev/null +++ b/wrappers/dotnet/tests/ClassicRevocationFailureTests.cs @@ -0,0 +1,255 @@ +using System.Text.Json; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; +using Xunit; + +namespace AnonCredsNet.Tests; + +public class ClassicRevocationFailureTests +{ + [Fact] + public void Revocation_Fails_After_Revoke() + { + var issuerId = "mock:uri"; + var schemaId = "mock:uri"; + var credDefId = "mock:uri"; + var revRegId = "mock:uri:revregid"; + var entropy = "entropy"; + uint revIdx = 1; + + var schema = Schema.Create( + "schema name", + "1.0.0", + issuerId, + JsonSerializer.Serialize(new[] { "name", "age", "sex", "height" }) + ); + + var (credDef, credDefPriv, keyProof) = CredentialDefinition.Create( + schemaId, + issuerId, + schema, + "tag", + "CL", + "{\"support_revocation\": true}" + ); + + var (revRegDef, revRegPriv) = RevocationRegistryDefinition.Create( + credDef, + credDefId, + issuerId, + "some_tag", + "CL_ACCUM", + 10, + null + ); + + ulong timeCreateRevStatusList = 12; + var revocationStatusList = RevocationStatusList.Create( + credDef, + revRegId, + revRegDef, + revRegPriv, + issuerId, + true, + timeCreateRevStatusList + ); + + var linkSecret = LinkSecret.Create(); + var linkSecretId = "default"; + var credOffer = CredentialOffer.Create(schemaId, credDefId, keyProof); + var (credReq, credReqMeta) = CredentialRequest.Create( + credDef, + linkSecret, + linkSecretId, + credOffer, + entropy + ); + + var credValues = JsonSerializer.Serialize( + new Dictionary + { + ["sex"] = "male", + ["name"] = "Alex", + ["height"] = "175", + ["age"] = "28", + } + ); + + var revConfig = new CredentialRevocationConfig + { + RevRegDef = revRegDef, + RevRegDefPrivate = revRegPriv, + RevStatusList = revocationStatusList, + RevRegIndex = revIdx, + }; + + var (credential, _) = Credential.Create( + credDef, + credDefPriv, + credOffer, + credReq, + credValues, + null, + null, + revocationStatusList, + revConfig + ); + + var processed = credential.Process(credReqMeta, linkSecret, credDef, revRegDef); + + var timeAfterCreatingCred = timeCreateRevStatusList + 1; + var issuedRevStatusList = revocationStatusList.Update( + credDef, + revRegDef, + revRegPriv, + new[] { (ulong)revIdx }, + null, + timeAfterCreatingCred + ); + + var nonce = AnonCreds.GenerateNonce(); + var presReqJson = $$""" + { + "nonce": "{{nonce}}", + "name": "pres_req_1", + "version": "0.1", + "requested_attributes": { + "attr1_referent": {"name": "name", "issuer_id": "{{issuerId}}"}, + "attr2_referent": {"name": "sex"}, + "attr3_referent": {"name": "phone"}, + "attr4_referent": {"names": ["name", "height"]} + }, + "requested_predicates": { + "predicate1_referent": {"name": "age", "p_type": ">=", "p_value": 18} + }, + "non_revoked": {"from": 10, "to": 200} + } + """; + presReqJson = presReqJson.Replace("{{nonce}}", nonce).Replace("{{issuerId}}", issuerId); + var presReq = PresentationRequest.FromJson(presReqJson); + + var revState = RevocationState.Create( + revRegDef, + issuedRevStatusList, + revIdx, + revRegDef.TailsLocation + ); + + var credentialsJson = JsonSerializer.Serialize( + new[] + { + new + { + credential = processed.ToJson(), + timestamp = timeAfterCreatingCred, + rev_state = revState.ToJson(), + }, + } + ); + + var selfAttestedJson = JsonSerializer.Serialize( + new Dictionary { ["attr3_referent"] = "8-800-300" } + ); + + var schemasJson = JsonSerializer.Serialize( + new Dictionary { [schemaId] = schema.ToJson() } + ); + var credDefsJson = JsonSerializer.Serialize( + new Dictionary { [credDefId] = credDef.ToJson() } + ); + var revRegsJson = JsonSerializer.Serialize( + new Dictionary { [revRegId] = revRegDef.ToJson() } + ); + var revListsJson = JsonSerializer.Serialize( + new Dictionary { [revRegId] = issuedRevStatusList.ToJson() } + ); + + var presentation = Presentation.CreateFromJson( + presReq, + credentialsJson, + selfAttestedJson, + linkSecret, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + revRegsJson, + revListsJson + ); + + var isValid = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { revRegId }), + null + ); + Assert.True(isValid); + + // Revoke and expect failure + var timeRevoke = timeAfterCreatingCred + 1; + var revokedStatusList = issuedRevStatusList.Update( + credDef, + revRegDef, + revRegPriv, + null, + new[] { (ulong)revIdx }, + timeRevoke + ); + + var revListsJson2 = JsonSerializer.Serialize( + new Dictionary { [revRegId] = revokedStatusList.ToJson() } + ); + + // Build a new revocation state at the revoke timestamp and create a new presentation + // so the proof is anchored at a time when the credential is revoked. + var revokedRevState = RevocationState.Create( + revRegDef, + revokedStatusList, + revIdx, + revRegDef.TailsLocation + ); + + var credentialsJson2 = JsonSerializer.Serialize( + new[] + { + new + { + credential = processed.ToJson(), + timestamp = timeRevoke, + rev_state = revokedRevState.ToJson(), + }, + } + ); + + var presentation2 = Presentation.CreateFromJson( + presReq, + credentialsJson2, + selfAttestedJson, + linkSecret, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + revRegsJson, + revListsJson2 + ); + + var isValidAfterRevoke = presentation2.Verify( + presReq, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + revRegsJson, + revListsJson2, + JsonSerializer.Serialize(new[] { revRegId }), + null + ); + Assert.False(isValidAfterRevoke); + } +} diff --git a/wrappers/dotnet/tests/MultiOverrideRevocationTests.cs b/wrappers/dotnet/tests/MultiOverrideRevocationTests.cs new file mode 100644 index 00000000..06097041 --- /dev/null +++ b/wrappers/dotnet/tests/MultiOverrideRevocationTests.cs @@ -0,0 +1,266 @@ +using System.Text.Json; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; +using Xunit; + +namespace AnonCredsNet.Tests; + +public class MultiOverrideRevocationTests +{ + private const string IssuerId = "mock:uri"; + private const string Schema1Id = "mock:uri:schemaMO1"; + private const string Schema2Id = "mock:uri:schemaMO2"; + private const string CredDef1Id = "mock:uri:cdMO1"; + private const string CredDef2Id = "mock:uri:cdMO2"; + private const string RevReg1Id = "mock:uri:revregMO1"; + private const string RevReg2Id = "mock:uri:revregMO2"; + + [Fact] + public void Two_Creds_Different_Local_Windows_Need_Two_Overrides() + { + // Create two revocable creds in different registries + var s1 = Schema.Create( + "gvt", + "1.0", + IssuerId, + JsonSerializer.Serialize(new[] { "name", "sex", "age", "height" }) + ); + var s2 = Schema.Create( + "pets", + "1.0", + IssuerId, + JsonSerializer.Serialize(new[] { "animal", "species" }) + ); + var (cd1, cd1Priv, k1) = CredentialDefinition.Create( + Schema1Id, + IssuerId, + s1, + "tag1", + "CL", + "{\"support_revocation\": true}" + ); + var (cd2, cd2Priv, k2) = CredentialDefinition.Create( + Schema2Id, + IssuerId, + s2, + "tag2", + "CL", + "{\"support_revocation\": true}" + ); + var (rev1, rev1Priv) = RevocationRegistryDefinition.Create( + cd1, + CredDef1Id, + IssuerId, + "tag1", + "CL_ACCUM", + 10, + null + ); + var (rev2, rev2Priv) = RevocationRegistryDefinition.Create( + cd2, + CredDef2Id, + IssuerId, + "tag2", + "CL_ACCUM", + 10, + null + ); + var t0 = 8ul; + var list1 = RevocationStatusList.Create(cd1, RevReg1Id, rev1, rev1Priv, IssuerId, true, t0); + var list2 = RevocationStatusList.Create(cd2, RevReg2Id, rev2, rev2Priv, IssuerId, true, t0); + + var ls = LinkSecret.Create(); + // IMPORTANT: Offer must be reused between request and issuance; don't recreate + var offer1 = CredentialOffer.Create(Schema1Id, CredDef1Id, k1); + var offer2 = CredentialOffer.Create(Schema2Id, CredDef2Id, k2); + var (req1, meta1) = CredentialRequest.Create(cd1, ls, "default", offer1, "entropy"); + var (req2, meta2) = CredentialRequest.Create(cd2, ls, "default", offer2, "entropy"); + + var vals1 = JsonSerializer.Serialize( + new Dictionary + { + { "sex", "male" }, + { "name", "Alex" }, + { "height", "175" }, + { "age", "28" }, + } + ); + var vals2 = JsonSerializer.Serialize( + new Dictionary { { "animal", "cat" }, { "species", "tabby" } } + ); + + var (cred1, _) = Credential.Create( + cd1, + cd1Priv, + offer1, + req1, + vals1, + null, + null, + list1, + new CredentialRevocationConfig + { + RevRegDef = rev1, + RevRegDefPrivate = rev1Priv, + RevStatusList = list1, + RevRegIndex = 9u, + } + ); + var (cred2, _) = Credential.Create( + cd2, + cd2Priv, + offer2, + req2, + vals2, + null, + null, + list2, + new CredentialRevocationConfig + { + RevRegDef = rev2, + RevRegDefPrivate = rev2Priv, + RevStatusList = list2, + RevRegIndex = 7u, + } + ); + + var p1 = cred1.Process(meta1, ls, cd1, rev1); + var p2 = cred2.Process(meta2, ls, cd2, rev2); + + // Issue at t=9 for first, t=11 for second + var list1Issued = list1.Update(cd1, rev1, rev1Priv, new[] { 9ul }, null, 9ul); + var list2Issued = list2.Update(cd2, rev2, rev2Priv, new[] { 7ul }, null, 11ul); + var rs1 = RevocationState.Create(rev1, list1Issued, 9u, rev1.TailsLocation); + var rs2 = RevocationState.Create(rev2, list2Issued, 7u, rev2.TailsLocation); + + // Request: local windows require from=10 for referents bound to cred1, and from=12 for referents bound to cred2 + var nonce = AnonCreds.GenerateNonce(); + var presReqJson = JsonSerializer.Serialize( + new + { + nonce, + name = "multi_override", + version = "0.1", + requested_attributes = new + { + attr1_referent = new + { + name = "name", + issuer_id = IssuerId, + non_revoked = new { from = 10, to = 20 }, + }, + attrX_referent = new + { + names = new[] { "animal", "species" }, + non_revoked = new { from = 12, to = 20 }, + }, + }, + requested_predicates = new + { + predicate1_referent = new + { + name = "age", + p_type = ">=", + p_value = 18, + non_revoked = new { from = 10, to = 20 }, + }, + }, + non_revoked = new { from = 5, to = 25 }, + } + ); + var presReq = PresentationRequest.FromJson(presReqJson); + + var credsArray = JsonSerializer.Serialize( + new[] + { + new + { + credential = p1.ToJson(), + timestamp = (int?)9, + rev_state = (string?)rs1.ToJson(), + referents = new[] { "attr1_referent", "predicate1_referent" }, + }, + new + { + credential = p2.ToJson(), + timestamp = (int?)11, + rev_state = (string?)rs2.ToJson(), + referents = new[] { "attrX_referent" }, + }, + } + ); + + var schemasJson = JsonSerializer.Serialize(new[] { s1.ToJson(), s2.ToJson() }); + var credDefsJson = JsonSerializer.Serialize(new[] { cd1.ToJson(), cd2.ToJson() }); + var schemaIdsJson = JsonSerializer.Serialize(new[] { Schema1Id, Schema2Id }); + var credDefIdsJson = JsonSerializer.Serialize(new[] { CredDef1Id, CredDef2Id }); + var revRegsJson = JsonSerializer.Serialize( + new Dictionary + { + { RevReg1Id, rev1.ToJson() }, + { RevReg2Id, rev2.ToJson() }, + } + ); + var revListsJson = JsonSerializer.Serialize( + new Dictionary + { + { RevReg1Id, list1Issued.ToJson() }, + { RevReg2Id, list2Issued.ToJson() }, + } + ); + + var presentation = Presentation.CreateFromJson( + presReq, + credsArray, + JsonSerializer.Serialize(new Dictionary()), + ls, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson + ); + + // Without overrides, should fail + var okNoOverride = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { RevReg1Id, RevReg2Id }), + null + ); + Assert.False(okNoOverride); + + // With overrides for 10->9 and 12->11 + var overrideJson = JsonSerializer.Serialize( + new Dictionary> + { + { + RevReg1Id, + new Dictionary { { "10", 9 } } + }, + { + RevReg2Id, + new Dictionary { { "12", 11 } } + }, + } + ); + var okWithOverride = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { RevReg1Id, RevReg2Id }), + overrideJson + ); + Assert.True(okWithOverride); + } +} diff --git a/wrappers/dotnet/tests/MultipleCredentialsTests.cs b/wrappers/dotnet/tests/MultipleCredentialsTests.cs new file mode 100644 index 00000000..5dc2e3b6 --- /dev/null +++ b/wrappers/dotnet/tests/MultipleCredentialsTests.cs @@ -0,0 +1,243 @@ +using System.Text.Json; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; +using Xunit; + +namespace AnonCredsNet.Tests; + +public class MultipleCredentialsTests +{ + [Fact] + public void Multiple_Credentials_Global_NonRevoked_Succeeds() + { + // Based on tests/multiple-credentials.rs happy path for classic + var issuerId = "mock:uri"; + var schema1Id = "mock:uri:schema1"; + var schema2Id = "mock:uri:schema2"; + var credDef1Id = "mock:uri:1"; + var credDef2Id = "mock:uri:2"; + var revReg1Id = "mock:uri:revregid1"; + var entropy = "entropy"; + + // Create two schemas + var schema1 = Schema.Create( + "gvt", + "1.0", + issuerId, + JsonSerializer.Serialize(new[] { "name", "sex", "age", "height" }) + ); + var schema2 = Schema.Create( + "hogwarts", + "1.0", + issuerId, + JsonSerializer.Serialize(new[] { "wand", "house", "year" }) + ); + + // cred def 1 supports revocation, cred def 2 does not + var (credDef1, credDef1Priv, k1) = CredentialDefinition.Create( + schema1Id, + issuerId, + schema1, + "tag1", + "CL", + "{\"support_revocation\": true}" + ); + var (credDef2, credDef2Priv, k2) = CredentialDefinition.Create( + schema2Id, + issuerId, + schema2, + "tag2", + "CL", + "{\"support_revocation\": false}" + ); + + // Revocation registry for credDef1 + var (revRegDef1, revRegPriv1) = RevocationRegistryDefinition.Create( + credDef1, + credDef1Id, + issuerId, + "tag", + "CL_ACCUM", + 10, + null + ); + var t0 = 8ul; + var revList = RevocationStatusList.Create( + credDef1, + revReg1Id, + revRegDef1, + revRegPriv1, + issuerId, + true, + t0 + ); + + // Link secret + var ls = LinkSecret.Create(); + var lsId = "default"; + + // Offers and requests + var offer1 = CredentialOffer.Create(schema1Id, credDef1Id, k1); + var offer2 = CredentialOffer.Create(schema2Id, credDef2Id, k2); + var (req1, meta1) = CredentialRequest.Create(credDef1, ls, lsId, offer1, entropy); + var (req2, meta2) = CredentialRequest.Create(credDef2, ls, lsId, offer2, entropy); + + // Issue creds + var values1 = JsonSerializer.Serialize( + new Dictionary + { + { "sex", "male" }, + { "name", "Alex" }, + { "height", "175" }, + { "age", "28" }, + } + ); + var values2 = JsonSerializer.Serialize( + new Dictionary + { + { "wand", "dragon-heart-string" }, + { "house", "Hufflepuff" }, + { "year", "1990" }, + } + ); + var revCfg = new CredentialRevocationConfig + { + RevRegDef = revRegDef1, + RevRegDefPrivate = revRegPriv1, + RevStatusList = revList, + RevRegIndex = 9u, + }; + + var (cred1, _) = Credential.Create( + credDef1, + credDef1Priv, + offer1, + req1, + values1, + null, + null, + revList, + revCfg + ); + var (cred2, _) = Credential.Create( + credDef2, + credDef2Priv, + offer2, + req2, + values2, + null, + null, + null, + null + ); + + // Process + var proc1 = cred1.Process(meta1, ls, credDef1, revRegDef1); + var proc2 = cred2.Process(meta2, ls, credDef2, null); + + // Update rev list to issue index 9 at t=9 + var tIssue = 9ul; // within global interval below + var revListIssued = revList.Update( + credDef1, + revRegDef1, + revRegPriv1, + new[] { 9ul }, + null, + tIssue + ); + var revState = RevocationState.Create( + revRegDef1, + revListIssued, + 9u, + revRegDef1.TailsLocation + ); + + // Request with global non_revoked window [5,25] + var nonce = AnonCreds.GenerateNonce(); + var presReqJson = $$""" + { + "nonce":"{{nonce}}", + "name":"global_rev", + "version":"0.1", + "requested_attributes":{ + "attr1_referent": {"name":"name","issuer_id":"{{issuerId}}"}, + "attr2_referent": {"name":"sex"}, + "attr4_referent": {"names":["height"]}, + "attr5_referent": {"names":["wand","house","year"]} + }, + "requested_predicates":{ + "predicate1_referent": {"name":"age","p_type":">=","p_value":18} + }, + "non_revoked": {"from":5, "to":25} + } + """; + presReqJson = presReqJson.Replace("{{nonce}}", nonce).Replace("{{issuerId}}", issuerId); + var presReq = PresentationRequest.FromJson(presReqJson); + + // Build present credentials (two credentials, attach rev state to the revocable one) + var credsArray = JsonSerializer.Serialize( + new[] + { + new + { + credential = proc1.ToJson(), + timestamp = (int?)tIssue, + rev_state = (string?)revState.ToJson(), + referents = new[] + { + "attr1_referent", + "attr2_referent", + "attr4_referent", + "predicate1_referent", + }, + }, + new + { + credential = proc2.ToJson(), + timestamp = (int?)null, + rev_state = (string?)null, + referents = new[] { "attr5_referent" }, + }, + } + ); + + var selfAtt = JsonSerializer.Serialize(new Dictionary()); + var schemasJson = JsonSerializer.Serialize(new[] { schema1.ToJson(), schema2.ToJson() }); + var credDefsJson = JsonSerializer.Serialize(new[] { credDef1.ToJson(), credDef2.ToJson() }); + var schemaIdsJson = JsonSerializer.Serialize(new[] { schema1Id, schema2Id }); + var credDefIdsJson = JsonSerializer.Serialize(new[] { credDef1Id, credDef2Id }); + var revRegsJson = JsonSerializer.Serialize( + new Dictionary { { revReg1Id, revRegDef1.ToJson() } } + ); + var revListsJson = JsonSerializer.Serialize( + new Dictionary { { revReg1Id, revListIssued.ToJson() } } + ); + + var presentation = Presentation.CreateFromJson( + presReq, + credsArray, + selfAtt, + ls, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson + ); + + var ok = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { revReg1Id }), + null + ); + + Assert.True(ok); + } +} diff --git a/wrappers/dotnet/tests/NonRevocableIntervalsTests.cs b/wrappers/dotnet/tests/NonRevocableIntervalsTests.cs new file mode 100644 index 00000000..642cb08d --- /dev/null +++ b/wrappers/dotnet/tests/NonRevocableIntervalsTests.cs @@ -0,0 +1,111 @@ +using System.Text.Json; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; +using Xunit; + +namespace AnonCredsNet.Tests; + +public class NonRevocableIntervalsTests +{ + private const string IssuerId = "mock:uri"; + private const string SchemaId = "mock:uri:schemaNR"; + private const string CredDefId = "mock:uri:cdNR"; + + [Fact] + public void NonRevocable_Cred_Ignores_NonRevoked_Windows() + { + var schema = Schema.Create( + "hogwarts", + "1.0", + IssuerId, + JsonSerializer.Serialize(new[] { "wand", "house", "year" }) + ); + var (cd, cdPriv, k) = CredentialDefinition.Create( + SchemaId, + IssuerId, + schema, + "tag", + "CL", + "{\"support_revocation\": false}" + ); + + var ls = LinkSecret.Create(); + var offer = CredentialOffer.Create(SchemaId, CredDefId, k); + var (req, meta) = CredentialRequest.Create(cd, ls, "default", offer, "entropy"); + + var values = JsonSerializer.Serialize( + new Dictionary + { + { "wand", "phoenix" }, + { "house", "Gryffindor" }, + { "year", "1997" }, + } + ); + var (cred, _) = Credential.Create(cd, cdPriv, offer, req, values, null, null, null, null); + var proc = cred.Process(meta, ls, cd, null); + + var nonce = AnonCreds.GenerateNonce(); + var presReqJson = JsonSerializer.Serialize( + new + { + nonce, + name = "nr_test", + version = "0.1", + requested_attributes = new + { + attr5_referent = new + { + names = new[] { "wand", "house", "year" }, + non_revoked = new { from = 10, to = 20 }, + }, + }, + requested_predicates = new { }, + non_revoked = new { from = 5, to = 25 }, + } + ); + var presReq = PresentationRequest.FromJson(presReqJson); + + var credsArray = JsonSerializer.Serialize( + new[] + { + new + { + credential = proc.ToJson(), + timestamp = (int?)null, + rev_state = (string?)null, + referents = new[] { "attr5_referent" }, + }, + } + ); + var schemasJson = JsonSerializer.Serialize(new[] { schema.ToJson() }); + var credDefsJson = JsonSerializer.Serialize(new[] { cd.ToJson() }); + var schemaIdsJson = JsonSerializer.Serialize(new[] { SchemaId }); + var credDefIdsJson = JsonSerializer.Serialize(new[] { CredDefId }); + + var presentation = Presentation.CreateFromJson( + presReq, + credsArray, + JsonSerializer.Serialize(new Dictionary()), + ls, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + null, + null + ); + + var ok = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + null, + null, + null, + null + ); + Assert.True(ok); + } +} diff --git a/wrappers/dotnet/tests/NonRevokedIntervalsTests.cs b/wrappers/dotnet/tests/NonRevokedIntervalsTests.cs new file mode 100644 index 00000000..7b12f9d0 --- /dev/null +++ b/wrappers/dotnet/tests/NonRevokedIntervalsTests.cs @@ -0,0 +1,255 @@ +using System.Text.Json; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; +using Xunit; + +namespace AnonCredsNet.Tests; + +public class NonRevokedIntervalsTests +{ + private const string IssuerId = "mock:uri"; + private const string Schema1Id = "mock:uri:schema1"; + private const string Schema2Id = "mock:uri:schema2"; + private const string CredDef1Id = "mock:uri:1"; + private const string CredDef2Id = "mock:uri:2"; + private const string RevReg1Id = "mock:uri:revregid1"; + + [Fact] + public void Global_Interval_Succeeds_Local_Fails_Without_Override() + { + // Setup two schemas and cred defs: one revocable, one not + var schema1 = Schema.Create( + "gvt", + "1.0", + IssuerId, + JsonSerializer.Serialize(new[] { "name", "sex", "age", "height" }) + ); + var schema2 = Schema.Create( + "hogwarts", + "1.0", + IssuerId, + JsonSerializer.Serialize(new[] { "wand", "house", "year" }) + ); + var (cd1, cd1Priv, k1) = CredentialDefinition.Create( + Schema1Id, + IssuerId, + schema1, + "tag1", + "CL", + "{\"support_revocation\": true}" + ); + var (cd2, cd2Priv, k2) = CredentialDefinition.Create( + Schema2Id, + IssuerId, + schema2, + "tag2", + "CL", + "{\"support_revocation\": false}" + ); + + var (revDef, revPriv) = RevocationRegistryDefinition.Create( + cd1, + CredDef1Id, + IssuerId, + "tag", + "CL_ACCUM", + 10, + null + ); + var t0 = 8ul; // initial list before issuance + var revList = RevocationStatusList.Create( + cd1, + RevReg1Id, + revDef, + revPriv, + IssuerId, + true, + t0 + ); + + var ls = LinkSecret.Create(); + var lsId = "default"; + var offer1 = CredentialOffer.Create(Schema1Id, CredDef1Id, k1); + var offer2 = CredentialOffer.Create(Schema2Id, CredDef2Id, k2); + var (req1, meta1) = CredentialRequest.Create(cd1, ls, lsId, offer1, "entropy"); + var (req2, meta2) = CredentialRequest.Create(cd2, ls, lsId, offer2, "entropy"); + + var values1 = JsonSerializer.Serialize( + new Dictionary + { + { "sex", "male" }, + { "name", "Alex" }, + { "height", "175" }, + { "age", "28" }, + } + ); + var values2 = JsonSerializer.Serialize( + new Dictionary + { + { "wand", "dragon-heart-string" }, + { "house", "Hufflepuff" }, + { "year", "1990" }, + } + ); + var revCfg = new CredentialRevocationConfig + { + RevRegDef = revDef, + RevRegDefPrivate = revPriv, + RevStatusList = revList, + RevRegIndex = 9u, + }; + + var (cred1, _) = Credential.Create( + cd1, + cd1Priv, + offer1, + req1, + values1, + null, + null, + revList, + revCfg + ); + var (cred2, _) = Credential.Create( + cd2, + cd2Priv, + offer2, + req2, + values2, + null, + null, + null, + null + ); + + var proc1 = cred1.Process(meta1, ls, cd1, revDef); + var proc2 = cred2.Process(meta2, ls, cd2, null); + + // Issue revocation at t=9 (within global [5,25]) + var tIssue = 9ul; + var revListIssued = revList.Update(cd1, revDef, revPriv, new[] { 9ul }, null, tIssue); + var revState = RevocationState.Create(revDef, revListIssued, 9u, revDef.TailsLocation); + + // Request with global non_revoked window [5,25] and local windows for two referents [10,20] + var nonce = AnonCreds.GenerateNonce(); + var presReqJson = JsonSerializer.Serialize( + new + { + nonce, + name = "both_rev_attr", + version = "0.1", + requested_attributes = new + { + attr1_referent = new { name = "name", issuer_id = IssuerId }, + attr2_referent = new { name = "sex", non_revoked = new { from = 10, to = 20 } }, + attr4_referent = new { names = new[] { "height" } }, + attr5_referent = new + { + names = new[] { "wand", "house", "year" }, + non_revoked = new { from = 10, to = 20 }, + }, + }, + requested_predicates = new + { + predicate1_referent = new + { + name = "age", + p_type = ">=", + p_value = 18, + }, + }, + non_revoked = new { from = 5, to = 25 }, + } + ); + var presReq = PresentationRequest.FromJson(presReqJson); + + // Build credentials array with referent mapping so attr5 (hogwarts) maps to second credential + var credsArray = JsonSerializer.Serialize( + new[] + { + new + { + credential = proc1.ToJson(), + timestamp = (int?)tIssue, + rev_state = (string?)revState.ToJson(), + referents = new[] + { + "attr1_referent", + "attr2_referent", + "attr4_referent", + "predicate1_referent", + }, + }, + new + { + credential = proc2.ToJson(), + timestamp = (int?)null, + rev_state = (string?)null, + referents = new[] { "attr5_referent" }, + }, + } + ); + + var selfAtt = JsonSerializer.Serialize(new Dictionary()); + var schemasJson = JsonSerializer.Serialize(new[] { schema1.ToJson(), schema2.ToJson() }); + var credDefsJson = JsonSerializer.Serialize(new[] { cd1.ToJson(), cd2.ToJson() }); + var schemaIdsJson = JsonSerializer.Serialize(new[] { Schema1Id, Schema2Id }); + var credDefIdsJson = JsonSerializer.Serialize(new[] { CredDef1Id, CredDef2Id }); + var revRegsJson = JsonSerializer.Serialize( + new Dictionary { { RevReg1Id, revDef.ToJson() } } + ); + var revListsJson = JsonSerializer.Serialize( + new Dictionary { { RevReg1Id, revListIssued.ToJson() } } + ); + + var presentation = Presentation.CreateFromJson( + presReq, + credsArray, + selfAtt, + ls, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson + ); + + // Without overrides, local windows [10,20] require rev status at from=10; our proof is at 9 -> expect failure + var okNoOverride = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { RevReg1Id }), + null + ); + Assert.False(okNoOverride); + + // With override mapping requested_from 10 -> use rev list at 9 + var overrideJson = JsonSerializer.Serialize( + new Dictionary> + { + { + RevReg1Id, + new Dictionary { { "10", 9 } } + }, + } + ); + var okWithOverride = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { RevReg1Id }), + overrideJson + ); + Assert.True(okWithOverride); + } +} diff --git a/wrappers/dotnet/tests/PredicatesOnlyTests.cs b/wrappers/dotnet/tests/PredicatesOnlyTests.cs new file mode 100644 index 00000000..fba2fe46 --- /dev/null +++ b/wrappers/dotnet/tests/PredicatesOnlyTests.cs @@ -0,0 +1,331 @@ +using System.Text.Json; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; +using Xunit; + +namespace AnonCredsNet.Tests; + +public class PredicatesOnlyTests +{ + private const string IssuerId = "mock:uri"; + private const string SchemaId = "mock:uri:schemaP"; + private const string CredDefId = "mock:uri:cdP"; + private const string RevRegId = "mock:uri:revregP"; + + [Fact] + public void Predicates_Only_Passes_With_Revocation() + { + var schema = Schema.Create( + "gvt", + "1.0", + IssuerId, + JsonSerializer.Serialize(new[] { "name", "sex", "age", "height" }) + ); + var (cd, cdPriv, k) = CredentialDefinition.Create( + SchemaId, + IssuerId, + schema, + "tag", + "CL", + "{\"support_revocation\": true}" + ); + var (revDef, revPriv) = RevocationRegistryDefinition.Create( + cd, + CredDefId, + IssuerId, + "tag", + "CL_ACCUM", + 10, + null + ); + var t0 = 10ul; + var revList = RevocationStatusList.Create( + cd, + RevRegId, + revDef, + revPriv, + IssuerId, + true, + t0 + ); + + var ls = LinkSecret.Create(); + var offer = CredentialOffer.Create(SchemaId, CredDefId, k); + var (req, meta) = CredentialRequest.Create(cd, ls, "default", offer, "entropy"); + + var values = JsonSerializer.Serialize( + new Dictionary + { + { "sex", "male" }, + { "name", "Alex" }, + { "height", "175" }, + { "age", "28" }, + } + ); + var revCfg = new CredentialRevocationConfig + { + RevRegDef = revDef, + RevRegDefPrivate = revPriv, + RevStatusList = revList, + RevRegIndex = 1u, + }; + var (cred, _) = Credential.Create( + cd, + cdPriv, + offer, + req, + values, + null, + null, + revList, + revCfg + ); + var proc = cred.Process(meta, ls, cd, revDef); + + // timestamp > t0 and inside global window + var tIssue = t0 + 2; // 12 + var revListIssued = revList.Update(cd, revDef, revPriv, new[] { 1ul }, null, tIssue); + var revState = RevocationState.Create(revDef, revListIssued, 1u, revDef.TailsLocation); + + var nonce = AnonCreds.GenerateNonce(); + var presReqJson = JsonSerializer.Serialize( + new + { + nonce, + name = "pred_only", + version = "0.1", + requested_attributes = new { }, + requested_predicates = new + { + predicate1_referent = new + { + name = "age", + p_type = ">=", + p_value = 18, + }, + }, + non_revoked = new { from = 10, to = 200 }, + } + ); + var presReq = PresentationRequest.FromJson(presReqJson); + + var credsArray = JsonSerializer.Serialize( + new[] + { + new + { + credential = proc.ToJson(), + timestamp = (int?)tIssue, + rev_state = (string?)revState.ToJson(), + referents = new[] { "predicate1_referent" }, + }, + } + ); + var schemasJson = JsonSerializer.Serialize(new[] { schema.ToJson() }); + var credDefsJson = JsonSerializer.Serialize(new[] { cd.ToJson() }); + var schemaIdsJson = JsonSerializer.Serialize(new[] { SchemaId }); + var credDefIdsJson = JsonSerializer.Serialize(new[] { CredDefId }); + var revRegsJson = JsonSerializer.Serialize( + new Dictionary { { RevRegId, revDef.ToJson() } } + ); + var revListsJson = JsonSerializer.Serialize( + new Dictionary { { RevRegId, revListIssued.ToJson() } } + ); + + var presentation = Presentation.CreateFromJson( + presReq, + credsArray, + JsonSerializer.Serialize(new Dictionary()), + ls, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson + ); + + var ok = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { RevRegId }), + null + ); + Assert.True(ok); + } + + [Fact] + public void Predicate_Fails_With_Local_Window_Then_Succeeds_With_Override() + { + var schema = Schema.Create( + "gvt", + "1.0", + IssuerId, + JsonSerializer.Serialize(new[] { "name", "sex", "age", "height" }) + ); + var (cd, cdPriv, k) = CredentialDefinition.Create( + SchemaId, + IssuerId, + schema, + "tag", + "CL", + "{\"support_revocation\": true}" + ); + var (revDef, revPriv) = RevocationRegistryDefinition.Create( + cd, + CredDefId, + IssuerId, + "tag", + "CL_ACCUM", + 10, + null + ); + var t0 = 8ul; + var revList = RevocationStatusList.Create( + cd, + RevRegId, + revDef, + revPriv, + IssuerId, + true, + t0 + ); + + var ls = LinkSecret.Create(); + var offer = CredentialOffer.Create(SchemaId, CredDefId, k); + var (req, meta) = CredentialRequest.Create(cd, ls, "default", offer, "entropy"); + + var values = JsonSerializer.Serialize( + new Dictionary + { + { "sex", "male" }, + { "name", "Alex" }, + { "height", "175" }, + { "age", "28" }, + } + ); + var revCfg = new CredentialRevocationConfig + { + RevRegDef = revDef, + RevRegDefPrivate = revPriv, + RevStatusList = revList, + RevRegIndex = 9u, + }; + var (cred, _) = Credential.Create( + cd, + cdPriv, + offer, + req, + values, + null, + null, + revList, + revCfg + ); + var proc = cred.Process(meta, ls, cd, revDef); + + // Issue at t=9, local window will require from=10 later + var tIssue = 9ul; + var revListIssued = revList.Update(cd, revDef, revPriv, new[] { 9ul }, null, tIssue); + var revState = RevocationState.Create(revDef, revListIssued, 9u, revDef.TailsLocation); + + var nonce = AnonCreds.GenerateNonce(); + var presReqJson = JsonSerializer.Serialize( + new + { + nonce, + name = "pred_local_window", + version = "0.1", + requested_attributes = new { }, + requested_predicates = new + { + predicate1_referent = new + { + name = "age", + p_type = ">=", + p_value = 18, + non_revoked = new { from = 10, to = 20 }, + }, + }, + non_revoked = new { from = 5, to = 25 }, + } + ); + var presReq = PresentationRequest.FromJson(presReqJson); + + var credsArray = JsonSerializer.Serialize( + new[] + { + new + { + credential = proc.ToJson(), + timestamp = (int?)tIssue, + rev_state = (string?)revState.ToJson(), + referents = new[] { "predicate1_referent" }, + }, + } + ); + var schemasJson = JsonSerializer.Serialize(new[] { schema.ToJson() }); + var credDefsJson = JsonSerializer.Serialize(new[] { cd.ToJson() }); + var schemaIdsJson = JsonSerializer.Serialize(new[] { SchemaId }); + var credDefIdsJson = JsonSerializer.Serialize(new[] { CredDefId }); + var revRegsJson = JsonSerializer.Serialize( + new Dictionary { { RevRegId, revDef.ToJson() } } + ); + var revListsJson = JsonSerializer.Serialize( + new Dictionary { { RevRegId, revListIssued.ToJson() } } + ); + + var presentation = Presentation.CreateFromJson( + presReq, + credsArray, + JsonSerializer.Serialize(new Dictionary()), + ls, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson + ); + + var okNoOverride = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { RevRegId }), + null + ); + Assert.False(okNoOverride); + + var overrideJson = JsonSerializer.Serialize( + new Dictionary> + { + { + RevRegId, + new Dictionary { { "10", 9 } } + }, + } + ); + var okWithOverride = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + schemaIdsJson, + credDefIdsJson, + revRegsJson, + revListsJson, + JsonSerializer.Serialize(new[] { RevRegId }), + overrideJson + ); + Assert.True(okWithOverride); + } +} diff --git a/wrappers/dotnet/tests/W3CTests.cs b/wrappers/dotnet/tests/W3CTests.cs new file mode 100644 index 00000000..8fb0c8ae --- /dev/null +++ b/wrappers/dotnet/tests/W3CTests.cs @@ -0,0 +1,486 @@ +using System.Collections.Generic; +using System.Text.Json; +using AnonCredsNet.Models; +using AnonCredsNet.Requests; +using Xunit; + +namespace AnonCredsNet.Tests; + +public class W3cTests +{ + [Fact] + public void W3cEndToEnd() + { + var issuerId = "mock:uri"; + var schemaId = "mock:uri"; + var credDefId = "mock:uri"; + var revRegId = "mock:uri:revregid"; + var entropy = "entropy"; + uint revIdx = 1; + + var schema = Schema.Create( + "schema name", + "1.0.0", + issuerId, + JsonSerializer.Serialize(new[] { "name", "age", "sex", "height" }) + ); + + var (credDef, credDefPriv, keyProof) = CredentialDefinition.Create( + schemaId, + issuerId, + schema, + "tag", + "CL", + "{\"support_revocation\": true}" + ); + + var (revRegDef, revRegPriv) = RevocationRegistryDefinition.Create( + credDef, + credDefId, + issuerId, + "some_tag", + "CL_ACCUM", + 10, + null + ); + + ulong timeCreateRevStatusList = 12; + var revocationStatusList = RevocationStatusList.Create( + credDef, + revRegId, + revRegDef, + revRegPriv, + issuerId, + true, + timeCreateRevStatusList + ); + + var linkSecret = LinkSecret.Create(); + var linkSecretId = "default"; + var credOffer = CredentialOffer.Create(schemaId, credDefId, keyProof); + var (credReq, credReqMeta) = CredentialRequest.Create( + credDef, + linkSecret, + linkSecretId, + credOffer, + entropy + ); + + var credValues = JsonSerializer.Serialize( + new Dictionary + { + ["sex"] = "male", + ["name"] = "Alex", + ["height"] = "175", + ["age"] = "28", + } + ); + var revConfig = new CredentialRevocationConfig + { + RevRegDef = revRegDef, + RevRegDefPrivate = revRegPriv, + RevStatusList = revocationStatusList, + RevRegIndex = revIdx, + }; + + var w3cCred = W3cCredential.Create( + credDef, + credDefPriv, + credOffer, + credReq, + credValues, + revConfig, + null + ); + + var processedW3c = w3cCred.Process(credReqMeta, linkSecret, credDef, revRegDef); + + // Convert to legacy and back to ensure conversions work + var legacy = processedW3c.ToLegacy(); + var w3cAgain = W3cCredential.FromLegacy(legacy, issuerId); + + // Prepare verification artifacts + var timeAfterCreatingCred = timeCreateRevStatusList + 1; + var issuedRevStatusList = revocationStatusList.Update( + credDef, + revRegDef, + revRegPriv, + new[] { (ulong)revIdx }, + null, + timeAfterCreatingCred + ); + + var nonce = AnonCreds.GenerateNonce(); + var presReqObj = new + { + nonce, + name = "pres_req_1", + version = "0.1", + requested_attributes = new Dictionary + { + ["attr1_referent"] = new Dictionary + { + ["name"] = "name", + ["issuer_id"] = issuerId, + }, + ["attr2_referent"] = new Dictionary + { + ["names"] = new[] { "name", "height" }, + }, + }, + requested_predicates = new Dictionary + { + ["predicate1_referent"] = new Dictionary + { + ["name"] = "age", + ["p_type"] = ">=", + ["p_value"] = 18, + }, + }, + non_revoked = new Dictionary { ["from"] = 10, ["to"] = 200 }, + }; + var presReqJson = JsonSerializer.Serialize(presReqObj); + var presReq = PresentationRequest.FromJson(presReqJson); + + // Build revocation state using the issued status list at the matching timestamp + var revState = RevocationState.Create( + revRegDef, + issuedRevStatusList, + revIdx, + revRegDef.TailsLocation + ); + + var credentialsJson = JsonSerializer.Serialize( + new[] + { + new + { + credential = processedW3c.ToJson(), + timestamp = timeAfterCreatingCred, + rev_state = revState.ToJson(), + }, + } + ); + var schemasJson = JsonSerializer.Serialize( + new Dictionary { [schemaId] = schema.ToJson() } + ); + var credDefsJson = JsonSerializer.Serialize( + new Dictionary { [credDefId] = credDef.ToJson() } + ); + + var presentation = W3cPresentation.CreateFromJson( + presReq, + credentialsJson, + linkSecret, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + null + ); + + var revRegDefsJson = JsonSerializer.Serialize( + new Dictionary { [revRegId] = revRegDef.ToJson() } + ); + var revRegDefIdsJson = JsonSerializer.Serialize(new[] { revRegId }); + + var isValid = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + revRegDefsJson, + JsonSerializer.Serialize(new[] { issuedRevStatusList.ToJson() }), + revRegDefIdsJson, + null + ); + + Assert.True(isValid); + + // Revoke and verify should fail + var timeRevoke = timeAfterCreatingCred + 1; + var revokedStatusList = issuedRevStatusList.Update( + credDef, + revRegDef, + revRegPriv, + null, + new[] { (ulong)revIdx }, + timeRevoke + ); + + var isValidAfterRevoke = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + revRegDefsJson, + JsonSerializer.Serialize(new[] { revokedStatusList.ToJson() }), + revRegDefIdsJson, + null + ); + Assert.False(isValidAfterRevoke); + } + + [Fact] + public void W3cNonRevocableCredential_Verifies() + { + var issuerId = "mock:uri"; + var schemaId = "mock:uri"; + var credDefId = "mock:uri"; + var entropy = "entropy"; + + var schema = Schema.Create( + "schema name", + "1.0.0", + issuerId, + JsonSerializer.Serialize(new[] { "name", "age" }) + ); + + var (credDef, credDefPriv, keyProof) = CredentialDefinition.Create( + schemaId, + issuerId, + schema, + "tag", + "CL", + "{\"support_revocation\": false}" + ); + + var linkSecret = LinkSecret.Create(); + var linkSecretId = "default"; + var offer = CredentialOffer.Create(schemaId, credDefId, keyProof); + var (req, reqMeta) = CredentialRequest.Create( + credDef, + linkSecret, + linkSecretId, + offer, + entropy + ); + + var values = JsonSerializer.Serialize( + new Dictionary { ["name"] = "Alex", ["age"] = "28" } + ); + + var w3cCred = W3cCredential.Create(credDef, credDefPriv, offer, req, values, null, null); + var processed = w3cCred.Process(reqMeta, linkSecret, credDef, null); + + var nonce = AnonCreds.GenerateNonce(); + var presReqObj = new + { + nonce, + name = "pres_req_1", + version = "0.1", + requested_attributes = new Dictionary + { + ["attr1_referent"] = new Dictionary { ["name"] = "name" }, + }, + requested_predicates = new Dictionary + { + ["predicate1_referent"] = new Dictionary + { + ["name"] = "age", + ["p_type"] = ">=", + ["p_value"] = 18, + }, + }, + }; + var presReq = PresentationRequest.FromJson(JsonSerializer.Serialize(presReqObj)); + + var credentialsJson = JsonSerializer.Serialize( + new[] { new { credential = processed.ToJson() } } + ); + var schemasJson = JsonSerializer.Serialize( + new Dictionary { [schemaId] = schema.ToJson() } + ); + var credDefsJson = JsonSerializer.Serialize( + new Dictionary { [credDefId] = credDef.ToJson() } + ); + + var presentation = W3cPresentation.CreateFromJson( + presReq, + credentialsJson, + linkSecret, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + null + ); + + var isValid = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + null, + null, + null, + null + ); + + Assert.True(isValid); + } + + [Fact] + public void W3c_Verify_WithIntervalOverride() + { + var issuerId = "mock:uri"; + var schemaId = "mock:uri"; + var credDefId = "mock:uri"; + var revRegId = "mock:uri:revregid"; + var entropy = "entropy"; + uint revIdx = 3; + + var schema = Schema.Create( + "schema name", + "1.0.0", + issuerId, + JsonSerializer.Serialize(new[] { "name" }) + ); + + var (credDef, credDefPriv, keyProof) = CredentialDefinition.Create( + schemaId, + issuerId, + schema, + "tag", + "CL", + "{\"support_revocation\": true}" + ); + + var (revRegDef, revRegPriv) = RevocationRegistryDefinition.Create( + credDef, + credDefId, + issuerId, + "some_tag", + "CL_ACCUM", + 10, + null + ); + + ulong t0 = 100; + var status0 = RevocationStatusList.Create( + credDef, + revRegId, + revRegDef, + revRegPriv, + issuerId, + true, + t0 + ); + + var linkSecret = LinkSecret.Create(); + var offer = CredentialOffer.Create(schemaId, credDefId, keyProof); + var (req, reqMeta) = CredentialRequest.Create( + credDef, + linkSecret, + "default", + offer, + entropy + ); + + var values = JsonSerializer.Serialize(new Dictionary { ["name"] = "Al" }); + var revConfig = new CredentialRevocationConfig + { + RevRegDef = revRegDef, + RevRegDefPrivate = revRegPriv, + RevStatusList = status0, + RevRegIndex = revIdx, + }; + + var w3cCred = W3cCredential.Create( + credDef, + credDefPriv, + offer, + req, + values, + revConfig, + null + ); + var processed = w3cCred.Process(reqMeta, linkSecret, credDef, revRegDef); + + var t1 = t0 + 1; + var status1 = status0.Update( + credDef, + revRegDef, + revRegPriv, + new[] { (ulong)revIdx }, + null, + t1 + ); + + var revState = RevocationState.Create(revRegDef, status1, revIdx, revRegDef.TailsLocation); + + var credentialsJson = JsonSerializer.Serialize( + new[] + { + new + { + credential = processed.ToJson(), + timestamp = t1, + rev_state = revState.ToJson(), + }, + } + ); + + var schemasJson = JsonSerializer.Serialize( + new Dictionary { [schemaId] = schema.ToJson() } + ); + var credDefsJson = JsonSerializer.Serialize( + new Dictionary { [credDefId] = credDef.ToJson() } + ); + + var nonce = AnonCreds.GenerateNonce(); + var presReqObj = new + { + nonce, + name = "pr", + version = "0.1", + requested_attributes = new Dictionary + { + ["a1"] = new Dictionary { ["name"] = "name" }, + }, + non_revoked = new Dictionary + { + ["from"] = (int)t0, + ["to"] = (int)(t1 + 10), + }, + }; + var presReq = PresentationRequest.FromJson(JsonSerializer.Serialize(presReqObj)); + + var presentation = W3cPresentation.CreateFromJson( + presReq, + credentialsJson, + linkSecret, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + null + ); + + var overrides = JsonSerializer.Serialize( + new Dictionary> + { + [revRegId] = new Dictionary { [t0.ToString()] = (int)t1 }, + } + ); + + var isValid = presentation.Verify( + presReq, + schemasJson, + credDefsJson, + JsonSerializer.Serialize(new[] { schemaId }), + JsonSerializer.Serialize(new[] { credDefId }), + JsonSerializer.Serialize( + new Dictionary { [revRegId] = revRegDef.ToJson() } + ), + JsonSerializer.Serialize(new[] { status1.ToJson() }), + JsonSerializer.Serialize(new[] { revRegId }), + overrides + ); + + Assert.True(isValid); + } +}