Zwei Units decken entgegengesetzte Richtungen desselben X.509-Formats ab: sgcCrypto_X509 liest und verifiziert ein Zertifikat, das Ihnen jemand anderes gegeben hat, sgcCrypto_X509_Gen schreibt ein eigenes Zertifikat oder einen eigenen Request. Eine dritte Unit, sgcCrypto_ASN1, stellt die DER/PEM-Grundlage bereit, auf der beide aufbauen.
Ein Zertifikat lesen und verifizieren
sgcCrypto_X509 parst ein DER-kodiertes Zertifikat in einen einfachen Record und prüft eine Kette oder eine Sperrliste dagegen. Verwenden Sie diese Seite der API, wenn Ihre Anwendung ein Zertifikat über die Leitung empfängt und entscheiden muss, ob sie ihm vertraut.
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 aTrustRootsDER: array of TBytes; aNowUnix: Int64): 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 aSerial: TBytes): Boolean;
function sgcX509_CSR_Parse(const aDER: TBytes; out aCSR: TsgcX509CSR): Boolean;
function sgcX509_CSR_Verify(const aCSR: TsgcX509CSR): Boolean;
sgcX509_VerifyChain nimmt die Kette und die Vertrauensanker als DER-Blobs entgegen, nicht als geparste Records, dazu einen Unix-Zeitstempel, gegen den das Gültigkeitsfenster geprüft wird. Element 0 der Kette ist das Blattzertifikat, und jedes Zertifikat muss vom nächsten signiert sein; das oberste muss von einem der Trust Roots signiert sein oder mit einem davon übereinstimmen. Der Pfad wird abgelehnt, wenn ein Zertifikat zum Zeitpunkt aNowUnix außerhalb seines Gültigkeitsfensters liegt, wenn ein Aussteller keine CA ist, wenn er basicConstraints pathLenConstraint verletzt (selbst ausgestellte Zertifikate zählen nicht gegen das Limit, RFC 5280 Abschnitt 6.1.4), oder wenn er eine nameConstraints-Erweiterung eines beliebigen Zertifikats im Pfad verletzt, den Vertrauensanker eingeschlossen. Geprüft werden die Constraint-Varianten dNSName, iPAddress, rfc822Name, uniformResourceIdentifier und directoryName; die DNS-Teilbäume gelten außerdem für einen Common Name des Blattzertifikats, der wie ein Hostname aussieht, wenn das Blattzertifikat keinen eigenen DNS-Namen trägt, und ein kritisches nameConstraints, das eine andere Variante benennt, führt zur Ablehnung des Pfads. Zertifikatsrichtlinien werden nicht geprüft.
TsgcX509Cert stellt die Felder bereit, die ein Aufrufer nach dem Parsen tatsächlich benötigt: Version, SerialNumber, die rohen Issuer-/Subject-RDN-Sequenzen, NotBeforeUnix/NotAfterUnix, den öffentlichen Schlüssel (je nachdem, was PubKeyType angibt, x509pkRSA, x509pkEC, x509pkEd25519, x509pkMLDSA, x509pkSLHDSA oder x509pkMLKEM) und IsCA. Ein Post-Quanten-Schlüssel kommt in PQCPublicKey an, und der Parametersatz, den sein Objektbezeichner benennt, steht in MLDSAParams, SLHDSAParams oder MLKEMParams, je nachdem, was zum Typ passt. TsgcX509CSR spiegelt dieselben Felder für einen PKCS#10-Request.
Ein Zertifikat oder einen Request erzeugen
sgcCrypto_X509_Gen erstellt ein selbstsigniertes Zertifikat oder einen PKCS#10-Certificate-Signing-Request zum Versenden an eine CA, aus einem RSA-Schlüsselpaar (siehe Public-Key-Kryptografie). Beide nehmen denselben Optionen-Record entgegen.
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;
Das Gültigkeitsfenster aus UTC aufbauen.
Die Zertifikatsgültigkeit wird als UTCTime mit einem Z-Suffix kodiert, was Greenwich-Zeit bedeutet, daher datiert das Befüllen von NotBefore und NotAfter aus einem lokalen Now das Zertifikat um Ihren Versatz zu UTC falsch. Östlich von Greenwich ist ein frisch ausgestelltes Zertifikat dann noch nicht gültig, bis dieser Versatz verstrichen ist, westlich davon läuft das Zertifikat vorzeitig ab. sgcX509_UTCNow liefert den richtigen Wert, und sgcX509_DefaultOptions verwendet ihn bereits mit einer Rückdatierung von fünf Minuten für Uhrabweichung, derselben Marge, die echte CAs anwenden.
function sgcX509_UTCNow: TDateTime;
var
oOptions: TsgcX509Options;
begin
oOptions := sgcX509_DefaultOptions;
oOptions.NotBefore := sgcX509_UTCNow;
oOptions.NotAfter := sgcX509_UTCNow + 365;
end;
Füllen Sie die Subject Alternative Names.
Moderne Browser und TLS-Bibliotheken ignorieren den Common Name vollständig und schauen nur auf die Erweiterung DNSNames; ein Zertifikat mit einem perfekten Common Name und ohne DNS-Namen in SubjectAltName wird abgelehnt, wie korrekt auch immer der Rest davon ist.
Neben DNSNames, IPAddresses und Emails führt TsgcX509Options ein URIs-Array, für Subject Alternative Names vom Typ uniformResourceIdentifier. Eine Diensteidentität, die eine URI statt eines Hostnamens ist, gehört hierher, der konkrete Fall ist eine SPIFFE-ID, die ein Workload-Mesh wörtlich abgleicht und die kein DNS-Name ausdrücken kann. Der Wert geht unverändert als IA5String-Inhalt ein, übergeben Sie also eine vollständige URI.
SetLength(oOptions.URIs, 1);
oOptions.URIs[0] := 'spiffe://example.com/ns/default/sa/payments';
Setzen Sie IsCA := True und ein nicht-negatives PathLength, um ein CA-Zertifikat statt eines Blattzertifikats zu erzeugen; belassen Sie PathLength bei -1, um die Einschränkung bei einem Blattzertifikat wegzulassen. Ein CSR trägt dieselben angeforderten Erweiterungen in seinem Attribut extensionRequest, wo eine CA bei der Ausstellung des endgültigen Zertifikats danach sucht.
IPv6-Subject-Alternative-Names.
IPAddresses nimmt IPv4 in Punktschreibweise und IPv6 in jeder der Textformen von RFC 4291 entgegen: die vollständige Form mit acht Gruppen, eine mit :: verkürzte Form und eine Form mit eingebetteter IPv4-Adresse in den letzten 32 Bit, in beliebiger Groß- und Kleinschreibung. Fügen Sie keine eckigen Klammern und keinen Zone-Index hinzu. Ein IPv4-Eintrag wird als 4 Oktette geschrieben und ein IPv6-Eintrag als 16, was RFC 5280 verlangt und was ein TLS-Client mit der Adresse der Gegenstelle vergleicht.
SetLength(oOptions.IPAddresses, 3);
oOptions.IPAddresses[0] := '192.0.2.10';
oOptions.IPAddresses[1] := '2001:db8::1';
oOptions.IPAddresses[2] := '::ffff:192.0.2.10';
Name Constraints in einem CA-Zertifikat.
Ein CA-Zertifikat kann einschränken, was die Zertifikate unterhalb davon benennen dürfen, RFC 5280 Abschnitt 4.2.1.10. Befüllen Sie eines von PermittedDNSNames, ExcludedDNSNames, PermittedIPRanges oder ExcludedIPRanges, und die Erweiterung wird geschrieben, als kritisch markiert. Ein DNS-Eintrag ist ein Teilbaum, example.com deckt also diesen Namen und jeden Namen darunter ab. Ein IP-Eintrag ist eine Adresse mit einer Präfixlänge. sgcX509_VerifyChain setzt durch, was Sie hier eintragen.
oOptions.IsCA := True;
oOptions.PathLength := 0;
oOptions.KeyUsage := [kuKeyCertSign, kuCRLSign];
SetLength(oOptions.PermittedDNSNames, 1);
oOptions.PermittedDNSNames[0] := 'internal.example.com';
SetLength(oOptions.PermittedIPRanges, 2);
oOptions.PermittedIPRanges[0] := '192.0.2.0/24';
oOptions.PermittedIPRanges[1] := '2001:db8::/32';
Aus einer CA ausstellen
Ein selbstsigniertes Zertifikat behauptet nichts, was ein Dritter prüfen kann. sgcX509_CreateSigned stellt ein Zertifikat aus einem CA-Schlüssel und dem eigenen Zertifikat der CA aus, und sgcX509_CreateSignedFromCSR tut dasselbe, ausgehend von einem PKCS#10-Request.
function sgcX509_CreateSigned(const aIssuerKey: TsgcX509SignKey;
const aIssuerCertDER, aSubjectPublicKeyInfo: TBytes; const aOptions: TsgcX509Options): TBytes;
function sgcX509_CreateSignedFromCSR(const aIssuerKey: TsgcX509SignKey;
const aIssuerCertDER, aCSRDER: TBytes; const aOptions: TsgcX509Options): TBytes;
Der Ausstellername wird Byte für Byte aus dem Subject des Ausstellerzertifikats kopiert, sodass die Kette exakt verbunden ist, und der authorityKeyIdentifier ist der subjectKeyIdentifier dieses Zertifikats. TsgcX509Options.Issuer wird von keinem Builder gelesen. Beide Funktionen lösen einen Fehler aus, bevor überhaupt etwas signiert wird, wenn das Ausstellerzertifikat fehlerhaft ist, keine CA ist oder eine Key Usage ohne keyCertSign trägt, wenn der übergebene Schlüssel nicht der Schlüssel dieses Zertifikats ist und wenn der Subject-Schlüssel fehlerhaft ist oder zu einem Algorithmus gehört, den die Unit nicht kennt.
Beim Ausstellen aus einem Request wird zuerst die Signatur des Requests verifiziert, und ein Request, der daran scheitert, löst einen Fehler aus. Der Subject-Name und der öffentliche Schlüssel stammen aus dem Request, aOptions.Subject wird also ignoriert, und die Erweiterungen stammen allein aus aOptions. Was der Request angefordert hat, wird nicht übernommen: Die CA entscheidet, was das Zertifikat behauptet.
Mit einem EC-Schlüssel signieren
sgcX509_CreateSelfSigned und sgcX509_CreateCSR signieren mit RSA. Das Ex-Paar nimmt stattdessen ein TsgcX509SignKey entgegen, das beide Schlüsselarten umschließt, sodass derselbe Optionen-Record je nach übergebenem Wert ein RSA- oder ein EC-Zertifikat erzeugt. Bauen Sie den Wrapper mit sgcX509_RSAKey oder sgcX509_ECKey, statt den Record von Hand zu befüllen, die ungenutzte Hälfte bleibt absichtlich undefiniert. Die beiden ursprünglichen Funktionen sind unverändert und umschließen jetzt intern lediglich sgcX509_RSAKey.
Ein P-256-Zertifikat ist etwa ein Drittel so groß wie ein 2048-Bit-RSA-Zertifikat und verifiziert weit schneller, weshalb moderne Deployments standardmäßig EC verwenden. TsgcX509Options.Hash benennt weiterhin nur den SHA; der Signaturalgorithmus folgt der Schlüsselart, sodass rhSHA256 mit einem EC-Schlüssel ecdsa-with-SHA256 bedeutet, und mit einem RSA-Schlüssel sha256WithRSAEncryption.
type
TsgcX509KeyKind = (xkRSA, xkEC, xkMLDSA, xkSLHDSA);
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 leitet den öffentlichen Punkt aus dem Skalar ab, sodass Sie ihn nie übergeben müssen. Die Kurve stammt aus sgcCrypto_ECCurves und kann jede der sieben von ihr implementierten sein, wobei eine öffentliche CA jedoch nur P-256 oder P-384 akzeptiert.
Post-Quanten-Zertifikate
Dieselben Ex-Builder signieren auch mit einem Post-Quanten-Schlüssel. sgcX509_MLDSAKey umschließt ein ML-DSA-Schlüsselpaar und sgcX509_SLHDSAKey einen privaten SLH-DSA-Schlüssel, und von da an ändert sich nichts weiter: Derselbe Optionen-Record erzeugt ein selbstsigniertes Zertifikat oder einen Certificate Signing Request, signiert mit ML-DSA oder SLH-DSA. Siehe Post-Quanten-Kryptografie für die Verfahren selbst und für die Schlüsselcontainer, die das Zertifikat trägt.
function sgcX509_MLDSAKey(aParams: TsgcMLDSAParams; const aPrivateKey, aPublicKey: TBytes): TsgcX509SignKey;
function sgcX509_SLHDSAKey(aParams: TsgcSLHDSAParams; const aPrivateKey: TBytes): TsgcX509SignKey;
var
oSeed, oPub, oPriv, oDER: TBytes;
oOptions: TsgcX509Options;
begin
sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPub, oPriv);
oOptions := sgcX509_DefaultOptions;
oOptions.Subject.CommonName := 'example.com';
SetLength(oOptions.DNSNames, 1);
oOptions.DNSNames[0] := 'example.com';
{ the default options ask for kuKeyEncipherment, which ML-DSA does not allow }
oOptions.KeyUsage := [kuDigitalSignature];
oDER := sgcX509_CreateSelfSignedEx(sgcX509_MLDSAKey(mldsa65, oPriv, oPub), oOptions);
Memo1.Text := sgcX509_ToPEM(oDER, 'CERTIFICATE');
end;
Beide Verfahren signieren im Pure-Modus mit leerem Kontext-String, und der Signaturalgorithmus ist der Objektbezeichner des Schlüssels selbst, ohne Parameter, gemäß RFC 9881 für ML-DSA und RFC 9909 für SLH-DSA. TsgcX509Options.Hash wird bei diesen Schlüsseln ignoriert, da keines von beiden den Body gesondert hasht. sgcX509_MLDSAKey prüft beide Schlüssellängen und lässt das Paar eine feste Nachricht signieren und verifizieren, sodass ein privater Schlüssel, dem der falsche öffentliche Schlüssel beigegeben wurde, dort einen Fehler auslöst, statt ein Zertifikat zu erzeugen, das niemand verifizieren kann.
Die Key Usage ist eingeschränkt.
Mit einem ML-DSA- oder SLH-DSA-Schlüssel werden nur kuDigitalSignature, kuNonRepudiation, kuKeyCertSign und kuCRLSign akzeptiert, und die Regel wird durchgesetzt, bevor irgendetwas signiert wird. sgcX509_DefaultOptions fordert kuKeyEncipherment an, löst mit diesen Schlüsseln also einen Fehler aus, bis Sie KeyUsage ändern. Eine leere KeyUsage lässt die Erweiterung weg, was beide RFCs erlauben.
Einen ML-KEM-Schlüssel zertifizieren.
Ein ML-KEM-Schlüssel kapselt, er signiert nie, er kann also Subject eines Zertifikats sein, aber niemals einen Request signieren, um eines anzufordern. Übergeben Sie sein SubjectPublicKeyInfo an sgcX509_CreateSigned, nachdem Sie den Besitz auf anderem Weg nachgewiesen haben, was RFC 9935 der Anwendung überlässt. kuKeyEncipherment ist das einzige Key-Usage-Flag, das auf einem solchen Zertifikat erlaubt ist. Aus demselben Grund kann sgcX509_CSR_Verify bei einem ML-KEM-Request niemals erfolgreich sein, und sgcX509_VerifySignedBy verifiziert mit einem ML-KEM-Ausstellerschlüssel niemals irgendetwas.
Eines wieder einlesen.
sgcX509_Parse und sgcX509_CSR_Parse behandeln ein Post-Quanten-Zertifikat als fehlerhaft, wenn sein SubjectPublicKeyInfo oder sein ML-DSA- oder SLH-DSA-Signaturalgorithmus Parameter trägt, und wenn die Schlüssellänge nicht zu dem Parametersatz passt, den der Objektbezeichner benennt. Ein ML-KEM-Schlüssel muss zusätzlich die Modulus-Prüfung gemäß FIPS 203 bestehen. sgcX509_VerifySignedBy verifiziert eine ML-DSA- oder SLH-DSA-Signatur im Pure-Modus mit leerem Kontext und gibt False zurück, wenn der vom Signaturalgorithmus benannte Parametersatz nicht der des Ausstellerschlüssels ist.
DER-, ASN.1- und PEM-Infrastruktur
sgcCrypto_ASN1 liest DER, dekodiert PEM und Base64, und parst private und öffentliche RSA-/EC-Schlüssel aus DER. sgcCrypto_DER ist der Writer, intern verwendet von sgcCrypto_X509_Gen und von den RSA-Schlüsselexportfunktionen auf der Seite Public-Key-Kryptografie; die meisten Anwendungen werden ihn nicht direkt aufrufen.
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;