教程:在 Delphi 中进行 XAdES XML 签名

实用演练,将普通 XML 发票转换为 XAdES-B-T 文档——这是 FatturaPA、FACTUR-X 以及欧洲大多数电子发票门户要求的级别。您将在包封式、包封内和分离式签名打包之间进行选择,附加 RFC 3161 时间戳,并验证结果,以便您确切了解对方将看到什么。

XAdES-B-B / B-T / B-LT / B-LTA
包封式 / 包封内 / 分离式
Delphi 7 – RAD Studio 13

三种打包模式,一个引擎

XAdES 通过时间戳和长期验证数据扩展了 W3C XML-DSig 规范。在签署任何内容之前,请先确定 ds:Signature 元素将放在哪里。

包封式(Enveloped)

ds:Signature 元素附加在文档根内部。这是 FatturaPA、FACTUR-X-XAdES 和大多数电子发票流程使用的格式。签名后的 XML 仍然是原始文档的有效副本——读者会忽略它们不理解的签名元素。

包封内(Enveloping)

原始 XML 成为 ds:Signature内部ds:Object 子项。当您需要单个自包含信封且不关心现有使用者是否能继续解析文档时,这种方式很有用。

分离式(Detached)

签名位于单独的文件中,并通过 URI 引用指向原始文档。常见于 SAML、ebXML 以及任何源文档必须逐字节保持不变的流程。

添加单元

XAdES 需要 XML 签名器、XAdES 配置文件和密钥提供程序。本教程使用 PFX;如果您有智能卡或云 HSM,可换用 10 种提供程序中的任意一种。

Delphi uses 子句

sgcSign_XML 是 XML 签名核心。sgcSign_Profile_XAdES 暴露级别、变换和时间戳配置。sgcSign_KeyProvider_PFX 从磁盘加载 PKCS#12 文件——最常见的起点。

对于长期级别(B-LT、B-LTA),您还需要 sgcSign_OCSPsgcSign_CRL,以便可以获取并嵌入吊销数据。

uXAdESSigning.pas
uses
  Classes, SysUtils,
  // sgc
  sgcSign_KeyProvider_PFX,
  sgcSign_XML,
  sgcSign_Profile_XAdES,
  sgcSign_TSA,
  sgcSign_OCSP,
  sgcSign_CRL,
  sgcSign_Verifier;

加载 XML 和密钥

两项输入——您想要签署的 XML,以及您用来签署的 PFX 证书。

PFX 密钥提供程序

TsgcPFXKeyProvider 通过 Windows CNG 导入 PKCS#12 文件,这意味着无论证书最初是为哪个 CSP 颁发的,您都会获得现代化的签名句柄。同一提供程序适用于 Windows 7 及以上版本,32 位和 64 位。

在整个签名操作期间保持提供程序处于活动状态——在 SignXML 返回之前,XML 签名器都引用底层密钥句柄。

step1-load.pas
var
  vXML: string;
  vKeyProvider: TsgcPFXKeyProvider;
begin
  vXML := TFile.ReadAllText('invoice.xml', TEncoding.UTF8);

  vKeyProvider := TsgcPFXKeyProvider.Create(nil);
  vKeyProvider.FileName := 'certificate.pfx';
  vKeyProvider.Password := 'secret';
  vKeyProvider.LoadFromFile;
end;

选择 XAdES-B-T 或 XAdES-B-LT

B-T 添加签名时间戳。B-LT 还额外嵌入 CA 链和吊销数据,使签名在多年内保持可验证。

配置文件配置

Level 选择 AdES 级别:xalBxalTxalLTxalLTAPackaging 选择 xpkEnveloped(默认)、xpkEnvelopingxpkDetached

Transforms 列表默认为 xtEnvelopedSignature + xtC14NExclusive,这是大多数电子发票规范要求的。仅当特定国家/地区配置文件需要包含式 C14N 或自定义 XSLT 变换时才覆盖。

对于 B-LT,OCSP.AutoFetch := True 告诉签名器为链中的每个证书检索 OCSP 响应并将其嵌入 RevocationValues 元素中。签名后的 XML 随后将携带验证者所需的一切——验证时无需网络调用。

step2-profile.pas
var
  vProfile: TsgcSignProfile_XAdES;
begin
  vProfile := TsgcSignProfile_XAdES.Create(nil);
  vProfile.Level := xalT;            // or xalLT, xalLTA
  vProfile.Packaging := xpkEnveloped;
  vProfile.HashAlgorithm := shaSHA256;

  // Timestamp authority for B-T and above
  vProfile.TSA.URL := 'https://freetsa.org/tsr';
  vProfile.TSA.HashAlgorithm := shaSHA256;

  // For B-LT: embed full chain and revocation data
  vProfile.OCSP.AutoFetch := True;
  vProfile.CRL.AutoFetch := True;
end;

签名——包封式、包封内和分离式

同一个 TsgcSignXML 组件处理所有三种打包模式——只需更改 Packaging 属性。

包封式签名

发票的默认选项。签名附加在文档根内部,并使用包封式签名变换将自身从摘要中排除。

对于支持 schema 的负载(FatturaPA、TicketBAI、KSeF),请设置 RootNamespace,以便签名继承正确的 XML 命名空间——国家/地区配置文件会自动设置此项。

step3-enveloped.pas
var
  vSigner: TsgcSignXML;
  vSigned: string;
begin
  vSigner := TsgcSignXML.Create(nil);
  try
    vSigner.KeyProvider := vKeyProvider;
    vSigner.Profile := vProfile;          // Packaging = xpkEnveloped
    vSigned := vSigner.SignXML(vXML);
    TFile.WriteAllText('invoice-signed.xml', vSigned, TEncoding.UTF8);
  finally
    vSigner.Free;
  end;
end;

包封内和分离式

在配置文件上切换打包方式,其他保持不变。对于分离式签名,请将 DetachedURI 设置为被签名文档的路径或 URL;验证者需要该引用来获取数据。

当您收到分离式签名时,将签名 XML 和原始文档字节都传递给 VerifyDetached——sgcSign 会重新计算摘要并确认绑定。

step3-other.pas
// Enveloping: original XML wrapped in a ds:Object
vProfile.Packaging := xpkEnveloping;
vSigned := vSigner.SignXML(vXML);

// Detached: signature file points at invoice.xml
vProfile.Packaging := xpkDetached;
vProfile.DetachedURI := 'invoice.xml';
vSigned := vSigner.SignXML(vXML);
TFile.WriteAllText('invoice.sig.xml', vSigned, TEncoding.UTF8);

验证已签名的 XML

在上线工作流之前,必须对每个已签名的文档进行验证。

一次调用,完整报告

VerifyXML 返回一个 TsgcSignatureReport,其中包含检测到的 AdES 级别、签署者主题、签名时间戳以及任何链或吊销问题。对于包封式签名,验证器会自动定位 ds:Signature 元素;对于分离式签名,请使用 VerifyDetached 并提供原始文档字节。

如果 StatussvValid,则摘要匹配,证书链锚定在受信任的根上,且时间戳完好无损。svInvalid(在 StatusDetail 中带原因)是典型的失败模式;svUnknown 表示验证器无法访问 OCSP 响应器或 CRL 分发点。

step4-verify.pas
var
  vVerifier: TsgcSignatureVerifier;
  vReport: TsgcSignatureReport;
begin
  vVerifier := TsgcSignatureVerifier.Create(nil);
  try
    vReport := vVerifier.VerifyXML(vSigned);
    Memo1.Lines.Add('Level:   ' + vReport.Signatures[0].LevelAsString);
    Memo1.Lines.Add('Status:  ' + vReport.Signatures[0].StatusAsString);
    Memo1.Lines.Add('Signer:  ' + vReport.Signatures[0].Subject);
    Memo1.Lines.Add('TSA:     ' +
      DateTimeToStr(vReport.Signatures[0].TimestampUTC));
  finally
    vVerifier.Free;
  end;
end;

通常会出什么问题

开发者首次签署 XAdES 信封时最常见的问题。

空白和规范化

对已签名的 XML 进行美化打印——在文本编辑器、IDE 中,或通过 XSLT——会破坏摘要。独占 C14N 对每个字节都敏感。将已签名 XML 存储为字节,作为字节传输,永远不要重新格式化。

输入文档中的 BOM

XML 开头的 UTF-8 BOM 是摘要不匹配的常见原因,尤其是当文档由记事本编写时。在签名之前去除 BOM——sgcSign 在内部进行规范化,但其他验证者可能不会。

FatturaPA 需要 xalLT

意大利电子发票门户拒绝 XAdES-B-T,因为它无法在归档时执行长期吊销查询。请使用 xalLT 并设置 OCSP.AutoFetch = True,以便 OCSP 响应随文档一起传输。

分离式签名与 URI 解析

如果 DetachedURI 是相对路径,验证器会针对其自己的工作目录解析它。对于跨机器流,请使用绝对 URL 或将文档字节直接传递给 VerifyDetached

接下来可以去哪里

国家/地区配置文件会为您完成 XAdES 配置。服务器将整个构建场的签名集中化。

国家/地区配置文件

VeriFactu、FatturaPA、KSeF、FACTUR-X、TicketBAI——一个常量切换所有变换、算法和必需属性。

阅读更多 →

PAdES 教程

同一引擎应用于 PDF 而非 XML。阅读 PAdES 演练以并排比较两种格式。

阅读更多 →

sgcSign 服务器

REST API、GitHub Actions 集成、Docker 和 Helm。将签名从个人开发者转移到受控服务。

阅读更多 →

准备好签署您的第一个 XML 了吗?

下载试用版,针对您自己的发票运行本教程,发布时再授权 sgcSign。