Issuing SD-JWT VC credentials via OID4VC

This guide provides a comprehensive walkthrough for issuing an SD-JWT VC credential based on the IETF standard using the walt.id issuer API. If you are interested to learn how to issue an W3C Verifiable Credentials, please go here. The issuance process will utilize the OID4VCI protocol, an extension of OpenID, facilitating secure and standardized communication between identities.

Setup

See how to access to the issuer API below.

Preparing for Issuance: Key Components

Before issuing a verifiable credential, you'll need:

  1. Raw Credential Data: The information you want to issue as a VC, typically following a specific template or schema.
  2. Issuer DID or X.509 Certificate: When issuing a SD-JWT VC credential, it can either be signed using the key of an issuer DID or a key which is trusted by an X.509 certificate.

If you don't make any specific configurations, the walt.id issuer API supports different types of SD-JWT VC credentials which can be found in the credential issuer medata endpoint, e.g. here. If you want to issue other custom credentials, you need to add them to the supported credentials in the issuer metadata. For this example we will issue the by default support Identity Credential, though the process is the same for other credentials.

Example ID Credential:

{
  "vct": "identity_credential_vc+sd-jwt",
  "given_name": "John",
  "family_name": "Doe",
  "email": "johndoe@example.com",
  "phone_number": "+1-202-555-0101",
  "address": {
    "street_address": "123 Main St",
    "locality": "Anytown",
    "region": "Anystate",
    "country": "US"
  },
  "birthdate": "1940-01-01",
  "is_over_18": true,
  "is_over_21": true,
  "is_over_65": true
}

Issuing a Credential

Choose below if you want to sign the credential using the key of a DID or an X.509 certificate.

DID
x.509 certificate

We will now generate a key and associated DID via the issuer API. However, we need to store the key material + DID ourselves. For production environments, we recommend using an external KMS provider for key management due to the enhanced security. Learn more about the different types of keys and the storage options here.

Step 1: Get a Signing Key & Issuer DID

As the issuer API doesn't store any cryptographic key material by default. You need to provide the key used for signing the credential in JWK format or a reference object that points to a key stored in an external KMS we support. At the moment, we support Hashicorp Vault and Oracle KMS. In a production environment, we recommend the usage of an external KMS provider to secure the key material.

To create keys for signing credentials, you can use the /onboard/issuer endpoint and choose from a variety of algorithms (ed25519, secp256k1, secp256r1, or RSA). The DID generated by the endpoint is not needed for the SD-JWT VC.

Please refer to our Key Management section to learn more about the different options.

For this guide, we will proceed with generating a JWK Ed25519 key using the onboard endpoint.

Create Key

CURL

Endpoint: /onboard/issuer | API Reference

Example Request

curl -X 'POST' \
  'http://0.0.0.0:7002/onboard/issuer' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "key": {
    "backend": "jwk",
    "keyType": "Ed25519"
  },
  "did": {
    "method": "jwk"
  }
}'

Body

{
  "key": {
    "backend": "jwk",
    "keyType": "Ed25519"
  },
  "did": {
    "method": "jwk"
  }
}

Body Parameters

  • key
    • backend: String - Specifies the storage type of key. It can be jwk (manged by you), TSE (managed by Hashicorp Vault) and others. Learn more about different types here.
    • keyType: String - the algorithm used to generate the key. For local, it can be ed25519, secp256k1, secp256r1, or RSA. For the other types and the supported algorithms, please go here.
  • did:
    • method: String - Specifies the DID method. It can be key, jwk, web, cheqd.

Example Response

The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.

{
  "issuerKey": {
    "type": "jwk",
    "jwk": {
      "kty": "OKP",
      "d": "Sr5-oOEXlYU_yCw7ADn-zsvJwMc7cEd2l5klg9i8n1k",
      "crv": "Ed25519",
      "kid": "Zf8NXHGvBlbzX3hdKX1YHZTindNf7LrixPZallJ7nxk",
      "x": "4Iviyx79FlJH5w81DxJwfeRD6JPB4vCJoHzKOQwJ0cs"
    }
  },
  "issuerDid": "did:cheqd:testnet:26948e3c-4493-48fe-a61c-9052ecb62718"
}

Important: As we've used the jwk key type, it's important to note that we need to save the returned values ourselves for future reference. The API doesn't save any information about created keys.

Step 2: Issue the Credential

To issue a verifiable credential, we will use the obtained key or any other supported JWK. When issuing an SD-JWT VC credential, we have the option to selectively disclose certain claims in the credential. By default, all claims are visible. The configuration for selectively disclosing claims can be done through a configuration object outlined below.

To facilitate the issuance of the credential from us (the issuer) to the holder, we will utilise the OID4VCI protocol. In particular, we will be generating an OID4VC offer URL that can be accepted by any OID compliant wallet to receive credential(s).

The credential offer URL specifies the credentials to be issued. This includes details such as the URL of the issuer, the authorisation and token endpoints, and information about the credential's format, type, and scope.


When we execute the issuance command, two things will happen.

  1. The credential will be signed with the provided key.
  2. Information about the offered credential will be embedded into the OID Credential Offer URL, which we can send off to our users to claim the credential(s) later on.

The Verifiable Credential Type (VCT) is resolved through the credentialConfigurationId. The vct is found as a parameter within the corresponding entry associated with that id. Issuer API generates proper resolvable urls for VCT based on provided configuration and host endpoints to retrieve type metadata with the scheme https://<authority>/.well-known/vct/<type> (e.g. https://issuer.portal.walt-test.cloud/.well-known/vct/identity_credential). Find more info here


CURL

Endpoint:/openid4vc/sdjwt/issue | API Reference

Example Request

curl -X 'POST' \
  'https://issuer.portal.walt.id/openid4vc/sdjwt/issue' \
  -H 'accept: text/plain' \
  -H 'statusCallbackUri: https://example.com/$id' \
  -H 'Content-Type: application/json' \
  -d '{
  "issuerKey": {
    "type": "jwk",
    "jwk": {
      "kty": "OKP",
      "d": "mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI",
      "crv": "Ed25519",
      "kid": "Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8",
      "x": "T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM"
    }
  },
  "issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
  "credentialConfigurationId": "identity_credential_vc+sd-jwt",
  "credentialData": {
    "given_name": "John",
    "family_name": "Doe",
    "email": "johndoe@example.com",
    "phone_number": "+1-202-555-0101",
    "address": {
      "street_address": "123 Main St",
      "locality": "Anytown",
      "region": "Anystate",
      "country": "US"
    },
    "birthdate": "1940-01-01",
    "is_over_18": true,
    "is_over_21": true,
    "is_over_65": true
  },
  "mapping": {
    "id": "<uuid>",
    "iat": "<timestamp-seconds>",
    "nbf": "<timestamp-seconds>",
    "exp": "<timestamp-in-seconds:365d>"
  },
  "selectiveDisclosure": {
    "fields": {
      "birthdate": {
        "sd": true
      }
    },
    "decoyMode": "NONE",
    "decoys": 0
  }
}'

Header Parameters

  • statusCallbackUri: URL - Receive updates on the created issuance process, e.g. when a credential was successfully claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a $id, which will be replaced by the issuance session id. For example: https://myurl.com/$id, https://myurl.com or https://myurl.com/test/$id

    Expand To Learn More

    Body

    The data send to the provided URL will contain a JSON body:

    • id : String - the issuance session id
    • type: String - the event type
    • data: JsonObject - the data for the event

    Event Types

    Possible events (event types) and their data are:

    • resolved_credential_offer with the credential offer as JSON (in our Web Wallet: called when the issuance offer is entered into the wallet, but not processing / accepted yet)
    • requested_token with the issuance request for the token as json object (called for the token required to receive the credentials)

    Credential issuance (called for every credential that's issued (= requested from wallet))

    • jwt_issue with jwt being the issued jwt
    • sdjwt_issue with sdjwt being the issued sdjwt
    • batch_jwt_issue with jwt being the issued jwt
    • batch_sdjwt_issue with sdjwt being the issued sdjwt
    • generated_mdoc with mdoc being the CBOR (HEX) of the signed mdoc

    To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.

Body

{
  "issuerKey": {
    "type": "jwk",
    "jwk": {
      "kty": "OKP",
      "d": "mDhpwaH6JYSrD2Bq7Cs-pzmsjlLj4EOhxyI-9DM1mFI",
      "crv": "Ed25519",
      "kid": "Vzx7l5fh56F3Pf9aR3DECU5BwfrY6ZJe05aiWYWzan8",
      "x": "T3T4-u1Xz3vAV2JwPNxWfs4pik_JLiArz_WTCvrCFUM"
    }
  },
  "issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
  "credentialConfigurationId": "identity_credential_vc+sd-jwt",
  "credentialData": {
    "given_name": "John",
    "family_name": "Doe",
    "email": "johndoe@example.com",
    "phone_number": "+1-202-555-0101",
    "address": {
      "street_address": "123 Main St",
      "locality": "Anytown",
      "region": "Anystate",
      "country": "US"
    },
    "birthdate": "1940-01-01",
    "is_over_18": true,
    "is_over_21": true,
    "is_over_65": true
  },
  "mapping": {
    "id": "<uuid>",
    "iat": "<timestamp-seconds>",
    "nbf": "<timestamp-seconds>",
    "exp": "<timestamp-in-seconds:365d>"
  },
  "selectiveDisclosure": {
    "fields": {
      "name": {
        "sd": true
      }
    },
    "decoyMode": "NONE",
    "decoys": 0
  }
}

Body Parameters

  • issuerKey: JSON - A JWK or reference object to a key stored in an external KMS to sign the credential with. Supported algorithms: ed25519, secp256k1, secp256r1, or RSA.
    JWK Format: {"type": "jwk", "jwk": Here JSON Web Key Object}. Can be provided as String Or JWK object to " issuerKey".
    KMS Key: Please refer to the Key Management Section and the KMS you want to use for more details on the structure of the reference object.
  • credentialConfigurationId: String - Reference to a specific credential configuration the issuer supports. As our issuer currently supports both JWT and SD-JWT signatures. The structure of the credentialConfigurationId is the following: " credentialType_jwt_vc_json" for JWT VCs and "credentialType_vc+sd-jwt" for SD-JWT VCs. E.g. "UniversityDegree_jwt_vc_json" or "UniversityDegree_vc+sd-jwt". You can also view the credentialConfigurationIds by visiting the /.well-known/openid-credential-issuer endpoint of your issuer. The metadata of our deployed issuer for testing can be seen here.
  • credentialData: JSON - Credential data structure to sign.
  • mapping (optional): JSON - The mapping object that allows for dynamic value insertion via data functions, executed at the time when the credentials is claimed. This feature enables personalized credentials based on real-time data. Learn more about it and see a list of supported data functions here. In case of IETF SD-JWT, the values could be id, iat, nbf and exp.
  • selectiveDisclosure (optional): JSON -An object that configures which claims in the credential should be selectively disclosable. It's manged through the following properties:
    Expand To Learn More
    • fields: An object illustrating the hierarchical structure of the credential contents. Each key in this object specifies the name of the fields in the credential. Every field, whether high-level or nested, can be represented by an object with a "sd" key. If the value for "sd" is set to true, it means that the corresponding field is selectively disclosable. Moreover, fields that have nested attributes are represented with a "children" key which contains another fields object reflecting the structure of the nested object. For example:
    {
     "fields": {
       "issuanceDate": {
         "sd": true
       },
       "credentialSubject": {
         "sd": false,
         "children": {
           "fields": {
             "degree": {
               "sd": false,
               "children": {
                 "fields": {
                   "name": {
                     "sd": true
                   }
                 }
               }
             }
           }
         }
       }
     }
    }
    

Example Response

The issuer endpoint will respond with Credential Offer URL.

Plain Response

openid-credential-offer://issuer.potential.walt-test.cloud/?credential_offer_uri=https%3A%2F%2Fissuer.potential.walt-test.cloud%2Fopenid4vc%2FcredentialOffer%3Fid%3D9aabdb65-defe-464b-baa0-9cc13b36074a

Decoded

{
  "credential_issuer": "https://issuer.potential.walt-test.cloud",
  "credential_configuration_ids": [
    "identity_credential_vc+sd-jwt"
  ],
  "grants": {
    "authorization_code": {
      "issuer_state": "9aabdb65-defe-464b-baa0-9cc13b36074a"
    },
    "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
      "pre-authorized_code": "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiI5YWFiZGI2NS1kZWZlLTQ2NGItYmFhMC05Y2MxM2IzNjA3NGEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3RlbnRpYWwud2FsdC10ZXN0LmNsb3VkIiwiYXVkIjoiVE9LRU4ifQ.vl9fBjoYUqr1nrA0jZ3ZjpS45yHLp2roMPCxCoqjuLNyBhTGO0g_PXMw8_NWhD-3qllRq5J0kLAw8WmfvT95Cw"
    }
  }
}

Step 3: Receive the Credential Offer

The created credential offer can now be embedded into a QR code for users to scan with their mobile wallet or pasted manually into the credential offer field of our web wallet.

Try It Out: Use our web wallet for a practical demonstration. After logging in, click the 'request credential' button and paste the received Offer URL into the text field below the camera.

🎉 Congratulations, you've issued a SD-JWT VC Credential using OID4VCI! 🎉