Certificates & PKI

Two units cover opposite directions of the same X.509 format: sgcCrypto_X509 reads and verifies a certificate someone else gave you, sgcCrypto_X509_Gen writes a certificate or request of your own. A third unit, sgcCrypto_ASN1, provides the DER/PEM plumbing both of them sit on.

Reading and verifying a certificate

sgcCrypto_X509 parses a DER encoded certificate into a plain record, and checks a chain or a revocation list against it. Use this side of the API when your application receives a certificate over the wire and has to decide whether to trust it.


function sgcX509_Parse(const aDER: TBytes; out aCert: TsgcX509Cert): Boolean;
function sgcX509_VerifySignedBy(const aCert, aIssuer: TsgcX509Cert): Boolean;
function sgcX509_VerifyChain(const aChainDER: array of TBytes; const aTrustedRoots: array of TsgcX509Cert): Boolean;
function sgcX509_SubjectOneLine(const aCert: TsgcX509Cert): string;
function sgcX509_IssuerRFC2253(const aCert: TsgcX509Cert): string;
function sgcX509_CRL_Parse(const aDER: TBytes; out aCRL: TsgcX509CRL): Boolean;
function sgcX509_IsRevoked(const aCRL: TsgcX509CRL; const aSerialNumber: TBytes): Boolean;

TsgcX509Cert exposes the fields a caller actually needs after parsing: Version, SerialNumber, the raw Issuer/Subject RDN sequences, NotBeforeUnix/NotAfterUnix, the public key (RSA, EC or Ed25519, whichever PubKeyType says it is) and IsCA.

Generating a certificate or a request

sgcCrypto_X509_Gen builds a self-signed certificate, or a PKCS#10 certificate signing request to send to a CA, from an RSA key pair (see Public Key Cryptography). Both take the same options record.


function sgcX509_CreateSelfSigned(const aKey: TsgcRSAPrivateKey; const aOptions: TsgcX509Options): TBytes;
function sgcX509_CreateCSR(const aKey: TsgcRSAPrivateKey; const aOptions: TsgcX509Options): TBytes;
function sgcX509_ToPEM(const aDER: TBytes; const aLabel: string): string;

var
  oKey: TsgcRSAPrivateKey;
  oOptions: TsgcX509Options;
  oDER: TBytes;
begin
  oKey := sgcRSA_GenerateKey(2048);
  oOptions := sgcX509_DefaultOptions;
  oOptions.Subject.CommonName := 'example.com';
  oOptions.NotBefore := Now;
  oOptions.NotAfter := Now + 365;
  SetLength(oOptions.DNSNames, 2);
  oOptions.DNSNames[0] := 'example.com';
  oOptions.DNSNames[1] := 'www.example.com';
  oOptions.KeyUsage := [kuDigitalSignature, kuKeyEncipherment];
  SetLength(oOptions.ExtKeyUsage, 1);
  oOptions.ExtKeyUsage[0] := '1.3.6.1.5.5.7.3.1'; { TLS server authentication }

  oDER := sgcX509_CreateSelfSigned(oKey, oOptions);
  Memo1.Text := sgcX509_ToPEM(oDER, 'CERTIFICATE');
end;

Build the validity window from UTC.

Certificate validity is encoded as UTCTime with a Z suffix, meaning Greenwich time, so filling NotBefore and NotAfter from a local Now dates the certificate wrong by your offset from UTC. East of Greenwich a freshly issued certificate is then not yet valid until that offset elapses, west of it the certificate expires early. sgcX509_UTCNow returns the right value, and sgcX509_DefaultOptions already uses it with a five minute backdate for clock skew, the same margin real CAs apply.


function sgcX509_UTCNow: TDateTime;

var
  oOptions: TsgcX509Options;
begin
  oOptions := sgcX509_DefaultOptions;
  oOptions.NotBefore := sgcX509_UTCNow;
  oOptions.NotAfter := sgcX509_UTCNow + 365;
end;

Fill in the subject alternative names.

Modern browsers and TLS libraries ignore the common name entirely and look only at the DNSNames extension; a certificate with a perfect common name and no DNS name in SubjectAltName will be rejected however correct the rest of it is.

Alongside DNSNames, IPAddresses and Emails, TsgcX509Options carries a URIs array, for subject alternative names of type uniformResourceIdentifier. A service identity that is a URI rather than a host name goes here, the concrete case being a SPIFFE ID, which a workload mesh matches literally and which no DNS name can express. The value goes in verbatim as IA5String content, so pass a full URI.


SetLength(oOptions.URIs, 1);
oOptions.URIs[0] := 'spiffe://example.com/ns/default/sa/payments';

Set IsCA := True and a non-negative PathLength to produce a CA certificate rather than a leaf certificate; leave PathLength at -1 to omit the constraint on a leaf. A CSR carries the same requested extensions in its extensionRequest attribute, which is where a CA looks for them when issuing the final certificate.

Signing with an EC key

sgcX509_CreateSelfSigned and sgcX509_CreateCSR sign with RSA. The Ex pair takes a TsgcX509SignKey instead, which wraps either kind of key, so the same options record produces an RSA or an EC certificate depending on what you hand it. Build the wrapper with sgcX509_RSAKey or sgcX509_ECKey rather than filling the record by hand, the unused half is left undefined on purpose. The two original functions are unchanged and now simply wrap sgcX509_RSAKey internally.

A P-256 certificate is roughly a third the size of a 2048 bit RSA one and verifies far faster, which is why modern deployments default to EC. TsgcX509Options.Hash still only names the SHA; the signature algorithm follows the key kind, so rhSHA256 with an EC key means ecdsa-with-SHA256, and with an RSA key it means sha256WithRSAEncryption.


type
  TsgcX509KeyKind = (xkRSA, xkEC);

function sgcX509_RSAKey(const aKey: TsgcRSAPrivateKey): TsgcX509SignKey;
function sgcX509_ECKey(aCurve: TsgcECCurve; const aPrivateKey: TBytes): TsgcX509SignKey;
function sgcX509_CreateSelfSignedEx(const aKey: TsgcX509SignKey; const aOptions: TsgcX509Options): TBytes;
function sgcX509_CreateCSREx(const aKey: TsgcX509SignKey; const aOptions: TsgcX509Options): TBytes;

var
  oPrivateKey, oPublicKey, oDER: TBytes;
  oOptions: TsgcX509Options;
begin
  sgcEC_GenerateKeyPair(eccP256, oPrivateKey, oPublicKey);
  oOptions := sgcX509_DefaultOptions;
  oOptions.Subject.CommonName := 'example.com';
  SetLength(oOptions.DNSNames, 1);
  oOptions.DNSNames[0] := 'example.com';

  oDER := sgcX509_CreateSelfSignedEx(sgcX509_ECKey(eccP256, oPrivateKey), oOptions);
  Memo1.Text := sgcX509_ToPEM(oDER, 'CERTIFICATE');
end;

sgcX509_ECKey derives the public point from the scalar, so you never have to pass it. The curve comes from sgcCrypto_ECCurves and can be any of the seven it implements, though a public CA will only accept P-256 or P-384.

DER, ASN.1 and PEM infrastructure

sgcCrypto_ASN1 reads DER, decodes PEM and Base64, and parses RSA/EC private and public keys out of DER. sgcCrypto_DER is the writer, used internally by sgcCrypto_X509_Gen and by the RSA key export functions on the Public Key Cryptography page; most applications will not call it directly.


function sgcPEM_Decode(const aPEM: string; out aLabel: string): TBytes;
function sgcPEM_Encode(const aDER: TBytes; const aLabel: string): string;
function sgcBase64Encode(const aData: TBytes): string;
function sgcBase64Decode(const aText: string): TBytes;