MobileAttest 1.0.0

dotnet add package MobileAttest --version 1.0.0
                    
NuGet\Install-Package MobileAttest -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MobileAttest" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MobileAttest" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="MobileAttest" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MobileAttest --version 1.0.0
                    
#r "nuget: MobileAttest, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MobileAttest@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MobileAttest&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=MobileAttest&version=1.0.0
                    
Install as a Cake Tool

MobileAttest

NuGet License

Server-side verification of mobile device attestation. Apple App Attest and Android key attestation, two mechanisms behind one result type.

dotnet add package MobileAttest

Targets net8.0. Two dependencies: BouncyCastle for cryptography and path validation, System.Formats.Cbor for the Apple objects.


What it answers

At enrollment, once: was this key created in real hardware, by this application, on a device that booted the way it says it did.

On every request after that: did that same key sign these bytes.


Configure once

Everything the check compares against is yours to supply. The library ships no trust root and downloads none, so an unconfigured anchor is a rejection rather than a pass.

var apple = new AppleAttestOptions
{
    TeamId                 = "A1B2C3D4E5",
    BundleIdAllowlist      = new[] { "com.example.app" },
    RequireProduction      = true,
    PinnedRootCertificates = PemRootStore.LoadFromPem(File.ReadAllText(appleRootPath)),
};
apple.Validate();          // empty allow list or no root throws here, not on first request

var android = new AndroidAttestOptions
{
    AllowedSignatureDigests = new[] { "44D6FB97…" },   // SHA-256 of the APK signing cert
    RequireStrongBox        = false,
    PinnedRootCertificates  = PemRootStore.LoadFromPem(File.ReadAllText(googleRootsPath)),
    RevocationPolicy        = RevocationPolicy.Skip,   // no default: you must choose
    StatusCacheTtl          = TimeSpan.FromHours(24),
};
android.Validate();

Google publishes two active roots and a deployment needs both; Apple publishes one. Pin them by fingerprint, and refresh at deployment time rather than at runtime.

Serving more than one application

An options object describes exactly one application: one team, one bundle allow list, one signing digest. A service that verifies for several applications keeps one set per application and picks it before verifying.

// Built once at startup, from your own registry of onboarded applications.
var verifiers = registry.ToDictionary(
    app => app.ClientId,
    app => new AndroidKeyAttestationVerifier(
        new AndroidAttestOptions
        {
            AllowedSignatureDigests = new[] { app.SigningDigest },   // theirs, not yours
            RequireStrongBox        = app.RequireStrongBox,
            PinnedRootCertificates  = googleRoots,                   // shared: platform-wide
            RevocationPolicy        = RevocationPolicy.Skip,
        },
        TimeProvider.System,
        statusSource));

// Per request: identify the application first, verify second.
if (!verifiers.TryGetValue(clientId, out var verifier))
{
    return Unauthorized();
}

AttestationResult result = await verifier.VerifyAsync(request);

Two things are worth stating plainly about that order.

Identify the caller before verifying, never from the attestation. Apple does not expose the application identity in the attestation at all — only a hash to compare against. Android does expose the package name, but selecting a configuration from it would be circular: the device would be choosing the rules it is judged by.

The client identifier need not be a secret. If it is taken and replayed by someone else, their application is checked against the registered team, bundle and signing digest — and refused, because those are what actually authenticate. Getting the identifier wrong fails closed.

Trust roots are the exception to per-application configuration: Apple's root is the same for every Apple device and Google's for every Android device, so they are shared.

Verify an enrollment

var verifier = new AppleAppAttestVerifier(apple);

AttestationResult result = await verifier.VerifyAsync(
    new AppleAttestationRequest(keyId, attestationObject, challengeYouIssued),
    receivedAt);          // the instant to judge certificates at, from your own record

if (!result.IsValid)
{
    return Unauthorized();   // result.Reason names which check refused it
}

Store(result.KeyId, result.PublicKeyDer, result.AppId, result.SignCount);

Android is the same call with AndroidKeyAttestationVerifier and an AndroidAttestationRequest carrying the certificate chain. Both return the same AttestationResult, so one storage path serves both platforms.

Verify what the key signs afterwards

SignatureVerificationResult ok = DeviceSignature.Verify(
    signature:    popFromTheDevice,     // DER ECDSA, as SHA256withECDSA produces
    signedData:   nonceYouIssued,       // whatever you decided to bind
    publicKeyDer: record.PublicKeyDer); // what enrollment returned, from your store

Apple App Attest assertions are a CBOR envelope rather than a bare signature, and carry a counter that must strictly advance:

AssertionResult op = await new AppleAssertionVerifier(record.AppId).VerifyAsync(
    assertionObject, clientDataHash, record.PublicKeyDer, record.SignCount);

What it deliberately does not do

  • No trust roots, no network of its own. Roots, allow lists and any status endpoint are configuration. Where a status list is consulted, the HttpClient and the address come from you.
  • No storage. Not a key, not a counter, not a challenge. Issuing nonces and refusing a second use stays with the caller — and without it, a verified signature only proves possession, never freshness.
  • No position it can avoid. Whether an unverifiable key is acceptable has no default; a configuration that never chose is refused at startup.
  • No opinion about your protocol. DeviceSignature compares bytes. Binding those bytes to an operation is yours; AnchorSignatureContext is offered for it and is optional.

Certificate validity is asked about an instant you name rather than the current clock, so a backend can ask whether a certificate was valid when the attestation actually arrived. That instant must come from your own record, never from the device.


Documentation

Tests

Device vectors are deliberately not in this repository: they carry a real device key and a real application identity. Tests locate them at run time, and a test that needs one is skipped by name, stating the path it looked for, rather than passing quietly.

License

Apache-2.0

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
1.0.0 124 9/8/2026
0.1.1 100 9/8/2026