pki-server/src/service.nu — HTTP routes & request handlers.
: ~ s g_ca_cert_path ./certs/ca.crt``: ~ s g_ca_key_path ./certs/ca.key``: ~ s g_crl_file_path ./certs/ca.crl``: ~ s g_index_file_path ./certs/index.txt``: ~ s g_initial_certs_dir ./certs/initial``: ~ s g_device_certs_dir ./certs/certificates``: ~ s g_device_init_key ``: ~ s g_management_key ``: ~ s g_ca_cn Private PKI CA``: ~ i g_ca_alg 0: ~ i g_ca_handle 0@ pki_service_init s ca_cert s ca_key s crl_file s index_file s initial_dir s device_dir s init_key s mgmt_key s ca_cn i alg → b@ pki_service_alg → i@ _resp_json i status String json_str → HttpResponse@ _resp_err_json i status s msg → HttpResponse@ _resp_html i status String html → HttpResponseHTML responses carry the policy that makes the escaping in ui.nu a belt-and-braces measure rather than the only one: no inline script may run, nothing may be loaded cross-origin, and the page may not be framed. The UI's one script lives at /js/app.js precisely so script-src 'self' can hold.
@ _wants_json HttpRequest req → bCheck if request prefers JSON (via header or content-type)
@ _device_path s dir s id s ext → String<dir>/<id>/<id>.<ext> — the one shape this service builds. id must already be sanitised.
@ _form_get ( Vec QueryPair ) pairs s name → StringPull a field out of an application/x-www-form-urlencoded body.
@ _json_str_field Json obj s name → String@ handle_health HttpRequest req Params p → HttpResponseGET /health
@ handle_ca_cert HttpRequest req Params p → HttpResponseGET /ca-cert
@ handle_crl HttpRequest req Params p → HttpResponseGET /crl
@ handle_init HttpRequest req Params p → HttpResponsePOST /init
@ _initial_cert_matches s stored_path s submitted_pem → bCompare a submitted PEM with the enrollment certificate on disk. Both are normalised to DER first, so re-wrapped armor or line endings do not change the answer.
@ _initial_cert_revoked s stored_path → bHas this device's current enrollment certificate been revoked? The serial is read back out of the stored certificate and checked against index.txt, so revoking by serial alone — with no CN to invalidate a file by — still locks the device out.
@ handle_renew_initial_cert HttpRequest req Params p → HttpResponsePOST /renew_initial_cert
@ handle_request_cert_get HttpRequest req Params p → HttpResponseGET /request-cert
@ _cert_error b is_json i status s msg → HttpResponse@ handle_request_cert_post HttpRequest req Params p → HttpResponsePOST /request-cert
@ handle_request_csr_post HttpRequest req Params p → HttpResponsePOST /request-csr — Issue certificate from a client-provided PKCS#10 CSR. Client private key never crosses the wire (Zero Trust PKI).
@ handle_revoke_get HttpRequest req Params p → HttpResponseGET /revoke
@ _revoke_error b is_json i status s msg → HttpResponse@ handle_revoke_post HttpRequest req Params p → HttpResponsePOST /revoke
@ _cert_issued_by_ca * PkiCa ca s cert_pem → bSignature-only check: was this certificate signed by our CA key? No validity-window test, because revoking an already-expired certificate is legitimate.
@ handle_index HttpRequest req Params p → HttpResponseGET /
@ handle_api_docs HttpRequest req Params p → HttpResponseGET /api
@ handle_style_css HttpRequest req Params p → HttpResponseGET /css/style.css
@ handle_app_js HttpRequest req Params p → HttpResponseGET /js/app.js
@ handle_favicon HttpRequest req Params p → HttpResponseGET /favicon.ico
@ pki_build_app → *HttpApppki-server/src/auth.nu — API Key & device key authentication helpers.
Every comparison here is between a client-supplied string and a server secret, so all of them go through std/subtle.nu's constant_time_eq. string_eq and nurl_str_cmp stop at the first differing byte, which turns each request into a measurement of how many leading bytes were right — enough to recover a key byte-by-byte over a few thousand requests.
The empty key is treated as "no key configured" and DENIES rather than admits: main.nu never leaves one empty (it mints a random key when none is given), so reaching here with one means the process is misconfigured, and open-by-default is the wrong answer to that.
@ auth_check_api_key HttpRequest req s management_key → bCheck if request carries a valid Management API key.
@ auth_check_api_key_value s provided_key s management_key → bCheck a key supplied in a request body (form field or JSON member) against the management key.
@ auth_check_device_key s provided_key s expected_key → bCheck if provided initialization key matches configured key.
pki-server/src/pki.nu — Pure-NURL PKI Engine & CA Operations.
The CA signs with one of two algorithm families, chosen at CA creation and then fixed for the life of the key:
PKI_ALG_P256 ecdsa-with-SHA256 over prime256v1 (classical) PKI_ALG_MLDSA44 \ PKI_ALG_MLDSA65 > ML-DSA (FIPS 204) (post-quantum) PKI_ALG_MLDSA87 /
Everything downstream — device keys, the CRL signature, chain verification — follows the CA's choice, so a PQ deployment has no classical signature anywhere in the trust path.
& c @ nurl_rand_fill *u buf i n → i@ pki_alg_p256 → i@ pki_alg_from_name s name → iMap a --algorithm name to the internal code. Returns -1 for an unknown name so the caller can reject it rather than silently fall back to the classical default.
@ pki_alg_name i alg → s@ pki_alg_display i alg → sHuman-facing description for the startup banner and the web UI.
@ pki_alg_is_pq i alg → b: PkiCert: PkiCert {
String cert_pem
String key_pem
String serial_hex
String expires_iso
}
@ pki_cert_free PkiCert c → v: PkiCertInfo: PkiCertInfo {
String serial_hex
String cn
b ok
}
@ pki_cert_info_free PkiCertInfo i → v: PkiCa: PkiCa {
i alg
( Vec u ) scalar // P-256 private scalar
( Vec u ) pubkey // P-256 public point
( Vec u ) ml_sk // ML-DSA private key
( Vec u ) ml_pk // ML-DSA public key
String cert_pem
String key_pem
String cn
}
alg selects which key pair below is live: 0 uses scalar/pubkey, 44/65/87 use ml_sk/ml_pk. The unused pair stays empty rather than being absent, so pki_ca_free has one shape to release.
@ pki_ca_new → *PkiCa@ pki_ca_free * PkiCa ca → v@ pki_ca_public * PkiCa ca → ( Vec u )The public key as it appears in the SubjectPublicKeyInfo BIT STRING — what the key identifier is computed over, and what verification needs.
@ _pki_rand_bytes i n → ( Vec u )@ _pki_scalar_ok ( Vec u ) d → b@ _pki_nibble i n → i@ _pki_bytes_to_hex ( Vec u ) b → String@ pki_normalise_serial s raw → StringA certificate serial as it may arrive from a client: lowercase hex, an even number of digits, at most 40 bytes (RFC 5280 caps a serial at 20 octets; twice that leaves room for encoders that pad). Anything else is rejected outright — the value is echoed into HTML, appended to index.txt and re-encoded as a DER INTEGER, and each of those is a place a stray <, tab or newline does damage.
@ pki_sanitize_id s raw → StringReduce an identifier to [A-Za-z0-9._-] and refuse the pure-dot forms. Every path this package builds under --initial-dir / --certs-dir goes through here first: the CN carried by a submitted certificate is attacker-chosen, and <dir>/<cn>/<cn>.crt with cn = "../.." writes outside the tree.
@ _pki_pow256 i k → i@ _pki_tlv i tag ( Vec u ) content → ( Vec u )@ _pki_int ( Vec u ) mag → ( Vec u )@ _pki_int1 i v → ( Vec u )@ _pki_int_hex s hex → ( Vec u )@ _pki_oid s hex → ( Vec u )@ _pki_bitstring ( Vec u ) content → ( Vec u )@ _pki_bool_true → ( Vec u )@ _pki_push2 String st i v → v@ pki_utctime_str i unix → StringUTCTime body, YYMMDDHHMMSSZ — also the on-disk form in index.txt, so a revocation date survives a restart instead of being restamped.
@ pki_utctime_parse s raw → iInverse of pki_utctime_str. Returns -1 when the field is not a well-formed UTCTime, so a hand-edited index.txt cannot inject a bogus revocationDate into the CRL.
@ _pki_utctime i unix → ( Vec u )@ pki_iso_timestamp i unix → String@ _pki_name s cn → ( Vec u )@ _pki_alg_id i alg → ( Vec u )AlgorithmIdentifier for the CA's signature algorithm. ML-DSA carries no parameters — the OID fixes the parameter set — while ecdsa-with- SHA256 omits them too (RFC 5758 §3.2).
@ _pki_spki i alg ( Vec u ) pubk → ( Vec u )@ _pki_ext s oid_hex b critical ( Vec u ) value → ( Vec u )Extension ::= SEQ { extnID OID, critical BOOLEAN DEFAULT FALSE, extnValue OCTET STRING }
@ _pki_key_id ( Vec u ) pubk → ( Vec u )RFC 7093 §2 method 1: leftmost 160 bits of SHA-256 over the raw public key. RFC 5280's own method 1 names SHA-1; the truncated SHA-256 form is the sanctioned modern replacement and keeps this package free of a SHA-1 dependency.
@ _pki_keyusage b is_ca → ( Vec u )@ _pki_eku → ( Vec u )@ _pki_extensions s cn b is_ca ( Vec u ) subject_pub ( Vec u ) issuer_pub → ( Vec u )The full extension block. A CA cert gets basicConstraints/keyUsage marked critical and a subjectKeyIdentifier; a leaf additionally gets extendedKeyUsage, the dNSName SAN and an authorityKeyIdentifier pointing at the issuer. RFC 5280 §4.2.1.9 and §4.2.1.3 require the first two on any cert that signs; the identifiers are what lets a verifier build a chain without trial-and-error.
@ _pki_sig_der ( Vec u ) rs → ( Vec u )@ _pki_pem s label ( Vec u ) der → String@ _pki_sign * PkiCa ca ( Vec u ) tbs → ( Vec u )The signatureValue bytes exactly as they go into the outer BIT STRING: a DER ECDSA-Sig-Value for P-256, the raw FIPS 204 signature for ML-DSA (which specifies no wrapper, and hashes internally — so there is no digest step on that path).
@ _pki_verify_sig i alg ( Vec u ) pubk ( Vec u ) tbs ( Vec u ) sig → bVerify a TBS blob against an issuer public key. alg is the CA's algorithm code; sig is the BIT STRING content as x509_parse hands it back.
@ _pki_tbs * PkiCa ca s issuer_cn s subject_cn ( Vec u ) sub_pub ( Vec u ) serial i not_before i not_after b is_ca → ( Vec u )Assemble a TBSCertificate. sub_pub is the subject's raw public key (65-byte EC point or ML-DSA pk); the CA supplies the issuer name, the issuer key identifier and the signature algorithm.
@ _pki_wrap_cert * PkiCa ca ( Vec u ) tbs → ( Vec u )Certificate ::= SEQ { tbsCertificate, signatureAlgorithm, signature }
@ _pki_priv_pem i alg ( Vec u ) sk ( Vec u ) pubk → StringSEC1 / RFC 5915 ECPrivateKey, or PKCS#8 OneAsymmetricKey for ML-DSA.
@ pki_generate_ca s cn i validity_days i alg → *PkiCa@ _pki_ca_from_pem s cert_pem s key_pem s ca_cn → *PkiCaRebuild a CA handle from a stored cert + key pair. Returns 0 when the pair does not parse, does not agree on the algorithm, or when the private key does not match the certificate's public key — a mismatch would otherwise produce certificates nothing can verify.
@ pki_load_or_create_ca s ca_cert_path s ca_key_path s ca_cn i alg → *PkiCaLoad the CA, or mint one when either half is missing. An existing CA that fails to load returns 0 instead of being silently replaced: overwriting a live CA key would invalidate every certificate ever issued under it, so that has to be an operator decision.
@ pki_issue_device_cert * PkiCa ca s device_id i validity_days → PkiCert@ _pki_csr_mldsa_level Csr csr → iRebuild the requester's SubjectPublicKeyInfo from the parsed CSR. csr.nu's key_alg codes: 1 RSA, 2 EC P-256, 3 Ed25519, 4 ML-DSA.
Csr records key_alg = 4 for all three ML-DSA parameter sets without keeping which one it was, but a PKCS#10 request is self-signed by definition — csr_verify checks the signature against this very key — so sig_alg (8/9/10) names the level unambiguously for any CSR that got this far.
@ _pki_spki_from_csr Csr csr → ( Vec u )@ pki_issue_cert_from_csr * PkiCa ca s csr_pem i validity_days → !PkiCert StringIssue an X.509 certificate from a verified PKCS#10 CSR. The subject key comes from the CSR untouched — the requester's algorithm need not match the CA's — while the signature is always the CA's.
@ pki_verify_cert * PkiCa ca s cert_pem s expected_cn → bFull check of a leaf against this CA: parses, enforces the validity window, matches the expected CN against the SANs and verifies the issuer signature with the CA's algorithm.
@ pki_extract_cert_info s cert_pem → PkiCertInfo@ pki_generate_crl * PkiCa ca ( Vec String ) revoked_serials ( Vec i ) revoked_times → String: PkiRevoked: PkiRevoked {
( Vec String ) serials
( Vec i ) times
}
One parsed R line of the OpenSSL-style index.txt.
@ pki_revoked_free PkiRevoked r → v@ pki_read_revoked s index_file_path → PkiRevokedRead the revoked set out of index.txt. Fields are R <notAfter> <revocationDate> <serial> <filename> <subject>, both dates in UTCTime — so the original revocation date is what goes back into the regenerated CRL. Lines whose serial or date does not parse are dropped rather than propagated.
@ pki_is_revoked s index_file_path s serial_hex → bIs this serial on the revocation list? Used to refuse an operational certificate to a device whose enrollment certificate was revoked — the file-level invalidation alone is not a check, it is a side effect.
@ pki_record_revocation s index_file_path s crl_file_path * PkiCa ca s serial_hex s cn → Stringserial_hex and cn must already have been through pki_normalise_serial / pki_sanitize_id — this appends them to a tab-separated file, where a raw tab or newline would forge records.
@ pki_load_crl s crl_file_path * PkiCa ca s index_file_path → String@ pki_invalidate_initial_cert s initial_dir s device_id → bOverwrite a device's stored enrollment certificate so the DER-equality gate on /request-cert and /renew_initial_cert can never match again. device_id is sanitised here, at the one place that turns it into a path — a CN lifted out of a submitted certificate is attacker-chosen.
pki-server/src/ui.nu — Server-rendered Web Interface for PKI management.
@ ui_html_escape s raw → StringEscape a value for interpolation into HTML text or a quoted attribute. Everything this module renders that did not come from a string literal in this file goes through here — a certificate serial and an error message both arrive from the request body, and <script> in either one used to reach the browser verbatim.
The five characters are the OWASP text-and-attribute set: escaping both quote forms is what makes the same function safe in an attribute context, not only between tags.
@ _ui_push_esc String out s raw → v@ _ui_wrap s title String body s active_nav → StringBase layout wrapper
@ ui_render_index → String@ ui_render_request_cert s error_msg → String@ ui_render_cert_result s device_id s cert s key s ca_cert s expires → String@ ui_render_revoke_cert s error_msg → String@ ui_render_revoke_result s message s serial s revocation_time s crl → String@ ui_render_api_docs s alg_display → String@ ui_default_css → String@ ui_app_js → StringThe page's only script, served from /js/app.js so the Content-Security- Policy can say script-src 'self' and mean it. Inline onclick= handlers would force 'unsafe-inline', which is the same as having no script policy at all.
pki-server/src/main.nu — Pure-NURL Private PKI Server.
@ _env_or s name s default_val → StringHelper: env var or default
@ _resolve ArgParser ap s flag s env_name s fallback → StringCLI flag beats environment variable beats built-in default.
@ _is_placeholder_key String k → bThe two placeholder secrets earlier releases shipped as defaults. A deployment that never overrode them was authenticating every caller against a string printed in the README, so they are treated as "no key given" rather than as a key.
@ _generate_key s label → StringMint a 192-bit key and tell the operator what it is — once, on stderr. Refusing to start would be the other defensible answer, but it breaks docker run with no volume; a key nobody can guess, printed where the logs will keep it, is secure without being unusable.
@ main → i