---
title: "X.509, certification requests, CRLs, OCSP and attribute certificates"
url: https://recursiveai.org/onecrypto/docs/x509/
description: "Normative behaviour of the PKI models in RecursiveAI.OneCrypto (the Certificate, CertificateSigningRequest, CertificateRevocationList, OcspRequest…"
version: "0.1.0-preview.1"
source: https://github.com/recursive-ai-oss/onecrypto-csharp/blob/3fcea7e7bbea11c5f528ec1865af04bcba8e23ac/docs/x509.md
---

# X.509, certification requests, CRLs, OCSP and attribute certificates

Normative behaviour of the PKI models in `RecursiveAI.OneCrypto` (the `Certificate`, `CertificateSigningRequest`, `CertificateRevocationList`, `OcspRequest`, `OcspResponse`, `OcspBasicResponse` and `AttributeCertificate` types and the names, general names, extensions and public keys they hold), with the typed extension values in `RecursiveAI.OneCrypto.X509`. Clause numbers are RFC 5280 unless another document is named. These types describe and check data. Path building, validity at a time, revocation decisions and trust belong to the validator, which builds on them.

## Shape of every model

* **Immutable and lossless.** An object keeps the octets it was read from (`Encoded`) and decodes from them on first use. A built object is the parse of the DER the builder wrote, so there is one code path. Unknown extensions, attributes and algorithms are preserved with their criticality and parameters.
* **Signatures cover the received bytes.** `tbsCertificate`, `tbsCertList`, `certificationRequestInfo`, `tbsRequest`, `tbsResponseData` and `acinfo` are kept as slices of the input and passed to the verifier as they are. Nothing is re-encoded to be hashed. A certificate in BER (indefinite lengths, long-form lengths) is read and verified over its own octets; `Certificate.IsDer` says whether it is strict DER.
* **Parsing reads BER, builders write DER.** The readers apply the BER rules of X.690 and keep a typed error for anything else. After a successful `Parse`, the structure is sound: the framing of every algorithm identifier, name, extension list, CRL entry, attribute and embedded certificate has been walked, so the accessors that decode them cannot fail. The one exception is the *content* of an extension value: `Certificate.BasicConstraints`, `Extensions.Get<T>()` and their kind throw `X509ParseException` for a malformed value, and `Extensions.TryGet<T>()` reports it as false. The certificate itself stays usable.
* **Typed errors.** `X509ParseException` (derives from `FormatException`) carries the structure name and an `X509Error` (`Asn1`, `UnexpectedStructure`, `UnsupportedVersion`, `InvalidValue`, `LimitExceeded`); an ASN.1 failure is its inner exception with the offset. Each `Parse` has a `TryParse`. No other exception type comes out of a reader.
* **Results, not booleans.** `VerifySignature` returns `SignatureVerification { Status, Algorithm, Reason, Reference }` with `Status` one of `Valid`, `Invalid`, `AlgorithmNotSupported`, `KeyMismatch`, `Malformed`. Reasons name the clause that decided them.

## Limits (`X509Limits`, on by default)

| Limit | Default | Applies to |
|---|---|---|
| `MaxEncodedSize` | 1 MiB | a certificate, request, attribute certificate or OCSP message |
| `MaxCrlSize` | 256 MiB | a CRL |
| `MaxExtensions` | 128 | one extension list |
| `MaxNameElements` | 64 | RDNs in a name, and attributes in one RDN |
| `MaxNameLength` | 64 KiB | one encoded name |
| `MaxListEntries` | 10,000 | entries of any list inside an extension value, OCSP request and response lists, attributes of a request or attribute certificate |
| `MaxEmbeddedCertificates` | 64 | certificates inside one OCSP message |
| `Asn1` | depth 32 | the ASN.1 limits of `docs/asn1.md` |

A serial number longer than 64 octets is refused (RFC 5280 limits it to 20; real certificates exceed that, so the reader is lenient and the builder is not).

## Names

`DistinguishedName` keeps the encoded `Name`. `ToString()` is RFC 4514: relative names in reverse order, keywords only for the types RFC 4514 §3 lists (others are dotted OIDs), the escapes of §2.4, and `#` plus the hex of the BER encoding for values that are not string types. `FromString` reads that syntax (and a few common keywords: `S`, `E`, `emailAddress`, `serialNumber`). Built names use PrintableString for country and serial number, IA5String for e-mail and domain component, UTF8String otherwise. `Matches` is the comparison of §7.1 in its practical form: equal attribute types, string values equal after trimming, collapsing inner white space and upper-casing, other values octet for octet, the attributes of a multi-valued RDN as a set. It does not apply the full RFC 4518 mapping; names that differ only by that mapping are not matched.

## Times

UTCTime years 50 to 99 read as 19xx and 00 to 49 as 20xx (§4.1.2.5.1). Built times use UTCTime for 1950 to 2049 and GeneralizedTime otherwise, to whole seconds.

## Public keys and signature algorithms

`PublicKeyInfo` classifies the algorithm: RSA, RSA-PSS and RSA-OAEP keys, EC keys (any named curve; `CurveOid`), Ed25519, Ed448, X25519, X448 (RFC 8410), ML-DSA, ML-KEM, SLH-DSA, composite ML-DSA (recognised by the draft's identifiers, preview), and `Unknown` for everything else, which is kept as received.

`VerifySignature` rules:

| Algorithm | Behaviour |
|---|---|
| RSA PKCS #1 v1.5 (SHA-1/224/256/384/512, SHA3, MD5 read) | platform `RSA`, key must be `rsaEncryption`. The hash is not judged here; a policy decides whether SHA-1 or MD5 is acceptable. |
| RSASSA-PSS | parameters read per RFC 4055 §3.1. Supported: MGF1 on any hash the library knows, trailer 1, any salt length. The platform's own PSS check handles only a salt as long as the digest with MGF1 on the same hash; every other parameter set goes to the library's verify-only RSA (public data only). A key that carries PSS parameters pins hash, MGF hash and minimum salt (RFC 4055 §1.2). |
| ECDSA (SHA-1/224/256/384/512, SHA3) | platform `ECDsa`. The signature must be strict DER of two positive INTEGERs (RFC 3279 §2.2.3), checked by the library first because the platform also accepts unpadded and BER integers (Wycheproof `MissingZero` cases). A curve the platform lacks gives `AlgorithmNotSupported`. |
| Ed25519, Ed448 | through the library's Edwards-curve primitives; `AlgorithmNotSupported` where a build has none. |
| ML-DSA-44/65/87 | platform `MLDsa` (empty context, RFC 9881) where the platform has it, otherwise `AlgorithmNotSupported` naming the platform. |
| SLH-DSA | verified through the platform class `SlhDsa`, which is still an evaluation API: the single use sits in one file under one documented warning suppression, results carry "(preview)" in their reference, and where the platform lacks it the answer is `AlgorithmNotSupported`. |
| Composite ML-DSA | recognised, never verified (`AlgorithmNotSupported`). |

An outer signature algorithm that differs from the one inside the signed structure, or a signature BIT STRING with unused bits, is `Invalid` (§4.1.1.2, §4.1.1.3).

## Extensions

Typed values (`IExtensionValue<T>`, read with `Extensions.Get<T>()`, written with `Extension.Create`): basicConstraints, keyUsage, extKeyUsage, subjectAltName, issuerAltName, subjectKeyIdentifier, authorityKeyIdentifier, nameConstraints, certificatePolicies, policyMappings, policyConstraints, inhibitAnyPolicy, cRLDistributionPoints, freshestCRL, authorityInfoAccess, subjectInfoAccess; for CRLs cRLNumber, deltaCRLIndicator, issuingDistributionPoint and the entry extensions cRLReason, invalidityDate, certificateIssuer; for OCSP the nonce and no-check. Every GeneralName form is decoded (otherName, rfc822Name, dNSName, x400Address and ediPartyName kept undecoded, directoryName, URI, iPAddress of 4/8/16/32 octets, registeredID). Duplicate extensions are kept in order; `ExtensionCollection.HasDuplicates` reports them (§4.2 forbids them; the validator decides).

The OCSP nonce is written as an OCTET STRING inside `extnValue` (RFC 6960 §4.4.1, RFC 8954) and read both in that form and as raw octets.

## Builders

Plain options objects and static methods; no fluent chains. `CertificateBuilder.CreateSelfSigned/Create`, `CertificateSigningRequestBuilder.Create`, `RevocationListBuilder.Create` (complete and delta), `OcspRequestBuilder.Create`, `OcspResponseBuilder.CreateSuccessful/CreateError`. They sign through `X509Signer`, a signature spec bound to a key: `FromKey` takes any key interface (see keys.md), and `FromRsa` (PKCS #1 or PSS), `FromECDsa`, `FromMLDsa` and `FromDelegate` are conveniences. A private key never leaves its object. Certificates, requests and revocation lists also build in two phases (`Prepare`, then `Complete`; keys.md). `CertificateOptions` adds a subjectKeyIdentifier (RFC 7093 method 1: the left 160 bits of the SHA-256 of the key bits) and an authorityKeyIdentifier unless given; serial numbers default to 159 random bits.

## OCSP

`OcspCertId.Create` defaults to SHA-1, which every responder must accept (RFC 6960 §4.3); the hash of the issuer name is taken over the certificate's own issuer field as received, and the key hash over the issuer's key bits. `OcspCertId.Matches` recomputes the hashes with the identifier's own algorithm. A response whose status is `successful` must carry response bytes and any other status must not (§4.2.1); both directions are refused. Checking the responder's authority, the validity window and the nonce against a request is the caller's or the validator's job.

## Platform conversion

`Certificate.ToX509Certificate2()` and `Certificate.FromX509Certificate2` pass the same octets. `TryToX509Certificate2` reports false for what the platform cannot load. A certificate held only as an `X509Certificate2` keeps its private key in the platform object; the model never sees it.

## Differences from `X509Certificate2` (x509-limbo, 30,379 distinct certificates and 18 CRLs)

Compared where both load: subject and issuer octets, serial number (leading zeros trimmed), validity, extension identifiers and criticality in order, SPKI algorithm and key bits. Of 30,379 distinct certificates in the pinned x509-limbo, 30,377 parse here and the platform loads all of them with no difference in any compared field. The other two (`pathological::nc-dos-1` and `nc-dos-3`) are refused with `X509Error.LimitExceeded` because their names have more than `MaxNameElements` relative names; the platform loads them. This is the one documented deviation: the limit is on by default and can be raised by passing `X509Limits`.

## Policy at signing and verification

The one-shot builders ask a `CryptoPolicy` (default `Standard`) before they sign: a SHA-1 or MD5 certificate, request, list, OCSP request or response is refused with `PolicyViolationException` (rule `hash.sha-1.new`), as is an RSA key under 2048 bits. Pass a permissive policy to build old data on purpose. `Certificate.VerifySignature(issuerKey, policy, at)` adds the policy's decisions to the result and ends as `SignatureStatus.PolicyRejected` when a correct signature is refused at `at`. The OCSP request and response builders have a two-phase `Prepare` / `Complete` like the other builders.
