sgcSign 2026.9.0은 대규모 릴리스입니다. 대부분은 고객 요청에서 비롯되었으며, 세 가지 영역에 걸쳐 있습니다. 어떤 인증서로 서명하려는지 파악하는 것, 10년 뒤에도 검증기가 받아들일 서명을 만드는 것, 그리고 서명을 그 자신이 아닌 다른 기준으로 검증하는 것입니다.
이 글에서는 각각의 새 기능을 Delphi 코드와 함께 살펴봅니다. 마지막에는 이전 버전에서 만들어져 다시 서명해야 하는 서명에 대한 짧은 섹션도 있습니다.
선택할 수 있는 인증서 목록
인증서 열거는 예전에 표시 이름 목록만 돌려주었습니다. 콤보 박스를 채우기에는 충분하지만 선택을 결정하기에는 부족합니다. 같은 기관이 같은 사람에게 발급한 두 장의 카드는 그 목록에서 똑같아 보입니다.
이제 열거 결과에는 SHA-1 지문, 세무 식별자, 일련번호, 발급자, 유효 기간이 함께 담기며, Windows 인증서 저장소와 PKCS#11 토큰, PFX 파일에서 모두 같은 방식으로 동작합니다. 만료된 인증서와 개인 키가 없는 인증서는 걸러낼 수 있습니다. 지문은 SelectCertificateByThumbprint에 그대로 전달되므로, 사용자가 고른 인증서가 실제로 서명하는 인증서가 됩니다.
uses
sgcSign_KeyProvider_WinCertStore, sgcSign_X509, sgcSign_Types;
var
oProvider: TsgcWindowsCertStoreProvider;
oList: TsgcX509CertificateList;
i: Integer;
begin
oProvider := TsgcWindowsCertStoreProvider.Create(nil);
Try
// only certificates that are still valid and hold a private key
oList := oProvider.EnumerateCertificateList([cfNotExpired, cfPrivateKey]);
Try
for i := 0 to oList.Count - 1 do
Memo1.Lines.Add(Format('%s | %s | %s | %s .. %s | %s',
[oList[i].Subject, oList[i].NIF, oList[i].SerialNumber,
DateToStr(oList[i].NotBefore), DateToStr(oList[i].NotAfter),
oList[i].Thumbprint]));
// and sign with exactly the one that was chosen
oProvider.SelectCertificateByThumbprint(oList[0].Thumbprint);
Finally
oList.Free;
End;
Finally
oProvider.Free;
End;
end;
매개변수가 없는 호출은 그대로이므로 기존 코드는 계속 동작합니다.
PIN 없이 목록을 확인하는 멀티 슬롯 카드
적격 서명 카드에는 각각 자체 PIN으로 보호되는 인증서가 여러 개 들어 있는 경우가 많습니다. 폴란드 카드가 대표적인데, 프로필이 두 개인 Certum 카드나 컨테이너가 세 개인 PWPW Sigillum 카드가 그렇습니다. 목록을 보여주려고 사용자에게 PIN을 세 번 물어보는 것은 쓸 만한 인터페이스가 아닙니다.
이제 PKCS#11 토큰은 로그인을 전혀 하지 않고도 목록을 확인할 수 있습니다. TokenSlotCount는 실제로 토큰이 들어 있는 슬롯의 개수를 알려주므로 다룰 가치가 있는 범위를 알 수 있고, 각 항목은 자신이 나온 슬롯과 토큰 레이블을 기록합니다. 덕분에 해당 인증서가 선택되었을 때만 알맞은 PIN을 요청할 수 있습니다.
uses
sgcSign_KeyProvider_PKCS11, sgcSign_X509, sgcSign_Types;
var
oPKCS11: TsgcPKCS11Provider;
oList: TsgcX509CertificateList;
i: Integer;
begin
oPKCS11 := TsgcPKCS11Provider.Create(nil);
Try
oPKCS11.LibraryPath := 'C:\Windows\System32\cryptoCertum3PKCS.dll';
ShowMessage(Format('%d slots hold a token', [oPKCS11.TokenSlotCount]));
// walks every slot that holds a token, never logs in, never needs a PIN
oList := oPKCS11.EnumerateCertificateListAllSlots([cfNotExpired]);
Try
for i := 0 to oList.Count - 1 do
Memo1.Lines.Add(Format('slot %d (%s): %s',
[oList[i].SlotIndex, oList[i].TokenLabel, oList[i].Subject]));
Finally
oList.Free;
End;
Finally
oPKCS11.Free;
End;
end;
내 인증서를 발급한 인증서 찾기
장기 보존 서명 프로필에는 발급자 인증서가 필요하지만, 대부분의 적격 서명 카드에는 본인 인증서만 들어 있습니다. 이제 모든 키 공급자에 두 가지 호출이 추가되어 이를 찾아줍니다. GetIssuerCertificate는 서명에 사용하는 인증서를 발급한 인증서를 반환하고, GetCertificateChain은 그 위의 전체 경로를 반환합니다. 일치 여부는 이름이 아니라 암호학적으로 확인하므로, 서명 키를 교체한 기관이 이전 기관과 혼동되지 않습니다.
어디를 찾을지는 선택의 문제이므로 속성으로 제공됩니다. 기본적으로 Windows 인증서 저장소를 검색하고, iluLocalStore는 애플리케이션과 함께 배포하는 PEM 또는 DER 파일을 검색하며, iluAIA는 본인 인증서 안에 적힌 주소에서 인증서를 내려받습니다. 마지막 항목은 네트워크에 접근하므로 기본적으로 꺼져 있습니다.
uses
sgcSign_Classes, sgcSign_X509;
var
vIssuerDER: TBytes;
oChain: TsgcCertificateChain;
begin
oProvider.IssuerLookup := [iluSystemStore, iluLocalStore];
oProvider.IssuerFiles.Add('certs\ca-intermediate.pem');
oProvider.IssuerFiles.Add('certs\ca-root.pem');
vIssuerDER := oProvider.GetIssuerCertificate;
oChain := oProvider.GetCertificateChain;
end;
두 가지 새로운 PAdES 프로필
spPAdESBasicT는 타임스탬프를 포함하되 폐기 정보 없이 서명합니다. 서명이 언제 만들어졌는지만 증명하면 될 때 적합합니다. spPAdESDocumentArchive는 반대 방향으로, 장기 보존 프로필 위에 아카이브 타임스탬프를 더해 폐기 정보를 포함한 문서 전체를 보호합니다. 덕분에 첫 번째 타임스탬프 자체의 유효 기간이 지난 뒤에도 파일을 계속 검증할 수 있습니다.
uses
sgcSign_PAdES, sgcSign_Types;
var
oPAdES: TsgcPAdESSigner;
begin
oPAdES := TsgcPAdESSigner.Create(nil);
Try
oPAdES.KeyProvider := oProvider;
oPAdES.Profile.Profile := spPAdESDocumentArchive;
oPAdES.TSAClient := TSAClient1;
oPAdES.OCSPClient := OCSPClient1;
// revocation lists you supply yourself, for an authority whose CRL is
// issued by its own root. IssuerCertificate is resolved automatically
// when it is left empty and OCSPClient is assigned
oPAdES.CRLFiles.Add('crl\ca-intermediate.crl');
oPAdES.SignPDFFile('contract.pdf', 'contract-signed.pdf');
Finally
oPAdES.Free;
End;
end;
인증서가 담고 있는 모든 정보를 보고합니다
주체와 발급자는 예전에 파서가 인식하는 일곱 개 속성만 보고하고 나머지는 버렸습니다. 이제는 인증서에 있는 모든 속성을 보고하고, 우편 주소는 읽기 쉬운 줄로 디코딩되며, 어떤 속성이든 OID로 읽을 수 있습니다.
uses
sgcSign_X509;
var
i: Integer;
begin
// by OID: organizationIdentifier
ShowMessage(oCert.GetSubjectAttribute('2.5.4.97'));
// or the full list, in the order the certificate declares it
for i := 0 to oCert.SubjectAttributeCount - 1 do
Memo1.Lines.Add(oCert.SubjectAttributeOID[i] + ' = ' +
oCert.SubjectAttributeValue[i]);
end;
서명된 타임스탬프 요청
일부 적격 타임스탬프 기관, 특히 폴란드의 기관은 일반적인 RFC 3161 요청에 응답하지 않습니다. 요청 자체를 CMS SignedData로 감싸서 서명하기를 요구합니다. 이제는 직접 만들어야 하는 것이 아니라 속성으로 제공됩니다.
uses
sgcSign_TSA;
begin
oTSA.URL := 'https://tsa.example.com';
oTSA.RequestFormat := trfCMS; // plain RFC 3161 is still the default
oTSA.KeyProvider := oProvider; // trfCMS needs one
// authorities differ in what they expect inside the wrapper
oTSA.SignOptions.IncludeSignedAttributes := True;
// and the exact bytes are available when an authority needs checking
oTSA.OnBeforeSendRequest := DoBeforeSendRequest;
oTSA.OnAfterReceiveResponse := DoAfterReceiveResponse;
end;
기본 형식은 PWPW Sigillum 타임스탬프 기관이 받아들이는 요청과 일치합니다. 기존 코드는 계속 일반 요청을 보내며, RequestFormat을 설정하지 않는 한 달라지는 것은 없습니다.
Authenticode 교차 인증서
커널 모드 드라이버 서명은 교차 인증서를 거쳐 Microsoft Code Verification Root까지 체인이 이어져야 하며, 그것이 바로 signtool /ac가 포함시키는 인증서입니다. 이제 sgcSign도 같은 방식으로 추가 인증서를 포함할 수 있습니다.
uses
sgcSign_Authenticode;
var
oSigner: TsgcAuthenticodeSigner;
begin
oSigner := TsgcAuthenticodeSigner.Create(nil);
Try
oSigner.KeyProvider := oProvider;
oSigner.AddCertificateFromFile('MSCV-VSClass3.cer');
// or from bytes you already hold
// oSigner.AddCertificate(vCrossCertDER);
oSigner.SignFile('driver.sys', 'driver-signed.sys');
Finally
oSigner.Free;
End;
end;
추가한 인증서는 모든 중첩 서명에 들어가며, 아무것도 추가하지 않으면 서명은 바이트 단위로 이전과 동일합니다. 서명 서버는 add_certs 필드를 받고, CLI는 반복 지정할 수 있는 --add-cert 옵션을 받습니다.
신뢰 앵커를 사용한 검증
이번 릴리스에서 가장 중요한 변경입니다. 지금까지 검증기는 검사 대상 문서에서 서명 인증서를 꺼내, 그 키가 해당 문서에 서명했는지를 확인했습니다. 이는 문서를 작성한 사람이 그 안의 서명도 작성했다는 사실만 증명할 뿐, 그 이상은 아닙니다. 누구든 통과하는 문서를 만들어 낼 수 있습니다.
이제 검증에 신뢰 앵커를 지정할 수 있고, 검증기는 그 앵커를 기준으로 인증서 체인을 구성하고 확인합니다. 앵커는 SHA-256 지문으로 대조하거나 앵커 자신의 키로 검증하여 일치시키며, 이름으로는 절대 대조하지 않습니다.
uses
sgcSign_Verifier, sgcSign_Types;
var
oVerifier: TsgcSignatureVerifier;
begin
oVerifier := TsgcSignatureVerifier.Create(nil);
Try
oVerifier.TrustedCertificates.Add('certs\qualified-root.pem');
// or a Windows certificate store by name
oVerifier.TrustedCertificateStore := 'ROOT';
oVerifier.RequireTrustedChain := True;
oVerifier.CheckKeyUsage := True;
oVerifier.RequireCompleteRevocationCheck := True;
if oVerifier.Verify(vSignedXML) = vsValid then
ShowMessage('signed, and chained to a root you trust');
Finally
oVerifier.Free;
End;
end;
앵커를 지정하지 않은 검증기는 이전과 같은 판정을 반환하므로 업그레이드로 깨지는 것은 없습니다. 다만 한 가지는 달라집니다. ETSI TS 119 102-2 보고서는 앵커까지 체인이 이어지지 않은 서명에 대해 더 이상 total-passed라고 하지 않고, NO_CERTIFICATE_CHAIN_FOUND와 함께 indeterminate를 보고합니다. 이전 버전에서 저장해 둔 보고서는 다시 생성해야 합니다.
프록시를 지원하는 단일 HTTP 전송 계층
라이브러리는 여러 곳에서 네트워크 요청을 보냅니다. 타임스탬프 클라이언트, OCSP 및 폐기 목록 클라이언트, EU 신뢰 목록 다운로드, 클라우드 키 공급자가 그렇습니다. 각각이 요청을 만드는 방식을 제각각 정하고 있었습니다. 이제는 하나의 HTTPOptions 속성을 가진 단일 전송 계층을 공유합니다.
uses
sgcSign_WinHTTP;
begin
oTSA.HTTPOptions.Proxy.Mode := pxCustom; // pxSystem is the default
oTSA.HTTPOptions.Proxy.URL := 'proxy.corp.local:8080';
oTSA.HTTPOptions.Proxy.Username := 'user';
oTSA.HTTPOptions.Proxy.Password := 'secret';
// the certificate presented when the gateway asks for client authentication
oTSA.HTTPOptions.ClientCertificate.StoreName := 'MY';
oTSA.HTTPOptions.ClientCertificate.Thumbprint := 'a1b2c3...';
oTSA.HTTPOptions.MinTLSVersion := tlsTLS1_2;
end;
프록시는 컴퓨터 전체 설정, 사용 안 함, 명시적인 주소, 또는 브라우저가 하는 것처럼 WPAD나 PAC 스크립트로 해석되는 사용자별 설정 중에서 고를 수 있습니다. 모든 설정의 기본값은 기존 요청이 하던 동작과 같습니다. 이 설정으로 표현할 수 없는 게이트웨이를 위해, 새로운 OnHTTPRequest 이벤트가 전송 계층 전체를 대체합니다.
알아 두면 좋은 소소한 변경
OCSP 논스. 폐기 요청에는 이제 시스템 암호 생성기에서 얻은 임의의 논스가 담기고, 응답은 그 값과 대조됩니다. 논스를 돌려주지 않는 응답은 여전히 허용됩니다. RFC 6960이 미리 생성된 응답을 허용하기 때문입니다. 다만 다른 논스를 돌려주는 응답은 거부됩니다. NonceEnabled는 이 확장을 받아들이지 않는 응답자를 위해 확장을 끕니다.
EU 신뢰 목록 피벗 고정. 새로운 RequirePinnedPivot 속성은 신뢰 목록의 목록이 고정된 관보 피벗 지문 중 하나까지 체인이 이어져야 하는지를 결정하며, LOTLPivotPinned와 LastPivotFingerprint가 그 결과를 보고합니다. 검사 코드는 있었지만 어디에서도 호출되지 않았습니다. 함께 배포되는 고정 상수는 아직 문서에 명시된 자리 표시자이므로, 실제 지문을 채워 넣고 속성을 켜기 전까지는 불일치로 보고됩니다.
직접 고르는 다이제스트. CAdES와 PKCS#11 모두 HashAlgorithm 속성을 갖게 되었고, 기본값은 SHA-256이므로 기존 코드는 동일한 바이트를 만들어 냅니다. CAdES는 예전에 모든 다이제스트 알고리즘 자리에 SHA-256을 리터럴로 기록했고, PKCS#11은 전달받은 값의 길이로 DigestInfo 헤더를 골랐기 때문에 다른 다이제스트를 표현할 방법이 없었습니다. 이제 카드는 요청한 대로 SHA-1, SHA-256, SHA-384, SHA-512로 서명할 수 있습니다.
서명 콜백을 사용하는 ASiC. 새로운 BuildCAdES 오버로드는 완성된 서명 바이트 대신 콜백을 받습니다. 먼저 META-INF/ASiCManifest.xml을 만들고, 그 바이트를 그대로 콜백에 넘긴 뒤 돌아온 결과를 META-INF/signature.p7s로 저장합니다. 서명이 매니페스트를 포함할 수 있는 유일한 순서입니다. 매니페스트가 없는 ASiC-S의 경우 콜백은 데이터 문서 자체를 받습니다. 두 단계를 명시적으로 나누고 싶은 호출자를 위해 GetCAdESSignedData가 같은 바이트를 반환합니다.
클라우드 KMS 인증서. AWS KMS와 Google Cloud KMS에 SetCertificate와 SetCertificateFromFile이 추가되어, HashiCorp Vault가 이미 갖추고 있던 조합과 같아졌습니다. 두 서비스 모두 공개 키만 그대로 넘겨주는데, 지금까지는 어느 X.509 인증서가 그 키에 속하는지 두 공급자 어느 쪽에도 알려줄 방법이 없었습니다.
CLI를 실행하는 컴퓨터에서 받는 타임스탬프. sgcsign 명령줄에 --tsa-direct가 추가되었습니다. sgcSign Server를 거치지 않고 타임스탬프 기관에 직접 요청합니다. --tsa와 함께 지정하세요.
서명 서버
서버 쪽에도 나름의 목록이 생겼습니다. 이제 Authenticode 서명은 sha1,sha256,sha384처럼 순서가 있는 hash_algorithms 목록이나 certA:sha256,certB:sha1처럼 순서가 있는 providers 목록을 통해, 각각 다른 인증서로 서명한 중첩 서명을 두 개 넘게 담을 수 있습니다. 어느 쪽이든 최대 네 개까지 지정합니다. 만료를 앞둔 인증서와 그 후속 인증서로 서명한 파일 하나를 배포할 때 쓰입니다. 모든 인증서는 서명이 시작되기 전에 API 키의 권한과 대조하여 확인합니다.
Windows 카탈로그 파일도 서명할 수 있습니다. 업로드 엔드포인트가 catalog를 형식으로 받아들여 makecat이 만들어 내는 기존 .cat 파일에 서명하므로, 드라이버 패키지를 프로그램과 같은 방식으로 서명합니다.
새로운 /api/v1/sign/raw 엔드포인트는 이미 계산해 둔 다이제스트에 서명하고 서명 값만 반환합니다. PKCS#7 래퍼도, 서명 속성도, 타임스탬프도 없습니다. signtool이 /dlib 콜백을 통해 요구하는 것이 정확히 이 형태입니다. 건네받은 다이제스트는 무엇이든 서명하기 때문에 기본적으로 꺼져 있으며, allow_raw_sign으로 공급자별로 하나씩 켭니다.
API 키와 그 키를 만드는 사용자는 이제 프로젝트별로 분리되며, 프로젝트 관리자는 자신의 프로젝트 안에서 키를 관리하고, 키는 한 방향으로 폐기만 하는 것이 아니라 활성화하고 비활성화할 수 있습니다. 키별 요청 제한과 일일 할당량은 키를 만든 뒤에도 수정할 수 있습니다. 새로운 SessionAbsoluteMaxMin 설정은 관리자 세션의 전체 수명을 기본 12시간으로 제한합니다. 지금까지는 인증된 요청이 있을 때마다 만료 시각이 상한 없이 뒤로 밀렸기 때문입니다. 감사 로그는 콘솔과 CSV 내보내기 모두에서 클라이언트 주소로 필터링할 수 있으며, 주소 일부를 왼쪽부터 일치시킵니다. 그리고 기본적으로 꺼져 있는 새 전달 헤더 설정은 서버가 리버스 프록시 뒤에서 실행될 때 실제 클라이언트 주소를 복원하며, 목록에 등록된 신뢰할 수 있는 프록시에서 연결이 들어올 때만 그 값을 신뢰합니다.
다시 서명해야 하는 서명
이전 버전의 결함 세 가지가 구조적으로 잘못된 파일을 만들어 냈으며, 업그레이드해도 이미 기록된 파일이 복구되지는 않습니다. 아래 중 하나라도 여러분이 서명한 내용에 해당한다면 2026.9.0으로 다시 서명하세요.
- EC 키로 서명한 모든 것. EC 키로 만든 PAdES, CAdES, Authenticode, NuGet, RFC 3161 서명은 모두 무효였습니다. 표준은 CMS 구조 안의 서명 값이 DER ECDSA-Sig-Value여야 한다고 요구하는데, 키 공급자는 r과 s를 그대로 이어 붙인 값을 만들어 냈고, 들어가는 길목에서 이를 변환하는 코드가 없었습니다. 이제 P-256, P-384, P-521이 모두 동작합니다. XAdES와 XML-DSig 경로는 원래 올바랐으므로 의도적으로 그대로 두었습니다.
- 모든 CAdES 서명, 그리고 카드, USB 토큰, 클라우드 키로 서명한 모든 것.
SignData에는 명시된 규약이 없어서 공급자마다 인수가 데이터인지 그 다이제스트인지 해석이 달랐고, 그 결과 CAdES 서명은 해시의 해시에 대한 RSA 연산이 되었습니다. 이제 인수는 원본 바이트이며, 공급자는 설정된 다이제스트로 이를 해시합니다. - 다른 서명기가 같은 키 공급자를 사용한 뒤에 서명한 PAdES 파일.
Profile.HashAlgorithm은 생성자에서 한 번 기록된 뒤 다시 읽히지 않았습니다. 그래서 Facturae나 SAF-T 서명기 다음에 서명한 PDF는 SHA-256이라고 선언한 서명 안에 SHA-1 다이제스트에 대한 RSA 연산이 들어갈 수 있었습니다.
검증도 같은 방향으로 바뀌었습니다. Authenticode 검증은 서명을 전혀 확인하지 않고 파일 해시를 다시 계산해 서명 안의 값과 비교하기만 했습니다. 그래서 sgcSign이 올바르게 서명되었다고 판정하는 파일을 위조하는 데 개인 키가 전혀 필요하지 않았습니다. 폐기 응답과 타임스탬프 토큰은 검증 없이 그대로 포함되었습니다. EU 신뢰 목록은 아무것도 확인하지 않은 채 내려받아 사용했습니다. 이제 이 모두가 이름에 걸맞은 검사를 수행하며, 각 항목의 전체 내용은 변경 이력에 있습니다.
사용해 보기
sgcSign 2026.9.0은 지금 바로 사용할 수 있으며, 전체 소스 코드와 1년간의 업데이트가 함께 제공됩니다. Delphi 7부터 Delphi 13 Florence까지, 그에 대응하는 C++ Builder 버전, 그리고 .NET을 지원합니다.
질문이나 의견이 있으신가요? 문의해 주세요. 코드를 직접 작성한 사람에게서 답변을 받으실 수 있습니다.
