sportuondo/xml-signer

用于创建和验证XAdES签名的PHP库

dev-main 2022-12-23 11:33 UTC

This package is auto-updated.

Last update: 2024-09-23 15:05:20 UTC


README

为PHP提供XML文档的签名和验证功能,专注于支持符合XAdES规范的签名。

XML-DSig

本项目基于Rob Richards的xmlseclibs项目,它提供了XML-DSig支持。然而,此基础项目已被修改以适应XAdES的要求。例如,但不限于

  • <Reference>节点引用XAdES的<QualifingProperties>需要支持Type属性
  • 在指定<Reference> Id和URI属性方面增加了灵活性
  • 增加了对XPath Filter 2.0的支持
  • 从XPath查询生成的节点列表(如节点列表)生成XML
  • 允许从签名内的证书或从证书内的发行者链接中构建证书链

由于这些更改很重要,因此从xmlseclibs的相关源代码已纳入本项目,并更改了命名空间以防止与原始项目冲突。

XAdES支持

XAdES支持有两个方面。一方面是一组类,它涵盖了XAdES规范中定义的所有元素类型。此代码类似于XAdESJS类,并且以类似方式使用。类构造函数接受适当的参数并能够验证它们。它们能够协同工作以创建节点层次结构,并且可以使用节点层次结构来加载现有和生成有效签名所需的新XML。还有支持导航和操作节点层次结构的功能。使用这些类,了解XAdES的人可以创建任意XAdES签名。

然而,用户很可能没有XAdES的百科全书式知识。因此,另一方面是允许不太专业的用户提供最少量的信息,并使用函数来生成XML签名、请求和处理时间戳以及添加附加签名。目前,方便的函数没有涵盖整个XAdES规范,尽管它们允许使用关键XAdES功能,如策略选择、承诺类型指示、用户角色、时间戳和附加签名。签名可以嵌入源文档,也可以作为独立文件保存。还有一个函数可以验证现有签名。

LT和LTA支持

长期(LT)和长期存档(LTA)是在XAdES规范中定义的配置文件,旨在使某人能够在签名证书已过期很长时间后验证签名。第6.3节中的表2定义了要包含在签名中的元素,以使签名被视为长期签名。

LT 支持来源于添加了 <CertificateValues>、<RevocationValues> 和 <TimeStampValidationData> 元素(参见 XAdES 规范的第 5.4.1、5.4.2 和 5.5.1 节。LTA 支持来源于添加了 <ArchiveTimeStamp> 元素(参见第 5.5.2 节)。

使用静态的 XAdES::archiveTimestamp() 函数,代码将添加这些元素并用相关数据填充它们。请注意,此支持是实验性的,目前不能适应所有 LTA 场景。例如,目前它无法处理在已包含存档时间戳的签名中添加存档时间戳的情况。如果签名中包含计数签名,则生成有效签名的可能性也极低。

LT 和 LTA 支持更为复杂,因此更可能出现错误。与可以通过本地信息生成的一般签名不同,LT 签名必须检索从签名证书的信任锚(根证书颁发机构)到所有证书。它还必须检索签名签名的撤消信息,然后检索用于签署撤消信息的证书。这种复杂性也意味着我对规范的理解可能是不正确的,至少是不精确的,因为可能的情况数量已显著增加。

使用以下静态函数将 LTA 支持添加到签名中。请注意,LTA 信息是添加到现有的签名中的,因此使用 SignedDocumentResourceInfo 实例选择现有的签名源。

	XAdES::archiveTimestamp(
		new SignedDocumentResourceInfo( 
			__DIR__ . '/my-existing-signature.xml', 
			ResourceInfo::file,
			XAdES::SignatureRootId, // optional id
			__DIR__,
			'my-existing-signature-with-archive-timestamp.xml',
			XMLSecurityDSig::generateGUID('archive-timestamp-')
		)
	);

@sangar82 能够使用欧盟委员会 XAdES 演示网站 独立确认创建的签名达到 LTA 级别

局限性

XAdES 定义了 140 多个不同的元素。尽管支持创建每个元素,但每个元素的测试级别并不相同。这尤其适用于 <UnsignedProperties> 区域中的元素。

另一个局限性是,唯一可用于签名的机制是在公共密钥基础设施(PKI)的上下文中使用 X509 证书。这些是网站和浏览器使用的同一类型证书,尽管通常具有不同的密钥用途属性。

不支持属性证书,因此不能在 <SignerRoleV2> 实例中使用。

最后一个局限性是缺少对清单的支持。这些是对 XML 文档中其他元素的引用,其中可以放置参与签名的元素。此签名者不会使用任何替代位置,并且无法验证使用此类替代位置的签名。

一致性

要检查生成的 XML 是否符合规范,它们使用由 XAdES 一致性检查器 (XAdESCC) 验证,该一致性检查器由 Juan Carlos Cruellas 创建。该工具使用 Java 创建,并通过 ETSI 提供,ETSI 是负责 XAdES 规范的 ESO。XAdESCC 检查签名的各个方面,包括计算出的摘要。使用此工具有助于确保由一个工具生成的签名可以被另一个工具验证,反之亦然。还使用 C# 程序来确认生成的签名可以通过 System.Security.Cryptography.Xml 命名空间中包含的 XML-DSig 支持进行验证。

依赖性

XAdES 使用时间戳、在线证书状态协议 (OCSP)证书撤消列表 (CRLs) 来支持签名的不可抵赖性。
因此,除了 XMLDSig 之外,还使用此 OCSP 请求者 项目。
它提供 OCSP、时间戳协议 (TSP) 和 CRL 请求支持。它还提供用于读取使用抽象语法表示法 (ASN.1) 编码的字符串的类。ASN.1 用于编码 OCSP 和 TSP 请求和响应。
它还用于编码PKI实体,例如X509证书、密钥、存储等,因此ASN.1是一个重要的支持标准。

OCSP请求者依赖于以下PHP扩展:php_curl、php_gmp、php_mbstring和php_openssl。

简单示例

下面是如何验证带有签名的文档。这里的文档位于Url的末尾,但它可能,很可能是,一个本地文件。

XAdES::verifyDocument( 'http://www.xbrlquery.com/xades/hashes-signed.xml' );

这是一个使用强大XAdES签名的Xml文档签名的示例。url指向一个真实文档,但要执行此示例,将需要提供您自己的证书和相应的私钥。使用我的测试证书和私钥的此函数输出可以在以下位置访问这里

use lyquidity\xmldsig\CertificateResourceInfo;
use lyquidity\xmldsig\InputResourceInfo;
use lyquidity\xmldsig\KeyResourceInfo;
use lyquidity\xmldsig\ResourceInfo;
use lyquidity\xmldsig\XAdES;
use lyquidity\xmldsig\xml\SignatureProductionPlaceV2;
use lyquidity\xmldsig\xml\SignerRoleV2;
use lyquidity\xmldsig\XMLSecurityDSig;

XAdES::signDocument( 
	new InputResourceInfo(
		'http://www.xbrlquery.com/xades/hashes for nba.xml', // The source document
		ResourceInfo::url, // The source is a url
		__DIR__, // The location to save the signed document
		'hashes for nba with signature.xml' // The name of the file to save the signed document in
	),
	new CertificateResourceInfo( '...some path to a signing certificate...', ResourceInfo::file ),
	new KeyResourceInfo( '...some path to a correspondoing private key...', ResourceInfo::file ),
	new SignatureProductionPlaceV2(
		'My city',
		'My address', // This is V2 only
		'My region',
		'My postcode',
		'My country code'
	),
	new SignerRoleV2(
		'CEO'
	),
	array(
		'canonicalizationMethod' => XMLSecurityDSig::C14N,
		'addTimestamp' => false // Include a timestamp? Can specify an alternative TSA url eg 'http://mytsa.com/' 
	)
);

最小输入

类的设计目标是尽量减少输入。本例中有几个情况。SignatureProductionPlaceV2类的参数实际上是类,例如,城市、街道地址、邮编等。如果可能,构造函数将转换字符串参数为适当的类实例。

本例中的另一个情况是new SignerRoleV2('CEO')。其完整格式将是

new SignerRoleV2(
	new ClaimedEoles(
		array(
			new ClaimedRole('CEO')
		)
	)
)

显然,如果存在多个声明的角色,则必须使用较长格式。

如果文件引用只是文件的路径,则可以使用文件路径。在此示例中,这适用于证书和私钥引用。这种简化不能用于要签名的文档的引用,因为源是URL。在这种情况下,生成的签名不能保存在同一位置,因此必须显式提供替代位置,方法是实例化InputResourceInfo类并将值分配给saveLocationsaveFilename属性。如果源是只读的本地文件,也需要这样做。

策略

在上面的示例中,直接使用了XAdES类。然而,这可能不太有帮助,因为XAdES签名是为与策略一起使用而设计的。也就是说,验证XAdES签名不仅仅是测试XML-DSig。

XAdES的目的是扩展核心XML-DSig规范,以便签名的文档能够经受法庭审判的审查。通过这种方式,XML签名可以具有与手写签名相同的法律地位。然而,对于某个司法管辖区来说是充分证据的签名证据可能不适用于另一个司法管辖区。为了帮助让所有在司法管辖区内运作的各方了解要求,每个司法管辖区都有能力发布一个策略(另一个签名的XML文档)来描述其要求。

对司法管辖区没有单一的定义。一个明显的司法管辖区可能是一个国家或政府部门。然而,它也可能是一组达成关于将接受哪些签名类型的协议的商业实体。

一个示例政策是由荷兰标准商业报告(SBR)创建的,SBR是荷兰政府的一个部门,负责接收和检查在荷兰运营的商业实体产生的报告。该部门接受以XBRL文档形式的电子报告。这些文档可以由审计师签名。签名中应包含的信息、证书使用的密钥长度等,在此XML策略文档中定义。

因此,在验证签名时,验证不仅检查由证书生成的摘要,而且还检查签名XML中包含的组件是否符合相关策略。因此,最好创建一个特定于司法管辖区类的类,该类扩展了类XAdES

项目包括一个针对SBR司法管辖区政策的此类。它被称为XAdES_SBR。它覆盖了XAdES的功能,既可以向签名添加额外的、与策略相关的信息,也可以在验证签名时检查是否包含适当的信息。

资源

在上面的例子中使用了三种类型的资源。每种资源都是特定于所需参数的资源类型的一个实例类型。这些资源用于向签署者传达适当的信息,这些信息可以以不同的形式存在。例如,待签署文档的资源可能是文件路径、URL、XML字符串或DOMDocument实例。同样,签名或私钥的资源可能是文件路径、PEM格式化的字符串、Base 64编码的字符串或证书或密钥的二进制表示。

除了具有特定于资源的属性外,每个资源还有一个'type'属性,它提供了一种描述资源性质的方法。可能的完整列表

在适当的情况下,这些标志可以或在一起。这样做意味着可以向签署者提供更多信息。例如,以字符串形式提供的证书可能是PEM格式、Base 64格式或DER字节。如果以PEM格式在字符串中提供证书,则类型值将是

ResourceInfo::string | ResourceInfo::pem

反对签名

除了主签名外,可能还需要添加一个或多个反对签名。例如,主签名可能是首席财务官的代表,然后由法律顾问和/或审计合伙人反对签名。为了便于添加反对签名,有一个静态方法

XAdES::counterSign( 
	new SignedDocumentResourceInfo( 
		'http://www.xbrlquery.com/xades/hashes for nba with signature.xml', 
		ResourceInfo::url,
		'source-sig-id', // this identifies the signature being counter signed
		__DIR__,
		'hashes-counter-signed.xml',
		XMLSecurityDSig::generateGUID('counter-signature-') // A unique id for this signature
	),
	'... path to a certificate file ...', // or a CertificateResourceInfo instance
	'... path to the certificate private key file ...', // or a KeyResourceInfo instance
	new SignatureProductionPlaceV2(
		'New Malden',
		'16 Lynton Road', // This is V2 only
		'Surrey',
		'KT3 5EE',
		'UK'
	),
	new SignerRoleV2(
		new ClaimedRoles( new ClaimedRole('Chief legal counsel') )
	)
);

注意使用SignedDocumentResourceInfo类向签署者提供关于要添加反对签名的文档的信息。该类公开了一个额外的属性,允许调用者定义签名元素的@id。如果要在反对签名中添加时间戳,则需要此属性。

时间戳

在创建原始签名时可以添加时间戳。还可能向现有文档添加时间戳,这对于需要时间戳的反对签名很有用。为了简化添加时间戳的过程,有一个静态方法

XAdES::timestamp( 
	new InputResourceInfo(
		'http://www.xbrlquery.com/xades/hashes for nba.xml', // The source document
		ResourceInfo::url, // The source is a url
		__DIR__, // The location to save the signed document
		'hashes for nba with timestamped signature.xml', // The name of the file to save the signed document in
		null,
		true,
		'signature-to-timestamp'
	),
	null // An optional url to an alternative timestamp authority (TSA)
);

InputResourceInfo实例的id参数对于标识应添加时间戳的签名至关重要。这可能是指主签名,也可能是反对签名。实际上,它还可以识别任何用作签名内引用的元素。

使用PKCS 12

证书和私钥可以存储在一个受密码保护的PKCS 12容器文件中(通常具有.p12扩展名)。签名代码不支持显式支持存储在PKCS 12文件中的资源,但使用此类存储的资源是直接的。

首先,访问文件内容

if ( ! openssl_pkcs12_read( file_get_contents( '/path_to_pkcs12_file/my.p12' ), $store, '<passphrase>' ) )
{
    echo "Oops unable to open the file\n";
    die();
}

然后,在您的代码中使用这些类的实例来使用PKCS12文件的内容

new CertificateResourceInfo( $store['cert'], ResourceInfo::string | ResourceInfo::binary | ResourceInfo::pem ),
new KeyResourceInfo( $store['pkey'], ResourceInfo::string() | ResourceInfo::binary | ResourceInfo::pem ),

每个资源的适当元素都是由openssl_pkcs12_read函数返回的数组。所使用的标志让处理器知道内容将是文本字符串或二进制字符串,并且将被PEM编码。

按ID值选择签名节点子集

使用一个或多个转换来选择要签名的节点是一个非常灵活的机制。然而,转换不能通过ID值选择一组节点,尽管这很有用。要通过ID值选择一组要签名的节点,请使用InputResourceInfo类的$uri属性。

	$input = new InputResourceInfo(
		'http://www.xbrlquery.com/xades/hashes for nba.xml', // The source document
		ResourceInfo::url, // The source is a url
		__DIR__, // The location to save the signed document
		'hashes for nba with signature.xml' // The name of the file to save the signed document in
	);

	$input->uri = 'TheIdValue';

注意,此属性不能通过构造函数设置,因此必须创建InputResourceInfo的显式实例。

大文本节点

PHP中的XML处理由第三方库LibXML2处理。默认情况下,此库将“仅”读取文本节点的第一个10MB,因此这也是PHP中所有XML函数的默认行为。如果节点包含更多文本,PHP将发出警告。对于某些应用程序,这种行为可能可以接受,但对于签名应用程序来说则不合适。

在LibXML2的较新版本中,可以设置一个标志来更改默认行为,以便读取所有可用文本。从PHP版本5.3开始,此标志可以传递给加载XML的任何函数。可以在本项目中将InputResourceInfo类的$hugeFile属性设置为true来启用此标志。此属性的默认值为false,因此默认情况下使用默认的XML处理。

可以在InputResourceInfo类的构造函数中设置此属性,通过将最后一个参数设置为true或false。

安装方法

使用composer.phar进行安装。

php composer.phar require "lyquidity/xml-signer"

参考

以下链接包含两个主要的XAdES规范。

ETSI EN 319 132-1 V1.1.1 (2016-04)

ETSI EN 319 132-2 V1.1.1 (2016-04)

这些不是唯一的XAdES规范,但其他规范不再适用。以下链接是来自参与规范开发的人员Juan Carlos Cruellas的提问和回答。

规范历史

幸运的是,Juan Carlos Cruellas创建了一个非常有用的工具,可以对任何工具创建的签名进行合规性测试。

XAdES合规性检查器

XAdES依赖于其他规范。它直接依赖于XMLDSig。反过来,XMLDSig又依赖于其他XML规范。

XMLDSig

XPath Filter 2.0

XML规范化

XML专用规范化

XAdES需要时间戳以提供不可否认性。请求、响应及其内容由IETF RFC定义。

PKI时间戳协议 [rfc3161][rfc5816]更新

用于签名签名的证书必须检查其有效性。检查证书状态的一种机制是在线证书状态协议(OCSP)

OCSP [rfc6960]