iyi

Standard library, 0.18.0

std/x509

X.509 certificates read and chains checked (RFC 5280), written in iyi: what a TLS client asks of a server's certificates.

import std/x509 to use it. Source: src/std/x509.iyi at v0.18.0.

  import std/x509::{Certificate, X509, X509Error}
  roots = Certificate.from_pem(File.read("/etc/ssl/certs/ca-certificates.crt")).or_panic
  leaf = Certificate.parse(der).or_panic
  chain = X509.verify(leaf, intermediates, roots, "example.com").or_panic
  # the chain from leaf to root, or an X509Error saying why there is none

Certificate.parse reads a certificate's DER through std/asn1 and its key through std/rsa, std/ecdsa or std/curve25519: version, serial, issuer and subject (kept as their DER, which is what a chain matches), validity, key, signature, and the extensions a chain is checked by - basicConstraints, keyUsage, extKeyUsage, subjectAltName's DNS names and IP addresses, nameConstraints, and the key identifiers. An extension that appears twice is refused. One that is critical and not among these is kept by name, and a chain through its certificate is refused: RFC 5280 section 4.2 says a certificate with a critical extension it does not understand must not be used. The signature algorithm written inside the signed part must be the one written outside it.

X509.verify builds a chain from the leaf to one of the given roots, through the given intermediates, matching each certificate's issuer to a candidate's subject byte for byte and trying every candidate that matches before giving up, ten certificates at most. A certificate checked as an issuer must be valid at the time given, a CA by its basicConstraints, allowed keyCertSign if it carries keyUsage, within its pathLenConstraint, allowed the chain's purpose if it carries extKeyUsage (serverAuth for a server's certificate, the default; clientAuth for a client's, X509Purpose::Client), and the leaf's names must fall inside its nameConstraints; it must have signed the certificate below it. Name constraints are checked for DNS names and IP addresses; a constraint of any other kind refuses the chain rather than being passed over. The leaf must be valid at that time, not a CA (a CA's certificate is not a server's, as the Web PKI's verifiers hold, nor a client's), allowed the purpose if it carries extKeyUsage, and, when a host is given, name it.

A host is matched against subjectAltName only, never the subject's common name, as browsers and RFC 6125 have it: a DNS name exactly, without regard to case or a final dot, or as *.rest where the * is the whole of the leftmost label and stands for exactly one; an IP address, v4 or v6, against the iPAddress entries.

Signatures: RSA PKCS #1 v1.5 and PSS, and ECDSA, under SHA-256, SHA-384 and SHA-512, and Ed25519 (RFC 8410: the key's and the signature's AlgorithmIdentifier carry no parameters at all). A certificate signed under SHA-1 is refused, as every browser refuses one. A root's own signature is not checked: a root is trusted for being in the store, not for what it says about itself.

Pure computation: std/asn1, std/rsa, std/ecdsa, std/curve25519, std/base64 for PEM and std/idna for a host written in Unicode.

Certificatestruct

std/x509iyi doc src/std/x509.iyi
pub struct Certificate
  def authority_key_id : Array(UInt8) | ::Nil
  # basicConstraints: whether it was present, cA, and pathLenConstraint.
  def basic_constraints : Bool
  def ca : Bool
  # subjectAltName's dNSName and iPAddress entries.
  def dns_names : Array(String)
  def excluded_dns : Array(String)
  def excluded_ips : Array(Array(UInt8))
  # extKeyUsage's OIDs; nil when the extension is absent.
  def extended_key_usage : Array(String) | ::Nil
  # Every CERTIFICATE block in a PEM text, read; the first one that is not
  # a certificate is the answer.
  def self.from_pem(text : String) : Array(Certificate) | X509Error
  def initialize(raw : Array(UInt8), tbs : Array(UInt8), version : Int32, serial : Array(UInt8), issuer : Array(UInt8), subject : Array(UInt8), subject_name : String, issuer_name : String, not_before : Time, not_after : Time, public_key : RsaPublicKey | EcdsaPublicKey | Ed25519PublicKey, signature_algorithm : String, signature_parameters : DerValue | ::Nil, signature : Array(UInt8), extensions : X509Extensions)
  def ip_addresses : Array(Array(UInt8))
  # Issuer and subject as their DER: two names are the same name when
  # these are the same bytes.
  def issuer : Array(UInt8)
  def issuer_name : String
  # keyUsage's bits, bit 0 digitalSignature to bit 8 decipherOnly; nil
  # when the extension is absent.
  def key_usage : Int32 | ::Nil
  # Whether *host* is a name this certificate is for: a DNS name or an IP
  # address in its subjectAltName (see the module's header).
  def matches_host?(host : String) : Bool
  def not_after : Time
  def not_before : Time
  def other_constraints : Bool
  # The certificate *der* holds (RFC 5280 section 4.1).
  def self.parse(der : Array(UInt8)) : Certificate | X509Error
  def path_length : Int32 | ::Nil
  # The DER of every CERTIFICATE block in a PEM text, unread: a store
  # can keep what it can read and count the rest.
  def self.pem_blocks(text : String) : Array(Array(UInt8)) | X509Error
  # nameConstraints: DNS names and IP ranges (address and mask), and
  # whether a constraint of any other kind is there.
  def permitted_dns : Array(String)
  def permitted_ips : Array(Array(UInt8))
  def public_key : RsaPublicKey | EcdsaPublicKey | Ed25519PublicKey
  # The whole certificate as it was read, and its signed part.
  def raw : Array(UInt8)
  # The serial number's two's-complement bytes, as written.
  def serial : Array(UInt8)
  def signature : Array(UInt8)
  # The signature algorithm's OID, and the signature.
  def signature_algorithm : String
  # Why *issuer*'s key did not make this certificate's signature, or nil.
  def signature_error(issuer : Certificate) : String | ::Nil
  # Whether *issuer*'s key made this certificate's signature.
  def signed_by?(issuer : Certificate) : Bool
  def subject : Array(UInt8)
  def subject_key_id : Array(UInt8) | ::Nil
  # The subject and issuer written out for a person, `CN=..., O=...`.
  def subject_name : String
  def tbs : Array(UInt8)
  # Critical extensions this does not understand, by OID.
  def unhandled_critical : Array(String)
  def valid_at?(time : Time) : Bool
  def version : Int32
end

X509struct

std/x509iyi doc src/std/x509.iyi
pub struct X509
  def initialize
  # The DER of every block labelled *label* in a PEM text - `"CERTIFICATE"`
  # or `"PRIVATE KEY"` - each checked to be base64 before it is decoded.
  def self.pem(text : String, label : String) : Array(Array(UInt8)) | X509Error
  # The chain from *leaf* to one of *roots*, through *intermediates*, for
  # *host* (nil: any) at *now* - leaf first, root last - or why there is
  # none. *purpose* is what the leaf is for: a server's certificate
  # (serverAuth, the default) or a client's (clientAuth), each certificate
  # that carries extKeyUsage allowing it. See the module's header for what
  # else is checked.
  def self.verify(leaf : Certificate, intermediates : Array(Certificate), roots : Array(Certificate), host : String | ::Nil = nil, now : Time = Time.utc, purpose : X509Purpose = X509Purpose::Server) : Array(Certificate) | X509Error
end

X509Errorstruct

What reading a certificate, or checking a chain, answers when it fails.

std/x509iyi doc src/std/x509.iyi
pub struct X509Error
  def initialize(message : String)
  def to_s : String
end

X509Purposeenum

What a chain is checked for: a TLS server's certificate or a client's (RFC 5280 section 4.2.1.12's id-kp-serverAuth and id-kp-clientAuth).

std/x509iyi doc src/std/x509.iyi
pub enum X509Purpose : Int32
  Server = 0
  Client = 1
  def holder : String
  def oid : String
  # The usage's name, as a sentence says it.
  def usage : String
end

Error for X509Errorimpl

std/x509iyi doc src/std/x509.iyi
impl Error for X509Error
  def message : String
end