Integracja aplikacji portfela z Menedżerem danych logowania

Interfejs Credential Manager Holder API umożliwia aplikacji na urządzeniu z Androidem (nazywanej też „portfelem”) zarządzanie cyfrowymi dokumentami tożsamości i przedstawianie ich weryfikatorom.

Ilustracja przedstawiająca interfejs cyfrowych dokumentów tożsamości w Menedżerze dokumentów tożsamości
Rysunek 1. Interfejs selektora cyfrowych dokumentów tożsamości.

Podstawowe pojęcia

Zanim zaczniesz korzystać z interfejsu Holder API, zapoznaj się z tymi pojęciami:

Formaty danych logowania

Dane logowania mogą być przechowywane w aplikacjach portfela w różnych formatach. Te formaty to specyfikacje określające, jak powinny być reprezentowane dane logowania. Każdy z nich zawiera te informacje o danych logowania:

  • Typ: kategoria, np. dyplom ukończenia studiów lub mobilne prawo jazdy.
  • Właściwości: atrybuty takie jak imię i nazwisko.
  • Kodowanie: sposób strukturyzacji danych logowania, np. SD-JWT lub mdoc.
  • Ważność: metoda kryptograficznej weryfikacji autentyczności dokumentu.

Każdy format danych logowania nieco inaczej koduje i weryfikuje dane, ale funkcjonalnie są one takie same.

Rejestr obsługuje 2 formaty:

Weryfikator może wysłać żądanie OpenID4VP dotyczące SD-JWT i mdocs, korzystając z Menedżera danych logowania. Wybór zależy od przypadku użycia i branży.

Rejestracja metadanych danych logowania

Menedżer danych logowania nie przechowuje bezpośrednio danych logowania posiadacza, ale metadane danych logowania. Aplikacja przechowująca musi najpierw zarejestrować metadane danych logowania w Menedżerze danych logowania za pomocą RegistryManager. Ten proces rejestracji tworzy rekord rejestru, który służy do 2 głównych celów:

  • Dopasowywanie: zarejestrowane metadane poświadczeń są używane do dopasowywania przyszłych żądań weryfikatora.
  • Wyświetlanie: w interfejsie selektora danych logowania użytkownikowi wyświetlane są dostosowane elementy interfejsu.

Do rejestrowania cyfrowych dokumentów tożsamości będziesz używać klasy OpenId4VpRegistry, ponieważ obsługuje ona formaty mdoc i SD-JWT. Weryfikatorzy będą wysyłać żądania OpenID4VP, aby poprosić o te dane logowania.

Rejestrowanie danych logowania aplikacji

Aby korzystać z interfejsu Credential Manager Holder API, dodaj te zależności do skryptu kompilacji modułu aplikacji:

Groovy

dependencies {
    // Use to implement credentials registrys

    implementation "androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha04"
    implementation "androidx.credentials.registry:registry-digitalcredentials-preview:1.0.0-alpha04"
    implementation "androidx.credentials.registry:registry-provider:1.0.0-alpha04"
    implementation "androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha04"

}

Kotlin

dependencies {
    // Use to implement credentials registrys

    implementation("androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha04")
    implementation("androidx.credentials.registry:registry-digitalcredentials-preview:1.0.0-alpha04")
    implementation("androidx.credentials.registry:registry-provider:1.0.0-alpha04")
    implementation("androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha04")

}

Tworzenie obiektu RegistryManager

Utwórz instancję RegistryManager i zarejestruj w niej żądanie OpenId4VpRegistry.

// Create the registry manager
val registryManager = RegistryManager.create(context)

// The guide covers how to build this out later
val registryRequest = OpenId4VpRegistry(credentialEntries, id)

try {
    registryManager.registerCredentials(registryRequest)
} catch (e: Exception) {
    // Handle exceptions
}

Tworzenie żądania OpenId4VpRegistry

Jak wspomnieliśmy wcześniej, musisz zarejestrować OpenId4VpRegistry, aby obsługiwać żądanie OpenID4VP od weryfikatora. Zakładamy, że masz już wczytane do portfela dane lokalne (np. sdJwtsFromStorage). Teraz przekonwertuj je na odpowiedniki w Jetpacku DigitalCredentialEntry na podstawie ich formatu – SdJwtEntry lub MdocEntry w przypadku SD-JWT lub mdoc.

Dodawanie tokenów Sd-JWT do rejestru

Mapuj każdy lokalny dokument SD-JWT na SdJwtEntry w rejestrze:

fun mapToSdJwtEntries(sdJwtsFromStorage: List<StoredSdJwtEntry>): List<SdJwtEntry> {
    val list = mutableListOf<SdJwtEntry>()

    for (sdJwt in sdJwtsFromStorage) {
        list.add(
            SdJwtEntry(
                verifiableCredentialType = sdJwt.getVCT(),
                claims = sdJwt.getClaimsList(),
                entryDisplayPropertySet = sdJwt.toDisplayProperties(),
                id = sdJwt.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

Dodawanie dokumentów mobilnych do rejestru

Zmapuj lokalne dane logowania mdoc na typ Jetpack MdocEntry:

fun mapToMdocEntries(mdocsFromStorage: List<StoredMdocEntry>): List<MdocEntry> {
    val list = mutableListOf<MdocEntry>()

    for (mdoc in mdocsFromStorage) {
        list.add(
            MdocEntry(
                docType = mdoc.retrieveDocType(),
                fields = mdoc.getFields(),
                entryDisplayPropertySet = mdoc.toDisplayProperties(),
                id = mdoc.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

Najważniejsze informacje o kodzie

  • Jedną z metod konfigurowania pola id jest zarejestrowanie zaszyfrowanego identyfikatora danych logowania, dzięki czemu tylko Ty będziesz mieć możliwość odszyfrowania wartości.
  • Pola interfejsu obu formatów powinny być zlokalizowane.

Rejestrowanie danych logowania

Połącz przekonwertowane wpisy i zarejestruj prośbę w RegistryManager:

val credentialEntries = mapToSdJwtEntries(sdJwtsFromStorage) + mapToMdocEntries(mdocsFromStorage)

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    id = "my-wallet-openid-registry-v1" // A stable, unique ID to identify your registry record.
)

Teraz możemy zarejestrować Twoje dane logowania w Credential Managerze.

try {
    val response = registryManager.registerCredentials(openidRegistryRequest)
} catch (e: Exception) {
    // Handle failure
}

Twoje dane logowania zostały zarejestrowane w Menedżerze danych logowania.

Zarządzanie metadanymi aplikacji

Metadane rejestrowane przez aplikację portfelową w CredentialManager mają te właściwości:

  • Trwałość: informacje są zapisywane lokalnie i przechowywane po ponownym uruchomieniu urządzenia.
  • Izolowane miejsce na dane: rekordy rejestru każdej aplikacji są przechowywane oddzielnie, co oznacza, że jedna aplikacja nie może zmieniać rekordów rejestru innej aplikacji.
  • Aktualizacje z kluczem: rekordy rejestru każdej aplikacji są kluczowane przez id, co umożliwia ponowne identyfikowanie, aktualizowanie lub usuwanie rekordów.
  • Aktualizowanie metadanych: warto aktualizować zapisane metadane za każdym razem, gdy aplikacja ulega zmianie lub jest ładowana po raz pierwszy. Jeśli rejestr jest wywoływany wiele razy w ramach tego samego id, ostatnie wywołanie zastępuje wszystkie poprzednie rekordy. Aby zaktualizować informacje, ponownie zarejestruj urządzenie bez konieczności usuwania starego rekordu.

Opcjonalnie: utwórz dopasowanie

Dopasowywacz to binarny plik Wasm, który Menedżer danych logowania uruchamia w piaskownicy, aby filtrować zarejestrowane dane logowania pod kątem przychodzącego żądania weryfikatora.

  • Domyślny dopasowywacz: klasa OpenId4VpRegistry automatycznie zawiera domyślny dopasowywacz OpenId4VP (OpenId4VpDefaults.DEFAULT_MATCHER) podczas tworzenia jej instancji. W przypadku wszystkich standardowych zastosowań OpenID4VP biblioteka zajmuje się dopasowywaniem.
  • Niestandardowy moduł dopasowujący: niestandardowy moduł dopasowujący należy wdrożyć tylko wtedy, gdy obsługujesz niestandardowy protokół, który wymaga własnej logiki dopasowywania.

Obsługa wybranego zestawu danych uwierzytelniających

Gdy użytkownik wybierze dane logowania, aplikacja portfel musi obsłużyć żądanie. Musisz zdefiniować działanie, które nasłuchuje filtra intencji androidx.credentials.registry.provider.action.GET_CREDENTIAL. Nasz przykładowy portfel pokazuje tę procedurę.

Intencja uruchamia aktywność z żądaniem weryfikacji i pochodzeniem wywołania, które wyodrębnisz za pomocą funkcji PendingIntentHandler.retrieveProviderGetCredentialRequest. Zwraca to ProviderGetCredentialRequest zawierający wszystkie informacje związane z żądaniem weryfikatora. Są 3 kluczowe komponenty:

  • Aplikacja do obsługi połączeń: aplikacja, która wysłała żądanie. Można ją pobrać za pomocą metody getCallingAppInfo.
  • Wybrane dane uwierzytelniające: informacje o tym, którego kandydata wybrał użytkownik, pobrane za pomocą selectedCredentialSet extension method. Będą one zgodne z zarejestrowanym przez Ciebie identyfikatorem danych uwierzytelniających.
  • Konkretne prośby: konkretna prośba weryfikatora pobrana z metody getCredentialOptions. W przypadku żądania danych cyfrowych na tej liście znajdziesz tylko 1 GetDigitalCredentialOption.

Najczęściej weryfikator wysyła prośbę o przesłanie cyfrowych danych uwierzytelniających, którą możesz przetworzyć za pomocą tego przykładowego kodu:

request.credentialOptions.forEach { option ->
    if (option is GetDigitalCredentialOption) {
        Log.i(TAG, "Got DC request: ${option.requestJson}")
        processRequest(option.requestJson)
    }
}

Przykład znajdziesz w przykładowym portfelu.

Sprawdź tożsamość weryfikatora

  1. Wyodrębnij ProviderGetCredentialRequest z intencji:
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
  1. Sprawdź, czy punkt początkowy ma uprawnienia: aplikacje z uprawnieniami (np. przeglądarki internetowe) mogą wykonywać połączenia w imieniu innych weryfikatorów, ustawiając parametr punktu początkowego. Aby pobrać to źródło, musisz przekazać listę uprzywilejowanych i zaufanych wywołujących (listę dozwolonych w formacie JSON) do interfejsu API CallingAppInfo getOrigin().
val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

Jeśli pochodzenie nie jest puste: pochodzenie jest zwracane, jeśli packageName i odciski palców certyfikatu uzyskane z signingInfo są zgodne z odciskami palców aplikacji znajdującej się na liście dozwolonych przekazanej do interfejsu getOrigin() API. Po uzyskaniu wartości pochodzenia aplikacja dostawcy powinna uznać to za wywołanie uprzywilejowane i ustawić to pochodzenie w odpowiedzi OpenID4VP zamiast obliczać pochodzenie na podstawie podpisu aplikacji wywołującej.

Menedżer haseł Google używa publicznie dostępnej listy dozwolonych w przypadku wywołań getOrigin(). Jako dostawca danych logowania możesz użyć tej listy lub podać własną w formacie JSON opisanym przez interfejs API. To dostawca wybiera, która lista będzie używana. Aby uzyskać dostęp uprzywilejowany za pomocą dostawców danych logowania innych firm, zapoznaj się z dokumentacją dostarczoną przez tę firmę.

Jeśli pole origin jest puste, żądanie weryfikatora pochodzi z aplikacji na Androida. Pochodzenie aplikacji, które ma zostać umieszczone w odpowiedzi OpenID4VP, powinno być obliczane jako android:apk-key-hash:<encoded SHA 256 fingerprint>.

val appSigningInfo = request?.callingAppInfo?.signingInfoCompat?.signingCertificateHistory[0]?.toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val certHash = Base64.encodeToString(md.digest(appSigningInfo), Base64.NO_WRAP or Base64.NO_PADDING)
return "android:apk-key-hash:$certHash"

Renderowanie interfejsu użytkownika posiadacza

Gdy dane logowania zostaną wybrane, zostanie wywołana aplikacja portfela, która przeprowadzi użytkownika przez interfejs aplikacji. Ten proces można obsługiwać na 2 sposoby:

  • Jeśli do zwolnienia danych uwierzytelniających wymagane jest dodatkowe uwierzytelnianie użytkownika, użyj interfejsu BiometricPrompt API. Pokazano to w przykładzie.
  • W przeciwnym razie wiele portfeli decyduje się na cichy powrót, renderując pustą aktywność, która natychmiast przekazuje dane z powrotem do aplikacji wywołującej. Minimalizuje to liczbę kliknięć użytkownika i zapewnia płynniejsze działanie.

Zwracanie odpowiedzi dotyczącej danych logowania

Gdy aplikacja portfela będzie gotowa do odesłania wyniku, zakończ działanie, przesyłając odpowiedź dotyczącą dokumentu:

PendingIntentHandler.setGetCredentialResponse(
    resultData,
    GetCredentialResponse(DigitalCredential(response.responseJson))
)
setResult(RESULT_OK, resultData)
finish()

Jeśli istnieje wyjątek, możesz w podobny sposób wysłać wyjątek dotyczący danych logowania:

PendingIntentHandler.setGetCredentialException(
    resultData,
    GetCredentialUnknownException() // Configure the proper exception
)
setResult(RESULT_OK, resultData)
finish()

Pełny przykład zwracania odpowiedzi dotyczącej danych logowania w kontekście znajdziesz w przykładowej aplikacji.