Provides signing and verification of XML documents for PHP focussing on support for signatures that are compliant with the XAdES specification.
This project builds on xmlseclibs by Rob Richards which provides XML-DSig support. However, this base project has been modified to accommodate requirements of XAdES. For example but not limited to:
Because these changes are important, the relevant source from xmlseclibs has been incorporated into this project and the namespaces changed to prevent conflict with the original.
There are two aspects to the XAdES support. One is a set of classes that cover all the element types defined in the XAdES specificiations. This code is analogous to the XAdESJS classes and is used in a similar way. The class constructors accept appropriate parameters and are able to validate them. They are able to work together to create a hierarchy of nodes and the node hierarchy can be used to both load existing and generate new XML required for a valid signature. There are also features to support navigating and manipulating the node hierarchy. Using these classes a person who knows XAdES is able to create arbitrary XAdES signatures.
However it is likely users will not have an encyclopaedic knowledge of XAdES. So the other aspect is to allow less expert users provide a minimum amount of information and use functions to generate XML signatures, request and process time stamps and add counter signatures. At the moment the handy functions do not cover the whole XAdES specification although they do allow use of the key XAdES features such as policy selection, commitment type indication, user roles, timestamps and counter signatures. Signatures can be embedded in a source document or they can be saved as detached files. A function is also available to validate existing signatures.
Long-term (LT) and Long-term archive (LTA) are profiles defined in the XAdES specificiations that are intended to enable someone to verify a signature long after the signing certificate has expired. Table 2 in section 6.3 of the defines the elements to be included within the signature for a signature to be considered long term.
LT support comes from the addition of <CertificateValues>,<RevocationValues> and <TimeStampValidationData> elements (see sections 5.4.1, 5.4.2 5.5.1 of the XAdES specification. LTA support comes from the addition of the <ArchiveTimeStamp> elements (see section 5.5.2).
Using the static XAdES::archiveTimestamp() the code will add these elements and populate them with relevant data. Note that this support is experimental and at the moment will not accommodate all LTA scenarios. For example, at the moment it will not handle the case of an archive timestamp being added to a signature that already includes an archive timestamp. It is also unlikely to generate a valid signature if the signature includes a counter signature.
LT and LTA support is also more complex and so more likely to fail. Unlike a regular signature which can be generated from only information available locally, LT signatures must retrieve certificates all the way to the trust anchor (root certificate authority) of the signing certificate. It must also retrieve revocation information for the signing signature and then retrieve certificates used to sign the revocation information. This complexity also means my comprehension of the specificiation may be incorrect or, at least, not precise since the number of possible scenarios has increased significantly.
Add LTA support to a signature using the static function below. Note that LTA information is added to an existing signature so the existing signature source is selected using a SignedDocumentResourceInfo instance:
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 has been able to use the EU commission XAdES demo site to confirm independently the signature created reaches the LTA level.
There are 140+ different elements defined by XAdES. Although support exists to create every one, the level of testing for each is not the same. This is particularly true of those in the <UnsignedProperties> area.
Another limitation is that the only mechanism available to sign a signature is by using X509 certificates in the context of the public key infrastructure (PKI). These are the same type of certificates used by web sites and browers though usually with different key use attributes.
Attribute certificates are not supported so cannot be used within <SignerRoleV2> instances.
The final limitation is the lack of support for manifests. These are references to other elements within an XML document within which elements involved in the signature can be placed. This signer will not use any alternative place and is unable to verify a signature that uses such alternative locations.
To check the generated XML conforms to the specification, they are verified using the XAdES Conformance Checker (XAdESCC) created by Juan Carlos Cruellas. This tool has been created using Java and is provided via ETSI which is the ESO responsible for the XAdES specification. XAdESCC checks all aspects of a signature including the computed digests. Using this tool helps to ensure a signature produced by one tool can be verified by another tool and vice-versa. A C# program is also used to confirm generated signatures can be verified by the XML-DSig support included within the System.Security.Cryptography.Xml namespace.
XAdES uses timestamps,
Online Certificate Status Protocol (OCSP) and
certificate revocation lists (CRLs) to support signature non-repudiation.
So in addition to XMLDSig, this OCSP requester project is used.
It provides OCSP, timestamp protocol (TSP) and CRL request support. It also provides classes to read strings
encoded using abstract syntax notation (ASN.1). ASN.1 is used to encode OCSP and TSP requests and responses.
It is also used to encode PKI entities such as X509 certificates, keys, stores and so on so ASN.1 is an important standard to support.
The OCSP requester relies on the following PHP extensions: php_curl, php_gmp, php_mbstring and php_openssl.
Here's how to verify a document with a signature. The document here is on the end of a Url but it could be, is likely to be, a local file.
XAdES::verifyDocument( 'http://www.xbrlquery.com/xades/hashes-signed.xml' );
Here's an example of signing an Xml document with a robust XAdES signature. The url is to a real document but to be able to execute this example, it will be necessary to provide your own certificate and corresponding private key. The output of this function when used with my test certificate and and private key can be accessed here.
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/'
)
);
A goal of the design of the classes is to try to minimize input. There are several cases in this example. The parameters to the SignatureProductionPlaceV2 class are really classes, for example, City, StreetAddress, Postcode, etc.. Constructors will convert a string paramter to an appropriate class instance if possible.
Another case in the example is new SignerRoleV2('CEO'). The full format of this is would be:
new SignerRoleV2(
new ClaimedEoles(
array(
new ClaimedRole('CEO')
)
)
)
Obviously the longer form will need to be used if and when there is more than one claimed role.
If the file references are just paths to a file then just the file path can be used. In this example, this will apply to the certificate and private key reference. This simplification cannot be used for the reference to the document to be signed because the source is a URL. As the generated signature cannot be saved into the same location in this case, an alternative location has to be supplied explicitly by instantiating the InputResourceInfo class and assigning values to the saveLocation and saveFilename properties. This will also need to be done if the source is a local file that is read-only.
In the examples above, the XAdES class is used directly. However, this is unlikely to be too helpful as XAdES signatures are designed to be used with policies. That is, verifying a XAdES signature is not just a case of testing the XML-DSig
The purpose of XAdES is to extend the core XML-DSig specification so that signed documents are able to withstand scrutiny in a court case. In this way, XML signatures can have the same legal status as a written signature. However, what constitutes adequate evidence of a signature for one jurisdiction may not be adequate for another. To help make all parties operating within a juridiction aware of the requirements, each jurisdiction is able to publish a policy (another signed XML document) to describe their requirements.
There's no single definition of a jurisdiction. An obvious jurisdiction might be a country or a govenment department. However, it might also be a collection of commerical entities that reach a mutual agreement regarding the types of signatures that will be accepted.
An example policy is the one created by the Netherlands Standard Business Reporting (SBR) which is the department of the Netherlands government that receives and checks returns produced by commerical entities operating in the Netherlands. This department accepts electronic returns in the form of XBRL documents. These documents can be signed by auditors. The information to be included in the signature, the key length to be used by certificates and so on is defined in this XML policy document.
It is important then, when validating a signature the validation checks not only the digest created by applying the certificate, but also that the components included in the signature XML conform with the relevant policy. It is better, then, that a jurisdiction specific class is created, one that extends the class XAdES.
Included with the project is such a class for the SBR jurisdiction policy. It is called XAdES_SBR. It overrides functions on XAdES both to add extra, policy relevant, information to the signature, and test that appropriate information is included in the signature when it is validated.
In the example above three types of resource are used. Each is an instance type specific to the type of resource being required for a parameter. The resources are used to convey appropriate information to the signer and this information can be in different forms. For example, the resource for the document to be signed might be a file path, a url, a string of XML or a DOMDocument instance. Equally the resource for a signature or private key might be file path, a PEM formatted string, a base 64 encoded string or a binary representation of the certificate or key.
As well as having resource specific properties, each has a 'type' property that provides a way to describe the nature of the resource. The complete list of possible
Flag | A flag indicating the resource is a... |
---|---|
file | Path to a local file. If no explicit type is provided this is the default type |
string | String |
binary | Binary string |
der | DER encoded binary string |
xmlDocument | A DOMDocument instance |
pem | PEM encoded string |
url | URL to the document source |
base64 | Base64 encoded |
Where appropriate these flags can be or'd together. Doing this means more information can be provided to the signer. For example a certificate provided in a string might be in a PEM format, a Base 64 format or as DER bytes. If a certificate is provided in a string in a PEM format the type value will be:
ResourceInfo::string | ResourceInfo::pem
In addition to a main signature, it may be necessary to add one or more counter signatures. For example, the main signature might be on behalf of the cheif financial officer which is then counter signed by the legal counsel and/or an audit partner. To make it easy to add a counter signature there is a static method:
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') )
)
);
Note the use of the SignedDocumentResourceInfo class to provide the signer with information about the document to which a counter-signature is being added. This class exposes an additional property that allows a caller to define an @id for the signature element. This is required if a timestamp is to be added to the counter-signature.
A timestamp can be added to a signature while the original signature is being created. It also possible to add timestamps to an existing document which can be useful if a counter-signature also needs a timestamp. To make adding a timestamp easy there is a static method:
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)
);
The id parameter to the InputResourceInfo instance is essential as it identifies the signature to which the timestamp should be added. This may be the main signature but it may also be a counter-signature. In fact it can also identify any element that is used as a reference within a signature.
A certificate and private key may be stored in a PKCS 12 container file (often with a .p12 extension) that is protected by a passphrase. The signing code has no explicit support for resources stored in a PKCS 12 file but using resources from this kind of store is straight forward.
First, access the file contents:
if ( ! openssl_pkcs12_read( file_get_contents( '/path_to_pkcs12_file/my.p12' ), $store, '<passphrase>' ) )
{
echo "Oops unable to open the file\n";
die();
}
Then use these instantiations of the CertificateResourceInfo and KeyResourceInfo classes in your code to use the respective contents of the PKCS12 file:
new CertificateResourceInfo( $store['cert'], ResourceInfo::string | ResourceInfo::binary | ResourceInfo::pem ),
new KeyResourceInfo( $store['pkey'], ResourceInfo::string() | ResourceInfo::binary | ResourceInfo::pem ),
The resource for each is the appropriate elements of the array returned by openssl_pkcs12_read function. The flags used let the processor know the contents will be a text string or a binary string and will be PEM encoded.
Using one or more Transforms to select the nodes to be signed is a very flexible mechanism. However a Transform cannot select a group of nodes by an Id value though this is a useful thing to be able to do. To select a group of nodes to sign by an Id value use the $uri propery of the InputResourceInfo class:
$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';
Note this property cannot be set via the constructor so an explicit instance of InputResourceInfo must be created.
XML handling in PHP is handled by the third party library LibXML2. By default this library will 'only' read the first 10MB of a text node so this is also the default behaviour of all XML functions in PHP. If the node contains more text, PHP will emit a warning. For some applications this behaviour may be OK but for a signing application it is not appropriate.
In recent versions of LibXML2 a flag can be set to change the default behaviour so that all available text is read. Since PHP version 5.3 this flag can be passed to any function that loads XML. This flag can be enabled in this project by setting the boolean $hugeFile propery of the InputResourceInfo class to true. The default default value of this proprerty is false so default XML handling is used by default.
This property can be set in the constructor of the InputResourceInfo class by setting the last parameter to true or false.
Install with composer.phar
.
php composer.phar require "lyquidity/xml-signer"
The two main XAdES specifications are linked below.
ETSI EN 319 132-1 V1.1.1 (2016-04)
ETSI EN 319 132-2 V1.1.1 (2016-04)
These are not the only XAdES specifications but the others are no longer applicable. The link below is to a question and response from Juan Carlos Cruellas, someone involved in the development of the specfications.
Fortunately, Juan Carlos Cruellas has created a really useful tool to perform conformance tests against signatures created by any tool.
XAdES is reliant on other specificiations. It is directly dependent on XMLDSig. This in turn is reliant other XML specifications.
XML Exclusive Canonicalization
XAdES requires timestamps to provide non-repudiation. Requests, responses and their contents are defined by IETF RFCs.
PKI Timestamp protocol [rfc3161] updated by [rfc5816]
Certificates used to sign signatures have to be checked for validity. One mechanism to check the status of certificate is Online Certificate Status Protocol (OCSP)