lyquidity/xml-signer

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

1.12 2023-01-10 10:03 UTC

This package is auto-updated.

Last update: 2024-09-23 11:11:36 UTC


README

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

XML-DSig

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

  • <Reference>节点引用XAdES的<QualifingProperties>需要支持类型属性
  • 在指定<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)证书撤销列表(CRL) 来支持签名不可否认性。
因此,除了 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类的参数实际上是类,例如,City、StreetAddress、Postcode等。如果可能,构造函数将转换字符串参数为适当的类实例。

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

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

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

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

策略

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

XAdES的目的是扩展核心XML-DSig规范,以便签名的文档能够在法庭案件中经得起审查。这样,XML签名就可以具有与手写签名相同的法律地位。然而,对于一个司法管辖区来说,构成签名充分证据的内容可能不足以满足另一个司法管辖区。为了帮助所有在该司法管辖区运营的各方了解要求,每个司法管辖区都可以发布一个策略(另一个签名的XML文档)来描述他们的要求。

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

一个示例策略是由荷兰标准商业报告(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();
}

然后使用这些 CertificateResourceInfoKeyResourceInfo 类的实例在您的代码中使用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]