Epsitec.Briefcases.Identity
2.8.1.2629
Prefix Reserved
dotnet add package Epsitec.Briefcases.Identity --version 2.8.1.2629
NuGet\Install-Package Epsitec.Briefcases.Identity -Version 2.8.1.2629
<PackageReference Include="Epsitec.Briefcases.Identity" Version="2.8.1.2629" />
<PackageVersion Include="Epsitec.Briefcases.Identity" Version="2.8.1.2629" />
<PackageReference Include="Epsitec.Briefcases.Identity" />
paket add Epsitec.Briefcases.Identity --version 2.8.1.2629
#r "nuget: Epsitec.Briefcases.Identity, 2.8.1.2629"
#:package Epsitec.Briefcases.Identity@2.8.1.2629
#addin nuget:?package=Epsitec.Briefcases.Identity&version=2.8.1.2629
#tool nuget:?package=Epsitec.Briefcases.Identity&version=2.8.1.2629
Epsitec.Briefcases.Identity
User identity lifecycle for Crésus Briefcases: bring a user from "nothing on
this device" to a live BriefcasesSession without re-implementing key
derivation, at-rest encryption, token acquisition, and keyset registration by
hand. This is the single briefcases library that depends on Epsitec.Rezo.Api;
the SDK stays identity-agnostic and merely consumes the
BriefcasesSessionOptions this package produces.
BriefcasesIdentity owns auth and keyset concerns and produces
BriefcasesSessionOptions; it never opens a BriefcasesSession itself. The host
constructs the session from the returned options, so the dependency direction
stays host → Identity → Sdk.
Usage — the shortest happy path
For a host that prefers one call, OpenAsync runs the whole tree (unlock,
recover, or provision) and returns ready session options. It asks the host for
input through IdentityPrompts.
var identity = new BriefcasesIdentity (new BriefcasesIdentityOptions
{
AuthUrl = new Uri ("https://auth.api.cresus.ch"),
BriefcasesServerUrl = new Uri ("https://briefcases.example.com"),
KeyStore = new FileKeyStore (keyDirectory),
});
var options = await identity.OpenAsync (new IdentityPrompts
{
Login = ct => ui.AskEmailAsync (ct), // auth login (the email)
AuthPassword = ct => ui.AskAuthPasswordAsync (ct), // sent to auth
Totp = ct => ui.AskTotpAsync (ct),
AtRestPassword = ct => ui.AskAtRestPasswordAsync (ct),// guards the local keys
EnterMnemonic = ct => ui.AskMnemonicAsync (ct),
ShowMnemonic = (mnemonic, ct) => ui.ShowMnemonicAsync (mnemonic, ct),
});
await using var session = new BriefcasesSession (options);
await session.ConnectAsync ();
OpenAsync authenticates first — the briefcases uid is the access token's sub
claim, so it is unknown until the login (email + auth password) succeeds — then
derives the uid, steps up MFA when the branch needs a keyset write, and
dispatches to unlock, recover, or provision per GetStatusAsync. The local keys
are always (un)locked with the at-rest password, never the auth password.
Two distinct secrets. The auth password is the account password sent to
authwith the email login to get a token; the at-rest password is a briefcases-only secret that derives the AES key encrypting the local private keys. They are never the same parameter —IdentityPromptscollects them separately, and the unlock / recover / provision / publish operations only ever receiveatRestPassword.
First run — provision a new identity
Provisioning generates a fresh mnemonic and key set, stores the private keys at
rest, and publishes the public keyset to the auth registry. Publishing is a
keyset write, so it requires an MFA-validated token: step up first, then
provision. The new mnemonic comes back in the result — show it to the user once.
await using var auth = await identity.AuthenticateAsync (login, authPassword);
// auth.Uid is the access token's `sub` claim — the briefcases uid.
await auth.StepUpMfaAsync (totpCode); // MFA is a precondition for the write
var result = await identity.ProvisionAsync (auth, atRestPassword);
ShowToUser (result.Mnemonic); // the only way to recover on a new device
await using var session = new BriefcasesSession (result.SessionOptions);
await session.ConnectAsync ();
ProvisionAsync enforces MFA as a client-side precondition: when the
identity is not MFA-validated it throws MfaStepUpRequiredException before any
network call or key store write, so the host gets one immediate, unambiguous
signal.
Returning user on a known device — unlock
The returning user authenticates once (to get a fresh token and the uid from the
token sub), then unlocks the local keys with the at-rest password — the registry
is never contacted. The decrypted keys plus a refreshing token provider come back
as session options.
await using var auth = await identity.AuthenticateAsync (login, authPassword);
if (await identity.HasLocalKeysAsync (auth.Uid))
{
var options = await identity.UnlockAsync (auth, atRestPassword);
await using var session = new BriefcasesSession (options);
await session.ConnectAsync ();
}
HasLocalKeysAsync is the cheap, network-free check; it delegates to
IKeyStore.ContainsAsync. UnlockAsync does not re-authenticate — it takes the
already-authenticated auth — and uses only the at-rest password to decrypt the
private keys.
New device — BIP-39 recovery
On a device with no local keys, recover from the BIP-39 mnemonic. Recovery
re-derives the keys from (uid, mnemonic) and verifies they match the
published keyset before trusting them, so a wrong mnemonic fails loudly instead
of silently creating a divergent identity.
await using var auth = await identity.AuthenticateAsync (login, authPassword);
try
{
var options = await identity.RecoverAsync (auth, mnemonic, atRestPassword);
await using var session = new BriefcasesSession (options);
await session.ConnectAsync ();
}
catch (IdentityMismatchException)
{
// Wrong mnemonic (or a keyset for another subject): ask the user to retry.
}
Recovery writes no keyset, so it needs no MFA.
Login then MFA step-up
AuthenticateAsync takes the auth login (the email) and the auth password and
returns a password-tier AuthenticatedIdentity whose IsMfaValidated is false
and whose Uid is the access token's sub claim. StepUpMfaAsync elevates it
with a TOTP code, reusing the refresh token so the auth password is not re-entered.
Both IsMfaValidated (the amr claim) and Uid (the sub claim) are read
locally from the token — no server round-trip.
await using var auth = await identity.AuthenticateAsync (login, authPassword);
while (auth.IsMfaValidated == false)
{
try
{
await auth.StepUpMfaAsync (await ui.AskTotpAsync ());
}
catch (MfaCodeInvalidException)
{
// Loop and re-prompt for a fresh code.
}
catch (MfaRateLimitedException ex)
{
await ui.ShowRateLimitAsync (ex.RetryAfter);
throw;
}
}
Every keyset write requires an MFA-validated identity. The host only ever handles
one exception type for "needs TOTP": ProvisionAsync / PublishAsync throw
MfaStepUpRequiredException as a local precondition, and a server-side MFA
rejection surfacing from a write is re-mapped to the same exception.
Discover a user's keys to invite them
Discovery reads another user's latest published keyset by uid, replacing the
manual profile.json exchange. Reads need no MFA. A keyset outside the
briefcases profile policy — ecdsa-p256-sha384, ml-dsa-65, x25519, and
ml-kem-768 — is rejected as a typed KeysetProfileMismatchException rather
than returning unusable keys.
await using var auth = await identity.AuthenticateAsync (login, authPassword);
UserPublicKeys inviteeKeys = await identity.GetPublicKeysAsync (auth, otherUid);
// Feed inviteeKeys straight into the SDK invitation flow instead of importing a
// profile.json — e.g. workspace.InviteAsync (otherUid, inviteeKeys).
Shared keyset bridge for server-side checks
BriefcasesKeysetBridge exposes the common conversion and identity checks used by both the identity flows and server-side validation code. It keeps the Epsitec.Rezo.Api dependency in this package while letting consumers work from the briefcases model types.
BriefcasesKeysetBridge.ComputeHkid(publicKeys)derives theauthhkidfrom the two signature keys inUserPublicKeys.BriefcasesKeysetBridge.ToUserPublicKeys(normalizedKeyset)maps a normalizedauthkeyset back to the four briefcases public-key slots.BriefcasesKeysetBridge.MatchesPublicKeys(normalizedKeyset, publicKeys)verifies both thehkidand the four SPKI DER public-key slots.BriefcasesKeysetProfiles.Policyis the profile policy for keysets briefcases can consume:ecdsa-p256-sha384,ml-dsa-65,x25519, andml-kem-768.
The intended server-side flow is to compute the hkid from received UserPublicKeys, read the keyset from auth by (uid, hkid), normalize it through ApiKeyset.ToNormalized(), then call MatchesPublicKeys(...). The comparison deliberately covers the encryption keys too: hkid identifies only the hybrid signing keys.
Choosing a key store
FileKeyStore writes one {uid}.keyset.json per user under a host-supplied
directory, in the same EncryptedSecret layout the CLI writes, so a user
provisioned by the CLI or by this package is interchangeable and the QR
import/export path keeps working. DpapiKeyStore keeps the same encrypted
material in a Windows DPAPI-protected file ([SupportedOSPlatform("windows")]).
IKeyStore store = OperatingSystem.IsWindows ()
? new DpapiKeyStore (keyDirectory)
: new FileKeyStore (keyDirectory);
The OS protection is applied in addition to the password encryption, never instead of it: the private keys stay password-encrypted at the briefcases layer, and DPAPI adds machine/user binding on top.
Handing off to the SDK
Every flow returns a BriefcasesSessionOptions that plugs straight into the SDK:
await using var session = new BriefcasesSession (options);
await session.ConnectAsync ();
The session's AccessTokenProvider is wired to the identity's refreshing token
provider, so the access token is refreshed transparently for every SignalR and
blob request — the host does not re-log-in mid-session. A QR-imported identity
feeds the same store-then-publish path through BriefcasesKeyImport.FromQrPayload,
so provisioning from a scanned pairing code reuses, rather than duplicates, the
publication flow.
Verifying workspace cards (cold-read defense in depth)
A cold card listing carries no ledger, so the SDK cannot verify the owner's
envelope signature on its own. AuthWorkspaceCardSignerKeyResolver plugs the
trusted auth directory into that gap (SPEC-000-024): given a card signer's id
and the hkid the server serves with each card, it resolves the owner's public
keys from auth so the SDK verifies the signature itself.
var resolver = new AuthWorkspaceCardSignerKeyResolver (
authUrl, options.AccessTokenProvider);
await using var session = new BriefcasesSession (
options, cardSignerKeyResolver: resolver);
Each WorkspaceCardInfo then carries SignatureVerified (null = not checked,
true = valid, false = invalid). Verification is opt-in (no resolver ⇒
cards list unverified) and best-effort — resolution is cached per
(signer, hkid), fetches the keyset in any state (a card signed by a
since-rotated key still verifies), and any failure leaves the card unverified
rather than failing the listing.
What the host still provides
- UI prompts —
IdentityPrompts(the email login, the auth password, TOTP, the at-rest password, mnemonic entry, mnemonic display). The package contributes no UI, and it keeps the two secrets — the auth password and the at-rest password — distinct. - The key-store choice —
FileKeyStoreversusDpapiKeyStore, and the directory they live in. - The auth endpoint configuration —
AuthUrl,BriefcasesServerUrl, and the tokenScopeonBriefcasesIdentityOptions.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- BouncyCastle.Cryptography (>= 2.7.0-beta.98)
- Epsitec.Briefcases.Sdk (= 2.8.1.2629)
- Epsitec.Rezo.Api (>= 11.5.1.2628)
- Konscious.Security.Cryptography.Argon2 (>= 1.3.1)
- Microsoft.AspNetCore.Http.Connections.Client (>= 10.0.9)
- Microsoft.AspNetCore.Http.Connections.Common (>= 10.0.9)
- Microsoft.AspNetCore.SignalR.Client (>= 10.0.9)
- Microsoft.AspNetCore.SignalR.Protocols.Json (>= 10.0.9)
- Microsoft.Extensions.Configuration (>= 10.0.9)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Http (>= 10.0.9)
- Microsoft.Extensions.Http.Resilience (>= 10.7.0)
- Microsoft.Extensions.Logging (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Logging.Configuration (>= 10.0.9)
- Microsoft.Extensions.ObjectPool (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.9)
- Microsoft.IdentityModel.Tokens (>= 8.19.1)
- Neo.Cryptography.BLS12_381 (>= 3.9.0)
- System.Security.Cryptography.ProtectedData (>= 10.0.9)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.8.1.2629 | 0 | 7/15/2026 |
| 2.8.0.2629 | 0 | 7/15/2026 |
| 2.7.3.2629 | 29 | 7/14/2026 |
| 2.7.2.2629 | 31 | 7/14/2026 |
| 2.7.1.2629 | 35 | 7/14/2026 |
| 2.7.0.2629 | 38 | 7/14/2026 |
| 2.6.2.2629 | 38 | 7/14/2026 |
| 2.6.1.2629 | 35 | 7/14/2026 |
| 2.6.0.2629 | 42 | 7/14/2026 |
| 2.5.0.2628 | 45 | 7/12/2026 |
| 2.3.0.2628 | 58 | 7/10/2026 |
| 2.2.0.2628 | 50 | 7/8/2026 |
| 2.1.6.2627 | 52 | 7/3/2026 |
| 2.1.5.2627 | 62 | 7/2/2026 |
| 2.1.4.2627 | 56 | 7/2/2026 |
| 2.1.3.2627 | 61 | 6/30/2026 |
| 2.1.2.2627 | 55 | 6/29/2026 |
| 2.1.1.2627 | 61 | 6/29/2026 |
| 2.1.0.2627 | 55 | 6/29/2026 |
| 2.0.0.2626 | 61 | 6/28/2026 |