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:
- Issuer with predefined credential subject
- Issuance Queue
{
"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.
{
"trustFramework": "NOOP",
"walletKeyIdentifier": "did",
"credentialIssuers": [
{
"name": "Identity Card",
"id": "id_card",
"signingKeyIdentifier": "x509",
"authorization": "openid",
"credentialFormat": "sd_jwt_vc",
"credentialIssuer": "IssuanceQueue",
"credentialType": "urn:identity_card",
"disclosableClaims": [
"$.family_name",
"$.given_name"
]
},
{
"name": "Commercial Invoice",
"id": "invoice",
"signingKeyIdentifier": "did",
"authorization": "openid",
"credentialFormat": "jwt_vc_vcdm",
"credentialIssuer": "IssuanceQueue",
"credentialType": "VerifiableCredential,VerifiableAttestation,CommercialInvoiceCredential"
},
],
"oidcRevision": {
"oidc4vci": "Draft15",
"oidc4vp": "Draft23"
}
}
With Issuance Queue, you are able to issue credentials with different subjects to different holders.
The first issuer issues credential of type urn:identity_card and format sd_jwt_vc.
The credential will be signed by an X509 certificate.
The second issuer issues a credential of type VerifiableCredential,VerifiableAttestation,CommercialInvoiceCredential and format jwt_vc_vcdm.
The credential will by signed by a DID.
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!
- Go
- TypeScript
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
const { data: { id: walletId } } = await client.walletCreate({
name: 'Issuer Wallet',
config: {
credentialIssuers: [
{
credentialFormat: 'sd_jwt_vc',
credentialIssuer: 'IssuanceQueue',
credentialType: 'urn:identity_card',
disclosableClaims: ['$.family_name', '$.given_name'],
id: 'id_card',
name: 'Identity Card',
},
],
oidcRevision: {
oidc4vci: 'Draft15',
oidc4vp: 'Draft23',
},
trustFramework: 'NOOP',
walletKeyIdentifier: 'did',
},
})
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"
}
- Go
- TypeScript
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
const expirationDate = new Date()
expirationDate.setFullYear(expirationDate.getFullYear() + 1)
const { data: { id: credentialId } } = await client.credentialCreate(walletId, {
credentialSubject: {
given_name: 'John',
family_name: 'Doe',
date_of_birth: '01-01-2000',
},
credentialDraftMetadata: {
expirationDate: expirationDate.toISOString(),
format: 'sd_jwt_vc',
id: 'urn:identity_card',
name: 'Test Credential',
type: 'urn:identity_card',
},
})
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:
- Go
- TypeScript
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
const { data: { issuanceQueueItemId } } = await client.credentialIssuanceInit(
credentialId, walletId, { issuerId: 'id_card' })
const { data: { offer: offerUrl, offerId } } = await client.issuerInitiateAuthOffer(walletId, {
issuanceQueueItemId,
issuerId: 'id_card',
})
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.
- Go
- TypeScript
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
const holderDid = 'did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbqNA5MAzGAuXbu4F3mpoTQYek4NumTwnw7iaZVZXQ9Eu5tbFAe1aDT4q3CKyYo6cvFRvNYAAzSZqarjkD2uszX2rsDbZVXvDYqCeEijQxtvxSuKXks1U3Nt8Ts1eGqfVkwv'
await client.credentialIssuanceInit(credentialId, walletId, {
issuerId: 'id_card',
clientId: holderDid,
})
const { data: { offer: offerUrl, offerId } } = await client.issuerInitiatePreauthOffer(walletId, {
clientId: holderDid,
issuerId: 'id_card',
})
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:
- Go
- TypeScript
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)
}
const { data: notification } = await client.walletNotificationGetByState(
walletId, 'offer.processed', offerId)
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:
- DIF Presentation Exchange
- DCQL Query
{
"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.
{
"trustFramework": "NOOP",
"walletKeyIdentifier": "did",
"credentialVerifiers": [
{
"dcqlQuery": {
"credentials": [
{
"id": "id_card",
"format": "dc+sd-jwt",
"meta": {
"vct_values": [
"urn:identity_card"
]
},
"claims": [
{
"id": "given_name",
"path": [
"given_name"
]
},
{
"id": "family_name",
"path": [
"family_name"
]
}
]
}
]
},
"id": "pid",
"toVerify": [
"verifyExpiration"
],
"name": "Triveria ID"
}
],
"oidcRevision": {
"oidc4vci": "Draft15",
"oidc4vp": "Draft23"
}
}
This verifier requests a credential of type urn:identity_card with selective disclosure of claims given_name and family_name using DCQL Query.
Use DCQL Query when interfacing with EUDI Wallet.
For more information about DCQL Query, please refer here.
Let's create a verifier wallet!
- Go
- TypeScript
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
const { data: { id: walletId } } = await client.walletCreate({
name: 'Verifier Wallet',
config: {
credentialVerifiers: [
{
id: 'id_card',
name: 'ID Card Verifier',
dcqlQuery: {
credentials: [
{
id: 'id_card',
format: 'dc+sd-jwt',
meta: { vctValues: ['urn:identity_card'] },
claims: [
{ id: 'given_name', path: ['given_name'] },
{ id: 'family_name', path: ['family_name'] },
],
},
],
},
},
],
oidcRevision: {
oidc4vci: 'Draft15',
oidc4vp: 'Draft23',
},
trustFramework: 'NOOP',
walletKeyIdentifier: 'did',
},
})
Verifying a Credential
We firstly create a verification request and provide it to a holder.
- Go
- TypeScript
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
const { data: { verifierUrl: verifyUrl, verifierState: verRequestState } } =
await client.verifierInitUrlCreate(walletId, { verifierId: 'id_card' })
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:
- Go
- TypeScript
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
}
try {
// Check for a valid presentation
const { data: vpVerified } = await client.walletNotificationGetByState(
walletId, 'vp.verified', verRequestState)
// Valid VP — fetch the verified credentials
const { data: creds } = await client.walletVerifiedCredentialsByState(walletId, verRequestState)
} catch {
// No vp.verified notification yet; check for invalid presentation
}
try {
const { data: vpInvalid } = await client.walletNotificationGetByState(
walletId, 'vp.invalid', verRequestState)
// Invalid VP
} catch {
// No vp.invalid notification yet
}