# Task: Your certificates in the browser

I want my application to do what Lacuna's "Your certificates in the browser" demo does: List the certificates on your computer with Web PKI and prove you hold a key, checked by Rest PKI Core.

- Docs: https://docs.lacunasoftware.com/en-us/articles/web-pki
- Live demo: https://try.lacunasoftware.com/en/web-pki/certificates-in-the-browser
- This demo for AI agents: https://try.lacunasoftware.com/en/web-pki/certificates-in-the-browser.md

## Samples

- .NET (aspnet-mvc): [List the user's certificates with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/ListCertJQueryController.cs)
- .NET (aspnet-mvc): [Read a certificate's details with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/ReadCertJQueryController.cs)
- Java (spring-boot): [Authenticate with a digital certificate using Rest PKI Core](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/AuthenticationRestPkiCoreController.java)
- Java (spring-boot): [List the user's certificates with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/ListCertJQueryController.java)
- Java (spring-boot): [Read a certificate's details with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/ReadCertJQueryController.java)
- Node.js (express): [List the user's certificates with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/list-cert-jquery.js)
- Node.js (express): [Read a certificate's details with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/read-cert-jquery.js)
- Python (flask): [List the user's certificates with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/python/flask/sample/views/list_cert_jquery.py)
- Python (flask): [Read a certificate's details with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/python/flask/sample/views/read_cert_jquery.py)
- Ruby (rails): [List the user's certificates with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/ruby/rails/app/controllers/list_cert_jquery_controller.rb)
- Ruby (rails): [Read a certificate's details with Web PKI](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/ruby/rails/app/controllers/read_cert_jquery_controller.rb)

## Call sequence

Recorded on 2026-10-12 against the real products with Lacuna's public test certificates and data. Keys, tokens, sessions, ids, file contents and personal data are masked.

### 1. List the visitor's certificates, with no filter.

In the browser: `pki.listCertificates()`

### 2. Rest PKI Core prepares an authentication in the Demo's security context and returns the nonce to sign and the state to keep.

On the server: `IRestPkiService.PrepareAuthenticationAsync`

`POST https://core.pki.rest/api/v2/authentication` → 200

Request:

```json
{
  "SecurityContextId": "00000000-0000-0000-0000-000000000000"
}
```

Response:

```json
{
  "state": "***",
  "toSign": {
    "signatureAlgorithm": {
      "algorithm": "SHA256WithRSA",
      "oid": "1.2.840.113549.1.1.11",
      "digestAlgorithm": "SHA256",
      "digestAlgorithmOid": "2.16.840.1.101.3.4.2.1"
    },
    "data": "<data, 16 bytes>",
    "hash": "<hash, 32 bytes>",
    "digestInfo": "<base64, 51 bytes>"
  },
  "toSignHash": {
    "algorithm": "SHA256",
    "value": "<base64, 32 bytes>",
    "hexValue": "<hex, 32 bytes>"
  }
}
```

### 3. Read the chosen certificate's public part: no PIN is asked, and the key stays where it is.

In the browser: `pki.readCertificate({ thumbprint })`

### 4. Sign the nonce over SHA-256 with the certificate's private key, behind the system's PIN prompt: the key never leaves the browser's store or token.

In the browser: `pki.signData({ thumbprint, data, digestAlgorithm: "SHA-256" })`

### 5. Rest PKI Core completes the authentication with the state, the certificate and the signature: it checks the signature, then validates the certificate, for the result lines.

On the server: `IRestPkiService.CompleteAuthenticationAsync`

`POST https://core.pki.rest/api/v2/authentication/completion` → 200

Request:

```json
{
  "State": "***",
  "Certificate": "<Certificate, 1645 bytes>",
  "Signature": "<Signature, 256 bytes>"
}
```

Response:

```json
{
  "success": true,
  "certificate": {
    "subjectName": {
      "country": "BR",
      "organization": "Lacuna Software",
      "commonName": "Alan Mathison Turing",
      "allValues": {
        "2.5.4.6": [
          "BR"
        ],
        "2.5.4.10": [
          "Lacuna Software"
        ],
        "2.5.4.3": [
          "Alan Mathison Turing"
        ]
      },
      "dnString": "CN=Alan Mathison Turing,O=Lacuna Software,C=BR"
    },
    "issuerName": {
      "country": "BR",
      "organization": "Lacuna Software",
      "organizationUnit": "IT",
      "commonName": "Lacuna CA Test v7",
      "allValues": {
        "2.5.4.6": [
          "BR"
        ],
        "2.5.4.10": [
          "Lacuna Software"
        ],
        "2.5.4.11": [
          "IT"
        ],
        "2.5.4.3": [
          "Lacuna CA Test v7"
        ]
      },
      "dnString": "CN=Lacuna CA Test v7,OU=IT,O=Lacuna Software,C=BR"
    },
    "issuerDisplayName": "Lacuna CA Test v7",
    "serialNumber": "<hex, 19 bytes>",
    "validityStart": "2025-01-03T14:16:57+00:00",
    "validityEnd": "2075-01-03T14:21:14+00:00",
    "issuer": {
      "subjectName": {
        "country": "BR",
        "organization": "Lacuna Software",
        "organizationUnit": "IT",
        "commonName": "Lacuna CA Test v7",
        "allValues": {
          "2.5.4.6": [
            "BR"
          ],
          "2.5.4.10": [
            "Lacuna Software"
          ],
          "2.5.4.11": [
            "IT"
          ],
          "2.5.4.3": [
            "Lacuna CA Test v7"
          ]
        },
        "dnString": "CN=Lacuna CA Test v7,OU=IT,O=Lacuna Software,C=BR"
      },
      "issuerName": {
        "country": "BR",
        "organization": "ICP-Brasil",
        "organizationUnit": "Lacuna Software - LS",
        "commonName": "Lacuna Root Test v3",
        "allValues": {
          "2.5.4.6": [
            "BR"
          ],
          "2.5.4.10": [
            "ICP-Brasil"
          ],
          "2.5.4.11": [
            "Lacuna Software - LS"
          ],
          "2.5.4.3": [
            "Lacuna Root Test v3"
          ]
        },
        "dnString": "CN=Lacuna Root Test v3,OU=Lacuna Software - LS,O=ICP-Brasil,C=BR"
      },
      "issuerDisplayName": "Lacuna Root Test v3",
      "serialNumber": "3",
      "validityStart": "2024-12-16T21:30:30+00:00",
      "validityEnd": "2074-12-16T03:00:00+00:00",
      "issuer": {
        "subjectName": {
          "country": "BR",
          "organization": "ICP-Brasil",
          "organizationUnit": "Lacuna Software - LS",
          "commonName": "Lacuna Root Test v3",
          "allValues": {
            "2.5.4.6": [
              "BR"
            ],
            "2.5.4.10": [
              "ICP-Brasil"
            ],
            "2.5.4.11": [
              "Lacuna Software - LS"
            ],
            "2.5.4.3": [
              "Lacuna Root Test v3"
            ]
          },
          "dnString": "CN=Lacuna Root Test v3,OU=Lacuna Software - LS,O=ICP-Brasil,C=BR"
        },
        "issuerName": {
          "country": "BR",
          "organization": "ICP-Brasil",
          "organizationUnit": "Lacuna Software - LS",
          "commonName": "Lacuna Root Test v3",
          "allValues": {
            "2.5.4.6": [
              "BR"
            ],
            "2.5.4.10": [
              "ICP-Brasil"
            ],
            "2.5.4.11": [
              "Lacuna Software - LS"
            ],
            "2.5.4.3": [
              "Lacuna Root Test v3"
            ]
          },
          "dnString": "CN=Lacuna Root Test v3,OU=Lacuna Software - LS,O=ICP-Brasil,C=BR"
        },
        "issuerDisplayName": "Lacuna Root Test v3",
        "serialNumber": "1",
        "validityStart": "2024-12-16T20:42:10+00:00",
        "validityEnd": "2074-12-16T03:00:00+00:00",
        "pkiBrazil": {
          "certificateType": "Unknown"
        },
        "pkiItaly": {
          "certificateType": "Undefined"
        },
        "binaryThumbprintSHA256": "<base64, 32 bytes>",
        "thumbprint": "<hex, 20 bytes>",
        "thumbprintSHA256": "<hex, 32 bytes>",
        "subjectCommonName": "Lacuna Root Test v3",
        "subjectDisplayName": "Lacuna Root Test v3",
        "organization": "ICP-Brasil"
      },
      "pkiBrazil": {
        "certificateType": "Unknown"
      },
      "pkiItaly": {
        "certificateType": "Undefined"
      },
      "binaryThumbprintSHA256": "<base64, 32 bytes>",
      "thumbprint": "<hex, 20 bytes>",
      "thumbprintSHA256": "<thumbprintSHA256, 44 chars>",
      "subjectCommonName": "Lacuna CA Test v7",
      "subjectDisplayName": "Lacuna CA Test v7",
      "organization": "Lacuna Software"
    },
    "pkiBrazil": {
      "certificateType": "A1",
      "cpf": "***.***.***-**",
      "responsavel": "Alan Mathison Turing",
      "dateOfBirth": "2000-01-01"
    },
    "pkiItaly": {
      "certificateType": "Undefined"
    },
    "binaryThumbprintSHA256": "<base64, 32 bytes>",
    "thumbprint": "<hex, 20 bytes>",
    "thumbprintSHA256": "<hex, 32 bytes>",
    "subjectCommonName": "Alan Mathison Turing",
    "subjectDisplayName": "Alan Mathison Turing",
    "subjectIdentifier": "***",
    "emailAddress": "someone@example.com"
  },
  "validationResults": {
    "passedChecks": [
      {
        "type": "CertificateIssuerInvalid",
        "message": "The certificate issuer is valid and trusted",
        "innerValidationResults": {
          "passedChecks": [
            {
              "type": "CertificateIssuerInvalid",
              "message": "The certificate issuer is valid and trusted",
              "innerValidationResults": {
                "passedChecks": [
                  {
                    "type": "Success",
                    "message": "Success",
                    "detail": "The root Lacuna Root Test v3 is trusted"
                  },
                  {
                    "type": "Success",
                    "message": "Success",
                    "detail": "The certificate's signature for the certificate of Lacuna Root Test v3 is valid"
                  },
                  {
                    "type": "Success",
                    "message": "Success",
                    "detail": "The certificate of Lacuna Root Test v3 is inside its validity period"
                  }
                ],
                "errors": [],
                "warnings": []
              }
            },
            {
              "type": "Success",
              "message": "Success",
              "detail": "The certificate's signature for the certificate of Lacuna CA Test v7 is valid"
            },
            {
              "type": "Success",
              "message": "Success",
              "detail": "The certificate of Lacuna CA Test v7 is inside its validity period"
            },
            "… and 1 more"
          ],
          "errors": [],
          "warnings": []
        }
      },
      {
        "type": "Success",
        "message": "Success",
        "detail": "The certificate's signature for the certificate of Alan Mathison Turing is valid"
      },
      {
        "type": "Success",
        "message": "Success",
        "detail": "The certificate of Alan Mathison Turing is inside its validity period"
      },
      "… and 1 more"
    ],
    "errors": [],
    "warnings": []
  }
}
```

## Patterns to copy

- Load Web PKI in the browser only once the page is interactive, never while it is prerendered on the server, and check the environment first: never call `init()` on a phone (`isSupportedMobile`), and stop in desktop Safari, which Web PKI doesn't support.
- Web PKI's calls aren't native promises: `init()` takes callbacks and later calls return the library's own promise with `.success`/`.fail`. Give every call its own `.fail`, or its failure falls through to `init()`'s `defaultFail`, and treat `user_cancelled` as a cancel, not an error.
- Call `listCertificates()` with no filter: Lacuna advises against filtering, because users then report "missing" certificates. Key certificates by thumbprint.
- Prove possession of the key with Rest PKI Core in two calls around the browser: `PrepareAuthenticationAsync` gives a state and a nonce, Web PKI's `signData` signs the nonce (SHA-256), then `CompleteAuthenticationAsync` takes the state, the certificate from `readCertificate` and the signature. Get a fresh challenge per attempt: the state lasts 60 minutes and is never sent twice.
- Handle every way the completion ends: success, `CertificateFailedValidation` (the signature is fine, the certificate isn't), `StaleSession`, and a bad signature, which arrives as a thrown `RestPkiExceptionV2` with `BadSignature` rather than as a result.

## What to do

Implement this in my project with its existing stack, language, framework and conventions, following the call sequence and the patterns above, and adapting the samples rather than copying them whole. Ask me before adding any dependency or package. Keep API keys and private keys out of the browser and out of the repository.
