Skip to main content

02 Issuing and Verifying credentials

Issuer Wallet Configuration

In order to issue and/or verify credentials with your wallet, you need to firstly configure the wallet. We'll firstly configure the wallet for issuance of credentials and then we will look at verification. Every wallet can have multiple credential issuers - sets of specific configuration properties tied to one specific credential type. Here are some examples of credential issuers with their description:

{
"trustFramework": "NOOP",
"walletKeyIdentifier": "did",
"credentialIssuers": [
{
"name": "Identity Card",
"id": "id_card",
"authorization": "openid",
"credentialFormat": "sd_jwt_vc",
"credentialIssuer": "CtWalletSame",
"credentialType": "urn:identity_card",
"disclosableClaims": [
"$.family_name",
"$.given_name"
],
"issuerConfiguration": {
"defaultCredentialSubject": {
"family_name": "Doe",
"given_name": "John",
"expiry_date": "31-12-2030"
}
}
}
],
"oidcRevision": {
"oidc4vci": "Draft15",
"oidc4vp": "Draft23"
}
}

This credential issuer issues credential of type urn:identity_card with sd_jwt_vc credential format. The credential subject will always be the same - the one specified in the configuration. As signingKeyIdentifier field is not set, the credential will be signed by the Wallet's default wallet key identifier. Credential claims family_name and given_name can be selectively disclosed.


There are other credential issuer types, with which you can create flows where credential is issued only when the holder firstly presents a different credentials, etc. We will come back to these issuers later. Let's create an issuer wallet!

walletConfig := wallet.WalletConfig{
CredentialIssuers: &[]wallet.CredentialIssuerDefinition{
{
CredentialFormat: wallet.SdJwtVc,
CredentialIssuer: wallet.IssuanceQueue,
CredentialType: "urn:identity_card",
DisclosableClaims: &[]string{
"$.family_name",
"$.given_name",
},
Id: "id_card",
Name: "Identity Card",
},
},
OidcRevision: &wallet.OidcRevision{
Oidc4vci: wallet.Draft15,
Oidc4vp: wallet.Draft23,
},
TrustFramework: wallet.NOOP,
WalletKeyIdentifier: wallet.Did,
}

walletResp, err := client.WalletCreateWithResponse(ctx, wallet.WalletCreatePayload{
Config: walletConfig,
Metadata: nil,
Name: "Issuer Wallet",
})
if err != nil {
log.Panicf("failed to create a wallet: %v", err)
}

if walletResp.StatusCode() != 200 {
log.Panicf("error while creating wallet: %d %s", walletResp.StatusCode(), walletResp.Body)
}

walletId := walletResp.JSON200.Id

Creating and Issuing a credential

Now, we can create a new credential draft. We will issue a credential with the following claims:

{
"given_name": "John",
"family_name": "Doe",
"date_of_birth": "01-01-2000"
}

credSubject := wallet.CredentialSubject{}
_ = credSubject.FromCredentialSubjectItem(wallet.CredentialSubjectItem{
AdditionalProperties: map[string]interface{}{
"given_name": "John",
"family_name": "Doe",
"date_of_birth": "01-01-2000"
},
})

expDate := time.Now().AddDate(1, 0, 0)
credPayload := wallet.CredentialPayload{
CredentialDraftMetadata: wallet.CredentialDraftMetadata{
ExpirationDate: &expDate,
Format: wallet.SdJwtVc,
Name: "Test Credential",
Type: "urn:identity_card",
},
CredentialSubject: credSubject,
}

credCreateResp, err := client.CredentialCreateWithResponse(
ctx, &wallet.CredentialCreateParams{WalletId: walletId}, credPayload)
if err != nil {
log.Panicf("failed to create a wallet: %v", err)
}

if credCreateResp.StatusCode() != 200 {
log.Panicf("error while creating wallet: %d %s", credCreateResp.StatusCode(), credCreateResp.Body)
}

credentialId := credCreateResp.JSON200.Id

This credential now needs to be added to the issuance queue of the credential issuer. When want to issue the credential using pre-authorized OIDC4VCI flow, you need to provide the client_id of the holder for which the credential will be issued. In case of using in-time authorized flow, this is not required. We will firstly use this flow:

issInitResp, err := client.CredentialIssuanceInitWithResponse(ctx, credentialId,
&wallet.CredentialIssuanceInitParams{WalletId: walletId}, wallet.CredentialIssuanceInit{IssuerId: "id_card"})
if err != nil {
log.Panicf("failed to create a wallet: %v", err)
}

if issInitResp.StatusCode() != 200 {
log.Panicf("error while creating wallet: %d %s", credCreateResp.StatusCode(), credCreateResp.Body)
}

issQueueItemId := issInitResp.JSON200.IssuanceQueueItemId

authOfferResp, err := client.IssuerInitiateAuthOfferWithResponse(ctx,
&wallet.IssuerInitiateAuthOfferParams{WalletId: walletId}, wallet.InitAuthOffer{
IssuanceQueueItemId: &issQueueItemId,
IssuerId: "id_card",
})
if err != nil {
log.Panicf("failed to create a wallet: %v", err)
}

if authOfferResp.StatusCode() != 200 {
log.Panicf("error while creating wallet: %d %s", credCreateResp.StatusCode(), credCreateResp.Body)
}

offerUrl := authOfferResp.JSON200.Offer
offerId := authOfferResp.JSON200.OfferId

And now we can provide the offer URL to the holder wallet.

Let's now focus on pre-authorized offers. We will issue a credential to a holder with the following DID: did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbqNA5MAzGAuXbu4F3mpoTQYek4NumTwnw7iaZVZXQ9Eu5tbFAe1aDT4q3CKyYo6cvFRvNYAAzSZqarjkD2uszX2rsDbZVXvDYqCeEijQxtvxSuKXks1U3Nt8Ts1eGqfVkwv.

holderDid := "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbqNA5MAzGAuXbu4F3mpoTQYek4NumTwnw7iaZVZXQ9Eu5tbFAe1aDT4q3CKyYo6cvFRvNYAAzSZqarjkD2uszX2rsDbZVXvDYqCeEijQxtvxSuKXks1U3Nt8Ts1eGqfVkwv"

issInitResp, err := client.CredentialIssuanceInitWithResponse(ctx, credentialId,
&wallet.CredentialIssuanceInitParams{WalletId: walletId}, wallet.CredentialIssuanceInit{
IssuerId: "id_card",
ClientId: &holderDid,
})
if err != nil {
log.Panicf("failed to create a wallet: %v", err)
}

if issInitResp.StatusCode() != 200 {
log.Panicf("error while creating wallet: %d %s", credCreateResp.StatusCode(), credCreateResp.Body)
}

preauthOfferResp, err := client.IssuerInitiatePreauthOfferWithResponse(ctx,
&wallet.IssuerInitiatePreauthOfferParams{WalletId: walletId}, wallet.InitPreAuthOffer{
ClientId: &holderDid,
IssuerId: "id_card",
})
if err != nil {
log.Panicf("failed to create a wallet: %v", err)
}

if preauthOfferResp.StatusCode() != 200 {
log.Panicf("error while creating wallet: %d %s", credCreateResp.StatusCode(), credCreateResp.Body)
}

offerUrl := preauthOfferResp.JSON200.Offer
offerId := preauthOfferResp.JSON200.OfferId

When Holder accepts the credential offer and the credential is issued, a Wallet Notification with type offer.processed is sent. You can check for it in the following way:

notificationResp, err := client.WalletNotificationGetByStateWithResponse(
ctx, walletId, string(wallet.CredentialNotificationEventTypeCredentialIssued), offerId)
if err != nil {
log.Panicf("failed to get notifications: %v", err)
}

if notificationResp.StatusCode() != 200 {
log.Panicf("error while getting notifications: %d %s", credCreateResp.StatusCode(), credCreateResp.Body)
}

Configuring a Verifier Wallet

As is the case with issuers, a wallet can also have multiple verifiers - unique configuration of verification properties. Here are some examples of credential verifiers with their description:

{
"trustFramework": "NOOP",
"walletKeyIdentifier": "did",
"credentialVerifiers": [
{
"presentationDefinition": {
"format": {
"jwt_vc": {
"alg": ["ES256"]
},
"jwt_vp": {
"alg": ["ES256"]
}
},
"id": "id_card_presentation",
"input_descriptors": [
{
"constraints": {
"fields": [
{
"filter": {
"contains": {
"const": "VerifiableId"
},
"type": "array"
},
"path": [
"$.vc.type"
]
}
]
},
"id": "id_card_VerifiableId"
}
]
},
"id": "id_card_jwt_vcdm",
"name": "ID Card"
},
{
"presentationDefinition": {
"format": {
"jwt_vc": {
"alg": ["ES256"]
},
"jwt_vp": {
"alg": ["ES256"]
}
},
"id": "id_card_presentation",
"input_descriptors": [
{
"constraints": {
"fields": [
{
"filter": {
"contains": {
"const": "urn:identity_card"
},
"type": "array"
},
"path": [
"$.vct"
]
}
]
},
"id": "id_card_VerifiableId"
}
]
},
"id": "id_card_sd_jwt",
"name": "ID Card"
}
],
"oidcRevision": {
"oidc4vci": "Draft11",
"oidc4vp": "Draft16"
}
}

Here are two configured Credential Verifiers, that use DIF Presentation Exchange. The first verifier matches W3C VCDM Credential Types with type VerifiableId. The second verifier matches SD-JWT-VC Credentials with type urn:identity_card. For more information about DIF Presentation exchange, its presentation syntax etc. refer here.


Let's create a verifier wallet!

givenNamePath := wallet.DCQLQueryClaim_Path_Item{}
_ = givenNamePath.FromDCQLQueryClaimString("given_name")

familyNamePath := wallet.DCQLQueryClaim_Path_Item{}
_ = familyNamePath.FromDCQLQueryClaimString("family_name")
walletConfig := wallet.WalletConfig{
CredentialVerifiers: &[]wallet.CredentialVerifierDefinition{
{
DcqlQuery: &wallet.DCQLQuery{
Credentials: []wallet.DCQLQueryCredentials{
{
Claims: &[]wallet.DCQLQueryClaim{
{
Id: "given_name",
Path: []wallet.DCQLQueryClaim_Path_Item{givenNamePath},
},
{
Id: "family_name",
Path: []wallet.DCQLQueryClaim_Path_Item{familyNamePath},
},
},
Format: "dc+sd-jwt",
Id: "id_card",
Meta: &wallet.DCQLQuerySdJwtVcMeta{
VctValues: &[]string{"urn:identity_card"},
},
},
},
},
Id: "id_card",
Name: "ID Card Verifier",
},
},
OidcRevision: &wallet.OidcRevision{
Oidc4vci: wallet.Draft15,
Oidc4vp: wallet.Draft23,
},
TrustFramework: wallet.NOOP,
WalletKeyIdentifier: wallet.Did,
}

walletResp, err := client.WalletCreateWithResponse(ctx, wallet.WalletCreatePayload{
Config: walletConfig,
Metadata: nil,
Name: "Issuer Wallet",
})
if err != nil {
log.Panicf("failed to create a wallet: %v", err)
}

if walletResp.StatusCode() != 200 {
log.Panicf("error while creating wallet: %d %s", walletResp.StatusCode(), walletResp.Body)
}

walletId := walletResp.JSON200.Id

Verifying a Credential

We firstly create a verification request and provide it to a holder.

verifyInitResp, err := client.VerifierInitUrlCreateWithResponse(ctx,
&wallet.VerifierInitUrlCreateParams{WalletId: walletId},
wallet.VerifyInitRequest{
VerifierId: "id_card",
},
)
if err != nil {
log.Panicf("failed to init verification: %v", err)
}

if notificationResp.StatusCode() != 200 {
log.Panicf("error while verify init: %d %s", verifyInitResp.StatusCode(), verifyInitResp.Body)
}

verifyUrl := verifyInitResp.JSON200.VerifierUrl
verRequestState := verifyInitResp.JSON200.VerifierState

When a credential is successfully verified, a Wallet Notification of type vp.verified is sent. When an invalid credential was presented, a Wallet Notification of type vp.invalid is sent. These notifications are checked in the following example:

vpVerifiedRaw, err := client.WalletNotificationGetByStateWithResponse(
ctx, walletId, string(wallet.WalletNotificationEventTypeVpVerified), verRequestState)
if err != nil {
log.Panicf("getting wallet notifications: %s", err.Error())
}

if vpVerifiedRaw.StatusCode() >= 400 {
log.Panicf(
"wallet notifications wallet error code: %d, error: %s",
vpVerifiedRaw.StatusCode(), string(vpVerifiedRaw.Body))
}

if vpVerifiedRaw.StatusCode() == 200 {
// Valid VP, fetch verified credentials
creds, err := client.WalletVerifiedCredentialsByStateWithResponse(
ctx, walletId, verRequestState)
if err != nil {
log.Panicf("getting verified credentials: %s", err.Error())
}

if vpVerifiedRaw.StatusCode() >= 400 {
log.Panicf(
"verified credentials wallet error code: %d, error: %s",
vpVerifiedRaw.StatusCode(), string(vpVerifiedRaw.Body))
}
}

vpInvalidRaw, err := client.WalletNotificationGetByStateWithResponse(
ctx, walletId, string(wallet.WalletNotificationEventTypeVpInvalid), verRequestState)
if err != nil {
log.Panicf("error getting wallet notifications: %s", err.Error())
}

if vpInvalidRaw.StatusCode() >= 400 {
log.Panicf(
"verification status monitor error, wallet notifications wallet error code: %d, error: %s",
vpVerifiedRaw.StatusCode(), string(vpVerifiedRaw.Body))
}

if vpInvalidRaw.StatusCode() == 200 {
// Invalid VP
}