Recursive AI

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

All onecrypto docs

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)

LimitDefaultApplies to
MaxEncodedSize1 MiBa certificate, request, attribute certificate or OCSP message
MaxCrlSize256 MiBa CRL
MaxExtensions128one extension list
MaxNameElements64RDNs in a name, and attributes in one RDN
MaxNameLength64 KiBone encoded name
MaxListEntries10,000entries of any list inside an extension value, OCSP request and response lists, attributes of a request or attribute certificate
MaxEmbeddedCertificates64certificates inside one OCSP message
Asn1depth 32the 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:

AlgorithmBehaviour
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-PSSparameters 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, Ed448through the library's Edwards-curve primitives; AlgorithmNotSupported where a build has none.
ML-DSA-44/65/87platform MLDsa (empty context, RFC 9881) where the platform has it, otherwise AlgorithmNotSupported naming the platform.
SLH-DSAverified 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-DSArecognised, 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.