> ## Documentation Index
> Fetch the complete documentation index at: https://infisical-platfor-532.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# PKCS#11 module

> Use jarsigner, cosign, osslsigncode, and other standard signing tools with Infisical Signers.

The Infisical **PKCS#11 module** is a small native library (`.so`, `.dylib`, or `.dll`) that exposes your Infisical [Signers](/documentation/platform/pki/code-signing/signers) to any tool that supports the [PKCS#11 v2.40 standard](https://www.oasis-open.org/standard/pkcs-11-v2-40/). Tools like `jarsigner`, `osslsigncode`, `cosign`, `apksigner`, `openssl`, and `gpg` work without modification. They make their usual PKCS#11 calls; the module forwards them to Infisical and returns the signature.

<Note>
  Signing on Windows with `signtool`? Use the [Windows KSP](/documentation/platform/pki/code-signing/windows-ksp) instead.
</Note>

## Before you start

You authenticate as a member of the Signer, using either a **Machine Identity** or your own Infisical **access token**. To use a Machine Identity, set it up once:

1. **Create a Machine Identity** and enable **[Universal Auth](/documentation/platform/identities/universal-auth)** on it, then copy its **Client ID** and **Client Secret**. The module uses these to authenticate, and you will set them in [Configuration](#configuration) below.
2. **Add the identity to the Signer** with the Administrator or Operator role, from the Signer's **Members** tab. Auditors cannot sign.
3. If the Signer has an [approval policy](/documentation/platform/pki/code-signing/approvals), get [active signing access](/documentation/platform/pki/code-signing/approvals#access-lifecycle) before signing (or use [Automatic Signing Access Requests](#automatic-signing-access-requests)).

<Note>
  To sign as yourself instead, use your own access token (see [Configuration](#configuration)). You still need to be a member of the Signer with the Administrator or Operator role.
</Note>

<Info>
  The [Signer](/documentation/platform/pki/code-signing/signers) itself is created by a Product Admin. If you do not have one yet, create it first.
</Info>

## Installation

Grab the pre-built binary for your platform from the [releases page](https://github.com/Infisical/infisical-pkcs-11/releases):

| Platform        | File                        |
| --------------- | --------------------------- |
| Linux x86\_64   | `libinfisical-pkcs11.so`    |
| Linux ARM64     | `libinfisical-pkcs11.so`    |
| macOS x86\_64   | `libinfisical-pkcs11.dylib` |
| macOS ARM64     | `libinfisical-pkcs11.dylib` |
| Windows x86\_64 | `libinfisical-pkcs11.dll`   |

Drop the binary in a known location:

```bash theme={"dark"}
# Linux
sudo cp libinfisical-pkcs11.so /usr/local/lib/
sudo chmod 755 /usr/local/lib/libinfisical-pkcs11.so

# macOS
sudo cp libinfisical-pkcs11.dylib /usr/local/lib/
sudo chmod 755 /usr/local/lib/libinfisical-pkcs11.dylib
```

### Building from source

If you want to build instead of downloading, you need Go 1.24+ and a C compiler:

```bash theme={"dark"}
git clone https://github.com/Infisical/infisical-pkcs-11.git
cd infisical-pkcs-11
make build
```

The output binary lands in the current directory.

## Configuration

The module reads its config from `/etc/infisical/pkcs11.conf` (override the path with `INFISICAL_CONFIG`):

```json theme={"dark"}
{
  "server_url": "https://app.infisical.com",
  "log_level": "info"
}
```

Then export the authentication credentials as environment variables:

```bash theme={"dark"}
export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID="<machine-identity-client-id>"
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET="<machine-identity-client-secret>"
```

<Note>
  Credentials can also live in the config file under `auth.client_id` and `auth.client_secret`, but environment variables are strongly recommended so secrets don't end up in version control, backups, or shared logs.
</Note>

#### Sign with an access token instead

Instead of a machine identity, you can hand the module an Infisical access token directly, either your own or a machine identity's. Set `INFISICAL_TOKEN` and the module uses it as-is:

```bash theme={"dark"}
export INFISICAL_SERVER_URL="https://app.infisical.com"
export INFISICAL_TOKEN="<your-access-token>"
```

<Warning>
  JWT tokens expire, so this access is **temporary**. The module does not refresh the token; once it expires, signing fails until you supply a new one. This is handy for signing as yourself or for a quick one-off; for unattended or CI/CD signing use a machine identity with Universal Auth (client ID and secret), which the module re-authenticates automatically.
</Warning>

If you put the config somewhere other than `/etc/infisical/pkcs11.conf`:

```bash theme={"dark"}
export INFISICAL_CONFIG=/path/to/your/pkcs11.conf
```

### Configuration reference

| Field                           | Required | Default  | Description                                                                                                                                                                                                                                                                    |
| ------------------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `server_url`                    | Yes      | None     | Infisical server URL. Must use `http://` (local dev) or `https://`.                                                                                                                                                                                                            |
| `auth.method`                   | No       | inferred | Authentication method: `universal-auth` or `token`. Inferred from the credentials when unset (a token means `token`, otherwise `universal-auth`).                                                                                                                              |
| `auth.client_id`                | No       | None     | Machine identity client ID, for `universal-auth`. Prefer the env var.                                                                                                                                                                                                          |
| `auth.client_secret`            | No       | None     | Machine identity client secret, for `universal-auth`. Prefer the env var.                                                                                                                                                                                                      |
| `auth.token`                    | No       | None     | An Infisical access token, for `token` auth. Prefer the env var.                                                                                                                                                                                                               |
| `tls.ca_cert_path`              | No       | None     | Custom CA bundle path for self-hosted Infisical with a private CA.                                                                                                                                                                                                             |
| `tls.skip_verify`               | No       | `false`  | Skip TLS verification (development only, never enable in production).                                                                                                                                                                                                          |
| `cache.token_ttl_seconds`       | No       | `300`    | How long the auth token is cached.                                                                                                                                                                                                                                             |
| `cache.cert_ttl_seconds`        | No       | `3600`   | How long fetched certificates are cached.                                                                                                                                                                                                                                      |
| `cache.signer_ttl_seconds`      | No       | `300`    | How long the list of Signers is cached.                                                                                                                                                                                                                                        |
| `approval.signing_duration`     | No       | None     | Auto-request signing access with this window when no active access exists. Valid range: `1m` to `30d`.                                                                                                                                                                         |
| `approval.signing_count`        | No       | None     | Auto-request signing access for this many signatures when no active access exists.                                                                                                                                                                                             |
| `approval.exclude_scope_fields` | No       | None     | Signing parameters to leave out of the requests the module opens, so one approval covers any value of them. Any of `command`, `signing_application`, `signing_application_hash`, `hostname`, `os_username`, `ip_address`, `data_hash`. An unknown name is rejected at startup. |
| `approval.ip_address`           | No       | None     | Pin the requests the module opens to this address instead of the one Infisical sees them arrive from, which is what it uses when this is unset. It does not have to be this machine's, so you can name a build agent's egress address.                                         |
| `log_level`                     | No       | `info`   | One of `trace`, `debug`, `info`, `warn`, `error`.                                                                                                                                                                                                                              |
| `log_file`                      | No       | stderr   | Optional path to a log file.                                                                                                                                                                                                                                                   |

### Environment variable reference

| Variable                                 | Description                                                                                                             |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID`     | Machine identity client ID. Overrides config.                                                                           |
| `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` | Machine identity client secret. Overrides config.                                                                       |
| `INFISICAL_TOKEN`                        | An Infisical access token (a user or machine identity token). Selects token auth; used instead of the client ID/secret. |
| `INFISICAL_SERVER_URL`                   | The Infisical instance URL. Sets `server_url` (and overrides the config file).                                          |
| `INFISICAL_CONFIG`                       | Path to the config file (default `/etc/infisical/pkcs11.conf`).                                                         |

<Note>
  Environment variables always take precedence over values in the configuration file.
</Note>

## Verify the module

Use `pkcs11-tool` (from OpenSC: `brew install opensc` or `apt install opensc`) to confirm everything is wired up.

```bash theme={"dark"}
pkcs11-tool \
  --module /usr/local/lib/libinfisical-pkcs11.so \
  --list-slots
```

You should see one slot per Signer your machine identity is a member of:

```text theme={"dark"}
Available slots:
Slot 0 (0x0): mobile-app-prod
  token label        : mobile-app-prod
  token manufacturer : Infisical
  token model        : Code Signing
  ...
Slot 1 (0x1): release-signer
  ...
```

To list the objects (private key, public key, X.509 cert) in a slot:

```bash theme={"dark"}
pkcs11-tool \
  --module /usr/local/lib/libinfisical-pkcs11.so \
  --slot 0 \
  --list-objects
```

A quick smoke-test sign:

```bash theme={"dark"}
echo "hello" > /tmp/payload.bin
pkcs11-tool \
  --module /usr/local/lib/libinfisical-pkcs11.so \
  --slot 0 \
  --sign --mechanism SHA256-RSA-PKCS \
  --input-file /tmp/payload.bin \
  --output-file /tmp/payload.sig
```

If the Signer has an approval policy and you don't have active access, the sign call is rejected with `CKR_GENERAL_ERROR` and the module logs a "signing requires approval" line. See [Automatic Signing Access Requests](#automatic-signing-access-requests) below to make this seamless.

## Automatic signing access requests

When a Signer has a policy attached, sign calls without active access normally fail. The module can automatically open a signing request for you on the first denied call. You just need to add an `approval` block to the config:

```json theme={"dark"}
{
  "server_url": "https://app.infisical.com",
  "approval": {
    "signing_duration": "8h",
    "signing_count": 10
  }
}
```

* **`signing_duration`** requests an access window of this duration (`30m`, `8h`, `2d`; range 1m to 30d).
* **`signing_count`** requests access good for this many signing operations.

Set one or both depending on what the Signer's policy expects. The values are capped at the policy's `maxWindow` / `maxSignings`.

The request is [scoped](/documentation/platform/pki/code-signing/approvals#scoping-an-approval) to what the module saw: the command, the tool, the machine, the OS account, and the artifact. Approvers review the real signing situation, and the approval only covers that situation.

<Note>
  The first sign call **still fails**, because an approver has to act on the request. Once approved, run the same command again and it succeeds.

  The approval covers that exact command and artifact, so a different artifact, a changed argument, or reordered flags needs a new request. A request's parameters are fixed once it is open, so when they are wrong the request is rejected and reopened rather than edited. To have one approval cover a batch, leave the parameters that vary out of the request from the start.
</Note>

### Leaving parameters out of a request

Every parameter on a request has to match at signing time, so a request that names the artifact hash covers exactly one artifact. To have one approval cover a series of builds, name the parameters that vary in `approval.exclude_scope_fields` and the module leaves them off every request it opens:

```json theme={"dark"}
{
  "approval": {
    "signing_duration": "8h",
    "signing_count": 10,
    "exclude_scope_fields": ["data_hash"]
  }
}
```

`data_hash` is the usual one. A timestamped signature changes the digest between runs even for the same file, so pinning it means a fresh approval for every build. The other names are `command`, `signing_application`, `signing_application_hash`, `hostname`, `os_username` and `ip_address`.

Leave out only what genuinely varies, and be aware that dropping a parameter widens the approval further than the machine you configured it on. An approval belongs to the identity that asked for it, not to a host, so once `hostname` is excluded the same approval covers that identity signing from anywhere its credentials are installed.

<Warning>
  Excluding every parameter leaves the request with no scope at all. The resulting approval permits any command, on any machine, against any artifact, for the whole of its window and signature count, which is the same access you would have with no scoping at all. Excluding `data_hash` alone is almost always what a build pipeline needs.
</Warning>

An excluded parameter is still recorded on the signing operation, so the command, machine, account and artifact hash behind every signature stay available to auditors even when they no longer constrain the approval.

### Scoping by address

The address is the one parameter the module cannot observe, because what counts is the address Infisical receives the sign call from, after any NAT or proxy in between. Infisical fills it in from the request itself, the same value it records in the audit log, so requests are scoped by address without any configuration.

Two settings change that:

```json theme={"dark"}
{
  "approval": {
    "ip_address": "203.0.113.10",
    "exclude_scope_fields": ["ip_address"]
  }
}
```

`ip_address` pins a different address than the request came from, which is how you tie an approval to a build agent's egress address from elsewhere. Excluding `ip_address` leaves signing unrestricted by address, which is what a fleet behind a rotating NAT pool needs, since a pinned address would stop matching between runs. Setting both leaves the address unscoped, because excluding wins.

Infisical always compares against the address it sees, never one a caller reports, so neither setting can widen access beyond what the approval already allows.

### Example CI workflow

1. Pipeline calls `jarsigner` (or any PKCS#11 tool) against the Infisical module.
2. No active access exists, so Infisical returns 403.
3. The module sees the `approval` block and auto-creates a signing request with the configured duration and count.
4. Pipeline fails on this run.
5. A reviewer approves the request in the Infisical UI.
6. Pipeline is rerun and the sign call succeeds under the freshly-issued access record.

For fully unattended CI, have an Administrator **pre-approve** signing access via [Pre-approve signing](/documentation/platform/pki/code-signing/approvals#approval-paths) before the pipeline runs.

## Troubleshooting

If the module fails before it can log anything, it prints the reason to stderr prefixed `infisical-pkcs11:`. That covers a config file it cannot read, since the log path lives in the file that just failed, and PKCS#11 gives the calling tool nothing but a generic `CKR_GENERAL_ERROR`.

For everything else, turn on debug logging in the config first. It'll usually tell you exactly what went wrong:

```json theme={"dark"}
{
  "log_level": "debug",
  "log_file": "/tmp/infisical-pkcs11.log"
}
```

<AccordionGroup>
  <Accordion title="No slots listed">
    Almost always one of three things:

    1. The machine identity is not a member of any Signer in your organization. Have an Admin add it to a Signer (any role works).
    2. The credentials are wrong. Check the auth token line in the log; if it says `401`, regenerate the universal-auth client secret.
    3. The `server_url` points at the wrong instance. The module log line `Initialized with universal-auth (auto-authenticated)` confirms a successful login.
  </Accordion>

  <Accordion title="CKR_GENERAL_ERROR on sign">
    Almost always means the server rejected the sign. Common reasons (visible in the debug log):

    * **No active access.** The Signer has an approval policy and you don't yet have access. See [Request to sign](/documentation/platform/pki/code-signing/approvals#approval-paths).
    * **Access expired or signatures exhausted.** Request access again.
    * **Signer is Disabled.** Re-enable it in the UI.
    * **Certificate expired.** Issue a new one or wait for auto-renewal.
  </Accordion>

  <Accordion title="CKR_DEVICE_ERROR or connection refused">
    Network or TLS problem. Check `server_url`, confirm the host is reachable from the machine, and verify TLS. If you self-host with a private CA, set `tls.ca_cert_path` to its bundle. As a last resort during local dev, you can set `tls.skip_verify: true` (**never in production**).
  </Accordion>

  <Accordion title="server_url scheme must be http or https">
    The module rejects any other scheme (`file://`, `gopher://`, etc.) to prevent credentials being sent to unintended targets. Make sure your `server_url` starts with `http://` or `https://`.
  </Accordion>

  <Accordion title="The PKCS#11 module sees the slot but says 'signer X has no certificate'">
    The Signer is in **Pending** or **Failed** status; no certificate has been issued yet. Check the Signer's status in the UI; for external CAs (AWS Private CA, Azure AD CS), issuance can take minutes. Once the Signer becomes Active, the cert is fetched automatically.
  </Accordion>
</AccordionGroup>

## What's next

<CardGroup cols={3}>
  <Card title="Sign JARs" icon="java" href="/documentation/platform/pki/guides/code-signing/jarsigner">
    `jarsigner` with the PKCS#11 module.
  </Card>

  <Card title="Sign containers" icon="docker" href="/documentation/platform/pki/guides/code-signing/cosign">
    `cosign sign` with Infisical.
  </Card>

  <Card title="Sign Windows binaries" icon="windows" href="/documentation/platform/pki/guides/code-signing/osslsigncode">
    `osslsigncode` for `.exe` / `.dll` / `.msi`.
  </Card>

  <Card title="Sign APKs" icon="android" href="/documentation/platform/pki/guides/code-signing/apksigner">
    `apksigner` for Android.
  </Card>

  <Card title="Sign with OpenSSL" icon="lock" href="/documentation/platform/pki/guides/code-signing/openssl">
    Raw signing primitives.
  </Card>

  <Card title="Sign with GPG" icon="key" href="/documentation/platform/pki/guides/code-signing/gpg">
    Use GPG via the PKCS#11 module.
  </Card>
</CardGroup>
