Add the 51Did client: two-step verification, redeem and the outcome types - #33
Add the 51Did client: two-step verification, redeem and the outcome types#33jwrosewell wants to merge 2 commits into
Conversation
…come types Every other 51Did language package carries a client for the two-step verification, and Rust had none, so a Rust server could create a 51Did and check its signature but could not take part in the verification at all. This adds the fodid-client crate as a port of the .NET DidClient, which is the model the Java, Node, Python and PHP ports follow. The crate fetches and caches the published signing keys, verifies a signature offline against the key in force when the identifier was created, verifies a signature through the cloud, and redeems the sealed creator context result a browser relays, reading the outcomes the other packages report, misconfigured and invaliddate included. A factor of misconfigured is read on its own and never falls through to a mismatch, because it says the checking service could not determine that factor and the identifier says nothing about it either way. Every request goes through a DidHttpClient trait, and the crate builds without a network stack by default so it compiles for wasm32-wasip1 and an edge runtime can supply its own transport. The reqwest-client feature turns on the built-in blocking reqwest transport, following the cloud request engine. Credentials never travel in a URL, and the licence key is sent only in the redeem form body. Closes #32.
Every method of DidClient that may reach the network (public_keys, public_key_for, verify_signature, verify_signature_detailed, verify, verify_encoded, redeem, redeem_encoded) is now `pub async fn` with the same name and return type, and the synchronous versions are gone. The methods that never touch the network (resource_key, endpoint, has_licence_key and the key selection helpers) are unchanged. The DidHttpClient trait's one method now returns a LocalBoxFuture, a boxed future that borrows the request and transport and is deliberately not required to be Send. The crate carries no async runtime and no async-trait dependency, so it builds for wasm32-wasip1 and a single-threaded host such as a Trusted Server appliance can implement the transport and await the client, which is the same shape the cloud request engine's awaitable transport takes. The built-in transport behind the reqwest-client feature is now the asynchronous reqwest client with rustls and the form feature, keeping the thirty second default timeout, zero meaning none, and the default redirect handling the blocking client had. The key cache keeps every rule the tests pin (fetch on first use, again after a day, when no key covers the date, or when the date is past the newest start) and now shares one in-flight fetch between concurrent callers. A caller that finds a fetch in flight waits for it and answers from the keys it landed, fetching for itself only when that fetch failed, and a fetch dropped before it lands clears the in-flight mark so no waiter is stranded. The lock is never held across an await. The tests run on a tokio current-thread runtime as a dev-dependency, with new cases for the shared fetch, a failed shared fetch, a dropped fetch, and a transport whose future holds an Rc across an await to prove the future need not be Send. The README and crate documentation show the awaited calls and the new trait shape, and their examples remain documentation tests.
|
Pushed one more commit, 247e4cf, making the client asynchronous with no synchronous form, so it ships that way from the start rather than shipping synchronous and being changed before anyone uses it. This is part of the set making every 51Did client and every OWID port asynchronous where the network is involved. The transport trait now returns a boxed future that is deliberately not required to be Every network method keeps its name, arguments and return type and becomes Verified from the workspace root: Written with AI assistance and needs human review. |
Closes #32.
Why
Every other 51Did language package carries a client for the two-step
verification. Rust had none, so a Rust server could create a 51Did and check
its signature but could not take part in the verification at all. The .NET
DidClientis the model the Java, Node, Python and PHP ports follow, andthis is the Rust port of it.
The gap surfaced on 3 September 2026 while the cloud gained two new creator
context outcomes. The other five ports were updated for those the same day
(51Degrees/pipeline-dotnet#386, 51Degrees/pipeline-java#122,
51Degrees/pipeline-node#189, 51Degrees/pipeline-python#72,
51Degrees/pipeline-php-did#10). Rust had nothing to update.
What the crate does
fodid-client, a new workspace member depending onfodid.stale, when no key is on or before an identifier's date, or when the date
is later than the newest start held.
was created, trying a neighbouring key within fifteen minutes of a boundary
so ordinary clock skew between two nodes does not read as a bad signature.
two of the two-step flow, sending the licence key the browser never sees.
SignatureCheckseparates "no published key covers this date", which is anoperational matter, from "the signature does not match", which is the only
answer that says anything about the identifier.
The outcome types
ContextOutcomeandFactorOutcomecarry the same vocabulary the other fiveports now report,
misconfiguredandinvaliddateincluded.A factor of
misconfiguredsays the checking service is not configured todetermine that factor, so it could not have checked it for any request. It is
read on its own and never falls through to a mismatch, because a mismatch is
a replay indicator and the identifier says nothing about that factor either
way. Everything else that is not the word
verifiedis still a mismatch, soan unexpected value never reads as a pass.
Transport design, and why it matters here
Every request goes through a
DidHttpClienttrait, following the same shapeas
CloudHttpClientincloud-request-engine. The crate carries no networkstack by default, and
reqwest-clientturns on a built-in blockingreqwesttransport for a native caller.That is not tidiness. This crate has to work where there is no
reqwest, inparticular an edge runtime that supplies its own fetch, which is why the
wasm32-wasip1build is one of the checks below rather than an afterthought.Verification
Every command below was run and passed.
The tests use a fake
DidHttpClientthat records requests and answers cannedresponses, covering the key cache refresh rules, the verify URL shape, the
redeem form shape including that no licence field is sent when no licence key
was given, and each status mapping. No test touches the network.
Not verified. There is no live cloud integration test in this change. The
fodidcrate has one (tests/cloud_51did.rs) and the same pattern wouldsuit, but it needs a resource key in the environment and a reviewer should
decide whether that belongs here or in CI.
Reference
The .NET implementation this ports:
https://github.com/51Degrees/pipeline-dotnet/tree/main/FiftyOne.Did/Client