Certyfikaty i PKI

Dwie jednostki obejmują przeciwne kierunki tego samego formatu X.509: sgcCrypto_X509 odczytuje i weryfikuje certyfikat otrzymany od kogoś innego, sgcCrypto_X509_Gen zapisuje własny certyfikat lub żądanie. Trzecia jednostka, sgcCrypto_ASN1, zapewnia infrastrukturę DER/PEM, na której obie się opierają.

 

Odczyt i weryfikacja certyfikatu

 

sgcCrypto_X509 parsuje certyfikat zakodowany w DER do zwykłego rekordu i sprawdza wobec niego łańcuch lub listę unieważnień. Używaj tej strony API, gdy aplikacja otrzymuje certyfikat przez sieć i musi zdecydować, czy mu zaufać.

 


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 przyjmuje łańcuch i kotwice zaufania jako bloby DER, a nie jako sparsowane rekordy, oraz znacznik czasu Unix, względem którego sprawdzane jest okno ważności. Element 0 łańcucha to certyfikat liścia, a każdy certyfikat musi być podpisany przez następny; szczyt łańcucha musi być podpisany przez jeden z certyfikatów głównych zaufania albo być mu równy. Ścieżka zostaje odrzucona, gdy certyfikat znajduje się poza swoim oknem ważności w chwili aNowUnix, gdy wystawca nie jest CA, gdy narusza basicConstraints pathLenConstraint (certyfikaty wystawione samemu sobie nie liczą się do tego limitu, RFC 5280 sekcja 6.1.4) albo gdy narusza rozszerzenie nameConstraints któregokolwiek certyfikatu na ścieżce, włącznie z kotwicą zaufania. Sprawdzane warianty ograniczeń to dNSName, iPAddress, rfc822Name, uniformResourceIdentifier i directoryName; poddrzewa DNS obejmują także nazwę pospolitą liścia, która wygląda jak nazwa hosta, gdy sam liść nie niesie żadnej własnej nazwy DNS, a krytyczne nameConstraints nazywające jakikolwiek inny wariant odrzuca ścieżkę. Zasady certyfikatów (certificate policies) nie są sprawdzane.

 

TsgcX509Cert udostępnia pola, których wywołujący faktycznie potrzebuje po sparsowaniu: Version, SerialNumber, surowe sekwencje RDN Issuer/Subject, NotBeforeUnix/NotAfterUnix, klucz publiczny (zależnie od tego, co wskazuje PubKeyType: x509pkRSA, x509pkEC, x509pkEd25519, x509pkMLDSA, x509pkSLHDSA albo x509pkMLKEM) oraz IsCA. Klucz postkwantowy trafia do PQCPublicKey, a zestaw parametrów nazwany przez jego identyfikator obiektu znajduje się w tym z pól MLDSAParams, SLHDSAParams albo MLKEMParams, które odpowiada typowi. TsgcX509CSR odzwierciedla te same pola dla żądania PKCS#10.

 

Generowanie certyfikatu lub żądania

 

sgcCrypto_X509_Gen buduje certyfikat samopodpisany albo żądanie podpisania certyfikatu PKCS#10 do przesłania do CA, na podstawie pary kluczy RSA (patrz Kryptografia klucza publicznego). Oba przyjmują ten sam rekord opcji.

 


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;

 

Zbuduj okno ważności z czasu UTC.

Ważność certyfikatu jest kodowana jako UTCTime z sufiksem Z, oznaczającym czas Greenwich, więc wypełnienie NotBefore i NotAfter lokalnym Now datuje certyfikat błędnie o Twoje przesunięcie względem UTC. Na wschód od Greenwich świeżo wystawiony certyfikat nie jest wtedy jeszcze ważny, dopóki to przesunięcie nie minie, na zachód od niego certyfikat wygasa przedwcześnie. sgcX509_UTCNow zwraca właściwą wartość, a sgcX509_DefaultOptions już jej używa, z pięciominutowym cofnięciem na przesunięcie zegara, tym samym marginesem, jaki stosują prawdziwe CA.

 


function sgcX509_UTCNow: TDateTime;

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

 

Wypełnij nazwy alternatywne podmiotu (subject alternative names).

Współczesne przeglądarki i biblioteki TLS całkowicie ignorują nazwę pospolitą (common name) i patrzą wyłącznie na rozszerzenie DNSNames; certyfikat z idealną nazwą pospolitą i bez żadnej nazwy DNS w SubjectAltName zostanie odrzucony, niezależnie od tego, jak poprawna jest reszta.

 

Obok DNSNames, IPAddresses i Emails, TsgcX509Options niesie tablicę URIs, dla alternatywnych nazw podmiotu typu uniformResourceIdentifier. Trafia tu tożsamość usługi będąca adresem URI, a nie nazwą hosta, typowym przypadkiem jest identyfikator SPIFFE, który mesh obciążeń roboczych dopasowuje dosłownie i którego żadna nazwa DNS nie potrafi wyrazić. Wartość trafia dosłownie jako zawartość IA5String, więc przekaż pełny URI.

 


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

 

Ustaw IsCA := True oraz nieujemny PathLength, aby uzyskać certyfikat CA zamiast certyfikatu liścia; pozostaw PathLength na -1, aby pominąć to ograniczenie dla certyfikatu liścia. CSR przenosi te same żądane rozszerzenia w swoim atrybucie extensionRequest, gdzie CA ich szuka przy wystawianiu ostatecznego certyfikatu.

 

Alternatywne nazwy podmiotu w formacie IPv6.

IPAddresses przyjmuje IPv4 w postaci z kropkami, a IPv6 w dowolnej z postaci tekstowych opisanych w RFC 4291: pełnej postaci ośmiu grup, postaci skróconej znakami :: oraz postaci z osadzonym adresem IPv4 w ostatnich 32 bitach, w dowolnej wielkości liter. Nie dodawaj nawiasów kwadratowych ani indeksu strefy. Wpis IPv4 jest zapisywany jako 4 oktety, a wpis IPv6 jako 16, czego wymaga RFC 5280 i z czym klient TLS porównuje adres drugiej strony.

 


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';

 

Ograniczenia nazw w certyfikacie CA.

Certyfikat CA może ograniczyć to, co wolno nazywać certyfikatom pod nim, RFC 5280 sekcja 4.2.1.10. Wypełnij którekolwiek z pól PermittedDNSNames, ExcludedDNSNames, PermittedIPRanges albo ExcludedIPRanges, a rozszerzenie zostanie zapisane i oznaczone jako krytyczne. Wpis DNS jest poddrzewem, więc example.com obejmuje tę nazwę i każdą nazwę poniżej niej. Wpis IP to adres z długością prefiksu. sgcX509_VerifyChain egzekwuje to, co tutaj zapiszesz.

 


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';

 

Wystawianie przez CA

 

Certyfikat samopodpisany nie potwierdza niczego, co strona trzecia mogłaby sprawdzić. sgcX509_CreateSigned wystawia certyfikat na podstawie klucza CA i własnego certyfikatu CA, a sgcX509_CreateSignedFromCSR robi to samo, zaczynając od żądania PKCS#10.

 


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;

 

Nazwa wystawcy jest kopiowana bajt po bajcie z podmiotu certyfikatu wystawcy, dzięki czemu łańcuch łączy się dokładnie, a authorityKeyIdentifier to subjectKeyIdentifier tego certyfikatu. TsgcX509Options.Issuer nie jest odczytywane przez żaden z budowniczych. Obie funkcje zgłaszają wyjątek, zanim cokolwiek podpiszą, gdy certyfikat wystawcy jest zniekształcony, nie jest certyfikatem CA albo niesie przeznaczenie klucza bez keyCertSign, gdy przekazany klucz nie jest kluczem tego certyfikatu oraz gdy klucz podmiotu jest zniekształcony albo należy do algorytmu, którego ta jednostka nie zna.

 

Przy wystawianiu na podstawie żądania najpierw weryfikowany jest podpis żądania, a żądanie, które tej weryfikacji nie przechodzi, powoduje zgłoszenie wyjątku. Nazwa podmiotu i klucz publiczny pochodzą z żądania, więc aOptions.Subject jest ignorowane, a rozszerzenia pochodzą wyłącznie z aOptions. To, o co prosiło żądanie, nie jest kopiowane: o tym, co potwierdza certyfikat, decyduje CA.

 

Podpisywanie kluczem EC

 

sgcX509_CreateSelfSigned i sgcX509_CreateCSR podpisują kluczem RSA. Para Ex przyjmuje zamiast tego TsgcX509SignKey, która opakowuje którykolwiek rodzaj klucza, więc ten sam rekord opcji tworzy certyfikat RSA albo EC, zależnie od tego, co jej przekażesz. Zbuduj opakowanie za pomocą sgcX509_RSAKey albo sgcX509_ECKey, zamiast wypełniać rekord ręcznie, nieużywana połowa jest celowo pozostawiona niezdefiniowana. Obie oryginalne funkcje pozostają bez zmian i teraz po prostu opakowują wewnętrznie sgcX509_RSAKey.

 

Certyfikat P-256 ma w przybliżeniu jedną trzecią rozmiaru certyfikatu RSA 2048-bitowego i weryfikuje się dużo szybciej, dlatego nowoczesne wdrożenia domyślnie wybierają EC. TsgcX509Options.Hash nadal nazywa wyłącznie SHA; algorytm podpisu zależy od rodzaju klucza, więc rhSHA256 z kluczem EC oznacza ecdsa-with-SHA256, a z kluczem RSA oznacza 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 wyprowadza punkt publiczny ze skalara, więc nigdy nie musisz go przekazywać. Krzywa pochodzi z sgcCrypto_ECCurves i może być którąkolwiek z siedmiu, które ta jednostka implementuje, choć publiczne CA zaakceptuje wyłącznie P-256 albo P-384.

 

Certyfikaty postkwantowe

 

Te same budowniczowie Ex podpisują kluczem postkwantowym. sgcX509_MLDSAKey opakowuje parę kluczy ML-DSA, a sgcX509_SLHDSAKey klucz prywatny SLH-DSA, i dalej nic się nie zmienia: ten sam rekord opcji tworzy certyfikat samopodpisany albo żądanie podpisania certyfikatu, podpisane algorytmem ML-DSA lub SLH-DSA. Zobacz Kryptografia postkwantowa, aby poznać same schematy oraz kontenery kluczy, które niesie certyfikat.

 


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;

 

Oba schematy podpisują w trybie czystym (pure) z pustym łańcuchem kontekstu, a algorytmem podpisu jest własny identyfikator obiektu klucza z nieobecnymi parametrami, zgodnie z RFC 9881 dla ML-DSA i RFC 9909 dla SLH-DSA. TsgcX509Options.Hash jest dla tych kluczy ignorowane, ponieważ żaden z nich nie liczy osobno skrótu treści. sgcX509_MLDSAKey sprawdza obie długości kluczy i każe parze podpisać oraz zweryfikować ustaloną wiadomość, więc klucz prywatny, któremu podano niewłaściwy klucz publiczny, zgłasza wyjątek już w tym miejscu, zamiast wytworzyć certyfikat, którego nic nie zweryfikuje.

 

Przeznaczenie klucza jest ograniczone.

Z kluczem ML-DSA albo SLH-DSA akceptowane są wyłącznie kuDigitalSignature, kuNonRepudiation, kuKeyCertSign i kuCRLSign, a reguła jest egzekwowana, zanim cokolwiek zostanie podpisane. sgcX509_DefaultOptions prosi o kuKeyEncipherment, więc z tymi kluczami zgłasza wyjątek, dopóki nie zmienisz KeyUsage. Puste KeyUsage pomija to rozszerzenie, na co pozwalają oba RFC.

 

Certyfikowanie klucza ML-KEM.

Klucz ML-KEM enkapsuluje, nigdy nie podpisuje, więc może być podmiotem certyfikatu, ale nigdy nie podpisze żądania o jego wystawienie. Przekaż jego SubjectPublicKeyInfo do sgcX509_CreateSigned po udowodnieniu posiadania w inny sposób, co RFC 9935 pozostawia aplikacji. kuKeyEncipherment to jedyna flaga przeznaczenia klucza dozwolona w takim certyfikacie. Z tego samego powodu sgcX509_CSR_Verify nigdy nie przejdzie dla żądania ML-KEM, a sgcX509_VerifySignedBy nigdy niczego nie zweryfikuje kluczem wystawcy ML-KEM.

 

Odczyt takiego certyfikatu.

sgcX509_Parse i sgcX509_CSR_Parse traktują certyfikat postkwantowy jako zniekształcony, gdy jego SubjectPublicKeyInfo albo jego algorytm podpisu ML-DSA lub SLH-DSA niesie parametry, oraz gdy długość klucza nie odpowiada zestawowi parametrów nazwanemu przez identyfikator obiektu. Klucz ML-KEM musi dodatkowo przejść kontrolę modułu z FIPS 203. sgcX509_VerifySignedBy weryfikuje podpis ML-DSA albo SLH-DSA w trybie czystym (pure) z pustym kontekstem i zwraca False, gdy zestaw parametrów nazwany przez algorytm podpisu nie jest zestawem klucza wystawcy.

 

Infrastruktura DER, ASN.1 i PEM

 

sgcCrypto_ASN1 odczytuje DER, dekoduje PEM i Base64 oraz parsuje klucze prywatne i publiczne RSA/EC z DER. sgcCrypto_DER to komponent zapisujący, używany wewnętrznie przez sgcCrypto_X509_Gen oraz przez funkcje eksportu kluczy RSA na stronie Kryptografia klucza publicznego; większość aplikacji nie będzie wywoływać go bezpośrednio.

 


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;