Attestation Providers

The attestation providers are the issuers of credentials like PID, MDL or other documents the users may posses. Also there is not only one attestation provider but multiple which could be a university or a government authority.

Here we will show how you could fetch a credential via a Credential Offer flow from an attestation provider. As trigger we assume the user did scan a QR code, whose content contains an URL that is triggering the application via the defined URL schemes from the setup process.

The SDK will provide the Attestations object for handling interactions with credentials and attestation providers.

Attestations

Also the SDK provides some identifier for known attestation types e.g. PID that can be used to filter for credentials.

EUDI_PID_IDENTIFIER
EUDI_WIA_IDENTIFIER
EUDI_WUA_IDENTIFIER

New ones will be added here in time e.g. for MDL.

Credential offer flow

The openid-credential-offer scheme will be received by the application via a deeplink. So the deeplink handling has to be implemented by the application. If the openid-credential-offer scheme is received the application has to forward this URL to the wallet SDK, which will parse all needed information from the string and check its validity and processes it further to contact the attestation provider (and its authentication server as needed).

In this flow the CredentialOfferInfo object will be returned containing the information which credential(s) will be downloaded. This can be accepted by the user and then the authentication step is started (jump to external browser) to authenticate the user. After the authentication a redirect (eu.europa.ec.euidi) for re-opening the application will happen, and the authorized download will fetch the credential and store it on the device.

Notice: the PAR (pre authorized request) flow does not need the authorization step via the external browser.

SDK interface

// The provided Flow will send different events that have to be continued to get to the next step of the offer flow (AsyncSequence).
// - CredentialOfferInfo object returned to see what will be downloaded
// - Authenticate at the attestation provider (browser) to allow access to the credential
// - After authentication redirect start the actual download of the credential
let deepLink = "openid-credential-offer://" // from the received deep link
// The function will provide `CredentialOfferEvent` objects
WalletSdk.shared.attestations.retrieveFromCredentialOffer(credentialOfferIntentData: deepLinks)

// returned deep link from authentication to procceed with the download
WalletSdk.shared.attestations.continueWithAuthCodeUrl(authCodeUrl: _url.relativeString)

App implementation example (Credential Offer flow)

public func fetchCredentialOffer(with _urlData: String) async throws(CredentialOfferError) -> ([CredentialOfferInfo], URL?) {
    var array: [CredentialOfferInfo] = []
    for await value in WalletSdk.shared.attestations.retrieveFromCredentialOffer(credentialOfferIntentData: _urlData) {
        if let error = CredentialOfferError.from(event: value) {
            throw error // End of flow (AsyncSequence)
        }
        switch onEnum(of: value) {
        case .infoEvent(let _event):
            // received CredentialOfferInfo containing information about the credentials too download
            array = _event.credentialOfferInfo
            _event.result.complete(value: KotlinUnit()) // Trigger to process for auth-url for download
            continue
        case .openUrlForAuthEvent(let _event):
            // Auth-URL for downloading, used with `continueWithAuthCodeUrl(authCodeUrl)` SDK functionality
            // to start the authentication process (will include a user activity to start the process of downloading)
            let authURL = _event.authorizationUrl
            return (array, URL(string: authURL)) // end of flow (AsyncSequence)
        default:
            // Error, end of flow (AsyncSequence)
            throw CredentialOfferError.unknown
        }
    }
    return (array, nil)
}

public enum CredentialOfferError: Error {
    case parsing
    case infoParsing
    case issuance
    case notImplemented
    case authenticationURLMissing
    case unknown
    
    /// Create from SDK Credential Offer Event an error if one occurred otherwise nil if successful
    /// - Parameter _error: the event to check if an error occurred
    /// - Returns: nil if successful, otherwise an error is returned
    public static func from(event _event: wallet_sdk.CredentialOfferEvent) -> CredentialOfferError? {
        switch onEnum(of: _event) {
        case .infoEvent:
            // successful
            return nil
        case .openUrlForAuthEvent:
            // successful
            return nil
        case .parsingError:
            return CredentialOfferError.parsing
        case .infoParsingError:
            return CredentialOfferError.infoParsing
        case .notImplementedError:
            return CredentialOfferError.notImplemented
        case .issuanceError:
            return CredentialOfferError.issuance
        }
    }
}

App implementation example (Download credential)

public func fetchCredential(withAuthCodeURL _url: URL) async throws(CredentialDownloadError) {
    for await value in WalletSdk.shared.attestations.continueWithAuthCodeUrl(authCodeUrl: _url.relativeString) {
        if let error = CredentialDownloadError.from(event: value) {
            throw error
        }
        switch onEnum(of: value) {
        case .success:
            // Success, sdk will trigger an update for the new/updated credential
            return
        default:
            // Error
            throw CredentialDownloadError.unknown
        }
    }
}

public enum CredentialDownloadError: Error {
    case unknown
    case parsing
    case saving
    case invalidAppData
    
    /// Create from SDK Credential Download Event an error if one occurred otherwise nil if successful
    /// - Parameter _error: the download event to check if an error occurred
    /// - Returns: nil if successful, otherwise an error is returned
    public static func from(event _event: wallet_sdk.CredentialDownloadEvent) -> CredentialDownloadError? {
        switch onEnum(of: _event) {
        case .success:
            // successful download
            return nil
        case .parsingError:
            return CredentialDownloadError.parsing
        case .savingError:
            return CredentialDownloadError.saving
        case .invalidAppData:
            return CredentialDownloadError.invalidAppData
        }
    }
}

The wallet SDK will inform the app about updates in the database for the attestations (credentials) on the device.

Credential List: Observe all credentials

As the application wants to display credentials - the user has downloaded on their device - it can use the provided observable property attestations from the wallet SDK Attestations object. The attestations are an Array of Attestation objects presenting a credential to the wallet. So the wallet app always interacts with the Attestation object when fetching the list of credentials or displaying a detail page for a credential.

Task { @concurrent in
        for await array in WalletSdk.shared.attestations.attestations {
            await MainActor.run {
                // Update ui with the updated credentials
                array ...
            }
        }
    }

The above AsyncSequence is going to be updated, whenever a change in the wallets database is happening to the credentials.

Credential Detail: Fetching a specific credential

To load a specific credential e.g. to display all credential information (claims) to the user, the SDK provides an getAttestation(attestationId) function. This function will also return an Attestation object representing the credential.

let value: Credential? = WalletSdk.shared.attestations.getAttestation(attestationId: "ID of the credential to load")

This is an async function which will provide an AsyncSequence. So you can also use it to observe this specific credential on the detail page so the detail page can be updated, if this credential is modified in the database.

Task { @concurrent [weak _queue] in
    for await value in WalletSdk.shared.attestations.getAttestation(attestationId: "ID of the credential to observe") {
        guard let value = value else {
            // no credential found for specified ID, stop the sequence
            break
        }
        // credential update for specified ID
        // e.g. update some ui
        continue
    }
}

Present a credential’s details

For presenting the credentials details to a user, the wallet SDK provides functions on the Attestation object, which will return claims and also automatically translated label values and differentiable claim types to display claim values differently.

The Attestation object provided by the SDK will present a credential outside of the SDK, providing all the following functions.

let attestation: Attestation = some attestation
let claimDictionary: [String: Claim] = attestation.claims

There are different types of claims which the wallet SDK will return. Claim itself is a protocol.

The Claim protocol can have different implementations e.g.:

To get the translated labels for a credential’s claim values, the SDK provides following functions:

let attestation: Attestation = some attestation
let claim = attestation.claims["some key"]
// 'de' provided by device local from app
let translatedLabel = mapClaim.getDisplayLabel(languageKey: "de",
                                               fallbackLanguageKey: "en")

In the provided example a fallback language will be provided, if the primary one is not existing the fallback language will be used. So the application does not need to build translations for the labels on its own.

Also the wallet SDK does provide the claim’s data in different types, so the app e.g. can differentiate a pure StringClaim value from a ListClaim value.

let attestation: Attestation = some attestation
let claimDictionary: [String: Claim] = attestation.claims
// ...
// StringClaim
let valueToDisplay = stringClaim.value as? String
// ListClaim
let valueToDisplay = listClaim.value.joined(separator: ", ")
// MapClaim
let value:s [String, Claim] = mapClaim.value // to run through nested claims
// ImageClaim
let valueToDisplay: Data = imageClaim.value // image Data convert with UIImage(data:) to get the image

PID Provider

The PID provider is a special attestation provider since it does not have to support every wallet. Normally a PID provider and Wallet Provider will be working together for the wallet that is supported to access credentials on the PID provider.

To receive a PID the above flow can be used and no additional steps or data is needed.