AdES Collection:CAdES、XAdES、PAdES 和 ASiC 在 C++ 中实现 Windows 版






4.96/5 (12投票s)
符合标准的数字签名库
引言
这是一篇关于高级电子签名(Advanced Electronic Signatures)的文章。我们将讨论 **CAdES**、**XAdES** 和 **PAdES** 及其在数字签名中的应用。
**CAdES**(CMS 高级电子签名)是对加密消息语法(CMS)签名数据的一组扩展,使其适用于高级电子签名。
为了使数字签名在欧盟及其他地区有效,它必须符合 **CAdES** 的某个配置文件。这些配置文件定义了如何将证书、CLRs(证书撤销列表)、时间戳等添加到标准的 CMS 中。
对 CMS 的扩展可以作为认证属性(与消息的其他部分一起签名)或非认证属性(在签名后添加)。
针对专门形式也存在类似的扩展:**XAdES** 用于 XML 签名(XML DSIG 的扩展)、**PAdES** 用于 PDF 签名(PDF 的扩展)以及 **ASiC**,它是 BDOC 的一个扩展,定义了数字容器如何构造以包含与数字签名相关的所有数据。
这些形式中的每一种都有不同级别的信息包含量。
- 基本形式(B)。它使用以下内容扩展了基本的 CMS 格式:
- 四个强制签名属性
- 包含内容 MIME 类型(始终为 PKCS#7 数据)的属性
- 包含已签名消息哈希值的属性
- 包含用于签名的证书的属性
- 包含消息签名时间的属性
- 一些可选签名属性
- 包含签名策略的属性
- 包含承诺原因的属性
- 四个强制签名属性
- 时间戳形式(T),其中还包含一个非认证属性,该属性包含来自受信任时间戳签名提供商的计数器签名。
- C 形式,其中包含对链证书和 CRL 的引用。
- X 形式,将时间戳附加到 C 形式,分为 Type 1 或 Type 2。
- XL 形式,其中包含完整的证书链和 CRL。
- XL Type 1 或 XL Type 2,也包含时间戳。
- A 和 LT 形式,用于周期性时间戳。
目前,我们的库将创建 **CAdES-B**、**CAdES-T**、**CAdES-C**、**CAdES-X Type 2** 和 **CAdES-XL Type 2** 形式,并且能够验证到 **CAdES-T** 级别。将来可能会添加更多形式。
编码注意事项
首先,让我们看看新协议为何要添加这些属性。当没有签名属性时,内容的哈希值将使用证书的私钥进行加密,这就是数字签名。CMS 可以仅包含这些信息,甚至不包含用于签名的证书信息(尽管 Windows API 仍会包含证书)。这意味着一个简单的 CMS 可能只包含一个加密的哈希值。
当存在签名属性时,实际加密的是**这些属性**的哈希值。这就是为什么内容类型和内容哈希这两个签名属性是强制性的。CAdES 还要求我们包含用于签名的证书,以便可以立即验证签名,并且这也能解决公钥用于生成多个证书(例如,具有不同策略)的情况。
CAdES 还强制 CMS 包含一个时间戳(不是来自外部服务器,如 -T 形式),而是来自签名者的计算机。这表明了签名放置的时间,无论其是否被认为是可信的。
当 CAdES 与 PDF 文件一起使用时,此时间戳不包含在内,因为它已在 PDF 对象中放置。
最后,CAdES 允许指定签名策略。策略只是一个参数字符串。策略允许外部验证者根据签名提供商的定义了解签名原因和其他参数。
要构建标准的 CMS,需要使用低级消息函数:
CryptMsgOpenToEncode
CryptMsgUpdate
CryptMsgOpenToDecode
CryptMsgGetParam
CryptMsgControl
CryptEncodeObjectEx
要添加属性,我们的工作会变得简单或困难,这取决于 Windows 能为我们自动完成多少。内容类型和已签名消息的哈希值会自动添加,无需执行任何操作。
添加时间戳也很容易,因为 CryptEncodeObjectEx
可以自动为我们编码。
// Add the timestamp
FILETIME ft = { 0 };
SYSTEMTIME sT = { 0 };
GetSystemTime(&sT);
SystemTimeToFileTime(&sT, &ft);
char buff[1000] = { 0 };
DWORD buffsize = 1000;
CryptEncodeObjectEx(PKCS_7_ASN_ENCODING, szOID_RSA_signingTime,
(void*)&ft, 0, 0, buff, &buffsize);
char* bb = AddMem<char>(mem, buffsize);
memcpy(bb, buff, buffsize);
CRYPT_ATTR_BLOB* b0 = AddMem<CRYPT_ATTR_BLOB>(mem);
b0->cbData = buffsize;
b0->pbData = (BYTE*)bb;
ca[0].pszObjId = szOID_RSA_signingTime;
ca[0].cValue = 1;
ca[0].rgValue = b0;
我们的助手 AddMem<>
会在 vector<vector<char>>
中为我们希望在整个函数中可见的任何类型的数据分配内存。CryptEncodeObjectEx
支持 szOID_RSA_signingTime
,因此它可以自动为我们以 ASN.1 格式编码时间戳。
当我们尝试编码 SigningCertificateV2
时,我们的问题就开始了,而 CryptEncodeObjectEx
不支持此功能。
SigningCertificateV2 ::= SEQUENCE
{
certs SEQUENCE OF ESSCertIDv2,
policies SEQUENCE OF PolicyInformation OPTIONAL
}
为了解决这个问题,我们必须使用 ASN.1 编译器,例如 **ASN1C**(我也将其放在了存储库中)。但上述 ASN.1 定义是不够的,因为我们还需要定义 ESSCertIDv2
、PolicyInformation
和许多其他结构。幸运的是,我将所有内容都放在了 cades.asn1 文件中。
ASN.1 编译器将根据 ASN.1 定义生成一组 .C 和 .H 文件供我们使用,以便我们可以构建 DER 消息并将其放入我们的 CMS 中。
// Hash of the cert
vector<BYTE> dhash;
HASH hash(BCRYPT_SHA256_ALGORITHM);
hash.hash(c->pbCertEncoded, c->cbCertEncoded);
hash.get(dhash);
BYTE* hashbytes = AddMem<BYTE>(mem, dhash.size());
memcpy(hashbytes, dhash.data(), dhash.size());
SigningCertificateV2* v = AddMem<SigningCertificateV2>(mem,sizeof(SigningCertificateV2));
v->certs.list.size = 1;
v->certs.list.count = 1;
v->certs.list.array = AddMem<ESSCertIDv2*>(mem);
v->certs.list.array[0] = AddMem<ESSCertIDv2>(mem);
v->certs.list.array[0]->certHash.buf = hashbytes;
v->certs.list.array[0]->certHash.size = (DWORD)dhash.size();
// SHA-256 is the default
// Encode it as DER
vector<char> buff3;
auto ec2 = der_encode(&asn_DEF_SigningCertificateV2,
v, [](const void *buffer, size_t size, void *app_key) ->int
{
vector<char>* x = (vector<char>*)app_key;
auto es = x->size();
x->resize(x->size() + size);
memcpy(x->data() + es, buffer, size);
return 0;
}, (void*)&buff3);
char* ooodb = AddMem<char>(mem, buff3.size());
memcpy(ooodb, buff3.data(), buff3.size());
::CRYPT_ATTR_BLOB bd1 = { 0 };
bd1.cbData = (DWORD)buff3.size();
bd1.pbData = (BYTE*)ooodb;
ca[1].pszObjId = "1.2.840.113549.1.9.16.2.47";
ca[1].cValue = 1;
ca[1].rgValue = &bd1;
当我们想要添加特定的签名策略(OID 1.2.840.113549.1.9.16.2.15)时,也会出现同样棘手的问题。我们的助手还包括一个 **OID** 类,该类是通过使用 此项目的代码部分创建的。当我们添加另一个可选属性“承诺类型”时,也会发生相同的情况。
调用 CryptMsgUpdate
生成已签名消息后,我们现在可以添加任何非认证属性。CryptEncodeObjectEx
支持 PKCS_ATTRIBUTE
格式,其中可以包含时间戳。要获取时间戳,Windows 提供了 CryptRetrieveTimeStamp
函数。
要将其他证书添加到消息中,我们可以使用 ASN.1 编译器,但包含所有 X.509 类型声明会很麻烦。相反,我们只手动编码一个简单的 ASN.1 序列,然后直接从 PCCERT_CONTEXT
或 PCCRL_CONTEXT
结构获取编码的证书或 CRL。
使用库
我们的 Sign()
函数如下所示:
struct CERTANDCRL
{
PCCERT_CONTEXT cert;
std::vector<PCCRL_CONTEXT> Crls;
};
struct CERT
{
CERTANDCRL cert;
std::vector<CERTANDCRL> More;
};
HRESULT Sign(LEVEL lev,const char* data,DWORD sz,const std::vector<CERT>& Certificates,
SIGNPARAMETERS& Params,std::vector<char>& Signature);
这里
lev
是LEVEL::CMS,B,T,C,X,XL
data
和 **sz
** 是数据和大小- **
Certificates
** 包含用于签名消息的所有证书。一条消息可以由多个证书签名。每个条目还包含一个可选的 CRL 列表,以及要添加的额外证书及其 CRL。如果您指定的级别低于 CAdES-C,则不会添加 CRL 或额外证书。 - **
Params
** 是一个可选结构,用于定义:- 哈希算法(默认为 SHA-256)
- 消息是附加的还是分离的
- 可选签名策略
- 时间戳参数(URL、策略、Nonce、扩展)
- 可选承诺类型的 OID(1.2.840.113549.1.9.16.6.1 至 6)
- **
Signature
** 接收签名。
我们的 Verify
函数如下所示:
HRESULT AdES::Verify(const char* data, DWORD sz, LEVEL& lev,const char* omsg,
DWORD len,std::vector<char>* msg,std::vector<PCCERT_CONTEXT>* Certs,
std::vector<string>* Policies)
其中
data
和sz
是要验证的签名。lev
接收检测到的级别(目前最高为 T 级别)。omsg
和len
在签名分离的情况下包含原始消息。msg
(可选)在签名附加的情况下接收原始消息。Certs
(可选)接收一个数组,包含用于签名消息的证书。Policies
(可选)接收一个数组,包含检测到的签名策略(如果找到),每个签名一个。
我们的项目包含库和测试项目。它还包括 ASN.1 编译器的二进制副本和所需的包含文件。该库还提供了 XAdES-T 实现和 ASiC-S 实现。
在撰写本文时,ETSI cades 工具存在两个错误:
- 如果包含策略,则评估为错误(即使在其自己的测试示例中也显示为错误 :p)。
- 如果包含多个证书,则必须将时间戳应用于每个加密的哈希值。但是,ETSI 工具将每个时间戳应用于第一个签名的加密哈希值。
XAdES
CMS 可以签名任何类型的二进制数据,那么为什么需要 XML 特定方法呢?原因很简单,因为一个不了解加密技术的应用程序无法读取封装在 CMS 格式中的数据。XML 签名允许加密元素存在于 XML 文档中,同时应用程序仍能读取其数据。
XMLDSIG 是一个描述如何对 XML 文档进行签名的协议。它定义了三种签名方法:
- **分离式**:签名位于另一个位置。
- **包围式**:签名包含要签名的元素。
- **包容式**:要签名的元素包含作为子节点的签名。
分离式 XML 签名可以引用任何类型的数据,而不仅仅是 XML。
我的库支持以上所有签名模式。它使用了我更新的 **XML** 库来支持规范化。
当前限制
- 签名最高支持 **XAdES-XL**。
- 验证最高支持 XMLDSIG。
- XML 文件不支持 CDatas、命名空间或注释。
- 哈希支持 SHA-1 和 SHA-256/384。
XML 规范化
对于相同 XML 数据存在无限数量的有效表示,例如:
<foo /> equals to <foo></foo> <foo val="yo" a= "b" /> equals to <foo a="b" val="yo" />
因此,为了确保哈希值不会变化,我们必须将 XML 规范化,即,将相同数据与相同 XML 进行一对一的映射。这个过程非常复杂,下面是一些规则(但不是全部):
DocType
标头被移除。- 元素不能使用
/>
关闭。 - 属性按字母顺序排序,但
xmlns: namespace
属性优先。 - 特定的空格被修剪。
Namespace
声明会传播到子元素(这对我在找出哈希值不匹配的原因时是个巨大的麻烦)。
这里有一个很好的实用指南:**这里**。为了简单起见,我们的库不支持 CDatas、注释或命名空间。官方文档位于**这里**。
如果您使用分离式 XML 签名来签名 XML 文件,那么该文件不需要规范化,因为分离式 XML 签名可以处理纯二进制数据。然而,如果考虑分离式签名,那么 XML 签名还有存在的必要吗?
XMLDSIG
签名过程如下:
- 对要签名的元素进行规范化(如果未使用分离式签名)。
- 对元素进行哈希。
- 创建一个
SignedInfo
元素,其中包含要签名的一切(转换、消息哈希、算法)。 - 对上述元素进行签名。
- 创建一个包含所有信息的元素。
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>...</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue xmlns:ds="http://www.w3.org/2000/09/xmldsig#">...</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>...</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
当引用 URI 为空时,表示包容式签名。对于分离式签名,它包含数据的 URI。请注意,签名值不是完整的 PKCS#7 消息,而只是加密的哈希值。因此,我们不能使用 CryptSignMessage
来构建它,而是使用低级消息函数(CryptMsgOpenToEncode, CryptMsgUpdate
等)。
SignedInfo
元素可以包含任意数量的引用,允许我们在一次操作中签名多个数据部分。
Windows 提供了 CryptXML API 来创建 XMLDSIG,但由于它对 XAdES 无用,因此我们在这里不使用它。
XAdES 在 XMLDSIG 的基础上,遵循以下规则:
- 所有元素都带有
ds:
命名空间。 SignedInfo
元素包含对一组SignedProperties
的引用。SignedInfo
还包含对证书的引用。这允许我们在一次操作中对消息、签名属性和证书进行签名。- 也会添加未签名属性,这与 CAdES 类似。使用 XAdES-T 时,整个
ds:SignatureValue
元素都会被打上时间戳,而不仅仅是签名。
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<doc>
<e2>hohoho</e2>
<e3 id="elem3"/>
<e6 a="http://www.w3.org">
<e7 b="http://www.ietf.org">
<e8 c="">
<e9 d="http://www.ietf.org"/>
</e8>
</e7>
</e6>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
Id="xmldsig-345B805C-ED11-469F-920A-AA82A6E02876">
<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:CanonicalizationMethod
Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="">
<ds:Transforms>
<ds:Transform Algorithm=
"http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>TOSPv1v7yuYBhD56IgG5Wp8+3pkWmJEUO+QecU5/A3g=</ds:DigestValue>
</ds:Reference>
<ds:Reference Type="http://uri.etsi.org/01903#SignedProperties"
URI="#xmldsig-345B805C-ED11-469F-920A-AA82A6E02876-sigprops">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>0o9tulf/mGgQCINlIJ/fcZHc6DziU6XH9x8iXiqMat8=</ds:DigestValue>
</ds:Reference>
<ds:Reference URI="#xmldsig-345B805C-ED11-469F-920A-AA82A6E02876-keyinfo">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>hflupFvhNJWQir2grNd7QK8RWtm0m2pAE8QNdRd8jIQ=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
Id="xmldsig-345B805C-ED11-469F-920A-AA82A6E02876-sigvalue">
lknnInZl2Xxp1ZeMLM+qUj/vyoyMvkxFoOB0EcqE0z14eEW1xmpLqWT/GcJRTqceOrFLZ98C6JXtIh1
mdhF45Avo3ZC98I3ZU/jdwZ3nOlKRa0NB8+sSQADPD3CKwLIgJh07Nr3xlHenc/yqn1whLTVU7aC1tc
MYXYQhyeux2DJ7+qyDTKgqKIMoH4NMc+JPMp3qwu0dxqBlgZz0g43kEpsgjrakwtqRp4VqFnmHQOsIr
6XEnBNPXk8tTV+5yshHkSF1ELRHV2feSr7RvNHA5ZtRFSs4jCd24gyVT/P5YR8MIaN3Ir4ictp9SnCX
a+/0+g6BfsKP1ykOIk5dQzOy5w==</ds:SignatureValue>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
Id="xmldsig-345B805C-ED11-469F-920A-AA82A6E02876-keyinfo">
<ds:X509Data>
<ds:X509Certificate>...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<ds:Object>
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"
xmlns:xades141="http://uri.etsi.org/01903/v1.4.1#"
Target="#xmldsig-345B805C-ED11-469F-920A-AA82A6E02876">
<xades:SignedProperties xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"
xmlns:xades141="http://uri.etsi.org/01903/v1.4.1#"
Id="xmldsig-345B805C-ED11-469F-920A-AA82A6E02876-sigprops">
<xades:SignedSignatureProperties>
<xades:SigningTime>2018-09-09T10:12:24Z</xades:SigningTime>
<xades:SigningCertificateV2>
<xades:Cert>
<xades:CertDigest>
<ds:DigestMethod Algorithm=
"http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>//0HypHbOffTJiry5S2iLFrxs6D1iPRmKZ4ShysSwxE=
</ds:DigestValue>
</xades:CertDigest>
<xades:IssuerSerialV2>
<ds:X509SerialNumber>18446744073709551615
</ds:X509SerialNumber>
</xades:IssuerSerialV2>
</xades:Cert>
</xades:SigningCertificateV2>
<xades:SignaturePolicyIdentifier>
<xades:SignaturePolicyId>
<xades:SigPolicyId>
<xades:Identifier>1.3.6.1.5.5.7.48.1</xades:Identifier>
</xades:SigPolicyId>
<xades:SigPolicyHash>
<ds:DigestMethod Algorithm=
"http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>i8brzJOzs5A+2MFR/jxNzm+LaGGBQ7pNHV2uImgbY68=
</ds:DigestValue>
</xades:SigPolicyHash>
</xades:SignaturePolicyId>
</xades:SignaturePolicyIdentifier>
</xades:SignedSignatureProperties>
<xades:SignedDataObjectProperties>
<xades:CommitmentTypeIndication>
<xades:CommitmentTypeId>
<xades:Identifier>http://uri.etsi.org/01903/v1.2.2#ProofOfOrigin
</xades:Identifier>
<xades:Description>Indicates that the signer recognizes
to have created, approved and sent the signed data object
</xades:Description>
</xades:CommitmentTypeId>
<xades:AllSignedDataObjects/>
</xades:CommitmentTypeIndication>
</xades:SignedDataObjectProperties>
</xades:SignedProperties>
<xades:UnsignedProperties>
<xades:UnsignedSignatureProperties>
<xades:SignatureTimeStamp>
<ds:CanonicalizationMethod Algorithm=
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<xades:EncapsulatedTimeStamp>...</xades:EncapsulatedTimeStamp>
</xades:SignatureTimeStamp>
</xades:UnsignedSignatureProperties>
</xades:UnsignedProperties>
</xades:QualifyingProperties>
</ds:Object>
</ds:Signature>
</doc>
这是一个包含签名属性和时间戳的有效的 XAdES-T
消息。
在 UnsignedAttributes
元素中,可以添加更多级别(例如,C 级别)。
Using the Code
struct FILEREF
{
const char* data = 0; // pointer to data
DWORD sz = 0; // size, or 0 if null terminated XML
const char* ref = 0;
std::string mime = "application/octet-stream";
};
HRESULT XMLSign(LEVEL lev, std::vector<FILEREF>& data,const std::vector<CERT>& Certificates,
SIGNPARAMETERS& Params, std::vector<char>& Signature);
其中
lev
是LEVEL
枚举中的一个值(XMLDSIG
、B,T
)。如果使用 XMLDSIG,则 XML 文件也可以使用 Windows CryptXML API 进行验证。data
包含要签名的数据。向量中的每个结构都包含:- 指向字节的指针。如果这是 XML 数据且签名模式为 ENVELOPED,则这是一个 null 终止的
string
,第二个参数(DWORD
)为零。在这种情况下,数据将被视为规范化 XML 进行签名,并以包容式模式返回。 - 如果模式为分离式,则这两个参数包含指向数据的指针和大小,该数据将作为原始数据进行签名。
- 第三个参数是放置在签名中的 URI 引用。如果是包容式签名且数据是 XML,则此参数可以为零。
- 第四个参数是内容的 MIME 类型,默认为
application/octet-stream
。
- 指向字节的指针。如果这是 XML 数据且签名模式为 ENVELOPED,则这是一个 null 终止的
Certificates
包含用于签名的证书。如果模式为 ENVELOPED,则只允许一个证书。- **
Params
** 是一个结构,用于定义:- 来自
ATTACHTYPE
枚举的附加类型(DETACHED
、ENVELOPING
或ENVELOPED
)。 - 哈希算法(默认为 SHA-256,也可指定 SHA-1)。
- 签名策略。
- 时间戳参数(
URL
、Policy
、Nonce
、Extensions
)。 - 承诺类型的 OID(1.2.840.113549.1.9.16.6.1 至 6)。
- 来自
- **
Signature
** 接收签名。
如果模式为 ENVELOPED
,则返回的签名是单个 XMLElement
,其中包含原始数据和包容式签名。
如果模式为 ENVELOPING
且有一个证书,则返回单个 ds:Signature
元素,其中包含签名和一个 ds:Object
元素,该元素包含原始数据。如果存在多个证书,则在 <root>
根元素中返回多个 ds:Signature
元素。
如果模式为 DETACHED
且有一个证书,则返回单个 ds:Signature
元素。如果存在多个证书,则在 <root>
根元素中返回多个 ds:Signature
元素。
对于包容式签名,不支持多次签名,因为当您向已包含 ds:Signature
元素的 XML 元素添加签名时,第一个签名将失效(内容的哈希值会改变)。
我的库生成的 XAdES 文件在 ETSI 兼容性检查器工具上验证为 100% 正确。:)
PAdES
与 CAdES 或 XAdES 不同,**PAdES** 不定义任何新的加密协议,而是描述如何签名 PDF 文件的元信息。在 PDF 文件中,您可以将 CAdES 格式或 XAdES 格式作为分离式签名包含在 PDF 中。签名级别与我们到目前为止看到的(B、T 等)相似,但有一些例外。因此,目前,我们的库将能够创建 B-B、B-T 和 B-LT 签名。
以下是一个简单的 Hello World
PDF 文件:
%PDF-1.7
1 0 obj % entry point
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<<
/Type /Pages
/MediaBox [ 0 0 200 200 ]
/Count 1
/Kids [ 3 0 R ]
>>
endobj
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/Resources <<
/Font <<
/F1 4 0 R
>>
>>
/Contents 5 0 R
>>
endobj
4 0 obj
<<
/Type /Font
/Subtype /Type1
/BaseFont /Times-Roman
>>
endobj
5 0 obj % page content
<<
/Length 44
>>
stream
BT
70 50 TD
/F1 12 Tf
(Hello, world!) Tj
ET
endstream
endobj
xref
0 6
0000000000 65535 f
0000000010 00000 n
0000000079 00000 n
0000000173 00000 n
0000000301 00000 n
0000000380 00000 n
trailer
<<
/Size 6
/Root 1 0 R
>>
startxref
492
%%EOF
我的库包含一个小型且非常实验性的 PDF 解析器,支持许多简单的 PDF 文件。大部分代码已与 jSignPDF
的结果进行了比较。如果您无法加载特定的 PDF 文件,请告诉我。
PDF 签名
PDF 签名具有以下属性:
- 它总是分离式的。
- 它被放置在 PDF 文件的一个特殊对象中。PDF 文件首先创建,其空间足以容纳签名,最初用零填充。
22 0 obj
<</Contents <000000 .... 00>
/Type/Sig/SubFilter/ETSI.CAdES.detached/M(D:20181006080704+00'00')
/ByteRange [0 64944 124946 1312]/Filter/Adobe.PPKLite>>
byterange
参数指定了 PDF 文件中被签名的一部分。理论上,您可以签名任何部分,但 Adobe Reader 会拒绝任何签名,除非整个 PDF 文件都被签名。因此,字节范围是从“<
”字符(在 00
s 之前)的开始,到“>
”字符(在 00
s 之后)到文件末尾。这是将被哈希的部分。
如果您放置标准 CMS,则标记为 adbe.pkcs7.detached
。如果您放置 CAdES 级别签名,则标记为 ETSI.CAdES.detached
。
普通 CAdES 签名与放置在 PDF 文件中的签名之间的主要区别在于,签名**不得**包含当前时钟的时间戳,因为此信息已通过 /M
参数放置在 PDF 文件中。因此,当 SIGNPARAMS.PAdES
= **true**
时,不会将 OID szOID_RSA_signingTime
添加到签名中。
PDF 重建
要添加签名,必须向 PDF 添加一个新修订版。因此,我的库:
- 创建一个新的根对象、一个指向创建的签名的指针以及一个新的
xref
表,该表引用旧的修订版。 - 替换 info、contents、pages 和 kids 对象,使其包含容纳 PDF 签名所需的结构,同时仍指向旧数据。
- 使用 CAdES 进行签名。
使用我的库对上述 hello world
PDF 文件后,添加的修订版如下所示:
42 0 obj
<</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(Signature1)/
V 40 0 R/P 7 0 R/AP<</N 41 0 R>>>>
endobj
40 0 obj
... signature
endobj
43 0 obj
<</BaseFont/Helvetica/Type/Font/Subtype/Type1/Encoding/WinAnsiEncoding/Name/Helv>>
endobj
44 0 obj
<</BaseFont/ZapfDingbats/Type/Font/Subtype/Type1/Name/ZaDb>>
endobj
41 0 obj
<</Type/XObject/Resources<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]>>/
Subtype/Form/BBox[0 0 0 0]/Matrix [1 0 0 1 0 0]/Length 8/FormType 1/Filter/FlateDecode>>stream
x endstream
endobj
7 0 obj
<</Type/Page/MediaBox[0 0 595 842]/Rotate 0/Parent 3 0 R/Resources<</ProcSet[/PDF/Text]/
ExtGState 22 0 R/Font 23 0 R>>/Contents 8 0 R/Annots[42 0 R]>>
endobj
3 0 obj
<</Type/Pages/Kids[7 0 R
24 0 R
28 0 R
32 0 R]/Count 4/Rotate 0>>
endobj
1 0 obj
<</Type/Catalog/AcroForm<</Fields[ 42 0 R]/DR<</Font<</Helv 43 0 R/ZaDb 44 0 R>>>>/
DA(/Helv 0 Tf 0 g )/SigFlags 3>>/Pages 3 0 R>>
endobj
2 0 obj
<</Producer(AdES Tools https://www.turboirc.com)/ModDate(D:20181009160112+00'00')>>
endobj
xref
0 4
0000000000 65535 f
0000166315 00000 n
0000166460 00000 n
0000166230 00000 n
7 1
0000166062 00000 n
40 5
0000105528 00000 n
0000165857 00000 n
0000105400 00000 n
0000165681 00000 n
0000165780 00000 n
trailer
<</Root 1 0 R/Prev 104519/Info 2 0 R>>
startxref
166559
%%EOF
XRef 流
目前我不会深入探讨,因为这篇文章是关于 PAdES 而不是 PDF 的。然而,一些 PDF 文件将它们的 XRef
存储为普通文本条目,而不是一个包含使用 zlib
压缩的 xref
表的对象。在这种情况下,生成的 XRef
是一个对象。
465 0 obj <</Type/XRef/Index [0 1 358 2 362 1 364 1 460 6 ]/W[1 4 2]/Root 364 0 R/Prev 116/Info 362 0 R/ Size 472/ID[<C570CC80F0638E5337E581345C7449FB><17DEE6C01B14632A778C3FC1D5297D97>]/Length 77/ Filter/FlateDecode>>stream .... endstream endobj
此 stream
的格式超出了本文的范围,但它包含与上面纯文本 XRef
相同的数据。
LT 类型
PDF 文件不能包含未签名部分。因此,将 XL 信息(证书和 CRL)作为非认证属性放入 CMS 中并不能自动使我们的签名兼容 XL。我们必须创建一个特殊的字典,称为 DSS,它包含对证书和 CRL 的间接引用,并在签名之前放置,这样它也会被签名。
Using the Code
HRESULT PDFSign(LEVEL lev,const char* data,DWORD sz,const std::vector<CERT>
& Certificates, SIGNPARAMETERS& Params,std::vector<char>& Signature);
HRESULTERROR PDFVerify(const char* d, DWORD sz, std::vector<PDFVERIFY>& VerifyX);
此函数调用中的参数与 **CAdES** 文章中的 Sign()
函数相同,但您必须在 SIGNPARAMETERS
结构中传递 PAdES = true
,并且只能传递一个证书。如果您传递更多证书,PAdES 也能成功工作,但 Adobe Reader 无法读取一个元素中的多个证书(您必须重新签名 PDF)。目前,该库支持到 XL 级别。
- 传递
LEVEL::CMS
-> 正常签名(旧方法)。 - 传递
LEVEL::B
-> 签名 PAdES B-B。 - 传递
LEVEL::T
-> 签名 PAdES B-T。 - 传递
LEVEL::XL
-> 签名 PAdES B-LT。
输出完全符合 ETSI 验证工具。
我的库可以签名大多数 PDF 文件。密码保护的 PDF 文件无法签名。签名已签名的 PDF 文件可能会导致不兼容。如果您遇到问题,请告知我。
由于这是 CAdES,它支持多个证书。但是,Adobe Reader 只会显示集合中最后一个找到的证书的信息。虽然 ETSI 工具会成功验证此类 PDF,但 Acrobat 只支持递归签名:您签名第一个 PDF 文件,创建一个新的已签名 PDF,然后您将此新 PDF 签名到另一个新的 PDF。这意味着新签名也将签名整个之前的签名。
ASiC
ASiC 文件是包含原始数据和数字签名的 ZIP 文件。
ASiC-S
容器的简单版本,称为 ASiC-S,可以包含一个文档。这是一个 ZIP 文件,包含以下内容:
- 可选的 mimetype 文件,包含容器的 MIME 类型,application/vnd.etsi.asic-s+zip。
- 要签名的文档。它可以是任何文件,包括另一个 ASiC。
- 一个 META-INF 文件夹,其中包含:
- 要么是 signatures.p7b(对文档文件的分离式 CAdES 签名),或者
- 是 signatures.xml(包含文档的分离式 XAdES 签名)。
因为签名总是分离式的,如果被签名的文档本身是 XML 文件,则无需对其进行规范化。
ASiC-E
容器的扩展版本,称为 ASiC-E,可以包含任意数量的文档。这是一个 ZIP 文件,包含以下内容:
- 可选的 mimetype 文件,包含容器的 MIME 类型,application/vnd.etsi.asic-e+zip。
- 要签名的文档。也可以将它们放在目录中。
- META-INF 文件夹内的 ASiCManifest.xml 文件。
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<ASiCManifest xmlns:ns2="http://www.w3.org/2000/09/xmldsig#"
xmlns="http://uri.etsi.org/02918/v1.2.1#">
<SigReference MimeType="application/x-pkcs7-signature"
URI="META-INF/signature.p7s"/>
<DataObjectReference URI="file1.txt">
<ns2:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ns2:DigestValue>...</ns2:DigestValue>
</DataObjectReference>
<DataObjectReference URI="test/hello2.xml">
<ns2:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ns2:DigestValue>...</ns2:DigestValue>
</DataObjectReference>
</ASiCManifest>
此文件包含对容器内所有文件的引用(在上例中,引用了 file1.txt 和 test 文件夹内的 hello2.xml)。
- signatures.xml、signatures1.xml、signatures2.xml 等,或 signatures.p7s、signatures1.p7s,它们引用所有文件或文件的一部分并对其进行签名。也可以有其他清单文件(ASiCManifest1.xml 等),它们引用一组不同的文件。
代码
HRESULT ASiC(ALEVEL alev,ATYPE typ,
LEVEL lev, std::vector<std::tuple<const BYTE*,DWORD,const char*>>& data,
std::vector<CERT>& Certificates, SIGNPARAMETERS& Params,
std::vector<char>& fndata);
其中
alev
是容器模式,可以是S
或E
。typ
是签名模式,可以是CAdES
或XAdES
。- 其余参数将传递给 **CAdES** 和 **XAdES** 函数,请查看相关文章了解完整描述。
fndata
接收容器 zip 数据。
MIME
ASiC 很有趣,但许多现有应用程序都支持 MIME。使用我的 MIME 库,您现在可以将多个文件放入一个 MIME 容器中,该容器现在使用 CAdES 进行签名,并且使用我的一项实验性函数,可以使用 XAdES 进行签名。
HTML
为了进一步发展,我创建了 HTML 中的包容式签名。HTML 不易于规范化,因此我将签名注入到 <html>
和下一个标签之间。文件被解析为二进制,结果是 XAdES-XL 签名。浏览器将来是否会喜欢我的实现——谁知道呢?
可移植可执行文件(实验性支持)
由于 Windows PE 可执行文件可以使用 PKCS#7 进行数字签名,我们可以使用 CAdES 来签名它。由于 signtool.exe 现在已无用,我们必须自己解析 PE 文件。
- 使用我的 PE 类解析文件。
- 对要哈希的文件部分进行哈希。
- 从开始到校验和。
- 从校验和之后到证书条目表之前。
- 从证书条目表之后到标头末尾。
- 所有节,排序后。
- 节之后可能出现的额外数据。
- 使用 CAdES 对该部分进行签名。
- 更新证书条目表。
- 将分离式签名追加到文件末尾。
HRESULT PESign(LEVEL levx, const char* d, DWORD sz, const std::vector<CERT>& Certificates,
SIGNPARAMETERS& Params, std::vector<char>& res);
最后的几句话...
我猜如果您很快将在欧盟生活,您就会需要我。但在此之前...
祝您好运!.
致谢
- ETSI - http://signatures-conformance-checker.etsi.org/pub/index.shtml
- RFC 5126 - https://tools.ietf.org/html/rfc5126
- SecureBlackBox - https://www.secureblackbox.com
- OID 库 - http://www.rtner.de/software/oid.html
- ASN.1 编译器 - https://github.com/vlm/asn1c
- XolidoSign - https://en.xolido.com/lang/xolidosign/modulo/xolidosign-desktop/descargar/
- https://www.w3.org/TR/2001/REC-xml-c14n-20010315 (规范化规则)
- https://www.di-mgt.com.au/xmldsig.html (协议描述)
- https://www.w3.org/TR/XAdES/ (XAdES 描述)
- jSignPDF - http://jsignpdf.sourceforge.net/
历史
- 2019 年 7 月 22 日:添加了 PAdES 验证、XAdES-B 验证、文章合并、EXE 相关内容。
- 2018 年 10 月 15 日:改进了 PDF 解析器,添加了 PAdES B-T、PAdES B-LT。
- 2018 年 10 月 14 日:更改了参数。
- 2018 年 10 月 9 日:增强了 PDF 解析器,实现了 100% ETSI 合规性。
- 2018 年 10 月 3 日:PAdES 相关工作,ETSI 错误修复。
- 2018 年 9 月 23 日:添加了多证书支持。
- 2018 年 9 月 22 日:添加了包围式模式。
- 2018 年 9 月 15 日:添加了 CAdES-XL Type 2。
- 2018 年 9 月 14 日:参数更新。
- 2018 年 9 月 14 日:添加了 CAdES-C 和 CAdES-X Type 2。
- 2018 年 9 月 12 日:规范化信息,ETSI 工具。
- 2018 年 9 月 1 日:添加了承诺类型。
- 2018 年 8 月 31 日:添加了关于 CMS 的技术信息。
- 2018 年 8 月 28 日:修复了错别字。
- 2018 年 8 月 19 日:首次发布。