> For the complete documentation index, see [llms.txt](https://docs.meeco.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.meeco.me/guides/certificates.md).

# Certificates

Certificates are used to establish trust between parties in [OID4VCI](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html) and [OID4VP](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) flows. They are included in the `x5c` header of issued credentials and presentation requests, allowing verifiers and wallets to validate the certificate chain back to a trusted root.

SVX Wallet supports multiple key types, but the two most relevant for certificate-backed trust are:

* **Credential key** – used to sign credentials (for example `mso_mdoc` Document Signing Certificate, DSC).
* **Presentation request key** – used to sign presentation requests by the verifier, establishing verifier identity.

## How Trust Is Established

A signature on its own does not identify a signer. A Verifier receiving a credential can check that it was signed by some key, but needs a reason to believe that key belongs to the Issuer named in the credential. The certificate chain supplies that reason: the signing key's certificate is signed by a root authority, and the Verifier accepts the credential only if that root is one it already trusts. The same applies in reverse when a wallet receives a presentation request and needs to establish which organisation is asking.

Two consequences shape the steps below.

**Two separate roots.** This guide creates one root authority for credential signing and another for presentation request signing. *Who may issue credentials* and *who may request them* are distinct decisions, often made by different governance bodies, so separate roots keep the two independent. In mobile document ecosystems the credential root is known as the IACA (Issuer Authority Certificate Authority).

**Signing requests rather than key uploads.** Signing keys are generated inside the Wallet's key management system and their private halves are never exported. Certificates are therefore obtained by generating a Certificate Signing Request against a named key — proving possession of the private key without revealing it — signing that request with your root CA, and importing the result.

For the concepts behind this, including how trust anchors and trust lists work, see [Certificates and Trust Chains](/concepts/certificates-and-trust-chains.md).

## Setup Overview

The typical setup flow is:

1. Create a root CA locally.
2. Create a Certificate Signing Request (CSR) via the SVX Wallet API.
3. Sign the CSR with your root CA using `openssl`.
4. Import the signed certificate back into the SVX Wallet API.
5. Optionally import trust anchor certificates for external chain validation.

## Who can undertake this operation?

Certificate management can only be performed by people who have access to the SVX Wallet Dashboard and API.

## Create a Root CA

Before creating CSRs, you need a root CA to sign leaf certificates. The following examples create a self-signed root CA for credential signing (IACA) and a separate root for presentation request signing.

```bash
# Create a root CA for credential signing (IACA)
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
  -x509 -sha256 -days 1825 \
  -keyout meeco_iaca.key \
  -out meeco_iaca.cer \
  -subj "/C=AU/O=Meeco/CN=Meeco IACA" \
  -addext "basicConstraints=critical,CA:true,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash" \
  -addext "subjectAltName=URI:<issuer_uri>"

# Create a root CA for presentation request signing
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
  -x509 -sha256 -days 1825 \
  -keyout meeco_reader_root.key \
  -out meeco_reader_root.cer \
  -subj "/C=AU/O=Meeco/CN=Meeco" \
  -addext "basicConstraints=critical,CA:true,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash" \
  -addext "subjectAltName=URI:<issuer_uri>"
```

## Create a CSR

CSRs are created via the SVX Wallet API against a named key. The API returns the CSR PEM for external signing.

Create a CSR for the credential signing key:

```bash
curl -sS -X POST "$SVX_WALLET_BASE_URL/system/certificates/csrs" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "key_name": "CredentialKey",
    "subject": {
      "common_name": "wallet.meeco.cloud",
      "country": "AU",
      "state": "New South Wales",
      "locality": "Sydney",
      "organization": "Meeco",
      "organizational_unit": "Identity"
    },
    "issuer_alternative_name_url": [
      "DNS:wallet.meeco.cloud"
    ]
  }' | tee credential-csr.json

jq -r '.csr.csr_pem' credential-csr.json > credential.csr.pem
```

Create a CSR for the presentation request signing key:

```bash
curl -sS -X POST "$SVX_WALLET_BASE_URL/system/certificates/csrs" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "key_name": "PresentationRequestKey",
    "subject": {
      "common_name": "wallet.meeco.cloud",
      "country": "AU",
      "state": "New South Wales",
      "locality": "Sydney",
      "organization": "Meeco",
      "organizational_unit": "Identity"
    },
    "issuer_alternative_name_url": [
      "DNS:wallet.meeco.cloud"
    ]
  }' | tee request-csr.json

jq -r '.csr.csr_pem' request-csr.json > request.csr.pem
```

## Sign the CSRs

Use `openssl` to sign each CSR with the appropriate root CA. Certificate extensions differ depending on the intended key usage.

Save the following extension configuration to a file (e.g. `openssl-extensions.ext`):

> **Note:** `openssl x509 -req` does not automatically copy all attributes from the CSR into the signed certificate. Properties such as `subjectAltName`, `keyUsage`, and `extendedKeyUsage` must be explicitly defined in the extension file to ensure they appear correctly in the issued certificate.

```ini
[ v3_mdoc_dsc ]
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature
extendedKeyUsage = 1.0.18013.5.1.2,1.3.6.1.4.1.61546.0
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName = DNS:wallet.meeco.cloud

[ v3_request ]
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName = DNS:wallet.meeco.cloud
```

Sign the credential CSR (DSC):

```bash
openssl x509 -req -sha256 \
  -in credential.csr.pem \
  -CA meeco_iaca.cer \
  -CAkey meeco_iaca.key \
  -CAcreateserial \
  -out credential-leaf.cert.pem \
  -days 365 \
  -extfile openssl-extensions.ext \
  -extensions v3_mdoc_dsc
```

Sign the presentation request CSR:

```bash
openssl x509 -req -sha256 \
  -in request.csr.pem \
  -CA meeco_reader_root.cer \
  -CAkey meeco_reader_root.key \
  -CAcreateserial \
  -out request-leaf.cert.pem \
  -days 365 \
  -extfile openssl-extensions.ext \
  -extensions v3_request
```

## Import Signed Certificates

Once signed, import each leaf certificate the SVX Wallet API. The certificate must be DER-encoded and base64-encoded in the `x5c` field.

```bash
# Import credential leaf certificate
curl -sS -X POST "$SVX_WALLET_BASE_URL/system/certificates/import" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"key_name\":\"CredentialKey\",\"x5c\":[\"$(openssl x509 -in credential-leaf.cert.pem -outform DER | openssl base64 -A)\"]}" | jq

# Import presentation request leaf certificate
curl -sS -X POST "$SVX_WALLET_BASE_URL/system/certificates/import" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"key_name\":\"PresentationRequestKey\",\"x5c\":[\"$(openssl x509 -in request-leaf.cert.pem -outform DER | openssl base64 -A)\"]}" | jq
```

## Import Trust Anchor Certificates

Trust anchor certificates allow the wallet to validate incoming certificate chains from external issuers or verifiers.

Import a trust anchor certificate chain:

```bash
curl -X POST "$SVX_WALLET_BASE_URL/system/certificates/import" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"x5c\":[\"$(openssl x509 -in root-ca.cert.pem -outform DER | openssl base64 -A)\"]}"
```

## Remaining endpoints

The following endpoints are used to maintain and read certificate information:

* `GET /system/certificates` - List all imported trust anchor certificates.
* `DELETE /system/certificates/{certificate_id}` - Delete a trust anchor certificate.

## API References

## Import certificate

> Import certificate material in \`x5c\` form (DER-encoded, base64-encoded items).\
> \
> Behavior depends on whether \`key\_name\` is provided:\
> \- With \`key\_name\`: imports a \`managed\` certificate for that managed signing key.\
> &#x20; \- \`x5c\` must contain at least one certificate.\
> &#x20; \- The first item (leaf certificate) public key must match the managed key.\
> &#x20; \- The full provided chain is stored.\
> &#x20; \- \`fingerprint256\` is stored as \`null\`.\
> \- Without \`key\_name\`: imports a \`trust\_anchor\` certificate.\
> &#x20; \- \`x5c\` must contain exactly one certificate.\
> &#x20; \- \`fingerprint256\` is computed and stored.\
> \
> Notes:\
> \- For \`mso\_mdoc\` (mDL) credential issuance the credential key must have a valid DSC certificate; issuance\
> &#x20; will fail without it (see ISO 18013-5 for mDL DSC requirements).\
> \- When signing CSRs externally (for example with \`openssl x509 -req\`), explicitly provide extensions\
> &#x20; such as \`subjectAltName\`, \`keyUsage\`, and \`extendedKeyUsage\` so they are present in the issued certificate.<br>

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"tags":[],"servers":[{"url":"/","description":"SVX Wallet deployment (relative)"}],"security":[{},{"bearerAuth":[]},{"mutualTLS":[]}],"components":{"securitySchemes":{"bearerAuth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"},"mutualTLS":{"type":"mutualTLS"}},"schemas":{"ImportCertificateRequestDto":{"type":"object","properties":{"x5c":{"type":"array","minItems":1,"description":"Certificate chain in DER base64 format.\n- With `key_name`, provide at least the leaf certificate (full chain is accepted and stored).\n- Without `key_name`, exactly one certificate is required.\n","items":{"type":"string"}},"key_name":{"type":"string","description":"Optional managed signing key name. When present, imports as `managed`."}},"required":["x5c"]},"CertificateResponseDto":{"type":"object","properties":{"certificate":{"$ref":"#/components/schemas/CertificateModelDto"}},"required":["certificate"]},"CertificateModelDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["trust_anchor","managed","iaca"]},"key_name":{"anyOf":[{"type":"string"},{"type":"null"}]},"kid":{"description":"Key identifier of the signing key. Present for `managed`; `null` for `trust_anchor`.","anyOf":[{"type":"string"},{"type":"null"}]},"x5c":{"type":"array","minItems":1,"items":{"type":"string"}},"fingerprint256":{"description":"Present for `trust_anchor`; `null` for `managed`.","anyOf":[{"type":"string"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"}},"required":["id","type","key_name","x5c","fingerprint256","created_at"]}}},"paths":{"/system/certificates/import":{"post":{"summary":"Import certificate","description":"Import certificate material in `x5c` form (DER-encoded, base64-encoded items).\n\nBehavior depends on whether `key_name` is provided:\n- With `key_name`: imports a `managed` certificate for that managed signing key.\n  - `x5c` must contain at least one certificate.\n  - The first item (leaf certificate) public key must match the managed key.\n  - The full provided chain is stored.\n  - `fingerprint256` is stored as `null`.\n- Without `key_name`: imports a `trust_anchor` certificate.\n  - `x5c` must contain exactly one certificate.\n  - `fingerprint256` is computed and stored.\n\nNotes:\n- For `mso_mdoc` (mDL) credential issuance the credential key must have a valid DSC certificate; issuance\n  will fail without it (see ISO 18013-5 for mDL DSC requirements).\n- When signing CSRs externally (for example with `openssl x509 -req`), explicitly provide extensions\n  such as `subjectAltName`, `keyUsage`, and `extendedKeyUsage` so they are present in the issued certificate.\n","operationId":"CertificatesController_importCertificate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportCertificateRequestDto"}}}},"responses":{"201":{"description":"Certificate imported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificateResponseDto"}}}}},"tags":["Certificates"]}}}}
```

## List certificates

> Returns a paginated list of imported certificates and metadata. The list can be filtered by \`type\`,\
> sorted, and paginated by using the available query parameters.\
> \
> Certificates are stored by type:\
> \- \`managed\`: bound to a managed signing key (\`key\_name\` present), may contain a full \`x5c\` chain,\
> &#x20; and has \`fingerprint256 = null\`.\
> \- \`trust\_anchor\`: imported without \`key\_name\`, must contain exactly one certificate in \`x5c\`, and has\
> &#x20; a non-null \`fingerprint256\`.\
> \- \`iaca\`: Issuer Authority Certificate Authority certificate.\
> \
> The response includes certificate id, type, associated \`key\_name\` (when bound), the stored \`x5c\`,\
> and \`fingerprint256\` according to the certificate type rules above.<br>

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"tags":[],"servers":[{"url":"/","description":"SVX Wallet deployment (relative)"}],"security":[{},{"bearerAuth":[]},{"mutualTLS":[]}],"components":{"securitySchemes":{"bearerAuth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"},"mutualTLS":{"type":"mutualTLS"}},"schemas":{"CertificatesResponseDto":{"type":"object","properties":{"certificates":{"type":"array","items":{"$ref":"#/components/schemas/CertificateModelDto"}},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["certificates","meta"]},"CertificateModelDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["trust_anchor","managed","iaca"]},"key_name":{"anyOf":[{"type":"string"},{"type":"null"}]},"kid":{"description":"Key identifier of the signing key. Present for `managed`; `null` for `trust_anchor`.","anyOf":[{"type":"string"},{"type":"null"}]},"x5c":{"type":"array","minItems":1,"items":{"type":"string"}},"fingerprint256":{"description":"Present for `trust_anchor`; `null` for `managed`.","anyOf":[{"type":"string"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"}},"required":["id","type","key_name","x5c","fingerprint256","created_at"]},"Meta":{"type":"object","properties":{"order_by":{"type":"string"},"order":{"type":"string","enum":["ASC","DESC"]},"order_from_params":{"type":"boolean"},"per_page":{"type":"number"},"per_page_from_params":{"type":"boolean"},"records_count":{"type":"number"},"page":{"type":"number"},"page_count":{"type":"number"}},"required":["order_by","order","order_from_params","per_page","per_page_from_params","records_count","page","page_count"]},"InvalidPaginationParamError":{"properties":{"error":{"description":"Unique error identifier","enum":["invalid_pagination_param"],"type":"string"},"extra_info":{"description":"Object that may contain more information about the error","type":"object"},"message":{"description":"User friendly error message","type":"string"}},"required":["error","message","extra_info"],"type":"object"}}},"paths":{"/system/certificates":{"get":{"summary":"List certificates","description":"Returns a paginated list of imported certificates and metadata. The list can be filtered by `type`,\nsorted, and paginated by using the available query parameters.\n\nCertificates are stored by type:\n- `managed`: bound to a managed signing key (`key_name` present), may contain a full `x5c` chain,\n  and has `fingerprint256 = null`.\n- `trust_anchor`: imported without `key_name`, must contain exactly one certificate in `x5c`, and has\n  a non-null `fingerprint256`.\n- `iaca`: Issuer Authority Certificate Authority certificate.\n\nThe response includes certificate id, type, associated `key_name` (when bound), the stored `x5c`,\nand `fingerprint256` according to the certificate type rules above.\n","operationId":"CertificatesController_listCertificates","parameters":[{"in":"query","name":"type","required":false,"description":"Filters the list to certificates of the given type.","schema":{"type":"string","enum":["trust_anchor","managed","iaca"]}},{"in":"query","name":"order","required":false,"schema":{"type":"string","enum":["ASC","DESC"]}},{"in":"query","name":"order_by","required":false,"schema":{"type":"string"}},{"in":"query","name":"per_page","required":false,"schema":{"type":"integer","minimum":1,"maximum":50}},{"in":"query","name":"page","required":false,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Certificates list retrieved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificatesResponseDto"}}}},"400":{"description":"Invalid query params","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvalidPaginationParamError"}}}}},"tags":["Certificates"]}}}}
```

## Delete certificate

> Delete an imported certificate by identifier.\
> \
> Removal deletes either:\
> \- a \`managed\` key-bound certificate chain, or\
> \- a trust entry (\`trust\_anchor\`).\
> \
> Removing a credential signing certificate can break \`mso\_mdoc\` issuance.<br>

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"tags":[],"servers":[{"url":"/","description":"SVX Wallet deployment (relative)"}],"security":[{},{"bearerAuth":[]},{"mutualTLS":[]}],"components":{"securitySchemes":{"bearerAuth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"},"mutualTLS":{"type":"mutualTLS"}}},"paths":{"/system/certificates/{certificate_id}":{"delete":{"summary":"Delete certificate","description":"Delete an imported certificate by identifier.\n\nRemoval deletes either:\n- a `managed` key-bound certificate chain, or\n- a trust entry (`trust_anchor`).\n\nRemoving a credential signing certificate can break `mso_mdoc` issuance.\n","operationId":"CertificatesController_deleteCertificate","parameters":[{"name":"certificate_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Certificate ID"}],"responses":{"204":{"description":"Certificate deleted"}},"tags":["Certificates"]}}}}
```

## The TrustedCertificateNotFoundError object

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"components":{"schemas":{"TrustedCertificateNotFoundError":{"properties":{"error":{"description":"Unique error identifier","enum":["trusted_certificate_not_found"],"type":"string"},"extra_info":{"description":"Object that may contain more information about the error","type":"object"},"message":{"description":"User friendly error message","type":"string"}},"required":["error","message","extra_info"],"type":"object"}}}}
```

## The InvalidTrustedCertificateFormatError object

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"components":{"schemas":{"InvalidTrustedCertificateFormatError":{"properties":{"error":{"description":"Unique error identifier","enum":["invalid_trusted_certificate_format"],"type":"string"},"extra_info":{"description":"Object that may contain more information about the error","type":"object"},"message":{"description":"User friendly error message","type":"string"}},"required":["error","message","extra_info"],"type":"object"}}}}
```

## The FailedToLoadCertificateError object

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"components":{"schemas":{"FailedToLoadCertificateError":{"properties":{"error":{"description":"Unique error identifier","enum":["failed_to_load_certificate"],"type":"string"},"extra_info":{"description":"Object that may contain more information about the error","type":"object"},"message":{"description":"User friendly error message","type":"string"}},"required":["error","message","extra_info"],"type":"object"}}}}
```

## The CertificateAlreadyRegistered object

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"components":{"schemas":{"CertificateAlreadyRegistered":{"properties":{"error":{"description":"Unique error identifier","enum":["certificate_already_registered"],"type":"string"},"extra_info":{"description":"Object that may contain more information about the error","type":"object"},"message":{"description":"User friendly error message","type":"string"}},"required":["error","message","extra_info"],"type":"object"}}}}
```

## The CertificateSubjectDto object

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"components":{"schemas":{"CertificateSubjectDto":{"type":"object","properties":{"common_name":{"type":"string"},"country":{"type":"string"},"state":{"type":"string"},"locality":{"type":"string"},"organization":{"type":"string"},"organizational_unit":{"type":"string"}},"required":["common_name"]}}}}
```

## The CreateCsrRequestDto object

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"components":{"schemas":{"CreateCsrRequestDto":{"type":"object","properties":{"key_name":{"type":"string","description":"Managed signing key identifier","enum":["AccessTokenKey","CredentialKey","PresentationRequestKey","AdminSigningKey"]},"subject":{"$ref":"#/components/schemas/CertificateSubjectDto"},"issuer_alternative_name_url":{"type":"array","items":{"type":"string"}}},"required":["key_name","subject"]},"CertificateSubjectDto":{"type":"object","properties":{"common_name":{"type":"string"},"country":{"type":"string"},"state":{"type":"string"},"locality":{"type":"string"},"organization":{"type":"string"},"organizational_unit":{"type":"string"}},"required":["common_name"]}}}}
```

## The ImportCertificateRequestDto object

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"components":{"schemas":{"ImportCertificateRequestDto":{"type":"object","properties":{"x5c":{"type":"array","minItems":1,"description":"Certificate chain in DER base64 format.\n- With `key_name`, provide at least the leaf certificate (full chain is accepted and stored).\n- Without `key_name`, exactly one certificate is required.\n","items":{"type":"string"}},"key_name":{"type":"string","description":"Optional managed signing key name. When present, imports as `managed`."}},"required":["x5c"]}}}}
```

## The CsrResponseDto object

```json
{"openapi":"3.1.0","info":{"title":"SVX Wallet","version":"1.2.0"},"components":{"schemas":{"CsrResponseDto":{"type":"object","properties":{"csr":{"type":"object","properties":{"key_name":{"type":"string"},"csr_pem":{"type":"string"},"created_at":{"type":"string","format":"date-time"}},"required":["key_name","csr_pem","created_at"]}},"required":["csr"]}}}}
```
