# Create an Issuer You want to create an issuer and issue attestations to NL Wallet users. An issuer (also known as a "verstrekkende voorziening" in Dutch) is essentially an entity that wants to issue attestations to the NL Wallet. An issuer issues cards to the wallet which attest to certain facts about a person. Things like diploma's, driving licenses, personalia, etc. This document provides a global outline of components used, the necessary decisions, data, and certificate(s), and guides the setup of a so-called issuer/verstrekkende-voorziening.

Open-source software

Did you know that the NL Wallet platform is fully open-source? You can find [the project on GitHub][1].
```{contents} :backlinks: none :depth: 5 :local: ``` ## What we're going to cover We'll start with a paragraph about the related architecture, with links to the relevant architecture documents. We'll then cover the creation of the technical attestation schema, the creation of issuer and reader authentication documents and the corresponding reader and issuer certificates, which are essential for identifying your service within the NL Wallet ecosystem. Next, we'll guide you through setting up the `issuance_server`. This includes obtaining the software, configuring it (with an optional database backend), and running it for the first time. We'll also cover how to validate that your setup is working correctly, and finally, we'll point you to the issuer API specifications. ## Architecture overview Our issuance implementation adheres to the [OpenID4VCI][2] specification. For PID issuance, we have a specialized issuer called `pid_issuer` which interacts with [DigiD][3] through [RDO Max][4], and which can obtain citizen data from [RViG's BRP][5]. This document does not cover PID issuance, although many parts of this document do apply to PID issuance equally. This document is about generic issuance, where the wallet can disclose certain attributes to an issuer in order to obtain new, different attestations, hence the name "disclosure-based issuance". You can find the general project start architecture documents for the NL Wallet on [Pleio][4] .These document the general platform architecture, solution architecture, design considerations and global functional design use cases. To get a clear view on what issuance (in both its specialized PID issuance, and generic disclosure-based issuance form) looks like on the NL Wallet platform, have a look at the following documents: * [Issuance with OpenID4VCI][6] * [Disclosure-based Iissuance][7] ### Platform components overview The NL Wallet platform consists of: * **Issuers**: (also known as Verstrekkende Voorzieningen), which can issue attested attributes, and which this document is mainly about; * **Verifiers**: (also known as Ontvangende Voorzieningen or Relying Parties), which can verify attested attributes they are interested in; * **Backend**: services that run in the NL Wallet datacenter(s) or cloud that facilitate various functions for the mobile app (usually not interacted with directly, by either Issuers or Verifiers); * **App**: the NL Wallet mobile app, which contains attested attributes, received from Issuers, and which it can disclose to Verifiers. Issuers configure and maintain an `issuance_server` on their own premises or cloud environments, which they integrate with their own application, and which interacts with the NL Wallet app, in order to issue attested attributes. ## Creating a technical attestation schema document In the next two sub-sections we'll dive into what's needed to create a TAS document, i.e., a "technical attestation schema". We'll first decide on the required attributes for your TAS and then create an actual document. You're free to change values mostly as you see fit, especially if you'll be running this in a testing environment, but keep in mind that when this document is intended for a production environment, more stringent rules might apply.

When you work on eventual production readiness

If you plan to eventually bring your issuer into production readiness, you might want to consider our [onboarding][11] process. When you are a member of the NL Wallet community, you have access to community resources that can help with validation of your TAS, `issuer_auth`, `reader_auth` and `issuance_server` configuration files.
### Decide on required metadata for your TAS The TAS contains the following particularly important data elements: **ROOT** | Key | Type | Description | | ------------- | ------ | --------------------------------------------------- | | `vct` | string | Verifiable credential type field | | `name` | string | Readable name of this verifiable credential | | `description` | string | Description of this verifiable credential | | `display` | array | Array of display objects, one per language | | `claims` | array | Array of claim objects | | `schema` | object | A v2020-12 JSON Schema object defining the claims | Claims have the following elements: **CLAIMS** | Key | Type | Description | | ------------- | ------ | --------------------------------------------------- | | `path` | array | An array of strings describing the claim path | | `display` | array | Array of display objects, one per language | | `sd` | string | Indicates whether a claim is selectively disclosable| | `svg_id` | string | Template identifier for the SVG rendering metadata | ### Creating the technical attestation schema JSON document Below you'll see an example of a TAS JSON document. You can use it as an example for your own, or use it verbatim as-is to test disclosure-based issuance. We'll assume the latter for now; in any case, later sections in this guide will assume you'll save it as `target/is-config/insurance_metadata.json` within the `nl-wallet` directory (i.e., where you cloned the git repository of the NL Wallet). If you change the name or location, keep in mind that the various script sections later on will have to be adjusted also). Here is the example TAS for an insurance: ```json { "vct": "com.example.insurance", "name": "Insurance credential", "description": "Insurance credential", "display": [ { "lang": "en-US", "name": "Insurance", "description": "An insurance credential", "summary": "{{coverage}}", "rendering": { "simple": { "background_color": "#b2e1ea", "text_color": "#152a62" } } }, { "lang": "nl-NL", "name": "Verzekering", "description": "Een verzekering attestatie", "summary": "{{coverage}}", "rendering": { "simple": { "background_color": "#b2e1ea", "text_color": "#152a62" } } } ], "claims": [ { "path": ["product"], "display": [ { "lang": "nl-NL", "label": "Product", "description": "Soort verzekering" }, { "lang": "en-US", "label": "Product", "description": "Type of insurance" } ], "sd": "always" }, { "path": ["coverage"], "display": [ { "lang": "nl-NL", "label": "Dekking", "description": "Dekking van de verzekering" }, { "lang": "en-US", "label": "Coverage", "description": "Coverage of the insurance" } ], "sd": "always", "svg_id": "coverage" }, { "path": ["start_date"], "display": [ { "lang": "nl-NL", "label": "Startdatum", "description": "Datum waarop de verzekering ingaat" }, { "lang": "en-US", "label": "Start date", "description": "Date on which the insurance starts" } ], "sd": "always" }, { "path": ["duration"], "display": [ { "lang": "nl-NL", "label": "Duur", "description": "Duur van de verzekering" }, { "lang": "en-US", "label": "Duration", "description": "Duration of the insurance" } ], "sd": "always" }, { "path": ["customer_number"], "display": [ { "lang": "nl-NL", "label": "Klantnummer", "description": "Klantnummer van de verzekerde" }, { "lang": "en-US", "label": "Customer number", "description": "Customer number of the insured" } ], "sd": "always" } ], "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Insurance VCT Schema", "description": "The JSON schema that defines a insurance VCT", "type": "object", "properties": { "vct": { "type": "string" }, "vct#integrity": { "type": "string" }, "iss": { "type": "string" }, "nbf": { "type": "number" }, "exp": { "type": "number" }, "cnf": { "type": "object" }, "status": { "type": "object" }, "sub": { "type": "string" }, "iat": { "type": "number" }, "attestation_qualification": { "type": "string" }, "product": { "type": "string" }, "coverage": { "type": "string" }, "start_date": { "type": "string", "format": "date" }, "duration": { "type": "string" }, "customer_number": { "type": "string" } }, "required": ["vct", "iss", "attestation_qualification", "product", "coverage"] } } ``` You can modify the above, keeping obvious constraints in mind (have a look at the [previous section](#decide-on-required-metadata-for-your-tas)). Modified or not, make sure you save it somewhere, keeping our earlier warnings about file name and location in mind. ## Creating an issuer authentication document We're first going to create a so-called `issuer_auth` document. The subsections below describe the decisions you need to make as an issuer with regards to attributes you want to issue, what data we require from you, how to create an issuer certificate for your disclosure-based issuance setup (which is configured for usage within the `issuance_server` configuration). In this guide, we assume you have [onboarded succesfully][11] - i.e., you are running your own CA and the public key of that CA has been shared with the operations team who will need to add your CA public key to the trust anchors of the app.

Onboarding optional

Do note that onboarding is not strictly necessary - you *can* follow all steps in this guide and observe things working in a local development environment - but when you want to test your issuer with the NL Wallet platform (i.e., our backend and mobile apps in our acceptance and pre-production environments), you do need to be onboarded to get access to those environments.
### Decide on required metadata for your issuer_auth An issuer certificate contains a bunch of metadata, which we store as a part of the certificate in a so-called X.509v3 extension. We use this data to present a view of you, the issuer, in the NL Wallet app GUI. **ROOT** | Key | Languages | Description | | ------------------------------- | --------- | -------------------------------------------------------------------- | | `organization.displayName` | `nl+en` | Name of the verifier as shown in the app app. | | `organization.legalName` | `nl+en` | Legal name of the verifier. | | `organization.description` | `nl+en` | Short one-sentence description or mission statement of the verifier. | | `organization.webUrl` | - | The home URL of the verifier. | | `organization.city` | `nl+en` | The home city of the verifier. | | `organization.category` | `nl+en` | Bank, Municipality, Trading, Delivery Service, etc. | | `organization.logo.mimeType` | - | Logo mimetype, can be image/svg+xml, image/png or image/jpeg | | `organization.logo.imageData` | - | Logo image data. When SVG, an escaped XML string, else base64 | | `organization.countryCode` | - | Two-letter country code of verifier residence. | | `organization.kvk` | - | Chamber of commerce number of verifier. | | `organization.privacyPolicyUrl` | - | Link to verifier's privacy policy. | Note: In the `Languages` column where it says `nl+en` for example, please provide both Dutch and English values. ### Creating the issuer_auth JSON document When you've collected all the required metadata, you are ready to create the `issuer_auth.json` file. Here is an example for our insurance company: ```json { "organization": { "displayName": { "nl": "VerzekerAar", "en": "InsurAnce" }, "legalName": { "nl": "VerzekerAar N.V.", "en": "VerzekerAar N.V." }, "description": { "nl": "VerzekerAar is een voorbeeld-verzekeraar.", "en": "InsurAnce is an exemplar insurance company." }, "webUrl": "https://insurance.example.com", "city": { "nl": "Den Haag", "en": "The Hague" }, "category": { "nl": "Verzekeringen", "en": "Insurance" }, "logo": { "mimeType": "image/svg+xml", "imageData": "" }, "countryCode": "nl", "kvk": "99876543", "privacyPolicyUrl": "https://insurance.example.com/privacy" } } ``` Take the above example, make sure you've read the previous sections which explain what the different key/values mean, and (optionally) construct your own `issuer_auth.json` file (or copy it verbatim if you're just testing). When we are going to be creating the issuer certificate in the next sections, we are going to need it at a specific location, so save it (inside the `nl-wallet` git directory): ``` target/is-config/issuer_auth.json ``` ## Creating a reader authentication document We're going to create a so-called `reader_auth` document. The subsections below describe the decisions you need to make as a verifier with regards to attributes you want to verify, what data we require from you, how to create a reader certificate for your usecase (which is configured for usage within the `verification_server` configuration). In this guide, we assume you have [onboarded succesfully][11] - i.e., you are running your own CA and the public key of that CA has been shared with the operations team who will need to add your CA public key to the trust anchors of the app.

Onboarding optional

Do note that onboarding is not strictly necessary - you *can* follow all steps in this guide and observe things working in a local development environment - but when you want to test your verifier with the NL Wallet platform (i.e., our backend and mobile apps in our acceptance and pre-production environments), you do need to be onboarded to get access to those environments.

This chapter is also a part of creating a verifier

Note that when you've also [created a verifier][31], this section will look familiar to you; that is because an issuer, like a verifier, needs a reader authentication document. This is because of how disclosure-based-issuance works: with disclosure-based-issuance, an issuer is essentially also a verifier (i.e., you disclose some attributes in order to obtain some new ones).
### Decide on required metadata for your reader_auth A reader certificate contains a bunch of metadata, which we store as a part of the certificate in a so-called X.509v3 extension. We use this data to know which attested attribute you want to verify, and to present a view of you, the verifier in the NL Wallet app GUI. **ROOT** | Key | Languages | Description | | ------------------------------- | --------- | -------------------------------------------------------------------- | | `purposeStatement` | `nl+en` | For what purpose are you attesting? Login? Age verification? etc. | | `retentionPolicy` | - | Do you have an intent to retain data? For how long? | | `sharingPolicy` | - | Do you have an intent to share data? With whom? | | `deletionPolicy` | - | Do you allow users to request deletion of their data, yes/no? | | `organization.displayName` | `nl+en` | Name of the verifier as shown in the app app. | | `organization.legalName` | `nl+en` | Legal name of the verifier. | | `organization.description` | `nl+en` | Short one-sentence description or mission statement of the verifier. | | `organization.webUrl` | - | The home URL of the verifier. | | `organization.city` | `nl+en` | The home city of the verifier. | | `organization.category` | `nl+en` | Bank, Municipality, Trading, Delivery Service, etc. | | `organization.logo.mimeType` | - | Logo mimetype, can be image/svg+xml, image/png or image/jpeg | | `organization.logo.imageData` | - | Logo image data. When SVG, an escaped XML string, else base64 | | `organization.countryCode` | - | Two-letter country code of verifier residence. | | `organization.kvk` | - | Chamber of commerce number of verifier. | | `organization.privacyPolicyUrl` | - | Link to verifier's privacy policy. | | `authorizedAttributes` | - | List of attributes you want to verify. | Note: In the `Languages` column where it says `nl+en` for example, please provide both Dutch and English values. ### Decide on attributes you want to verify You can verify any attribute (also known as a claim path) provided by any issuer on the plaform, but since we don't have an issuer registry yet, you would need to know or otherwise get your hands on the JSON documents that define the claim paths that belong to a given `vct` (a Verifiable Credential Type). For our own issuer(s), you can have a look at our [supported authorized attributes][32] document for an overview of claim paths you can use, and some background information on how the `authorizedAttributes` object works. ### Creating the reader_auth JSON document When you've collected all the required metadata, you are ready to create the `reader_auth.json` file. Here is an example for our insurance company: ```json { "purposeStatement": { "nl": "Uitgifte", "en": "Issuance" }, "retentionPolicy": { "intentToRetain": true, "maxDurationInMinutes": 525600 }, "sharingPolicy": { "intentToShare": false }, "deletionPolicy": { "deleteable": false }, "organization": { "displayName": { "nl": "VerzekerAar", "en": "InsurAnce" }, "legalName": { "nl": "VerzekerAargh N.V.", "en": "VerzekerAargh N.V." }, "description": { "nl": "VerzekerAargh is een voorbeeld-verzekeraar.", "en": "InsurAnce is an exemplar insurance company." }, "webUrl": "https://insurance.example.com", "city": { "nl": "Den Haag", "en": "The Hague" }, "category": { "nl": "Verzekeringen", "en": "Insurance" }, "logo": { "mimeType": "image/svg+xml", "imageData": "" }, "countryCode": "nl", "kvk": "99876543", "privacyPolicyUrl": "https://insurance.example.com/privacy" }, "requestOriginBaseUrl": "https://insurance.example.com", "authorizedAttributes": { "urn:eudi:pid:nl:1": [ ["urn:eudi:pid:nl:1", "bsn"], ["bsn"] ] } } ``` Take the above example, make sure you've read the previous sections which explain what the different key/values mean, and (optionally) construct your own `reader_auth.json` file (or copy it verbatim if you're just testing). When we are going to be creating the reader certificate in the next sections, we are going to need it at a specific location, so save it (inside the `nl-wallet` git directory): ``` target/is-config/reader_auth.json ``` ## Creating issuer, reader and tsl certificates Let's create the issuer, reader and tsl certificates. We're going to clone the NL Wallet repository, enter its directory, set a target directory and specify an identifier (this identifies your organization, and should be in lowercase characters a-z, can end with numbers but may not begin with them). We then make sure the target directory exists, and invoke `cargo` (rust's build tool) to in turn invoke `wallet_ca` which will create the issuer, reader and tsl certificates and keys. Finally, we invoke `openssl` to convert our PEM certificates and key into DER format.

Do you have a working toolchain?

Make sure you have a working toolchain as documented in our GitHub project root `README.md` [here][19]. Specifically, you need to have `rust` and `openssl` installed and working.

Did you create an issuer_auth.json and reader_auth.json?

You need valid `issuer_auth.json` and `reader_auth.json`, documents, which you should have, if you followed along with the previous sections where we created [issuer](#creating-the-issuer_auth-json-document) and [reader](#creating-the-reader_auth-json-document) authorization documents.

Did you create your own CA?

You need a CA certificate and key. By default, when you're running locally, the `setup-devenv.sh` script will have created these for you. You can also opt to create your own custom self-signed CA certificate and key, which is documented in the [Create a CA][27] document, and which is required if you need to participate in the NL Wallet community platform.

Do you intend to test your issuer on the NL Wallet platform?

You can test your issuer locally (more or less exactly like we do with our `demo-issuer` app) for which you don't need anything except the code in our git repository. But if you want to test your issuer with the NL Wallet platform (i.e., the NL Wallet apps on our Test Flight and Play Store Beta environments plus backends), you will need to have succesfully completed the [onboarding][11] process.
```shell # Git clone and enter the nl-wallet repository if you haven't already done so. git clone https://github.com/MinBZK/nl-wallet cd nl-wallet # Set and create target directory, identifier for your certificates. export CA_DIR=target/ca-cert export TARGET_DIR=target/vs-config export IDENTIFIER=foocorp mkdir -p "${CA_DIR}" "${TARGET_DIR}" # Create the issuer certificate using wallet_ca. cargo run --manifest-path "wallet_core/Cargo.toml" --bin "wallet_ca" issuer \ --ca-key-file "${CA_DIR}/ca.${IDENTIFIER}.key.pem" \ --ca-crt-file "${CA_DIR}/ca.${IDENTIFIER}.crt.pem" \ --common-name "issuer.${IDENTIFIER}" \ --issuer-auth-file "${TARGET_DIR}/issuer_auth.json" \ --file-prefix "${TARGET_DIR}/issuer.${IDENTIFIER}" # Create the reader certificate using wallet_ca. cargo run --manifest-path "wallet_core/Cargo.toml" --bin "wallet_ca" reader \ --ca-key-file "${CA_DIR}/ca.${IDENTIFIER}.key.pem" \ --ca-crt-file "${CA_DIR}/ca.${IDENTIFIER}.crt.pem" \ --common-name "reader.${IDENTIFIER}" \ --reader-auth-file "${TARGET_DIR}/reader_auth.json" \ --file-prefix "${TARGET_DIR}/reader.${IDENTIFIER}" # Create the tsl certificate using wallet_ca. cargo run --manifest-path "wallet_core/Cargo.toml" --bin "wallet_ca" tsl \ --ca-key-file "${CA_DIR}/ca.issuer.key.pem" \ --ca-crt-file "${CA_DIR}/ca.issuer.crt.pem" \ --common-name "tsl.${IDENTIFIER}" \ --file-prefix "${TARGET_DIR}/tsl.${IDENTIFIER}" # Convert certificates PEM to DER. openssl x509 \ -in "${TARGET_DIR}/issuer.${IDENTIFIER}.crt.pem" -inform PEM \ -out "${TARGET_DIR}/issuer.${IDENTIFIER}.crt.der" -outform DER openssl x509 \ -in "${TARGET_DIR}/reader.${IDENTIFIER}.crt.pem" -inform PEM \ -out "${TARGET_DIR}/reader.${IDENTIFIER}.crt.der" -outform DER openssl x509 \ -in "${TARGET_DIR}/tsl.${IDENTIFIER}.crt.pem" -inform PEM \ -out "${TARGET_DIR}/tsl.${IDENTIFIER}.crt.der" -outform DER # Convert keys PEM to DER. openssl pkcs8 -topk8 -nocrypt \ -in "${TARGET_DIR}/issuer.${IDENTIFIER}.key.pem" -inform PEM \ -out "${TARGET_DIR}/issuer.${IDENTIFIER}.key.der" -outform DER openssl pkcs8 -topk8 -nocrypt \ -in "${TARGET_DIR}/reader.${IDENTIFIER}.key.pem" -inform PEM \ -out "${TARGET_DIR}/reader.${IDENTIFIER}.key.der" -outform DER openssl pkcs8 -topk8 -nocrypt \ -in "${TARGET_DIR}/tsl.${IDENTIFIER}.key.pem" -inform PEM \ -out "${TARGET_DIR}/tsl.${IDENTIFIER}.key.der" -outform DER ``` The used CA public certificate (referenced in the previous `wallet_ca` command) needs to be in the list of various so-called trust anchors. Specifically, issuers and verifiers, and the NL Wallet app itself need to know if this CA is a trusted CA, and our software "knows" that by checking its trust anchors. When you run locally, when using `setup-devenv.sh` and `start-devenv.sh`, the generated CA certificate is automatically added to the trust anchors within the configuration files of `pid_issuer`, `demo_issuer`, `verification_server`, `issuance_server`, `demo_relying_party` and the NL Wallet app config. When you [create your own CA][27], you need to make sure the public key of your CA is in the relevant trust anchor configuration settings. When you are a member of the [NL Wallet community][11], and so using NL Wallet managed backend services and mobile apps, this is done for you (i.e., you just need to sign your issuer certificate with your CA, which the `wallet_ca` utility invocation above did for you, and during the NL Wallet community [onboarding][11] process you shared your CA certificate with the operations team who ensure your CA is in the various trust anchor lists). When you run locally, but with a manually created CA, you need to add the CA public certificate to your services and wallet app config yourself. When we generate the configuration later in this guide, we will do this automatically, provided you used the naming conventions we used in the previous `wallet_ca` invocations. ## Issuance server setup In the following sections, we'll guide you through obtaining the software, setting up a database backend and creating the `issuance_server` configuration file. ### Obtaining the software The `issuance_server` binary can be obtained by downloading a pre-compiled binary from our [releases][20] page, or by compiling from source. To compile from source, make sure you have our git repository checked out and make sure you've [configured your local development environment][19]. Then: ```shell cd nl-wallet cargo build \ --manifest-path wallet_core/Cargo.toml \ --package issuance_server \ --bin issuance_server \ --locked --release ``` The above command creates `wallet_core/target/release/issuance_server`, which is a release binary for the platform you're running on. Let's copy that binary to our target config directory for usage later: ```shell mkdir -p target/is-config cp wallet_core/target/release/issuance_server target/is-config ```

About default feature flags

Note that since we don't specify a `--features` argument in the above `cargo` command, the default feature flags apply. For `issuance_server`, this happens to be just `postgres`. When you build for local development, the build script enables another feature flag called `allow_insecure_url`, which would allow you to talk to an `issuance_server` using an (insecure) `http://` URL.

Don't allow insecure URLs on production-like environments

Don't enable `allow_insecure_url` on anything remotely production-like. Doing so anyway, accidentally or not, could expose you to man-in-the-middle attacks.
### Using a database backend The `issuance_server` needs a PostgreSQL database for maintaining status lists. When compiled with the `postgres` feature flag (which is the default), it can also use a PostgreSQL database backend to store state. This is done by configuring a `postgres://` storage URL in the `issuance_server.toml` configuration file. In this section, we'll create a PostgreSQL database, configure credentials and configure the schema (tables, columns).

You can also run with a memory session store

Note that you can run `issuance_server` with a storage URL `memory://` This makes the server store session state in memory. **When using in-memory session state, on server shutdown or crash, any session state will be lost.**
#### Setting up PostgreSQL You might already have a PostgreSQL database running, in which case you need the credentials of a database user with `createdb` and `createrole` role attributes, and the hostname of the system running the PostgreSQL database (can be localhost or any fully-qualified domain name). When you don't have a PostgreSQL database service running, you can create one following the [installation instructions][22] or you can use something like [docker][23] to run a containerized PostgreSQL service, which we'll document here.

Use correct credentials and hostname in commands below

When you decide to use your own previously configured PostgreSQL database service, make sure you don't execute the `docker run` command which creates a new PostgreSQL database service, and make sure you use the correct hostname, username an password values.
```shell # Specify database hostname, superuser name and password for PostgreSQL itself # (change these if you're using you own previously created database service): export PGHOST=localhost export PGPORT=5432 export PGUSER=postgres export PGPASSWORD="$(openssl rand -base64 12)" # Specify database hostname, issuance_server database name, user name and # password for verification_server: export DB_NAME=issuance_server export DB_USERNAME=wallet export DB_PASSWORD="$(openssl rand -base64 12)" ``` Let's use Docker to run PostgreSQL, using a volume named `postgres` for the database storage. We'll run in the background (the `--detach` option) and auto-clean up the running container after stop (`--rm`). We're using the previously declared environment variables for hostname, user and password values: ```shell # Run a Docker image named postgres, set superuser password to $PGPASSWORD. docker run --name postgres --volume postgres:/var/lib/postgresql/data \ --rm --detach --publish $PGPORT:5432 --env POSTGRES_PASSWORD="$PGPASSWORD" postgres ``` The next sections will use the environment variables declared previously (and whichever database they point to). #### Create user and database Next, we'll create a user for the database and the database itself: ```shell # Below psql commands use PGHOST, PGPORT, PGUSER and PGPASSWORD to execute. psql -c "create user $DB_USERNAME with password '$DB_PASSWORD';" psql -c "create database $DB_NAME owner $DB_USERNAME;" ``` #### Apply database schema To initialize the `issuance_server` database schema, we will utilize the migration tool helper:

Applying the database schema using fresh is destructive

Applying the `issuance_server` database schema using the `fresh` argument is destructive! Any tables are cleared, data will be destroyed. Be sure you don't run this on a currently operational production copy of `issuance_server`. The migration tool also supports an ostensibly non-destructive argument `up` which would not re-initialize the entire database, but as of this writing (2025-10-26) we don't yet guarantee that our database initialization scripts are non-changing, and hence, `up` might not work as intended.
```shell cd nl-wallet DATABASE_URL="postgres://$DB_USERNAME:$DB_PASSWORD@$PGHOST:$PGPORT/$DB_NAME" \ cargo run \ --manifest-path wallet_core/wallet_server/issuance_server/Cargo.toml \ --package issuance_server_migrations \ --bin issuance_server_migrations -- fresh ``` You can show the configuration by issuing the following (might be a good idea to keep this safe somewhere): ```shell echo -e "\npostgres.host: '$PGHOST'\npostgres.port: '$PGPORT'\npostgres.user: '$PGUSER'\npostgres.pass: '$PGPASSWORD'\ndatabase.name: '$DB_NAME'\ndatabase.user: '$DB_USERNAME'\ndatabase.pass: '$DB_PASSWORD'\n" ``` ### Creating a configuration In the following sections we'll create the parts which will make up the `issuance_server.toml` configuration file. Sections marked as "(optional)" can be skipped. In the [final section]((#writing-the-configuration-file) we assemble the actual `issuance_server.toml` file.

Example configuration file

For reference, we have an annotated [example configuration file][24] which you can check for the various settings you can configure. We cover most (all?) of them here.

Similarity to verification_server

If you've also [created a verifier][9], this part of the documentation will look familiar, if slightly different here and there. This is in part because the configuration of an `issuance_server` relies for a significant part on the shared `wallet_server` code, which a `verification_server` also relies upon.
#### Logging settings (optional) To configure request logging and specify if we want the log output in the JSON format, we set the following: ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/01-logging-settings.toml" log_requests = true structured_logging = false EOF ```

Optional runtime logging using env_logger

In addition to the above, the NL Wallet uses [env_logger][17], which means you can use the `RUST_LOG` environment variable when running `issuance_server` later on. For example, to run with debug log output, you can prefix the command with the `RUST_LOG` environment variable: `RUST_LOG=debug ./issuance_server`
#### Configuring trust anchors [When you created the issuer, reader and tsl certificates](#creating-issuer-reader-and-tsl-certificates), you signed those certificates using a CA, either generated by the development setup script or specifically [created by you][27] as part of the (optional) [community onboarding process][11]. The `issuance_server` distinguishes two kinds of trust anchors: * `issuer_trust_anchors` - a string array of CA certificates which are considered trusted to sign issuer certificates, in DER format, base64 encoded; * `reader_trust_anchors` - a string array of CA certificates which are considered trusted to sign reader certificates, in DER format, base64 encoded; The trust anchor arrays tell the `issuance_server` which certificates it can trust. If an `issuance_server` is presented with certificates signed by a CA that is not in its trust anchor arrays, operations will fail (by design). We need to trust our own CA, whether it is created by the development setup scripts or explicitly by you. The development scripts create a separate CA for issuers and readers (usually at `scripts/devenv/target/ca.issuer.crt.der` and `scripts/devenv/target/ca.reader.crt.der`). When you create and use your own CA for community development purposes as [documented here][27], you can use that CA generally for signing both issuance and reader certificates, and hence, add it to both the issuer and reader trust anchors. The below code block will initialize the issuer and reader trust anchor environment variables with the CA certificates it can find, both generated by development scripts and any you created yourself, provided you [followed the CA creation instructions to the letter][27] and used the naming convention documented there which means you would have a `target/ca-cert` directory with your CA certificates in DER format there. The code block assumes you have the `nl-wallet` git repository checked out. ```shell cd nl-wallet export IS_ISSUER_TRUST_ANCHORS=() export IS_READER_TRUST_ANCHORS=() for i in scripts/devenv/target/ca.issuer.crt.der target/ca-cert/ca.*.crt.der; do \ [[ -f $i ]] && IS_ISSUER_TRUST_ANCHORS+=($(openssl base64 -e -A -in $i)); done for r in scripts/devenv/target/ca.reader.crt.der target/ca-cert/ca.*.crt.der; do \ [[ -f $r ]] && IS_READER_TRUST_ANCHORS+=($(openssl base64 -e -A -in $r)); done export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/03-trust-anchors.toml" issuer_trust_anchors = [$(printf '"%s",' "${IS_ISSUER_TRUST_ANCHORS[@]}" | sed 's/,$//')] reader_trust_anchors = [$(printf '"%s",' "${IS_READER_TRUST_ANCHORS[@]}" | sed 's/,$//')] EOF unset IS_ISSUER_TRUST_ANCHORS IS_READER_TRUST_ANCHORS ``` #### Determine public URL The `public_url` is the URL that is used by the NL Wallet app to reach the address and port of the `issuance_server`: ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/04-public-url.toml" public_url = "https://issuer.example.com/" EOF ```

Use a valid domain name here

In the above, we use `issuer.example.com` as the fully-qualified domain name. Technically, this domain needs not be world-reachable, but it does need to DNS resolve for the NL Wallet app and the `issuance_server`. Make sure you use a domain that is yours and that you control.

A note about allowed public URL schemes

When you [built or otherwise obtained](#obtaining-the-software) the software, you did **not** specify the `allow_insecure_url` feature flag. This means you cannot specify an `http://` URL here, and *need* to specify an `https://` URL.
#### Universal link base URL The `issuance_server` uses the universal link base URL to construct the correct environment-specific universal link. A universal link is used to to associate a specific domain name and/or part of an URL with a specific app on the mobile device. In our case, it results in the link provided by the `issuance_server` being handled by the NL Wallet app when a user clicks on the link or scans the QR code. A universal link base URL is usually associated with a specific backend environment like pre-production or testing. When you're integrating with the NL Wallet platform, you would use a universal link base URL that was provided to you as part of our community [onboarding][11] process. ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/05-universal-link-base-url.toml" universal_link_base_url = "https://app.example.com/ul/" EOF ```

Make sure your domain is configured correctly

You as the owner of the domain (`example.com` in the above example setting) need to make sure the domain is configured correctly for universal links to work correctly. On Apple iOS devices this is done with [associated domains][25]. On Google Android this is configured using [app links][26].
#### Configuring allowed client IDs You can restrict which NL Wallet apps are accepted by the `issuance_server` by configuring a `wallet_client_ids` array. The entries of this array would contain the `client_id` value of a wallet implementation. This allows you to allow-list groups of wallet apps based on their `client_id` value. For example, for allowing apps that have `https://wallet.edi.rijksoverheid.nl` configured as `client_id`: ```shell cd nl-wallet export IS_WALLET_CLIENT_IDS=("https://wallet.edi.rijksoverheid.nl") export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/06-wallet-client-ids.toml" wallet_client_ids = [$(printf '"%s",' "${IS_WALLET_CLIENT_IDS[@]}" | sed 's/,$//')] EOF unset IS_WALLET_CLIENT_IDS ``` #### Configuring metadata document references We [previously](#creating-the-technical-attestation-schema-json-document) made a technical attestation schema JSON document. The `issuance_server` needs to know about these schemas. We can tell the server about available schemas through the `metadata` setting. In this section, we're going to reference the previously created JSON document `insurance_metadata.json`, which, if you followed the instructions, was copied to `target/is-config` within the `nl-wallet` directory, where the `issuance_server` will find it using the below configuration (provided it is started from the `target/is-config` directory): ```shell cd nl-wallet export IS_WALLET_METADATA_FILES=("insurance_metadata.json") export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/08-wallet-metadata-files.toml" metadata = [$(printf '"%s",' "${IS_WALLET_METADATA_FILES[@]}" | sed 's/,$//')] EOF unset IS_WALLET_METADATA_FILES ``` #### Configuring listener address and port The server can be configured to listen on a single IP address and port. The address needs to be reachable by the NL Wallet mobile app: ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/09-listener-addresses-and-ports.toml" [wallet_server] ip = "0.0.0.0" port = 8001 EOF ```

Configure a correct IP address

In the above configuration settings, we set `0.0.0.0` as the address, which means the server binds to all network interfaces, on the specified port. This might be fine or it might not be in your specific case. If you need the server to bind to a specific IP address, specify that instead of `0.0.0.0`.
#### The storage settings (optional) The default storage settings URL is `memory://`, which causes the server to store session state in-memory, which is ephemeral - i.e., on server crash or shutdown, any existing session state is lost. If you don't have a `[storage]` section in your configuration, then `memory://` is used. ##### Using in-memory session state ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/10-storage-settings.toml" [storage] url = "memory://" EOF ``` ##### Using database persisted session state ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/10-storage-settings.toml" [storage] url = "postgres://$DB_USERNAME:$DB_PASSWORD@$PGHOST:$PGPORT/$DB_NAME" EOF ```

Make sure database setting environment variables are set

When you use the `postgres://` URL, you tell the server to store session state in a PostgreSQL database. In the above, we assume you still have the environment variables configured like we documented in the database configuration section (i.e., [Using a database backend](#setting-up-postgresql)).
#### Configuring a hardware security module (optional) You can opt to use a hardware security module (HSM) to store private keys for `disclosure_settings` and `attestation_settings`. To do so we need to configure a few things: ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/11-hardware-security-module.toml" [hsm] library_path = "/path/to/some/pkcs11-compatible/library.so" user_pin = "12345678" max_sessions = 3 max_session_lifetime_in_sec = 900 EOF ```

Make sure you specify a correct library path

The HSM functionality depends on a PKCS#11 compatible shared library which will have been provided by your HSM vendor. Technically you can also use any PKCS#11 implementation here. For development purposes we test with the [softhsm2][28] library, which is usually called something like `libsofthsm2.so` (the path location and filename extension differs per operating system and/or packaging environment).

Private key field needs to be a key label when using HSM type

When using a hardware security module, the `private_key` field of `disclosure_settings` and/or `attestation_settings` need to be the HSM key label.

It is possible to use *both* hardware *and* software private keys in the same `issuance_server` instance. Simply make sure you set `private_key_type` to `hsm` for HSM managed keys and to `software` when using base64 encoded DER strings in the `private_key` field.

#### Configuring the status list settings (optional) The issuance_server maintains binary format token status lists which are used for revocation and validity checking. We'll set the defaults here: ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/12-status-lists.toml" [status_lists] list_size = 100_000 create_threshold_ratio = 0.1 expiry_in_hours = 24 refresh_threshold_ratio = 0.25 serve = true EOF ``` By default the issuance server will serve the status lists it creates. You can opt out of this behaviour and serve the status lists yourself. Ensure that you map a request with URL path ending on `/id` to filesystem path of with name `id.jwt`. By default the status lists uses the same database as configured for the session store. If you have a memory store as session store you need to configure a PostgreSQL `storage_url` under the `[status_lists]` block. You can also configure a `storage_url` under the `[status_lists]` block if you want to store the status list in a different database or use a different user.

```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/12-status-lists.toml" [status_lists] url = "postgres://$DB_USERNAME:$DB_PASSWORD@$PGHOST:$PGPORT/$DB_NAME" EOF ``` #### Configuring disclosure-based issuance elements We're now going to configure the configuration blocks that together make up a disclosure-based issuance element. We're going to call our `insurance` elements as follows: * `[disclosure_settings.insurance]` * `[[disclosure_settings.insurance.dcql_query.credentials]]` * `[disclosure_settings.insurance.attestation_url_config]` * `[attestation_settings.insurance]` In the next sub-sections we'll cover each one of these. ##### The disclosure settings We're going to base64 encode the reader key and certificate within the `private_key` and `certificate` fields of the `disclosure_settings`. This is the certificate that embedded the previously created `reader_auth.json`. Let's create the section: ```shell cd nl-wallet export BASE64="openssl base64 -e -A" export IDENTIFIER="foocorp" export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/13-disclosure-settings.toml" [disclosure_settings.insurance] private_key_type = "software" private_key = "$(< "${TARGET_DIR}/reader.${IDENTIFIER}.key.der" $BASE64)" certificate = "$(< "${TARGET_DIR}/reader.${IDENTIFIER}.crt.der" $BASE64)" EOF unset BASE64 IDENTIFIER ``` ##### The DCQL query credentials Add the [DCQL query credentials][29] (note, the double brackets, i.e., `[[`, and `]]`, are intentional): ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/14-dcql-query-credentials.toml" [[disclosure_settings.insurance.dcql_query.credentials]] id = "insurance_credential" format = "dc+sd-jwt" meta = { vct_values = ["urn:eudi:pid:nl:1"] } claims = [ { path = ["urn:eudi:pid:nl:1", "bsn"], intent_to_retain = false } ] EOF ``` ##### The attestation URL configuration The attestation URL configuration section configures where the `issuance_server` is expected to fetch its attestable attributes from. The `base_url` setting points to the attestation server (when you run in a local development environment, this is the `demo_issuer`, running on port `3006`): ```shell cd nl-wallet export TRUST_ANCHORS=() for i in scripts/devenv/target/demo_issuer/ca.crt.der target/ca-cert/ca.*.crt.der; do \ [[ -f $i ]] && TRUST_ANCHORS+=($(openssl base64 -e -A -in $i)); done export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/15-attestation-url-config.toml" [disclosure_settings.insurance.attestation_url_config] base_url = "https://your-attestation-server.example.com/insurance" trust_anchors = [$(printf '"%s",' "${TRUST_ANCHORS[@]}" | sed 's/,$//')] EOF unset TRUST_ANCHORS ``` ##### The attestation settings We're now going to base64 encode the issuer key and certificate within the `private_key` and `certificate` fields of the `attestation_settings`. This is the certificate that embedded the previously created `issuer_auth.json`. Let's create the section: ```shell cd nl-wallet export BASE64="openssl base64 -e -A" export IDENTIFIER="foocorp" export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/16-attestation-settings.toml" [attestation_settings.insurance] valid_days = 365 copies_per_format = { "mso_mdoc" = 4, "dc+sd-jwt" = 4 } private_key_type = "software" private_key = "$(< "${TARGET_DIR}/issuer.${IDENTIFIER}.key.der" $BASE64)" certificate = "$(< "${TARGET_DIR}/issuer.${IDENTIFIER}.crt.der" $BASE64)" EOF ``` ##### The attestation settings associated status list settings Next to the previously done `[status_lists]` settings, which control how big and when status lists are created, an `attestation_settings` associated `status_list` block. This block configures the `context_path`, which together with a `base_url` (if empty the `public_url` of the issuance server is used) is added to the attestation as a location for the Status List Claim. This url needs to be publicly reachable by the NL Wallet app and the verifiers. Next to the location a `publish_dir` needs to be configured where the status lists need to be published. This can be served via either a separate static file server or via the issuance server. Additionally, the `private_key` settings are used to sign the Status Token List, which the wallet and the `verification_server` validate when interacting with Status Token Lists. The service pointed to by `base_url` needs to be publicly reachable by the NL Wallet app and the verifiers. Let's create it: ```shell cd nl-wallet export BASE64="openssl base64 -e -A" export IDENTIFIER="foocorp" export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat < "$TARGET_DIR/parts/17-attestation-status-list.toml" [attestation_settings.insurance.status_list] context_path = "/tsl" publish_dir = "/srv/html/tsl" private_key_type = "software" private_key = "$(< "${TARGET_DIR}/tsl.${IDENTIFIER}.key.der" $BASE64)" certificate = "$(< "${TARGET_DIR}/tsl.${IDENTIFIER}.crt.der" $BASE64)" EOF ``` #### Writing the configuration file In the previous sections, you've created a bunch of partial configuration blocks which we will use in this section to generate our `issuance_server.toml` configuration file. To generate our configuration file, issue the following command: ```shell cd nl-wallet export TARGET_DIR=target/is-config && mkdir -p "$TARGET_DIR/parts" cat "$TARGET_DIR"/parts/*.toml > "$TARGET_DIR/issuance_server.toml" ``` You should now have a configuration file in the `$TARGET` directory called `issuance_server.toml`. Feel free to check the file to see if everything looks like you'd expect. ### Running the server for the first time In section [Obtaining the software](#obtaining-the-software) we have described how you can obtain the software. In this section, we assume you have a Linux AMD64 static executable called `issuance_server` that you can run. We're going to `cd` into the `target/is-config` directory, and we assume the binary exists there (it does if you [followed along](#obtaining-the-software) previously): ```shell cd nl-wallet/target/is-config ./issuance_server ``` If all went well, the server is now running and ready to serve requests. To test the service, you can send session initiation and status requests to it (check out the [API specifications](#issuer-api-specifications) section for how to do that). Make sure to consider your [logging settings](#logging-settings-optional) if you need to troubleshoot. ### Validating your setup During startup, the `issuance_server` performs some checks on the configuration to prevent common configuration problems. Most notably the following checks are performed: - Verify all `disclosure_settings` and `attestation_settings` certificates are valid; - Verify all `disclosure_settings` and `attestation_settings` certificates are signed by any of the `reader_trust_anchors` and `issuer_trust_anchors`; - Verify all `disclosure_settings` certificates are valid reader-certificates, and contain the necessary Extended Key Usages and the `reader_auth.json`; - Verify all `attestation_settings` certificates are valid issuer-certificates, and contain the necessary Extended Key Usages and the `issuer_auth.json`; - Verify all `disclosure_settings` and `attestation_settings` key-pairs are valid, i.e., the public and private keys should belong together; If this process discovers any configuration errors, the application will report an error and abort. For more insights into this process, [enable logging](#logging-settings-optional). ## Issuer API specifications The API specifications for the issuer endpoints are available in the `wallet_docs/openapi` part of of the git repository. Have a look at the [OpenAPI Specifications][30] section to learn how to open and use these. ## Integrating your app with your issuance server The wallet starts disclosure based issuance if it encounters a UL (or a QR with a UL within it) of a specific format. To create this UL, proceed as follows. 1. If your `issuance_server` is reachable on the internet by the wallet at `https://issuer.example.com`, create a URL of the following form: ``` https://issuer.example.com/disclosure/foo/request_uri?session_type=same_device ``` In the above URL, `foo` has to be the identifier you used when you configured the disclosure-based issuance settings in the configuration file (so, it relates to `[disclosure_settings.foo]`, `[disclosure_settings.foo.dcql_query.credentials]`, and `[disclosure_settings.foo.attestation_url_config]`) 2. URL-encode the above URL. 3. Create the UL as follows (newlines only for readability purposes): ``` https://app.test.voorbeeldwallet.nl/deeplink/disclosure_based_issuance ?request_uri_method=post &client_id=disclosure_based_issuance.example.com &request_uri=https%3A%2F%2Fissuer.example.com... ``` In which the `client_id` has to be the SAN DNS name from the RP `certificate`, and the `request_uri` is the URL-encoded URL from the previous step. 4. Place this UL on your website (within in a QR code in case of cross device flows). [1]: https://github.com/minbzk/nl-wallet [2]: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html [3]: https://www.logius.nl/onze-dienstverlening/toegang/digid [4]: https://github.com/minvws/nl-rdo-max [5]: https://www.rvig.nl/basisregistratie-personen [6]: https://edi.pleio.nl/news/view/93f40956-3671-49c9-9c82-2dab636b59bf/psasad-documenten-nl-wallet [7]: ../architecture/use-cases/issuance-with-openid4vci [8]: ../architecture/use-cases/disclosure-based-issuance [9]: create-a-verifier [11]: ../community/onboarding [17]: https://docs.rs/env_logger/latest/env_logger/#enabling-logging [19]: https://github.com/MinBZK/nl-wallet#user-content-development-requirements [20]: https://github.com/MinBZK/nl-wallet/releases [22]: https://www.postgresql.org/download/ [23]: https://www.docker.com/ [24]: https://github.com/MinBZK/nl-wallet/blob/main/wallet_core/wallet_server/issuance_server/issuance_server.example.toml [25]: https://developer.apple.com/documentation/xcode/supporting-associated-domains [26]: https://developer.android.com/training/app-links [27]: ../community/create-a-ca [28]: https://github.com/softhsm/SoftHSMv2 [29]: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-digital-credentials-query-l [30]: ../development/openapi-specifications [31]: create-a-verifier [32]: ../development/authorized-attributes