| English | 中文版 |
RFC Number: NPS-RFC-0002 Title: Adopt X.509 + ACME for NID certificates Status: Accepted Author(s): Ori Lynn iamzerolin@gmail.com (LabAcacia) Shepherd: TBD — assigned on PR open Created: 2026-04-21 Last-Updated: 2026-05-27 Accepted: 2026-05-27 Activated: (set when first reference SDK ships) Supersedes: none Superseded-By: none Affected Specs: NPS-3 NIP, tools/nip-ca-server (all language variants), spec/error-codes.md Affected SDKs: .NET, Python, TypeScript, Java, Rust, Go —
Replace the NIP custom certificate format with X.509v3 certificates
carrying a LabAcacia-registered Extended Key Usage (EKU) OID for
“agent-identity”, and move NIP CA issuance from the bespoke REST API to
ACME (RFC 8555) with an agent-01 challenge type suited to
programmatic NID control. Ed25519 remains the primary signature
algorithm (RFC 8410 — Ed25519 in X.509). CRL / OCSP revocation semantics
are preserved.
This RFC responds to a 2026-04-20 review comment: “CA 不如直接兼容 现有的 CA 套路” (CAs should be compatible with existing CA practice).
The commenter’s point stands. Current NIP CA (tools/nip-ca-server-*)
replicates the operational model of a classic X.509 CA — CSR → issuance
→ CRL/OCSP revocation → renewal — but uses a bespoke serialization.
This creates two unnecessary costs:
step-ca,
HashiCorp Vault PKI, HSM vendors, cert-manager for Kubernetes —
none of them can sign, validate, or store NIP certs without custom
integration. The NIP CA server variants in 6 languages each
re-implement the ceremony.Switching to X.509 + ACME costs us ASN.1 parser exposure and ~2–4× cert size, but buys us 15 years of PKI tooling and the ability to run “Let’s Encrypt for Agents” as a future public service — or to delegate NID issuance to existing CAs via cross-signing.
IdentFrame — the certificate
still travels as cert_chain bytes; only the internal encoding
shifts from NIP-proprietary to X.509.NID format (nid:{algo}:{base64url(pubkey)}).IdentFrame.cert_chain encoding changes from nip-cert-v1 (custom)
to DER-encoded X.509 certificate chain concatenated or length-prefixed
per rule below.
Subject field mapping:
| X.509 Field | NIP Value |
|---|---|
Subject CN |
NID string (nid:ed25519:...) |
| Subject Alternative Name (SAN) URI | Same NID as a URI entry for RFC 5280 compliance |
| Issuer | Issuing CA’s NID |
| NotBefore / NotAfter | Standard X.509 validity period |
| Public Key | Ed25519 (RFC 8410 OID 1.3.101.112) |
| Serial Number | 128-bit random, per CA/B Forum baseline |
Critical extension — Extended Key Usage:
Register LabAcacia IANA Private Enterprise Number (PEN) — once assigned,
reserve OID arc 1.3.6.1.4.1.<PEN>.1.
| OID | Meaning |
|---|---|
1.3.6.1.4.1.<PEN>.1.1 |
agent-identity — this cert’s subject is an NPS Agent |
1.3.6.1.4.1.<PEN>.1.2 |
node-identity — this cert’s subject is an NPS Node |
1.3.6.1.4.1.<PEN>.1.3 |
ca-intermediate-agent — this CA may sign agent-identity certs |
EKU is marked critical. A verifier that doesn’t recognize these EKUs MUST reject the certificate for NIP purposes. (Generic TLS clients would also reject, which is intentional — NID certs MUST NOT be mistaken for TLS server certs.)
Custom non-critical extension — nid:assurance-level:
Reserved for NPS-RFC-0003. This RFC defines the encoding shape only:
nid-assurance-level EXTENSION ::= {
SYNTAX NidAssuranceLevel
IDENTIFIED BY id-nid-assurance-level -- 1.3.6.1.4.1.<PEN>.2.1
}
NidAssuranceLevel ::= ENUMERATED {
anonymous (0),
attested (1),
verified (2)
}
Non-critical so v0.1 verifiers ignore the field; RFC-0003 flips it to critical when assurance-level enforcement lands.
None. X.509 cert chain travels inside IdentFrame, transparent to NWM.
New entries in spec/error-codes.md:
| Error Code | NPS Status | Description |
|---|---|---|
NIP-CERT-FORMAT-INVALID |
NPS-CLIENT-BAD-FRAME |
Cert chain is not DER-encoded X.509 or fails ASN.1 parsing |
NIP-CERT-EKU-MISSING |
NPS-CLIENT-BAD-FRAME |
Required NPS EKU (agent-identity or node-identity) absent |
NIP-CERT-SUBJECT-NID-MISMATCH |
NPS-CLIENT-BAD-FRAME |
Cert Subject CN / SAN URI does not match the NID in IdentFrame |
NIP-ACME-CHALLENGE-FAILED |
NPS-CLIENT-BAD-FRAME |
ACME agent-01 challenge validation failed |
ACME agent-01 challenge — a new challenge type for NID identity
validation (parallels HTTP/DNS/TLS-ALPN challenges in RFC 8555 §8):
Client (Agent) ACME Server (NIP CA)
│ │
│── newAccount (Ed25519 JWK) ──→ │
│ ←── 201 + kid ──────────────── │
│ │
│── newOrder (identifiers) ────→ │
│ identifier: type=nid, │
│ value=nid:ed25519:... │
│ ←── order + authz URL ──────── │
│ │
│── GET authz URL ──────────────→ │
│ ←── challenge: type=agent-01, │
│ token=T ────────────────── │
│ │
│ (Agent signs `T` with its │
│ NID private key; serves │
│ the signature at a │
│ well-known /nip/auth/T │
│ on its announced endpoint │
│ — OR — posts back to ACME │
│ as a JWS) │
│ │
│── POST challenge (signed T) ─→ │
│ ←── 200 + status=valid ──────── │
│ │
│── finalize (CSR) ─────────────→ │
│ ←── 200 + cert URL ──────────── │
│── GET cert URL ───────────────→ │
│ ←── 200 + X.509 DER ────────── │
The agent-01 challenge proves possession of the NID private key in a
way that mirrors TLS-ALPN-01’s simplicity: one signed token, no external
DNS / HTTP dependency. Servers MUST implement constant-time (timing-safe) comparison when
verifying the signed challenge token against the expected value, to prevent
timing-oracle attacks on the NID private key. (NIP §10.2 covers a distinct
concern — OCSP response-time normalization — and does not apply here.)
cert_format field
(v1-proprietary | v2-x509). Deployments running v0.2 stacks can
phase the rollout.min_agent_version bump with 21-day window per RFC process.Keep current cert format; offer a one-way export to X.509 for interop
(e.g., nipc export --x509).
Use X.509 but keep the current bespoke /certs/issue REST endpoint.
step-ca, boulder,
pebble) for the .NET variant at minimum, then port.System.Security.Cryptography.X509Certificates
for .NET, cryptography for Python, x509-parser for Rust, etc.) —
never hand-roll.cert_format version field.newAccount gives account holders
rotation keys. NIP CA MUST enforce that Ed25519 is the account-key
algorithm (matches NID algorithm family).| Phase | Scope | Exit criterion |
|---|---|---|
| 1 | .NET NIP + .NET CA server emit + accept X.509; ACME agent-01 in .NET CA; v1 + v2 coexist |
Unit tests green; cross-format interop (v1 client ↔ v2 server and vice versa) |
| 2 | All 6 SDKs + 6 CA servers support X.509 + ACME; cert_format=v2 default-off |
Cross-SDK cert-accept matrix green; ACME issuance works across all 6 CA variants |
| 3 | Flip cert_format=v2 default-on; 21-day deprecation notice for v1 |
No regressions for 1 release cycle |
| 4 | Remove v1 codepath | v1 support removed from all 12 repos |
| SDK | Owner | Status | Notes |
|---|---|---|---|
| .NET | Ori Lynn | pending | Primary reference |
| Python | TBD | pending | Use cryptography lib |
| TypeScript | TBD | pending | Use @peculiar/x509 |
| Java | TBD | pending | Bouncy Castle |
| Rust | TBD | pending | x509-parser + rcgen |
| Go | TBD | pending | stdlib crypto/x509 |
agent-identity EKU; Agent
presents it; verifier accepts.NIP-CERT-EKU-MISSING.NIP-CERT-SUBJECT-NID-MISMATCH.agent-01 happy path end-to-end.Thresholds revised after the 2026-04-27 prototype run (see §9.2):
dotnet publish -c Release. Not yet measured; will land in the
npsd / nip-ca-server port phase.The prototype lives at feat/rfc-0002-x509-acme-prototype on dev. It
delivers, in order:
impl/dotnet/src/NPS.NIP/X509/{NpsX509Oids,NipX509Builder,NipX509Verifier,Ed25519X509SignatureGenerator}.cs.
Five conformance tests in tests/NPS.Tests/Nip/X509/NipX509Tests.cs
cover round-trip, EKU-missing rejection, subject/NID-mismatch
rejection, and v1↔v2 cross-format compatibility.agent-01 challenge type —
impl/dotnet/src/NPS.NIP/Acme/{AcmeJws,AcmeMessages,AcmeServer,AcmeClient}.cs
tests/NPS.Tests/Nip/Acme/AcmeAgent01Tests.cs. Two tests cover
end-to-end issuance through the new challenge type and a tampered-
signature negative path returning NIP-ACME-CHALLENGE-FAILED.agent-01 (a non-
standard challenge type proposed by this RFC); validating standard
ACME conformance against pebble would only add HTTP-01 round-trip
coverage on top of agent-01. tools/pebble/setup.sh ships the
binary download helper for that follow-up.Numbers below are produced by NPS.Benchmarks.NipCert
(dotnet run -c Release --project impl/dotnet/benchmarks/NPS.Benchmarks.NipCert -- --emit),
which writes a Markdown report to docs/benchmarks/nip-cert-prototype.md.
| Metric | Baseline (v1) | Proposed (v2) | Delta | Method |
|---|---|---|---|---|
IdentFrame JSON size |
459 B | 1512 B | +1053 B (+229%) | UTF-8 byte count of JsonSerializer.Serialize(IdentFrame) |
| Verification latency (mean of 2000 iters) | 597.8 µs | 1698.5 µs | 2.84× | Stopwatch over verifier.VerifyAsync(frame) |
Two RFC §8.4 thresholds are revisited by this data:
The prototype ran X.509 first, then layered ACME. Effort: ~5 days of
working time for X.509 + verifier + tests; ~2 days for ACME (client +
in-process server with agent-01 + tests). pebble HTTP-01 interop
would have added another ~1 day if the new agent-01 challenge type
weren’t blocking direct interop.
Recommendation for OQ-1: bundle X.509 + ACME in the same acceptance (the original “both together” default position). The ACME piece is mostly mechanical once X.509 issuance is wired; splitting forces a second wave of cross-language port work that can be saved by doing it once.
1.3.6.1.4.1.99999 is retired. All NpsX509Oids
constants now anchor on 1.3.6.1.4.1.65715. Any cert issued under
1.3.6.1.4.1.99999.* MUST be revoked and re-issued under the
assigned arc before being relied on for conformance, production NID
issuance, or cross-organisation interop. With PEN assigned, this RFC
moves from Draft to Proposed.spec/NPS-3-NIP.md — current NIP spectools/nip-ca-server/README.md — current CA server| Date | Author | Change |
|---|---|---|
| 2026-04-27 | Claude (prototype) | Backfill §9 Empirical Data with measurements from feat/rfc-0002-x509-acme-prototype (.NET prototype + NPS.Benchmarks.NipCert). Resolve OQ-1 (bundle X.509 + ACME). Revise §8.4 thresholds (size 1200 → 1600 B; drop absolute latency target, keep ratio). |
| 2026-04-21 | Ori Lynn | Initial draft |