Przejdź do głównej treści

Wystawianie poświadczeń OpenID4VCI wyjaśnione: od oferty do zaakceptowanego poświadczenia

OpenID4VCI, OpenID for Verifiable Credential Issuance, określa, jak portfel żąda i otrzymuje poświadczenie od wystawcy. Pojedyncze wystawienie przechodzi przez kilka odrębnych kroków, zanim portfel faktycznie uzyska użyteczne poświadczenie, a każdy z nich ma swój własny sposób niepowodzenia. Ta strona krok po kroku omawia cały ten cykl życia, na oryginalnych przykładach praktycznych.

Dwa sposoby na rozpoczęcie: kod wstępnie autoryzowany i kod autoryzacyjny

Wystawienie często zaczyna się od Credential Offer wysłanej przez wystawcę, a wskazany w niej grant decyduje, jak portfel zostanie autoryzowany. Portfel może też sam rozpocząć wystawienie, bez żadnej oferty, korzystając z przepływu kodu autoryzacyjnego. Oba przepływy kończą się w tym samym miejscu: portfel posiada token dostępu, którego może użyć, aby zażądać poświadczenia.

Przepływ z kodem wstępnie autoryzowanym

1. Wystawca

Zna już posiadacza, wydaje Credential Offer z parametrem pre-authorized_code

2. Portfel

Wymienia kod w punkcie końcowym tokenu, opcjonalnie z kodem transakcji

3. Portfel

Żąda poświadczenia z dowodem posiadania swojego klucza

Przepływ z kodem autoryzacyjnym

1. Portfel

Skanuje Credential Offer wskazującą grant authorization_code lub sam rozpoczyna przepływ bez oferty

2. Serwer autoryzacji

Prowadzi posiadacza przez logowanie i wyrażenie zgody, a następnie wydaje kod

3. Portfel

Wymienia kod na token, a następnie żąda poświadczenia

Pełny cykl życia oferty poświadczenia

Specyfikacja nie definiuje nazwanych stanów, ale przepływ zainicjowany przez wystawcę, w którym poświadczenie jest wystawiane od razu, najłatwiej śledzić jako sekwencję poniżej. Każdy krok może zawieść na swój sposób, a implementacja portfela musi obsłużyć te ścieżki, a nie tylko tę pomyślną.

Oferta utworzona
Kod QR lub link otwiera portfel
Oferta odebrana
grant wymieniony
Token uzyskany
nonce i dowód posiadania
Poświadczenie zażądane
poświadczenie wygenerowane
Poświadczenie wystawione
portfel waliduje i zapisuje
Poświadczenie zaakceptowane
Gdzie może dojść do błędu, w kolejności wykonywania kroków:
Kod wygasł przed wykorzystaniem
Żądanie tokenu odrzucone
Żądanie poświadczenia odrzucone
Poświadczenie niezapisane

Przykład praktyczny: świadectwo sprawności technicznej dla floty transportowej

Stacja kontroli pojazdów wystawia świadectwo sprawności technicznej do portfela biznesowego firmy transportowej po rutynowej kontroli. Inspektor uwierzytelnił już kierownika floty w punkcie kontroli, więc wystawca korzysta z przepływu kodu wstępnie autoryzowanego. Poniższe kroki śledzą to pojedyncze wystawienie, od oferty do zaakceptowanego poświadczenia.

Przykład przedstawia podstawową wersję OpenID4VCI. Wdrożenia o wysokim poziomie zaufania, takie jak EUDI Wallet, stosują na jej bazie profil HAIP, który dodaje tokeny dostępu powiązane z DPoP, atestację portfela w punkcie końcowym tokenu oraz atestację kluczy dla kluczy poświadczeń. Pominięto je tutaj, aby każdy krok pozostał czytelny.

1. Oferta poświadczenia

Terminal stacji kontroli pojazdów wyświetla kod QR. Zawiera on adres URI zaczynający się od openid-credential-offer://, który przenosi poniższą ofertę zakodowaną w formacie URL w parametrze credential_offer, albo credential_offer_uri, z którego portfel ją pobiera. Portfel skanuje kod i odczytuje, jakie poświadczenie jest oferowane oraz jak je odebrać.

Wystawca

Wyświetla kod QR z Credential Offer

Wskazuje konfigurację poświadczenia oraz grant pre-authorized_code

{
  "credential_issuer": "https://issuer.fleetinspect.example",
  "credential_configuration_ids": ["roadworthiness_certificate"],
  "grants": {
    "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
      "pre-authorized_code": "fi-8f2c0b3a",
      "tx_code": {
        "length": 6,
        "input_mode": "numeric",
        "description": "Enter the code sent to your phone by text message"
      }
    }
  }
}

2. Odkrycie metadanych wystawcy

Zanim o cokolwiek poprosi, portfel pobiera metadane wystawcy, aby dowiedzieć się, co zawiera roadworthiness_certificate oraz jakie punkty końcowe wywoływać. Metadane nie wymieniają osobnych serwerów autoryzacji, więc wystawca jest jednocześnie swoim własnym serwerem autoryzacji, a portfel odczytuje punkt końcowy tokenu z metadanych tego serwera.

Portfel

GET /.well-known/openid-credential-issuer

Poznaje format poświadczenia, atrybuty i akceptowane typy dowodów, a także punkty końcowe nonce, poświadczeń, odroczeń i powiadomień

Metadane wystawcy poświadczeń (fragment)

{
  "credential_issuer": "https://issuer.fleetinspect.example",
  "nonce_endpoint": "https://issuer.fleetinspect.example/nonce",
  "credential_endpoint": "https://issuer.fleetinspect.example/credential",
  "deferred_credential_endpoint": "https://issuer.fleetinspect.example/deferred",
  "notification_endpoint": "https://issuer.fleetinspect.example/notify",
  "credential_configurations_supported": {
    "roadworthiness_certificate": {
      "format": "dc+sd-jwt",
      "vct": "https://fleetinspect.example/vct/roadworthiness",
      "cryptographic_binding_methods_supported": ["jwk"],
      "credential_signing_alg_values_supported": ["ES256"],
      "proof_types_supported": {
        "jwt": { "proof_signing_alg_values_supported": ["ES256"] }
      },
      "credential_metadata": {
        "claims": [
          { "path": ["vehicle_registration"] },
          { "path": ["inspection_result"] },
          { "path": ["valid_until"] }
        ]
      }
    }
  }
}

Metadane serwera autoryzacji (fragment), z /.well-known/oauth-authorization-server

{
  "issuer": "https://issuer.fleetinspect.example",
  "token_endpoint": "https://issuer.fleetinspect.example/token",
  "pre-authorized_grant_anonymous_access_supported": true
}

3. Żądanie tokenu

Portfel wymienia kod wstępnie autoryzowany w punkcie końcowym tokenu wraz z kodem transakcji, który stacja kontroli wysłała na telefon kierownika floty. Wysłanie tego kodu drugim kanałem oznacza, że osoba, która sfotografuje kod QR zza ramienia, nadal nie może go wykorzystać.

Portfel

POST /token

Wysyła pre-authorized_code i tx_code, otrzymuje token dostępu ograniczony do tej oferty

POST /token HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code
&pre-authorized_code=fi-8f2c0b3a
&tx_code=482913

Odpowiedź

{
  "access_token": "fi-at-3d91e0",
  "token_type": "Bearer",
  "expires_in": 86400
}

4. Dowód posiadania

Ponieważ metadane wymieniają nonce_endpoint, portfel najpierw pobiera stamtąd świeży c_nonce. Następnie udowadnia, że posiada klucz prywatny, z którym poświadczenie zostanie powiązane, podpisując proof JWT nad identyfikatorem wystawcy i tym c_nonce.

Portfel

POST /nonce, a następnie podpisuje proof JWT kluczem, z którym poświadczenie zostanie powiązane

Wiąże poświadczenie z tym kluczem, a nie jedynie z tym, kto posiada token dostępu

POST /nonce HTTP/1.1
Host: issuer.fleetinspect.example

Odpowiedź

{
  "c_nonce": "fi-nonce-77aa"
}

Portfel buduje teraz proof JWT z dwóch obiektów JSON, nagłówka i ładunku, i podpisuje je kluczem prywatnym, z którym poświadczenie zostanie powiązane.

Nagłówek: czym jest ten JWT i jaki klucz go podpisał

{
  "typ": "openid4vci-proof+jwt",
  "alg": "ES256",
  "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
  • typ: oznacza to jako dowód klucza OpenID4VCI, więc nie można go pomylić z żadnym innym rodzajem JWT
  • alg: algorytm podpisu, jeden z tych, które wystawca wymienił w proof_signing_alg_values_supported
  • jwk: klucz publiczny, z którym poświadczenie zostanie powiązane; wystawca sprawdza podpis względem niego

Ładunek: dla kogo jest dowód i kiedy powstał

{
  "aud": "https://issuer.fleetinspect.example",
  "iat": 1789376400,
  "nonce": "fi-nonce-77aa"
}
  • aud: identyfikator wystawcy, dzięki czemu dowodu nie można odtworzyć u innego wystawcy
  • iat: czas utworzenia dowodu, w sekundach od 1970 roku
  • nonce: c_nonce z punktu końcowego nonce, który pokazuje, że dowód jest świeży

Wynik podpisany

Nagłówek i ładunek są każdy kodowane w base64url i łączone kropką. Portfel podpisuje ten ciąg swoim kluczem prywatnym i dołącza zakodowany w base64url podpis po drugiej kropce. Powstały ciąg to proof JWT, który portfel wysyła w żądaniu poświadczenia w kroku 5.

base64url(header) . base64url(payload) . base64url(signature)

eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIs...
  .eyJhdWQiOiJodHRwczovL2lzc3Vlci5mbGVldGluc3BlY3QuZXhhbXBsZSIs...
  .<ES256 signature>

5. Żądanie poświadczenia

Portfel wywołuje punkt końcowy poświadczeń z tokenem dostępu i dowodem, a wystawca generuje i zwraca podpisane poświadczenie.

Portfel

POST /credential

Wysyła token dostępu, identyfikator konfiguracji i proof JWT, otrzymuje podpisane poświadczenie oraz notification_id

POST /credential HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/json
Authorization: Bearer fi-at-3d91e0

{
  "credential_configuration_id": "roadworthiness_certificate",
  "proofs": {
    "jwt": ["<proof JWT from step 4>"]
  }
}

Odpowiedź

{
  "credentials": [
    { "credential": "<issuer-signed SD-JWT VC>" }
  ],
  "notification_id": "fi-notif-9012"
}

Cały przepływ w skrócie

Ten diagram sekwencji łączy pięć kroków przykładu praktycznego w całość, od oferty po powiadomienie. Strzałki ciągłe to żądania, przerywane to odpowiedzi, a kropkowana strzałka to kod transakcji podróżujący poza protokołem w wiadomości SMS.

Portfel

Portfel biznesowy firmy transportowej

Serwer autoryzacji

W tym przykładzie prowadzony przez samego wystawcę

Wystawca poświadczeń

Stacja kontroli pojazdów

  1. Wystawca poświadczeń do Portfel: Credential Offer, przedstawiona jako kod QR
  2. Wystawca poświadczeń do Portfel: tx_code, wysłany SMS-em na telefon kierownika floty
  3. Portfel do Wystawca poświadczeń: GET /.well-known/openid-credential-issuer
  4. Wystawca poświadczeń do Portfel: metadane wystawcy poświadczeń
  5. Portfel do Serwer autoryzacji: GET /.well-known/oauth-authorization-server
  6. Serwer autoryzacji do Portfel: metadane serwera autoryzacji
  7. Portfel do Serwer autoryzacji: POST /token: pre-authorized_code, tx_code
  8. Serwer autoryzacji do Portfel: access_token
  9. Portfel do Wystawca poświadczeń: POST /nonce
  10. Wystawca poświadczeń do Portfel: c_nonce
  11. Portfel: podpisuje proof JWT
  12. Portfel do Wystawca poświadczeń: POST /credential: token dostępu, dowody
  13. Wystawca poświadczeń do Portfel: credentials, notification_id
  14. Portfel: waliduje i zapisuje
  15. Portfel do Wystawca poświadczeń: POST /notify: credential_accepted
  16. Wystawca poświadczeń do Portfel: 204 No Content

Gdy poświadczenie nie jest jeszcze gotowe: wystawienie odroczone

Powyższy przykład zakłada, że wynik kontroli jest już ostateczny. Jeśli stacja kontroli musi zamiast tego przekazać wynik graniczny starszemu inspektorowi, punkt końcowy poświadczeń nie może od razu zwrócić poświadczenia, więc odracza wystawienie.

Natychmiastowe wystawienie

Punkt końcowy poświadczeń zwraca podpisane poświadczenie w tej samej odpowiedzi co żądanie.

Odroczone wystawienie

Punkt końcowy poświadczeń odpowiada kodem HTTP 202, wartością transaction_id oraz interwałem zamiast poświadczenia. Portfel odpytuje deferred_credential_endpoint tym identyfikatorem, czekając co najmniej tyle sekund między żądaniami, ile wskazuje interwał, aż poświadczenie będzie gotowe.

transaction_idodpytywanie deferred_credential_endpoint

Odroczona odpowiedź z /credential

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "transaction_id": "fi-tx-55c2",
  "interval": 900
}

Odpytywanie /deferred, aż będzie gotowe

POST /deferred HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/json
Authorization: Bearer fi-at-3d91e0

{ "transaction_id": "fi-tx-55c2" }

// While the review is open: 202 with the same transaction_id and interval.
// Once approved: 200 with the credentials, optionally with a notification_id.
// If the review rejects the result: a credential_request_denied error.

Zamykanie pętli: punkt końcowy powiadomień

Po wystawieniu portfel może poinformować wystawcę, co stało się z poświadczeniem, używając notification_id z odpowiedzi poświadczenia. Portfele nie są zobowiązane do wysyłania tych powiadomień, a dostarczenie nie jest gwarantowane, więc wystawca nie może interpretować braku powiadomienia w żaden sposób.

Portfel

Wysyła zdarzenie do notification_endpoint wystawcy dla otrzymanego notification_id, które obejmuje każde poświadczenie z tej odpowiedzi

credential_accepted

Zapisane w portfelu

credential_failure

Wystawienie nie powiodło się z innego powodu, na przykład poświadczenie nie przeszło walidacji

credential_deleted

Wystawienie nie powiodło się z powodu posiadacza, na przykład odmówił on zapisania poświadczenia

POST /notify HTTP/1.1
Host: issuer.fleetinspect.example
Content-Type: application/json
Authorization: Bearer fi-at-3d91e0

{
  "notification_id": "fi-notif-9012",
  "event": "credential_accepted"
}

Powiązane pojęcia

Najczęściej zadawane pytania

Skąd portfel wie, jakiego typu grantu użyć?

Credential Offer wskazuje grant w swoim obiekcie grants. authorization_code występuje, gdy wystawca chce, aby posiadacz zalogował się w ramach przepływu. pre-authorized_code występuje, gdy posiadacz był już uwierzytelniony na kanale, na którym utworzono ofertę, na przykład przez inspektora w punkcie kontroli w poniższym przykładzie praktycznym. Oferta może wymieniać oba, a portfel wybiera wtedy jeden z nich. Jeśli oferta w ogóle nie ma obiektu grants, portfel sprawdza w metadanych, jakie typy grantów obsługuje serwer autoryzacji.

Dlaczego portfel pobiera metadane wystawcy, zanim o cokolwiek poprosi?

Credential Offer wskazuje jedynie credential_configuration_ids, adres URL wystawcy oraz grants. To metadane wystawcy, udostępniane pod dobrze znaną ścieżką, opisują każdą konfigurację: jej format, atrybuty i akceptowane typy dowodów, a także punkty końcowe nonce, poświadczeń, odroczeń i powiadomień. Metadane wskazują też, z jakiego serwera autoryzacji korzystać, a metadane tego serwera podają punkt końcowy tokenu. Bez obu tych źródeł portfel nie wiedziałby, jak zbudować poprawne żądania ani co pokazać posiadaczowi przed wyrażeniem zgody.

Co właściwie udowadnia dowód posiadania?

Udowadnia, że portfel żądający poświadczenia posiada klucz prywatny, z którym poświadczenie zostanie powiązane, a nie jedynie to, że ma ważny token dostępu. Portfel podpisuje proof JWT nad identyfikatorem wystawcy i świeżym c_nonce z punktu końcowego nonce wystawcy, używając tego klucza. Wystawca osadza odpowiadający mu klucz publiczny w poświadczeniu. Weryfikator wymagający powiązania z kluczem prosi posiadacza o ponowne podpisanie tym samym kluczem podczas prezentacji, więc skopiowane poświadczenie bez klucza nie przejdzie tej kontroli.

Dlaczego wystawca miałby odroczyć wystawienie zamiast od razu zwrócić poświadczenie?

Niektóre kontrole, które wystawca wykonuje przed wygenerowaniem poświadczenia, nie mogą zakończyć się w ramach jednego żądania HTTP, na przykład ręczna weryfikacja lub zapytanie do wolnego, zewnętrznego rejestru. Odroczone wystawienie pozwala punktowi końcowemu poświadczeń odpowiedzieć od razu wartością transaction_id zamiast blokować połączenie, a portfel odpytuje punkt końcowy odroczeń tym identyfikatorem, aż kontrola się zakończy i poświadczenie będzie gotowe do odebrania.

Czy punkt końcowy powiadomień jest obowiązkowy do zaimplementowania przez wystawcę?

Nie. Jest opcjonalny dla wystawców, a portfele również nie muszą go używać. Gdy obie strony go obsługują, wystawca dowiaduje się, co stało się po wystawieniu: poświadczenia zostały zapisane (credential_accepted), posiadacz przerwał wystawienie, na przykład odmawiając zapisania poświadczeń (credential_deleted), albo wystąpił inny błąd (credential_failure). Dostarczenie nie jest gwarantowane, więc wystawca powinien traktować powiadomienie jako przydatną informację, nigdy jako wiarygodny zapis, i nie może wyciągać żadnych wniosków z jego braku.

Źródła

  1. OpenID for Verifiable Credential Issuance 1.0
  2. OpenID4VC High Assurance Interoperability Profile 1.0
  3. EUDI Wallet Architecture and Reference Framework

Ta strona ma charakter informacyjny i nie stanowi porady prawnej. Aby uzyskać wiążące wskazówki, skontaktuj się bezpośrednio z OpenID Foundation oraz Komisją Europejską.

Porozmawiaj z nami o integracji EUDI Wallet