# mTLS, CSR and certificate lifecycle

The operator portal and machine API are separate trust boundaries. Browsers use
passkeys on `integration.boringordertracker.com`; only
`api.integration.boringordertracker.com` requests a client certificate. Never
send a private key, P12 password, one-time claim token, or live certificate to
BOT support, source control, a CI log, or an AI assistant.

Sandbox and production have different clients, scopes, quotas and certificates.
The certificate selects the environment; there is no environment request header
or query parameter. A sandbox certificate cannot read production data, and a
production certificate cannot read sandbox fixtures.

## Recommended: create a private key and CSR locally

In the integration detail page, open **Certificates** and copy the displayed
**Required CSR common name**. It is derived from the already-created client and
has this exact form:

```text
bot:<environment>:<client-id>
```

Copy the complete value from the portal. Do not substitute the integration ID,
slug, operator ID, or a self-selected name. A client ID is not a secret, but the
private key is.

The following commands create an encrypted EC P-256 key and a SHA-256 PKCS#10
CSR. Run them on the server or secret-management workstation that will retain
the key:

```sh
umask 077
export BOT_CSR_CN='bot:sandbox:PASTE-THE-EXACT-CLIENT-ID-FROM-THE-PORTAL'

openssl genpkey \
  -algorithm EC \
  -pkeyopt ec_paramgen_curve:prime256v1 \
  -aes-256-cbc \
  -out bot-sandbox-client-key.pem

openssl req \
  -new \
  -sha256 \
  -key bot-sandbox-client-key.pem \
  -subj "/CN=${BOT_CSR_CN}" \
  -out bot-sandbox-client.csr.pem
```

OpenSSL asks for a private-key passphrase; store it in the service's secret
manager. For an unattended service, inject it through the process secret store
or TLS runtime rather than placing it in a command line, image, repository, or
world-readable configuration file.

RSA is supported when EC is unavailable. Use 3072–8192 bits; this example uses
an encrypted 3072-bit key:

```sh
umask 077
openssl genpkey \
  -algorithm RSA \
  -pkeyopt rsa_keygen_bits:3072 \
  -aes-256-cbc \
  -out bot-sandbox-client-key.pem

openssl req \
  -new \
  -sha256 \
  -key bot-sandbox-client-key.pem \
  -subj "/CN=${BOT_CSR_CN}" \
  -out bot-sandbox-client.csr.pem
```

BOT accepts RSA 3072–8192 and EC P-256/P-384 public keys. Before uploading the
CSR, verify its self-signature and inspect the exact subject and public key:

```sh
openssl req -in bot-sandbox-client.csr.pem -noout -verify
openssl req -in bot-sandbox-client.csr.pem -noout -subject -nameopt RFC2253
openssl req -in bot-sandbox-client.csr.pem -noout -text
```

The `subject` output must contain exactly the portal value as its `CN`. The text
output must show the intended key algorithm/size. Upload only
`bot-sandbox-client.csr.pem`; never upload the private-key file. Certificate
issuance is asynchronous because only the private signing worker can access the
intermediate CA.

## Verify the issued certificate

The portal's certificate download is a PEM bundle containing the issued leaf
certificate first, followed by the public CA chain. It never contains a private
key. Save the download beside, but with different permissions from, the private
key and inspect it:

```sh
export BOT_CERT=/secure/path/bot-integration-certificate-chain.pem
export BOT_KEY=/secure/path/bot-sandbox-client-key.pem

openssl x509 -in "$BOT_CERT" \
  -noout -subject -issuer -serial -dates -fingerprint -sha256

openssl crl2pkcs7 -nocrl -certfile "$BOT_CERT" \
  | openssl pkcs7 -print_certs -noout
```

Confirm that the leaf subject is bound to the same portal common name, its
validity window is expected, and the bundle contains only the expected BOT
certificates. Then prove that the certificate and retained private key contain
the same public key:

```sh
openssl pkey -in "$BOT_KEY" -pubout -outform DER \
  | openssl dgst -sha256

openssl x509 -in "$BOT_CERT" -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256
```

The two SHA-256 lines must be identical. This is a key-match check, not a
replacement for TLS chain validation. curl and the operating system must still
validate the API server certificate normally, and BOT validates the presented
client chain plus its current database/CRL status.

Test the binding before using any data endpoint:

```sh
export BOT_API='https://api.integration.boringordertracker.com/v1'

curl --fail-with-body \
  --proto '=https' \
  --tlsv1.2 \
  --cert "$BOT_CERT" \
  --key "$BOT_KEY" \
  "$BOT_API/capabilities"
```

For sandbox, assert that `environment` is `sandbox`. For production, assert the
expected `integration_status`, exact scopes and quotas, and
`production_data_enabled=true` before calling data endpoints. Production
certificates can be prepared after approval, but production data stays blocked
until the independent mobile-consent, legal and operational gates are enabled.

## One-time encrypted P12 comfort flow

The alternative portal flow generates an EC P-256 key only in private worker
memory and exports an encrypted P12. A fresh passkey confirmation creates a
one-time claim token; only its HMAC is stored. Keep that token until issuance is
ready, then claim the package in the portal within 15 minutes. The browser
downloads the P12 and displays its password once. The response is `no-store`,
the claim is atomically consumed, and the encrypted server artifact is removed.
Unencrypted private keys are never persisted by BOT.

Inspect a claimed package locally without extracting its private key:

```sh
export BOT_P12=/secure/path/bot-integration-certificate.p12
openssl pkcs12 -in "$BOT_P12" -info -noout
```

curl can use the P12 directly. Omitting the password from `--cert` avoids putting
it in shell history or the process list; use curl's interactive prompt for a
manual smoke test:

```sh
curl --fail-with-body \
  --proto '=https' \
  --tlsv1.2 \
  --cert-type P12 \
  --cert "$BOT_P12" \
  "$BOT_API/capabilities"
```

For unattended operation, load the P12 password from the service's secret
manager using the TLS client's protected configuration mechanism. Do not append
the password to `--cert`, commit it to a curl config, or export it from a
long-lived interactive shell.

## Certificate rotation

Certificates are valid for 90 days. Begin rotation before the expiry shown by
`openssl x509 -dates`; do not wait for the final day. A client may have at most
two active certificates per environment, with a maximum 14-day overlap. Rate
limits are integration-wide across both certificates, so rotation does not
increase quota.

Use this sequence for a zero-downtime rotation:

1. In the correct environment, create a new private key/CSR with the exact same
   portal-displayed client common name, or request a new one-time P12.
2. Keep the existing certificate active while issuance completes.
3. Validate the new certificate, key match, expiry and chain as above.
4. Deploy the new credential to one instance/canary and call
   `GET /v1/capabilities` through the normal production network path.
5. Confirm environment, integration status, scopes and quotas, then switch all
   callers to the new credential and observe successful requests.
6. Revoke the old certificate in the portal before the 14-day overlap ends.
7. Remove the old key from every runtime and secret store according to the
   operator's auditable key-destruction procedure.

Do not reuse private keys between sandbox and production or between separate
integrations.

## Revocation and incident response

Revoke a certificate immediately if its private key, P12, password, host, CI
secret, or backup may have been exposed, or when the credential is no longer in
use. Portal revocation changes backend authorization immediately and queues the
serial for CA/CRL publication; a certificate that still passes edge TLS must
also pass the backend's active-client, environment, integration, serial,
fingerprint, status and scope checks.

After revocation:

1. stop every process using the affected credential;
2. remove it from runtime secret stores and rotate any copied secret material;
3. issue a fresh key/certificate, rather than reusing the suspect private key;
4. test `GET /v1/capabilities` with the replacement;
5. review portal audit/usage information and contact BOT support with the public
   certificate ID, serial/fingerprint and time window—never the private key or
   P12 password.

Suspending or retiring an integration is broader than revoking one certificate:
it stops production authorization for the integration. Keep portal passkeys and
recovery codes available through a separate incident-recovery path so a lost
API credential never becomes a lost operator account.
