기술 문서

Delphi에서 Excel 전자 서명 검증과 재서명

HotXLS는 Excel이 설치되지 않은 상태에서 Delphi로 Excel 전자 서명을 검증하고 재서명합니다. lxSignatureOpcIntegrity 유닛은 통합 문서 바이트에서 ECMA-376 Part 2 패키지 서명을 직접 검사합니다. 정규화된 SignedInfo에 대한 RSA-SHA256, 모든 SHA-256 Reference 다이제스트, 각 서명이 선택하는 정확한 파트들입니다. 인증서 체인 신뢰는 별개의 명시적 단계이고, 편집이나 재서명은 다시 검증되는 출력만 발행합니다

흔한 방아쇠는 데스크톱을 벗어난 문서 워크플로입니다. 관리자가 Excel에서 예산 통합 문서에 서명하고, Delphi 서비스가 나중에 수치를 임포트하기 전에 서명을 확인해야 하며, 세 번째 단계는 한 시트를 고쳐 서비스 인증서로 다시 서명해야 합니다. 서버에서 Excel을 자동화하는 건 깨지기 쉽고 지원되지도 않으며, 서명을 예/아니오 플래그로 치부하면 실제로 중요한 두 질문이 숨습니다. 서명자가 어떤 바이트에 약속했는가, 그리고 그 키를 쥔 사람을 신뢰하는가

OPC 패키지 서명은 실제로 무엇을 커버하나

OPC 패키지 서명은 Manifest가 나열한 파트만 커버하며, XLSX 파일 전체가 아닙니다. ECMA-376 Part 2, §13(Digital Signatures)은 패키지에 서명 원점 파트를 두는데, 보통 /_xmlsignatures/origin.sigs이며 /_xmlsignatures/sig1.xml 같은 하나 이상의 서명 파트로의 관계를 갖습니다. 각 서명 파트는 SignedInfo가 내부 idPackageObject를 참조하는 W3C XML-DSig Signature이고, 그 Object는 개별 패키지 파트에 대한 Reference의 Manifest를 실으며 각각 자기 다이제스트를 갖습니다

HotXLS는 Reference가 파트를 묶는 세 가지 방식을 지원하며, 이들은 호환되지 않습니다. raw Reference는 저장된 파트 바이트를 해싱합니다. 정규화 XML Reference는 Canonical XML(주석 없음) 형태를 해싱하므로, 속성 순서와 네임스페이스 선언 표기는 더 이상 중요하지 않습니다. 관계 Reference는 OPC RelationshipTransform을 먼저 적용한 뒤 정규화하는데, 그 변환은 SourceId나 SourceType 선택자와 일치하는 관계만 남깁니다. 선택자는 자신이 선택한 관계를 정확히 인증합니다. 그 .rels 파트의 나머지는 서명이 유효한 동안 자유롭게 바뀔 수 있습니다

Delphi용 HotXLS OPC 전자 서명 해부도: origin.sigs 파트가 XML-DSig 서명 파트로 이어지고, 그 Manifest는 raw, 정규화 XML 또는 RelationshipTransform 참조로 각 패키지 파트를 묶음
세 Reference 모드는 호환되지 않습니다. raw는 저장된 바이트를 해싱하고, 정규화 XML은 속성 표기를 무시하며, 관계 선택자는 선택되지 않은 .rels 항목을 자유롭게 남겨 둡니다

마지막 항목이 바로 HotXLS가 녹색 체크 하나 대신 커버리지를 보고하는 이유입니다. TXLSSignatureOpcIntegrityResult.Coverage는 TXLSSignatureOpcCoverage 레코드의 배열로 인증된 Manifest Reference마다 하나씩이며, 각각 ReferenceUri, PartName, ContentType, Transforms, RelationshipSourceIds, RelationshipSourceTypes를 갖습니다. 임포트가 sheet3.xml을 읽는데 어느 서술자도 그것을 이름 짓지 않았다면, 서명은 곧 적재하려는 수치에 대해 아무 말도 하지 않는 것입니다

불변 ZIP 스냅샷에서 서명 검증하기

HotXLS는 하나의 불변 바이트 스냅샷에서 패키지를 검증하며, 디스크의 파일이나 살아있는 TWorkbook에서 절대 그렇게 하지 않습니다. XlsVerifySignatureOpcPackage와 그 다중 서명 형제들은 TBytes 배열을 받아 그 배열만 읽고, 외부 대상이나 인증서, 로컬 파일, 네트워크 자원을 가져오지 않습니다. 요점은 여러분이 검증한 바이트가 이후에 파싱하는 바이트라는 것입니다. 파일을 나중에 다시 읽으면 그 사이에 다른 프로세스가 바꿔 놓았을 수 있습니다

정규화는 직접 만든 검증기가 보통 틀리는 곳입니다. SignedInfo와 패키지 Object는 완전한 Signature 봉투의 네임스페이스 문맥에서 정규화되어야 합니다. 문서에서 잘라낸 고립된 조각은 검증기가 제자리에서 보는 것과 다른 네임스페이스 선언을 물려받으므로, C14N 바이트가 달라지고 다이제스트가 달라지며, 완벽히 유효한 서명이 실패합니다. HotXLS는 검증 쪽과 발행 쪽 모두에서 전체 봉투 문맥을 씁니다

하나 이상의 서명을 실을 수 있는 패키지에는 XlsVerifyAllSignaturesOpcPackage가 첨부된 모든 서명을 검증하고 개별 결과를 유지합니다. 집계 상태는 승인된 모든 서명이 검증될 때만 xsoiIntegrityVerified이고, 그렇지 않으면 인벤토리 순서의 첫 실패이며, 각 보고서는 여전히 어느 서명 파트가 실패했는지 알려 줍니다

uses
  System.SysUtils, System.IOUtils, lxSignatureOpcIntegrity;

function AllSignaturesIntact(const FileName: string): Boolean;
var
  Bytes: TBytes;
  Reports: TXLSSignatureOpcVerifications;
  Status: TXLSSignatureOpcIntegrityStatus;
  I, J: Integer;
begin
  Bytes := TFile.ReadAllBytes(FileName);  // 하나의 스냅샷, 검증과 파싱 대상
  Status := XlsVerifyAllSignaturesOpcPackage(Bytes, Reports);
  for I := 0 to High(Reports) do
  begin
    Writeln(String(Reports[I].SignaturePart), ': ', Ord(Reports[I].Status));
    if Reports[I].Status = xsoiIntegrityVerified then
      for J := 0 to High(Reports[I].Integrity.Coverage) do
        Writeln('  covers ', String(Reports[I].Integrity.Coverage[J].PartName));
  end;
  Result := Status = xsoiIntegrityVerified;  // xsoiUnsigned는 통과가 아님
end;

상태 값들은 행동으로 이어질 만큼 구체적입니다. xsoiDigestMismatch는 선택된 파트가 서명 뒤에 바뀌었다는 뜻이고, xsoiInvalidSignature는 RSA 값이 내장 인증서 키로 SignedInfo에 대해 검증되지 않는다는 뜻이며, xsoiPartUnavailable은 선택된 파트가 없다는 뜻이고, xsoiUnsupportedProfile이나 xsoiResourceLimit은 검증기가 추측하는 대신 거부했다는 뜻입니다. 그 아래 ZIP 층은 신뢰할 수 없는 XLSX 파일의 ZIP EOCD 검증 글이 설명하는 것과 같은 실패 시 폐쇄 읽기를 적용하고, 파트 조회는 XLSX OPC 관계 해석의 규칙을 따릅니다

서명된 통합 문서를 편집하면 왜 서명이 무효화될까

변경된 바이트가 서명된 선택 안에 들어가는 순간 편집은 서명을 무효화합니다. 그 Reference의 저장된 SHA-256 다이제스트가 더 이상 맞지 않기 때문입니다. 통합 문서를 적재했다가 다시 저장하는 스프레드시트 라이터는 거의 항상 서명된 파트를 건드립니다. XML을 재직렬화하고, 공유 문자열 번호를 다시 매기고, workbook.xml과 콘텐츠 타입 매니페스트를 다시 쓰죠. 셀 값이 동일해도 바이트는 그렇지 않고, 다음 검증은 xsoiDigestMismatch를 돌려줍니다

HotXLS는 편집에 약속하기 전에 질문할 수 있게 해 줍니다. XlsEditVerifiedOpcPackage는 검증된 패키지의 기존 파트를 교체한 뒤 후보를 검증합니다. xsoePreserve에서는 서명이 여전히 검증될 때만, 즉 편집된 파트가 모든 서명된 선택 밖에 있을 때 xsoePreserved를 반환하고, 그 외에는 xsoeWouldInvalidate를 반환합니다. xsoeInvalidateChanged에서는 루트 관계와 [Content_Types].xml에서 서명 바인딩을 제거하고 xsoeInvalidated로 명시적으로 서명 없는 패키지를 반환합니다. 여전히 서명된 것처럼 보이는 파일을 건네주는 대신입니다

function EditKeepsSignature(const Signed, NewSheetXml: TBytes;
  out Edited: TBytes): Boolean;
var
  Edits: TXLSSignatureOpcEdits;
  Details: TXLSSignatureOpcEditResult;
begin
  SetLength(Edits, 1);
  Edits[0].PartName := '/xl/worksheets/sheet2.xml';
  Edits[0].Data := NewSheetXml;
  case XlsEditVerifiedOpcPackage(Signed, Edits, xsoePreserve, Edited, Details) of
    xsoePreserved:       Result := True;   // sheet2는 서명된 선택 밖이었음
    xsoeWouldInvalidate: Result := False;  // 서명된 다이제스트가 sheet2를 커버
  else
    raise Exception.CreateFmt('Edit refused, original integrity %d',
      [Ord(Details.OriginalIntegrity)]);
  end;
end;

무결성 대 신뢰: 검증된 서명은 무엇을 증명하나

검증된 서명은 개인 키를 쥔 누군가가 서명한 이후 선택된 파트가 변하지 않았음을 증명하며, 그 누군가가 누구인지에 대해서는 아무것도 증명하지 않습니다. HotXLS는 두 층을 의도적으로 분리해 둡니다. 모든 TXLSSignatureOpcIntegrityResult는 성공한 검사를 포함해 Trust = xsstUntrusted를 실고, ClaimedSignatureTime은 신뢰된 타임스탬프가 아니라 서명자가 준 데이터입니다. 누구나 자체 서명 인증서를 만들고, 통합 문서에 서명하고, 무결성 검사를 통과할 수 있습니다

HotXLS는 Delphi에서 Excel OPC 패키지 전자 서명을 두 층으로 검증합니다. 항상 xsstUntrusted를 보고하는 불변 바이트 스냅샷 위의 무결성 다이제스트 검사, 그리고 호출자 소유 정책의 XlsVerifySignatureOpcPackageWithTrust
검증된 서명은 선택된 파트가 변하지 않았음을 증명할 뿐이고, 신뢰는 xsotTrustedSelection이 의미를 갖기 위해 여러분 자신의 앵커와 UTC 검증 시각, 문서 서명 EKU를 필요로 합니다

신뢰는 호출자 소유 정책의 별도 호출입니다. XlsDefaultSignatureOpcTrustPolicy는 앵커도 검증 시각도 없고, 문서 서명 EKU 1.3.6.1.4.1.311.10.3.12와 xsrvRequireCached 폐기를 갖는 실패 시 폐쇄 정책을 반환합니다. XlsVerifySignatureOpcPackageWithTrust는 먼저 완전한 무결성 검증기를 돌린 뒤 실제로 통과한 인증서를 평가합니다. 호출자가 다른 리프 인증서로 바꿔치기할 수 없습니다

uses
  System.SysUtils, System.DateUtils, lxSignatureOpcIntegrity;

function SignedUnderCorporateCA(const Bytes, RootDer, IssuingCaDer: TBytes): Boolean;
var
  Policy: TXLSSignatureOpcTrustPolicy;
  Trust: TXLSSignatureOpcTrustResult;
begin
  Policy := XlsDefaultSignatureOpcTrustPolicy;
  SetLength(Policy.TrustAnchors, 1);
  Policy.TrustAnchors[0] := RootDer;
  SetLength(Policy.Intermediates, 1);
  Policy.Intermediates[0] := IssuingCaDer;
  Policy.VerificationTimeUtc := TTimeZone.Local.ToUniversalTime(Now);

  Result := XlsVerifySignatureOpcPackageWithTrust(Bytes, Policy, Trust)
    = xsotTrustedSelection;
  if not Result then
    Writeln(Format('trust %d, integrity %d, policy error %.8x, chain errors %.8x',
      [Ord(Trust.Status), Ord(Trust.IntegrityStatus),
       Trust.NativePolicyError, Trust.NativeChainErrors]));
end;

Windows 백엔드에는 직접 WinCrypt를 부를 때 놓치기 쉬운 함정이 둘 있습니다. CertVerifyCertificateChainPolicy가 True를 돌려주는 것은 호출이 실행됐다는 뜻일 뿐입니다. 수용에는 여전히 정책 상태의 0 dwError, 집계 체인 오류 0, 리프의 명시적 digitalSignature 키 사용, 필요한 EKU가 필요합니다. HotXLS는 키 사용이 없는 인증서가 평범한 베이스 정책을 통과할 수 있음을 발견했고, 그래서 DigitalSignatureUsage와 RequiredEkuPresent를 스스로 검사합니다. 두 번째 함정입니다. 체인 엔진 설정에서 배타적 루트 저장소와 hRestrictedOther를 조합하면 현재 백엔드에서 E_INVALIDARG를 반환하므로, HotXLS는 배타적 호출자 루트 저장소를 쓴 뒤 반환된 1차 체인의 모든 요소를 여러분이 준 DER 바이트에 대해 승인합니다. 우연히 로컬 시스템 저장소에 앉아 있던 발급자가 신뢰되는 일은 없습니다

xsotTrustedSelection은 여전히 그 서명과 보고된 커버리지만 기술합니다. 서명되지 않은 셀과 선택되지 않은 관계는 선택된 서명이 신뢰된 채로 바뀔 수 있으므로, 신뢰 결과는 감사 로그를 위해 PackageSha256과 CertificateSha256도 돌려줍니다

Excel 없이 통합 문서를 재서명하거나 연대 서명하는 방법

바이트 배열 수준에서 HotXLS는 명시적이고 승인된 트랜잭션으로 재서명하며, Workbook 서명 저장 경로는 같은 검증된 파이프라인을 재사용합니다. XlsReplaceVerifiedSignatureOpcPackage는 검증된 서명 하나를 새 것으로 바꾸고, XlsEditAndReplaceSelectedSignatureOpcPackage는 파트 편집과 선택된 서명 하나의 교체를 같은 트랜잭션에서 적용하며, XlsAppendVerifiedSignatureOpcPackage는 검증된 모든 원본을 바이트 단위로 유지하면서 서명을 추가합니다. 교체 호출은 TXLSSignatureOpcReplaceAuthorization을 받고, 추가 호출은 모든 원본을 먼저 검증하며, 셋 모두 TXLSSignatureOpcIssueOptions 선택과 서명자 콜백을 받습니다. 라이브러리는 여러분의 개인 키를 건드리지 않습니다

승인 레코드는 연산을 여러분이 승인한 정확한 원본에 묶습니다. ExpectedPackageSha256, ExpectedSignaturePart, ExpectedCertificateSha256이며, 셋 모두 오래된 서명이 완전히 검증된 뒤에만 비교됩니다. 편집된 모든 파트는 새 선택이 커버해야 하고, 선택되지 않은 모든 서명은 서명자가 불리기 전에 스테이징된 패키지에 대해 여전히 검증되어야 하며, 그렇지 않으면 호출은 xspsWouldInvalidate를 반환합니다

HotXLS는 Delphi에서 XlsEditAndReplaceSelectedSignatureOpcPackage로 Excel XLSX 패키지를 재서명합니다. 오래된 서명을 검증하고 예상되는 패키지, 파트, 인증서 해시를 확인하며 서명자 콜백을 부름
승인 레코드는 교체를 승인된 원본에 묶고, 선택되지 않은 서명은 스테이징된 패키지에서 여전히 검증되어야 하며, 개인 키는 여러분의 CNG, HSM 또는 스마트 카드 안에 남습니다
uses
  System.SysUtils, System.DateUtils, lxHash, lxSignatureOpcIntegrity;

type
  TWorkbookResigner = class
  private
    function SignDigest(const Request: TXLSSignatureOpcSelectedReplaceRequest;
      var Signature: TBytes): TXLSSignatureOpcSignerStatus;
  public
    function Resign(const Approved, NewSheetXml, NewCertDer: TBytes;
      const OldCertSha256: WideString; var Output: TBytes): Boolean;
  end;

function TWorkbookResigner.SignDigest(
  const Request: TXLSSignatureOpcSelectedReplaceRequest;
  var Signature: TBytes): TXLSSignatureOpcSignerStatus;
begin
  // Request.NewSignature.Digest는 라이브러리가 계산한 32바이트 SHA-256이며,
  // RsaSignSha256Pkcs1은 여러분의 CNG, HSM 또는 스마트 카드 래퍼를 대표하고
  // 전체 빅엔디안 RSASSA-PKCS1-v1_5 서명을 반환해야 합니다
  if RsaSignSha256Pkcs1(Request.NewSignature.Digest, Signature) then
    Result := xsosSigned
  else
    Result := xsosFailed;
end;

function TWorkbookResigner.Resign(const Approved, NewSheetXml, NewCertDer: TBytes;
  const OldCertSha256: WideString; var Output: TBytes): Boolean;
var
  Auth: TXLSSignatureOpcReplaceAuthorization;
  Options: TXLSSignatureOpcIssueOptions;
  Edits: TXLSSignatureOpcEdits;
  Details: TXLSSignatureOpcSelectedReplaceResult;
begin
  Auth.ExpectedPackageSha256 := XLSComputeSHA256Hex(Approved); // 또는 승인 시 저장한 해시
  Auth.ExpectedSignaturePart := '/_xmlsignatures/sig1.xml';
  Auth.ExpectedCertificateSha256 := OldCertSha256;

  SetLength(Edits, 1);
  Edits[0].PartName := '/xl/worksheets/sheet1.xml';
  Edits[0].Data := NewSheetXml;

  SetLength(Options.References, 2);
  Options.References[0].PartName := '/xl/workbook.xml';
  Options.References[0].Mode := xsimCanonicalXml;
  Options.References[1].PartName := '/xl/worksheets/sheet1.xml';
  Options.References[1].Mode := xsimCanonicalXml;
  Options.ClaimedSignatureTimeUtc := FormatDateTime('yyyy"-"mm"-"dd"T"hh":"nn":"ss"Z"',
    TTimeZone.Local.ToUniversalTime(Now));

  Result := XlsEditAndReplaceSelectedSignatureOpcPackage(Approved, NewCertDer,
    Auth, Options, Edits, SignDigest, Output, Details) = xspsReplaced;
end;

그 코드의 두 디테일은 의도적입니다. Output과 Details는 별개 변수여야 합니다. Details의 필드를 Output으로 넘기는 것은 명시적으로 지원되지 않는데, 라이브러리가 패키지와 인증서 보고서를 같은 저장소에 발행할 수 없기 때문입니다. 그리고 오래된 커버리지는 상속되지 않습니다. 새 서명은 여러분이 나열한 Reference를 정확히 인증하므로, 다운스트림 소비자가 무엇에 의존할지 다시 정하십시오

연대 서명은 같은 추가 호출을 씁니다. HotXLS 2.384.296부터 XlsAppendVerifiedSignatureOpcPackage에 넘기는 TXLSSignatureOpcIssueOptions.References는 검증된 원본 서명 인벤토리의 파트를 이름 지을 수 있으므로, 검토자는 기존 서명 파트 위에 서명하거나, 여러 연대 서명을 사슬로 잇거나, 하나의 선택에서 서명 파트와 콘텐츠 파트를 섞을 수 있습니다. 각 추가된 서명은 자기 직접 선택만 기록합니다. 그 릴리스의 자격 검증은 7개 출력 패키지에 걸쳐 .NET WindowsBase 패키징 검증기로 222개 경계 검사와 122개 독립 검사를 돌려, 정확히 하나의 새 서명과 바이트 단위로 동일한 원본 서명 및 인증서를 확인했습니다

알아 둘 가치가 있는 함정과 한계

가장 교훈적인 버그는 완벽히 합법인 문자에서 나왔습니다. OPC Reference URI는 콘텐츠 타입을 질의로 실는데, 예를 들어 /xl/worksheets/sheet1.xml?ContentType=application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml입니다. 초기 HotXLS 발행자는 리터럴 +를 %2B로 퍼센트 인코딩했습니다. HotXLS 자체의 RSA, C14N, Reference 검사는 결과를 받아들였지만 WindowsBase는 InvalidSignature를 보고했습니다. 그 표기만 바꾼 통제 샘플이 InvalidSignature에서 Success로 갔습니다. 발행자는 이제 지원되는 리터럴 ASCII 질의 문장부호를 유지하면서 XML 속성은 여전히 이스케이프하며, 서명자에게 주는 교훈은 자기 검증기뿐 아니라 독립적인 네이티브 검증기로 테스트하라는 것입니다

  • 신뢰 백엔드는 Windows 전용입니다. 다른 타깃은 무결성 성공 뒤 xsotTrustUnavailable을 반환합니다
  • 검증기는 64 MiB까지, 파트당 16 MiB, 하나의 원점 아래 최대 32개 서명 파트의 패키지를 받습니다
  • 발행은 서명당 최대 256개 Reference와 4096개 결합 선택자를 받습니다
  • 지원 프로필은 RSA-SHA256, SHA-256 다이제스트, 주석 없는 Canonical XML입니다. 다른 알고리즘, 추가 애플리케이션 Object, 외부 Reference는 xsoiUnsupportedProfile을 반환합니다
  • xsrvRequireCached는 캐시 전용 폐기 데이터를 씁니다. 없는 상태는 거부하고, xsrvNotChecked는 폐기 보증을 주지 않습니다
  • Excel도 받아들여야 하는 통합 문서 서명은 여전히 Excel이 유효한 XLSX를 수리하는 이유의 패키지 규칙에 부딪힙니다. 중복된 Override나 관계 Id는 서명이 검사되기 전에 파일을 깨뜨립니다

FAQ

HotXLS 무결성 검사는 서명자가 신뢰된다는 뜻인가요?

아니요. XlsVerifySignatureOpcPackage는 내장 인증서 키 아래에서 선택된 파트가 다이제스트와 일치함을 증명하고 항상 xsstUntrusted를 보고합니다. 인증서 체인을 평가하려면 여러분의 앵커와 UTC 검증 시각으로 XlsVerifySignatureOpcPackageWithTrust를 쓰십시오

Delphi 편집 뒤에 Excel이 통합 문서 서명을 무효로 보여 주는 이유는요?

저장된 통합 문서는 서명된 XML 파트를 다시 쓰므로, 최소 하나의 Reference의 SHA-256 다이제스트가 바뀝니다. 편집을 먼저 시험하려면 xsoePreserve와 함께 XlsEditVerifiedOpcPackage를, 하나의 검증된 트랜잭션에서 편집하고 재서명하려면 XlsEditAndReplaceSelectedSignatureOpcPackage를 쓰십시오

HotXLS는 스마트 카드나 HSM에 저장된 키로 서명할 수 있나요?

네. 서명은 호출자 콜백이기 때문입니다. 라이브러리는 정규화된 SignedInfo와 그 32바이트 SHA-256 다이제스트를 넘기고, 여러분의 코드가 RSASSA-PKCS1-v1_5 서명을 돌려주며, 개인 키는 여러분이 통제하는 프로바이더를 절대 떠나지 않습니다

OPC 서명 검증, 신뢰 평가, 연대 서명, 검증된 재서명은 Delphi 및 C++Builder용 네이티브 Excel 스프레드시트 컴포넌트인 HotXLS의 일부이며, Excel이나 COM 자동화 없이 XLS, XLSX, XLSB, ODS를 읽고 씁니다