教程:在 Delphi 中进行 XAdES XML 签名
实用演练,将普通 XML 发票转换为 XAdES-B-T 文档——这是 FatturaPA、FACTUR-X 以及欧洲大多数电子发票门户要求的级别。您将在包封式、包封内和分离式签名打包之间进行选择,附加 RFC 3161 时间戳,并验证结果,以便您确切了解对方将看到什么。
实用演练,将普通 XML 发票转换为 XAdES-B-T 文档——这是 FatturaPA、FACTUR-X 以及欧洲大多数电子发票门户要求的级别。您将在包封式、包封内和分离式签名打包之间进行选择,附加 RFC 3161 时间戳,并验证结果,以便您确切了解对方将看到什么。
XAdES 通过时间戳和长期验证数据扩展了 W3C XML-DSig 规范。在签署任何内容之前,请先确定 ds:Signature 元素将放在哪里。
ds:Signature 元素附加在文档根内部。这是 FatturaPA、FACTUR-X-XAdES 和大多数电子发票流程使用的格式。签名后的 XML 仍然是原始文档的有效副本——读者会忽略它们不理解的签名元素。
原始 XML 成为 ds:Signature内部的 ds:Object 子项。当您需要单个自包含信封且不关心现有使用者是否能继续解析文档时,这种方式很有用。
签名位于单独的文件中,并通过 URI 引用指向原始文档。常见于 SAML、ebXML 以及任何源文档必须逐字节保持不变的流程。
XAdES 需要 XML 签名器、XAdES 配置文件和密钥提供程序。本教程使用 PFX;如果您有智能卡或云 HSM,可换用 10 种提供程序中的任意一种。
uses 子句sgcSign_XML 是 XML 签名核心。sgcSign_Profile_XAdES 暴露级别、变换和时间戳配置。sgcSign_KeyProvider_PFX 从磁盘加载 PKCS#12 文件——最常见的起点。
对于长期级别(B-LT、B-LTA),您还需要 sgcSign_OCSP 和 sgcSign_CRL,以便可以获取并嵌入吊销数据。
uses Classes, SysUtils, // sgc sgcSign_KeyProvider_PFX, sgcSign_XML, sgcSign_Profile_XAdES, sgcSign_TSA, sgcSign_OCSP, sgcSign_CRL, sgcSign_Verifier;
两项输入——您想要签署的 XML,以及您用来签署的 PFX 证书。
TsgcPFXKeyProvider 通过 Windows CNG 导入 PKCS#12 文件,这意味着无论证书最初是为哪个 CSP 颁发的,您都会获得现代化的签名句柄。同一提供程序适用于 Windows 7 及以上版本,32 位和 64 位。
在整个签名操作期间保持提供程序处于活动状态——在 SignXML 返回之前,XML 签名器都引用底层密钥句柄。
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;
B-T 添加签名时间戳。B-LT 还额外嵌入 CA 链和吊销数据,使签名在多年内保持可验证。
Level 选择 AdES 级别:xalB、xalT、xalLT、xalLTA。Packaging 选择 xpkEnveloped(默认)、xpkEnveloping 或 xpkDetached。
Transforms 列表默认为 xtEnvelopedSignature + xtC14NExclusive,这是大多数电子发票规范要求的。仅当特定国家/地区配置文件需要包含式 C14N 或自定义 XSLT 变换时才覆盖。
对于 B-LT,OCSP.AutoFetch := True 告诉签名器为链中的每个证书检索 OCSP 响应并将其嵌入 RevocationValues 元素中。签名后的 XML 随后将携带验证者所需的一切——验证时无需网络调用。
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 命名空间——国家/地区配置文件会自动设置此项。
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 会重新计算摘要并确认绑定。
// 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);
在上线工作流之前,必须对每个已签名的文档进行验证。
VerifyXML 返回一个 TsgcSignatureReport,其中包含检测到的 AdES 级别、签署者主题、签名时间戳以及任何链或吊销问题。对于包封式签名,验证器会自动定位 ds:Signature 元素;对于分离式签名,请使用 VerifyDetached 并提供原始文档字节。
如果 Status 为 svValid,则摘要匹配,证书链锚定在受信任的根上,且时间戳完好无损。svInvalid(在 StatusDetail 中带原因)是典型的失败模式;svUnknown 表示验证器无法访问 OCSP 响应器或 CRL 分发点。
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 存储为字节,作为字节传输,永远不要重新格式化。
XML 开头的 UTF-8 BOM 是摘要不匹配的常见原因,尤其是当文档由记事本编写时。在签名之前去除 BOM——sgcSign 在内部进行规范化,但其他验证者可能不会。
意大利电子发票门户拒绝 XAdES-B-T,因为它无法在归档时执行长期吊销查询。请使用 xalLT 并设置 OCSP.AutoFetch = True,以便 OCSP 响应随文档一起传输。
如果 DetachedURI 是相对路径,验证器会针对其自己的工作目录解析它。对于跨机器流,请使用绝对 URL 或将文档字节直接传递给 VerifyDetached。