NURLNURL registrynurl-lang.org →

← all packages

pki-server

owner @Hindurable

repository https://github.com/nurl-lang/nurl-lang

Install

[dependencies]
pki-server = "^0.3.1"

Versions

Dependencies (latest)

pki-server — Pure-NURL Private PKI Service & CA

A high-performance, self-contained Private PKI (Public Key Infrastructure) Certificate Authority and HTTP microservice written entirely in pure NURL.

It is a complete, native implementation of a private CA: zero external dependencies, no shellouts to openssl binaries, native ASN.1 DER encoding, classical ECDSA P-256 or post-quantum ML-DSA (FIPS 204) signatures, automated CA initialization, device enrollment/renewal, operational certificate issuance, PKCS#10 CSR signing, RFC 5280 CRL generation, API key authentication, and a built-in server-rendered Web UI.

the pki-server web UI: the Private PKI Service dashboard with cards for requesting certificates, revoking certificates, and the REST API


Features

  1. Enrollment / Initial Certificate (POST /init): devices register with a shared initialization key (DEVICE_INIT_KEY).
  2. Renewal (POST /renew_initial_cert): renew initial certificates before expiration.
  3. Operational Issuance (POST /request-cert): issue short- or long-lived operational certificates by presenting and cryptographically proving possession of the initial certificate.

Post-quantum mode

A classical CA is a store-now-decrypt-later liability of a particular kind: a recorded handshake is not the problem, a forged certificate is. Once a cryptographically relevant quantum computer exists, every P-256 CA key ever published can be recovered from its own certificate, and anything that still trusts that root can be impersonated for as long as the root is installed. Roots are long-lived by design, so the migration has to happen well before the machine does.

./pki-server --algorithm mldsa65 --ca-cn "Acme Internal Root"
ValueParameter setNIST levelPublic keySignatureCertificate
p256 (default)ECDSA P-256 / SHA-256— (classical)65 B~72 B~0.5 KB
mldsa44ML-DSA-4421312 B2420 B~4 KB
mldsa65ML-DSA-6531952 B3309 B~5.5 KB
mldsa87ML-DSA-8752592 B4627 B~7.5 KB

Notes:

--ca-cert/--ca-key pair keeps its own algorithm, and the server reports the one actually in force via GET /health and on the startup banner. To change algorithms, mint a new CA in a new directory and re-enroll.

P-256 keys as SEC1 -----BEGIN EC PRIVATE KEY-----. Both are written mode 0600.

classical and a PQ signature, per the LAMPS drafts) are not implemented. The choice here is pure-classical or pure-PQ.

the confidentiality of a recorded session additionally needs a PQ key exchange — NURL's TLS stack offers X25519MLKEM768 for that.


Security posture

What this service does, and what it deliberately does not do:

duration does not leak how many leading bytes of a key were correct.

your-device-init-key / your-management-key-here as working defaults; those strings now authenticate nothing. When no key is configured, a 192-bit key is generated at startup and printed to stderr once.

must be even-length hex (it is echoed into HTML, appended to a tab-separated index.txt and re-encoded as a DER INTEGER); a device ID and a certificate CN are reduced to [A-Za-z0-9._-] before naming a path.

submitted certificate names a directory, so a certificate is signature-checked against the CA before any of it is believed. The expiry window is deliberately not part of that check — revoking an expired certificate is legitimate.

Content-Security-Policy: default-src 'none'; script-src 'self'; …. The page's one script lives at /js/app.js so that policy can hold.

--ca-cert/--ca-key exist but do not load, or do not agree with each other, the server exits rather than minting a replacement — silently rotating a root invalidates every certificate ever issued under it.

PKCS#11 key storage, OCSP, delta CRLs, an intermediate-CA hierarchy, or TLS termination. Run it behind a reverse proxy on a trusted network.

host as the trust boundary.


Directory Structure

packages/pki-server/
├── nurl.toml               # Package metadata & dependencies (deps/http)
├── README.md               # Documentation & API specifications
├── docs/
│   └── pki-web.png         # Web UI screenshot
├── src/
│   ├── main.nu             # CLI entry point, argument parsing & server startup
│   ├── service.nu          # HTTP route controllers & request handlers
│   ├── pki.nu              # Core pure-NURL PKI engine (CA, X.509, CRL, ECDSA, ML-DSA)
│   ├── auth.nu             # API Key & device key authentication
│   └── ui.nu               # Server-rendered Web UI views & default styling
├── static/
│   └── css/
│       └── style.css       # Modern CSS stylesheet
└── tests/
    ├── smoke.nu            # In-process pure-NURL crypto & engine unit tests
    └── pki_test.sh         # End-to-end integration suite (classical + PQ passes)

Installing, Building and Running

1. Install with nurlpkg

nurlpkg install pki-server

2. Optionally build manually from source

From the repository root:

./nurl.sh packages/pki-server/src/main.nu packages/pki-server/pki-server

3. Run Server

./pki-server --port 8080 --host 0.0.0.0

CLI Options & Environment Variables

CLI OptionEnvironment VariableDefaultDescription
-p, --portPORT8080TCP listen port
-h, --hostHOST0.0.0.0Bind host address
--algorithmPKI_ALGORITHMp256Signature algorithm for a new CA: p256, mldsa44, mldsa65, mldsa87
--ca-certCA_CERT./certs/ca.crtPath to Root CA certificate
--ca-keyCA_KEY./certs/ca.keyPath to Root CA private key
--crl-fileCRL_FILE./certs/ca.crlPath to generated CRL file
--index-fileINDEX_FILE./certs/index.txtPath to OpenSSL-style index.txt
--initial-dirINITIAL_CERTS_DIR./certs/initialDirectory storing initial enrollment certs
--certs-dirDEVICE_CERTS_DIR./certs/certificatesDirectory storing operational certs
--init-keyDEVICE_INIT_KEYgeneratedDevice enrollment shared secret
--mgmt-keyMANAGEMENT_KEYgeneratedManagement API authentication key
--ca-cnPKI_FQDNPrivate PKI CARoot CA Common Name
--serial-fileAccepted and ignored; serials are 96-bit CSPRNG values

validity_days is capped at 3650 on every issuance endpoint.


REST API Reference

1. Service Health Check

GET /health

Response (200 OK):

{
  "status": "healthy",
  "timestamp": "2026-08-19T20:00:00Z",
  "algorithm": "mldsa65",
  "post_quantum": true
}

2. Download Root CA Certificate

GET /ca-cert

Response (200 OK):

{
  "ca_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
  "algorithm": "mldsa65"
}

3. Device Initialization (Enrollment)

POST /init
Content-Type: application/json

{
  "device_id": "sensor-node-01",
  "key": "your-device-init-key"
}

Response (200 OK):

{
  "certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
  "private_key": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n",
  "serial": "3a8f12c9b4e10023f1a2b3c4"
}

device_id is restricted to [A-Za-z0-9._-]; anything else is a 400.


4. Renew Initial Certificate

POST /renew_initial_cert
Content-Type: application/json

{
  "device_id": "sensor-node-01",
  "key": "your-device-init-key",
  "initial_cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"
}

Response (200 OK): the renewed certificate and its private_key.


5. Request Operational Certificate

Supports both application/json and application/x-www-form-urlencoded.

POST /request-cert
Content-Type: application/json

{
  "device_id": "sensor-node-01",
  "initial_cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
  "validity_days": 90
}

Response (200 OK):

{
  "device_id": "sensor-node-01",
  "certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
  "private_key": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n",
  "ca_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
  "serial": "3a8f12c9b4e10023f1a2b3c4",
  "algorithm": "p256",
  "expires": "2026-11-16T20:00:00Z"
}

Returns 403 when the device's enrollment certificate has been revoked.


6. Request Certificate from PKCS#10 CSR (Zero Trust)

The device generates its own private key locally and sends only the signed PKCS#10 request. The private key never leaves the client.

POST /request-csr
Content-Type: application/json
X-API-Key: your-management-key-here

{
  "csr": "-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----\n",
  "validity_days": 365
}

Response (200 OK):

{
  "status": "success",
  "certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
  "ca_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
  "serial": "3a8f12c9b4e10023f1a2b3c4",
  "algorithm": "mldsa65",
  "expires": "2027-08-19T20:00:00Z"
}

The CSR's self-signature is verified before anything is issued; a tampered request is a 400.


7. Revoke Certificate

Requires the management API key in X-API-Key, Authorization: Bearer <key>, ?api_key=, or an api_key form/JSON field.

POST /revoke
Content-Type: application/json
X-API-Key: your-management-key-here

{
  "certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"
}

Or revoke by hex serial number:

{
  "serial": "3a8f10b2c94d"
}

Response (200 OK):

{
  "status": "success",
  "message": "Certificate with serial 3a8f10b2c94d has been revoked",
  "serial": "3a8f10b2c94d",
  "revocation_time": "2026-08-19T20:00:00Z",
  "crl": "-----BEGIN X509 CRL-----\n...\n-----END X509 CRL-----\n"
}

Revoking by PEM requires a certificate this CA issued (400 otherwise) and also invalidates the named device's enrollment. Revoking by serial alone is enforced at the next issuance attempt via index.txt.


8. Download CRL (Certificate Revocation List)

GET /crl?api_key=your-management-key-here

Response (200 OK):

Content-Type: application/pkix-crl
Content-Disposition: attachment; filename=ca.crl

-----BEGIN X509 CRL-----
...
-----END X509 CRL-----

Running the Test Suite

1. In-process Pure-NURL Unit & Crypto Smoke Tests

Runs the whole engine against both a P-256 and an ML-DSA-65 CA, plus the input validators and the HTML escaper.

./nurl.sh packages/pki-server/tests/smoke.nu packages/pki-server/tests/smoke
./packages/pki-server/tests/smoke

2. End-to-End HTTP & OpenSSL Compatibility Test Suite

Runs the full lifecycle twice — classical and post-quantum — including the security regression tests (reflected XSS, index.txt injection, path traversal via a certificate SAN, serial-only revocation lockout, retired default credentials).

./packages/pki-server/tests/pki_test.sh