두 개의 유닛이 같은 X.509 형식의 반대 방향을 각각 다룹니다: sgcCrypto_X509는 다른 누군가가 건네준 인증서를 읽고 검증하며, sgcCrypto_X509_Gen은 자신만의 인증서나 요청을 작성합니다. 세 번째 유닛인 sgcCrypto_ASN1은 두 유닛이 기반으로 삼는 DER/PEM 배관 역할을 제공합니다.
인증서 읽기와 검증
sgcCrypto_X509는 DER로 인코딩된 인증서를 단순한 레코드로 파싱하고, 이에 대한 체인이나 폐기 목록을 검사합니다. 애플리케이션이 네트워크를 통해 인증서를 받아 신뢰할지 여부를 결정해야 할 때 이 API를 사용하십시오.
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은 체인과 신뢰 앵커를 파싱된 레코드가 아니라 DER 블롭으로 받고, 유효 기간을 검사할 Unix 타임스탬프도 함께 받습니다. 체인의 0번 요소가 리프이며 각 인증서는 그다음 인증서로 서명되어 있어야 합니다. 최상위 인증서는 신뢰 루트 중 하나로 서명되었거나 그중 하나와 같아야 합니다. 어떤 인증서가 aNowUnix 시점에 유효 기간을 벗어난 경우, 발급자가 CA가 아닌 경우, basicConstraints의 pathLenConstraint를 위반하는 경우(자체 발급 인증서는 이 한도에 포함되지 않습니다. RFC 5280 6.1.4절), 또는 신뢰 앵커를 포함한 경로상의 어떤 인증서에 있는 nameConstraints 확장을 위반하는 경우에 경로가 거부됩니다. 검사되는 제약 선택지는 dNSName, iPAddress, rfc822Name, uniformResourceIdentifier, directoryName입니다. 리프에 자체 DNS 이름이 없고 그 공용 이름이 호스트 이름처럼 읽히는 경우에는 DNS 서브트리가 그 공용 이름에도 적용되며, 그 밖의 선택지를 지정한 중요(critical) nameConstraints는 경로를 거부합니다. 인증서 정책은 검사되지 않습니다.
TsgcX509Cert는 파싱 후 호출자가 실제로 필요로 하는 필드를 노출합니다: Version, SerialNumber, 원시 Issuer/Subject RDN 시퀀스, NotBeforeUnix/NotAfterUnix, 공개 키(PubKeyType이 가리키는 대로 x509pkRSA, x509pkEC, x509pkEd25519, x509pkMLDSA, x509pkSLHDSA 또는 x509pkMLKEM 중 하나), 그리고 IsCA입니다. 포스트 양자 키는 PQCPublicKey에 담겨 오며, 그 객체 식별자가 가리키는 매개변수 집합은 MLDSAParams, SLHDSAParams, MLKEMParams 가운데 유형에 맞는 곳에 들어 있습니다. TsgcX509CSR은 PKCS#10 요청에 대해 동일한 필드를 그대로 제공합니다.
인증서나 요청 생성하기
sgcCrypto_X509_Gen은 RSA 키 쌍(공개 키 암호화 참조)으로부터 자체 서명 인증서, 또는 CA에 보낼 PKCS#10 인증서 서명 요청을 생성합니다. 둘 다 동일한 옵션 레코드를 받습니다.
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;
유효 기간은 UTC를 기준으로 설정하십시오.
인증서 유효 기간은 Z 접미사가 붙은 UTCTime으로 인코딩되며, 이는 그리니치 시간을 의미합니다. 따라서 NotBefore와 NotAfter를 로컬 Now로 채우면 UTC와의 시차만큼 인증서 날짜가 잘못됩니다. 그리니치 동쪽에서는 방금 발급한 인증서가 그 시차만큼의 시간이 지날 때까지 아직 유효하지 않게 되고, 서쪽에서는 인증서가 일찍 만료됩니다. sgcX509_UTCNow는 올바른 값을 반환하며, sgcX509_DefaultOptions는 이미 이 값을 사용하면서 시계 오차를 위해 5분을 앞당겨 설정합니다. 이는 실제 CA들이 적용하는 것과 같은 여유입니다.
function sgcX509_UTCNow: TDateTime;
var
oOptions: TsgcX509Options;
begin
oOptions := sgcX509_DefaultOptions;
oOptions.NotBefore := sgcX509_UTCNow;
oOptions.NotAfter := sgcX509_UTCNow + 365;
end;
주체 대체 이름을 채우십시오.
최신 브라우저와 TLS 라이브러리는 공용 이름(common name)을 완전히 무시하고 오직 DNSNames 확장만 확인합니다. 공용 이름은 완벽하지만 SubjectAltName에 DNS 이름이 하나도 없는 인증서는, 나머지가 아무리 올바르더라도 거부될 것입니다.
DNSNames, IPAddresses, Emails와 더불어, TsgcX509Options는 uniformResourceIdentifier 유형의 주체 대체 이름을 위한 URIs 배열도 갖고 있습니다. 호스트 이름이 아니라 URI인 서비스 신원이 여기에 해당하며, 구체적인 예로는 워크로드 메시가 문자 그대로 대조하는 SPIFFE ID가 있는데, 이는 어떤 DNS 이름으로도 표현할 수 없습니다. 값은 IA5String 내용으로 그대로 들어가므로, 완전한 URI를 전달하십시오.
SetLength(oOptions.URIs, 1);
oOptions.URIs[0] := 'spiffe://example.com/ns/default/sa/payments';
리프 인증서가 아니라 CA 인증서를 만들려면 IsCA := True로 설정하고 PathLength를 음수가 아닌 값으로 설정하십시오. 리프에서 이 제약을 생략하려면 PathLength를 -1로 두십시오. CSR은 extensionRequest 속성에 동일한 요청 확장을 담으며, CA는 최종 인증서를 발급할 때 여기서 이를 찾습니다.
IPv6 주체 대체 이름.
IPAddresses는 IPv4를 점으로 구분된 형식으로, IPv6를 RFC 4291의 어떤 텍스트 형식으로든 받습니다. 여덟 그룹을 모두 쓴 형식, ::로 압축한 형식, 마지막 32비트에 IPv4 주소를 넣은 형식 모두 가능하며 대소문자도 가리지 않습니다. 대괄호를 붙이지 말고 존 인덱스도 붙이지 마십시오. IPv4 항목은 4옥텟, IPv6 항목은 16옥텟으로 기록되며, 이는 RFC 5280이 요구하는 바이자 TLS 클라이언트가 피어 주소와 비교하는 형식입니다.
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';
CA 인증서의 이름 제약.
CA 인증서는 그 아래의 인증서들이 어떤 이름을 지정할 수 있는지 제한할 수 있습니다. RFC 5280 4.2.1.10절입니다. PermittedDNSNames, ExcludedDNSNames, PermittedIPRanges, ExcludedIPRanges 가운데 아무거나 채우면 해당 확장이 중요(critical) 표시와 함께 기록됩니다. DNS 항목은 서브트리이므로 example.com은 그 이름과 그 아래의 모든 이름을 포함합니다. IP 항목은 접두사 길이가 붙은 주소입니다. 여기에 기록한 내용은 sgcX509_VerifyChain이 강제합니다.
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';
CA에서 발급하기
자체 서명 인증서는 제3자가 확인할 수 있는 것을 아무것도 주장하지 못합니다. sgcX509_CreateSigned는 CA 키와 CA 자신의 인증서로 인증서를 발급하고, sgcX509_CreateSignedFromCSR는 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;
발급자 이름은 발급자 인증서의 주체에서 바이트 단위로 그대로 복사되므로 체인이 정확히 연결되며, authorityKeyIdentifier는 그 인증서의 subjectKeyIdentifier입니다. TsgcX509Options.Issuer는 어떤 빌더도 읽지 않습니다. 발급자 인증서가 잘못되었거나, CA가 아니거나, keyCertSign 없이 키 사용을 지정한 경우, 전달한 키가 그 인증서의 키가 아닌 경우, 그리고 주체 키가 잘못되었거나 이 유닛이 모르는 알고리즘인 경우에는 두 함수 모두 아무것도 서명하기 전에 오류를 발생시킵니다.
요청에서 발급할 때는 요청의 서명을 먼저 검증하며, 검증에 실패한 요청은 오류를 발생시킵니다. 주체 이름과 공개 키는 요청에서 가져오므로 aOptions.Subject는 무시되고, 확장은 오직 aOptions에서만 가져옵니다. 요청이 무엇을 요구했는지는 복사되지 않습니다. 인증서가 무엇을 주장할지는 CA가 결정합니다.
EC 키로 서명하기
sgcX509_CreateSelfSigned와 sgcX509_CreateCSR는 RSA로 서명합니다. Ex 쌍은 대신 어느 한 종류의 키든 감싸는 TsgcX509SignKey를 받으므로, 같은 옵션 레코드로도 무엇을 건네느냐에 따라 RSA 인증서나 EC 인증서를 만들어낼 수 있습니다. 레코드를 직접 채우지 말고 sgcX509_RSAKey나 sgcX509_ECKey로 래퍼를 만드십시오. 사용되지 않는 절반은 의도적으로 정의되지 않은 채로 남습니다. 기존 두 함수는 변경되지 않았으며 이제 내부적으로 단순히 sgcX509_RSAKey를 감쌉니다.
P-256 인증서는 2048비트 RSA 인증서의 대략 3분의 1 크기이며 검증 속도도 훨씬 빠릅니다. 이것이 최신 배포 환경이 기본적으로 EC를 선택하는 이유입니다. TsgcX509Options.Hash는 여전히 SHA만을 지정하며, 서명 알고리즘은 키 종류에 따라 결정됩니다. 따라서 rhSHA256은 EC 키에서는 ecdsa-with-SHA256을, RSA 키에서는 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는 스칼라 값으로부터 공개 점을 유도하므로, 이를 직접 전달할 필요가 전혀 없습니다. 곡선은 sgcCrypto_ECCurves에서 가져오며 그것이 구현하는 일곱 개 중 어느 것이든 사용할 수 있지만, 공인 CA는 P-256이나 P-384만 받아들일 것입니다.
포스트 양자 인증서
같은 Ex 빌더가 포스트 양자 키로도 서명합니다. sgcX509_MLDSAKey는 ML-DSA 키 쌍을, sgcX509_SLHDSAKey는 SLH-DSA 개인 키를 감싸며, 그 외에는 달라지는 것이 없습니다. 같은 옵션 레코드가 ML-DSA나 SLH-DSA로 서명된 자체 서명 인증서 또는 인증서 서명 요청을 만들어 냅니다. 방식 자체와 인증서가 담는 키 컨테이너에 대해서는 포스트 양자 암호화를 참조하십시오.
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;
두 방식 모두 빈 컨텍스트 문자열로 순수(pure) 모드에서 서명하며, 서명 알고리즘은 매개변수가 없는 키 자신의 객체 식별자입니다. ML-DSA는 RFC 9881을, SLH-DSA는 RFC 9909를 따릅니다. 두 방식 모두 본문을 따로 해싱하지 않으므로 이러한 키에서는 TsgcX509Options.Hash가 무시됩니다. sgcX509_MLDSAKey는 두 키의 길이를 확인하고 그 쌍으로 고정된 메시지를 서명하고 검증해 보므로, 개인 키에 엉뚱한 공개 키를 건네면 아무것도 검증할 수 없는 인증서를 만들어 내는 대신 그 자리에서 오류가 발생합니다.
키 사용은 제한됩니다.
ML-DSA나 SLH-DSA 키에는 kuDigitalSignature, kuNonRepudiation, kuKeyCertSign, kuCRLSign만 허용되며, 이 규칙은 무엇인가를 서명하기 전에 적용됩니다. sgcX509_DefaultOptions는 kuKeyEncipherment를 요구하므로, KeyUsage를 바꾸기 전까지는 이러한 키에서 오류가 발생합니다. KeyUsage가 비어 있으면 확장이 빠지며, 이는 두 RFC가 모두 허용하는 바입니다.
ML-KEM 키를 인증하기.
ML-KEM 키는 캡슐화만 할 뿐 서명은 하지 않으므로, 인증서의 주체는 될 수 있어도 인증서를 요청하는 요청에는 절대 서명할 수 없습니다. 다른 방법으로 소유를 증명한 뒤 그 SubjectPublicKeyInfo를 sgcX509_CreateSigned에 전달하십시오. 이는 RFC 9935가 애플리케이션에 맡긴 부분입니다. 그런 인증서에 허용되는 키 사용 플래그는 kuKeyEncipherment뿐입니다. 같은 이유로 sgcX509_CSR_Verify는 ML-KEM 요청에서 절대 통과할 수 없고, sgcX509_VerifySignedBy는 ML-KEM 발급자 키로는 아무것도 검증하지 않습니다.
다시 읽어 들이기.
sgcX509_Parse와 sgcX509_CSR_Parse는 포스트 양자 인증서의 SubjectPublicKeyInfo나 ML-DSA 또는 SLH-DSA 서명 알고리즘이 매개변수를 담고 있을 때, 그리고 키 길이가 객체 식별자가 가리키는 매개변수 집합과 맞지 않을 때 그 인증서를 잘못된 것으로 처리합니다. ML-KEM 키는 여기에 더해 FIPS 203 계수 검사를 통과해야 합니다. sgcX509_VerifySignedBy는 빈 컨텍스트로 순수 모드에서 ML-DSA 또는 SLH-DSA 서명을 검증하며, 서명 알고리즘이 가리키는 매개변수 집합이 발급자 키 자신의 것이 아니면 False를 반환합니다.
DER, ASN.1, PEM 인프라
sgcCrypto_ASN1은 DER을 읽고, PEM과 Base64를 디코딩하며, DER에서 RSA/EC 개인 키와 공개 키를 파싱합니다. sgcCrypto_DER은 그 반대인 작성기로, sgcCrypto_X509_Gen 내부와 공개 키 암호화 페이지의 RSA 키 내보내기 함수에서 사용됩니다. 대부분의 애플리케이션은 이를 직접 호출할 필요가 없습니다.
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;