light-mode-image
Learn
Guides

Handling verifier authentication

Inspect the verifier authentication result during a proximity presentation so users can decide whether to share credentials with an unauthenticated verifier

Overview

When a verifier requests credentials during a proximity presentation, your holder application can inspect whether the verifier proved its identity before any credential data is shared. This lets you show the user who is asking for their data and how trustworthy the request is, and lets the user decline to share with a verifier that cannot be authenticated.

In a proximity presentation, the verifier can sign its request. The Holder SDK validates that signature against the trusted verifier root certificates configured on the device and exposes the outcome as a verifier authentication result on each request. Your application reads this result and decides how to proceed.

This is the mechanism that ISO/IEC 18013-5:2021 calls mDoc Reader Authentication (section 9.1.4). The verifier (the "mdoc reader") signs the request, and the holder ("mdoc") authenticates the reader before responding. The SDK surfaces it under the more general name verifier authentication, so the same concept is consistent with the remote presentation flow.

Proximity and remote authenticate verifiers differently

This guide covers proximity (ISO/IEC 18013-5) verifier authentication, where the verifier signs the device request and the holder validates it offline. Remote (OpenID4VP / ISO/IEC 18013-7) presentations authenticate the verifier through the signed authorization request instead. See Handling verifier authentication for remote presentations for that flow.

Prerequisites

This guide builds on the Proximity Presentation tutorial. Complete that tutorial first so you have a working holder application that can create a proximity presentation session and receive requests, then return here to add verifier authentication handling.

Trust evaluation is performed against the trusted verifier root certificates configured on the holder device. Whether a request is reported as trusted depends on those certificates being present. For background on verifier certificates, see Certificates overview.

Understanding the verifier authentication result

When the SDK receives a device request, it evaluates any reader authentication it contains and attaches a verifier authentication result to each requested document. The result is one of three outcomes:

  • Trusted: The request was signed and the signature chained to a trusted verifier root certificate. The result carries a VerifierInfo value describing the trusted root that was matched (its common name and the identifier of the stored certificate).
  • Untrusted: A reader signature was present but could not be trusted. The result carries the X.509 certificate chain from the request (as PEM strings) and an error describing the first validation failure detected.
  • Unsigned: The request carried no reader authentication. The result optionally carries the request origin if one is available.

An untrusted result reports one of the following reasons. These map directly to the requirements in ISO/IEC 18013-5:2021 section 9.1.4:

ReasonMeaning
Certificate not foundThe verifier authentication object is missing a verifier certificate (section 9.1.4.4).
Invalid verifier certificateThe verifier certificate in the authentication object is invalid.
Inactive certificateThe certificate's validFrom date is in the future.
Expired certificateThe certificate's validUntil date is in the past.
Failed to evaluate trustThe certificate chain or the integrity of the verifier authentication object could not be verified, for example because it does not chain to a trusted root.

Adding trusted verifier certificates

A request is reported as trusted only when the verifier's signature chains to a trusted verifier root certificate stored on the device. Your application provisions these certificates through the SDK. The same trusted verifier certificate store is used for both proximity and remote presentations.

Certificates are supplied as PEM-encoded or Base64-encoded DER strings and conform to the IACA profile defined in ISO/IEC 18013-5. Adding a certificate is idempotent: adding the same certificate again returns the identifier of the existing entry rather than creating a duplicate.

Use addTrustedVerifierCertificates, getTrustedVerifierCertificates, and deleteTrustedVerifierCertificate:

CertificateSettings.swift
let mobileCredentialHolder = MobileCredentialHolder.shared

// Add one or more trusted verifier certificates (PEM or Base64 DER).
let addedIds = try mobileCredentialHolder.addTrustedVerifierCertificates(
    certificates: [verifierCertificatePem]
)

// List the stored trusted verifier certificates.
let stored: [TrustedCertificate] = try mobileCredentialHolder.getTrustedVerifierCertificates()

// Remove one by its identifier.
try mobileCredentialHolder.deleteTrustedVerifierCertificate(certificateId: addedIds[0])

Each stored certificate is a TrustedCertificate carrying its id, pem, and commonName.

Use addTrustedVerifierCertificates, getTrustedVerifierCertificates, and deleteTrustedVerifierCertificate:

CertificateSettings.kt
val mobileCredentialHolder = MobileCredentialHolder.getInstance()

// Add one or more trusted verifier certificates (PEM or Base64 DER).
val addedIds: List<String> = mobileCredentialHolder.addTrustedVerifierCertificates(
    listOf(verifierCertificatePem)
)

// List the stored trusted verifier certificates.
val stored: List<TrustedCertificate> = mobileCredentialHolder.getTrustedVerifierCertificates()

// Remove one by its identifier.
mobileCredentialHolder.deleteTrustedVerifierCertificate(addedIds.first())

Each stored certificate is a TrustedCertificate carrying its id, pem, and commonName.

Use addTrustedVerifierCertificates, getTrustedVerifierCertificates, and deleteTrustedVerifierCertificate:

trustedVerifierCertificates.ts
import {
  addTrustedVerifierCertificates,
  getTrustedVerifierCertificates,
  deleteTrustedVerifierCertificate,
} from "@mattrglobal/mobile-credential-holder-react-native";

// Add one or more trusted verifier certificates (PEM or Base64 DER).
const result = await addTrustedVerifierCertificates([verifierCertificatePem]);
if (result.isErr()) {
  console.error("Failed to add trusted verifier certificates:", result.error);
  return;
}
const addedIds = result.value;

// List the stored trusted verifier certificates.
const stored = await getTrustedVerifierCertificates();

// Remove one by its identifier.
await deleteTrustedVerifierCertificate(addedIds[0]);

Each stored certificate is a TrustedVerifierCertificate carrying its id, pem, and commonName.

Inspecting the result

Each requested document delivered to your request handler carries its verifier authentication result. Read it before presenting the consent UI so you can reflect the verifier's status to the user. The session is created exactly as in the Proximity Presentation tutorial; the examples below focus on inspecting the result inside the request handler.

On iOS, each entry delivered to your onRequestReceived handler is a MobileCredentialRequest. Read its verifierAuthenticationResult property and switch over the VerifierAuthenticationResult cases:

ViewModel.swift
func onRequestReceived(
    _ mobileCredentialRequests: [(request: MobileCredentialRequest, matchedMobileCredentials: [MobileCredentialMetadata])]?,
    error: Error?
) {
    guard let mobileCredentialRequests else {
        // Handle error
        return
    }

    for (request, _) in mobileCredentialRequests {
        switch request.verifierAuthenticationResult {
        case .trusted(let verifierInfo):
            // The request was signed by a trusted verifier root certificate.
            print("Trusted verifier: \(verifierInfo.commonName)")

        case .untrusted(let x509CertChain, let error):
            // A reader signature was present but could not be trusted.
            print("Untrusted verifier: \(error.localizedDescription)")
            print("Presented chain: \(x509CertChain)")

        case .unsigned(let origin):
            // No reader authentication was provided.
            print("Unsigned request from: \(origin ?? "unknown origin")")
        }
    }
}

See VerifierInfo and VerifierAuthenticationError for the associated values.

On Android, each item delivered to your onRequestReceived callback is a CredentialRequestInfo whose request property is a MobileCredentialRequest. Read its verifierAuthenticationResult and match over the VerifierAuthenticationResult subtypes:

ProximityPresentationViewModel.kt
session = MobileCredentialHolder.getInstance().createProximityPresentationSession(
    activity,
    onRequestReceived = { _, requests, error ->
        if (error != null) {
            // Handle error
            return@createProximityPresentationSession
        }

        requests?.forEach { requestInfo ->
            when (val result = requestInfo.request.verifierAuthenticationResult) {
                is VerifierAuthenticationResult.Trusted ->
                    // The request was signed by a trusted verifier root certificate.
                    Log.i(TAG, "Trusted verifier: ${result.verifierInfo.commonName}")

                is VerifierAuthenticationResult.Untrusted ->
                    // A reader signature was present but could not be trusted.
                    Log.w(TAG, "Untrusted verifier: ${result.error.message}")

                is VerifierAuthenticationResult.Unsigned ->
                    // No reader authentication was provided.
                    Log.i(TAG, "Unsigned request from: ${result.origin ?: "unknown origin"}")
            }
        }
    }
)

See VerifierInfo and VerifierAuthenticationFailureType for the associated values.

On React Native, the request handler receives a single data argument. When the request succeeds, data.request is an array of entries whose request property is a MobileCredentialRequest. Read its verifierAuthenticationResult and switch on the result discriminator of VerifierAuthenticationResult:

usePresentations.ts
const result = await createProximityPresentationSession({
  onRequestReceived: (data) => {
    if ("error" in data) {
      // Handle error
      return;
    }

    data.request.forEach(({ request }) => {
      const auth = request.verifierAuthenticationResult;
      switch (auth.result) {
        case "trusted":
          // The request was signed by a trusted verifier root certificate.
          console.log(`Trusted verifier: ${auth.verifierInfo.commonName}`);
          break;
        case "untrusted":
          // A reader signature was present but could not be trusted.
          console.warn(`Untrusted verifier: ${auth.error.message}`);
          break;
        case "unsigned":
          // No reader authentication was provided.
          console.log(`Unsigned request from: ${auth.origin ?? "unknown origin"}`);
          break;
      }
    });
  },
});

if (result.isErr()) {
  console.error("Failed to create session:", result.error);
}

See VerifierInfo and VerifierAuthenticationError for the associated values.

What the outcomes mean

The SDK reports the verifier authentication result but does not act on it. What each outcome tells your application:

  • Trusted: The request was signed and the signature chained to a trusted verifier root certificate. The VerifierInfo identifies which root was matched, including its common name.
  • Untrusted: A reader signature was present but could not be validated. The result includes the reason and the certificate chain that was presented.
  • Unsigned: The request carried no reader authentication, so the verifier's identity was not established by the request.

How an application responds to each outcome is a product and trust-framework decision that the SDK does not make for you. Different implementations make different choices depending on their ecosystem and risk tolerance. For example, an application might:

  • refuse to respond to unsigned or untrusted requests;
  • accept any request and record the outcome for audit;
  • present the outcome to the user and let them decide whether to continue;
  • proceed automatically for trusted requests while requiring explicit user confirmation for unsigned or untrusted ones.

Whichever approach you take, the outcome is available before you send a response, so you can carry it into your flow and respond only for the credentials involved, as shown in the Proximity Presentation tutorial.

An unsigned or untrusted request is not inherently malicious

Many legitimate verifiers do not sign their requests, and validation can fail for benign reasons such as a recently rotated certificate that is not yet trusted on the device. The result describes what could be established about the verifier; what that means for a given interaction depends on your ecosystem.

Testing verifier authentication

To exercise each outcome:

  1. Claim a credential in your holder application.
  2. Present to a verifier that signs its request with a reader certificate that chains to a root trusted on the device, and confirm the result is reported as trusted with the expected common name.
  3. Present to a verifier whose reader certificate is not trusted on the device (for example, expired or signed by an unknown root), and confirm the result is reported as untrusted with the expected reason.
  4. Present to a verifier that does not sign its request, and confirm the result is reported as unsigned.

How would you rate this page?

Last updated on

On this page