SignedData
One of the features the Cryptographic Message Syntax (CMS) provides is to act as a wrapper format that bundles files and digital signatures of those files together.
If a CMS contains a signatures but no file then those signatures are known as detached signatures.
Signatures can be verified and multiple signatures can be present.
Comparison to OpenSSL in PHP
openssl_cms_sign() doesn't do CAdES-B / ESS compliant signatures, it only does one signature and you can't add additional SignedAttr's. phpseclib can and does do CAdES-B / ESS compliant signatures, it can do multiple signatures and you can add all the SignedAttr's you want.
openssl_cms_verify() verifies all signatures and that's it. With phpseclib you can see how many signers exist, you can see which of those signers have valid signatures and which ones don't and, when phpseclib is validating signatures, it takes CAdES-B / ESS into consideration.
Reading SignedData CMS
The method for loading a SignedData CMS is the same as loading any other CMS type:
use phpseclib4\File\CMS;
$cms = CMS::load(file_get_contents('sample.p7m'));
print_r($cms);
$cms, in this case, is a CMS\SignedData object which, in turn, is basically a thin wrapper around an instance of \phpseclib4\File\ASN1\Constructed. Passing $cms through print_r() or var_dump() triggers the __debugInfo() magic method which, in turn, produces the following output:
contentType
phpseclib4\File\ASN1\Types\OID
content
version
phpseclib4\File\ASN1\Types\Integer
digestAlgorithms
0
algorithm
phpseclib4\File\ASN1\Types\OID
encapContentInfo
eContentType
phpseclib4\File\ASN1\Types\OID
eContent
phpseclib4\File\ASN1\Types\OctetString
certificates
0
certificate
phpseclib4\File\X509
-----BEGIN CERTIFICATE----- MIIFtjCCBJ6gAwIBAgIDKi0EMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJJ VDEcMBoGA1UEChMTSS5ULiBUZWxlY29tIFMuUi5MLjEiMCAGA1UECxMZU2Vydml6 aSBkaSBjZXJ0aWZpY2F6aW9uZTE8MDoGA1UEAxMzUmVnaW9uZSBMb21iYXJkaWEg Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgQ2l0dGFkaW5pMB4XDTA4MTAxODAwMDAw MFoXDTE0MTAxNTAwMDAwMFowgYUxCzAJBgNVBAYTAklUMREwDwYDVQQKEwhDUlMt U0lTUzEaMBgGA1UECxMRUmVnaW9uZSBMb21iYXJkaWExRzBFBgNVBAMTPkRMR01S QzY1TDE1QTc5NFkvNjAzMDYwMTI5MzMzNTAwMi44b3pCVG9nbzYvZ3RpLzIvbThQ SnBabG03NGM9MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrqhNO4MatPVhH 69DQWlyxcLCmDU8lPVd7JTHDVUqA93NfrYkSrXfClk4WgHHD3XUohBvqJv5pDfcJ 9P7v7J2oTYU8SFwYtlfKPZM8VahmBijT+vhVA2azlIGC8tVNF4/fvdwr+Er4u0rD bwKsgpL3NjPHr49Rkhf6iRntExNbrwIDAQABo4ICpzCCAqMwggEoBgNVHSAEggEf MIIBGzCB2gYFK0wQAgEwgdAwgZ4GCCsGAQUFBwICMIGRGoGOSWRlbnRpZmllcyBY LjUwOSBhdXRoZW50aWNhdGlvbiBjZXJ0aWZpY2F0ZXMgaXNzdWVkIGZvciB0aGUg aXRhbGlhbiBOYXRpb25hbCBTZXJ2aWNlIENhcmQgKENOUykgcHJvamVjdCBpbiBh Y2NvcmRpbmcgdG8gdGhlIGl0YWxpYW4gcmVndWxhdGlvbjAtBggrBgEFBQcCARYh aHR0cHM6Ly93d3cudGlwa2kuaXQvUkxDQUNJVFQvQ05TMDwGCStMDAEBCgICCjAv MC0GCCsGAQUFBwIBFiFodHRwczovL3d3dy50aXBraS5pdC9STENBQ0lUVC9DUFMw OAYIKwYBBQUHAQEELDAqMCgGCCsGAQUFBzABhhxodHRwOi8vb2NzcC5jcnNzaXNz LnRpcGtpLml0MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAf BgNVHSMEGDAWgBQIn3g7EcXy+WFmgQJfioSjJxwcJTCB1QYDVR0fBIHNMIHKMCeg JaAjhiFodHRwOi8vY2RwLnJsLnRpcGtpLml0L0NSTC9DQ1JMMTYwgZ6ggZuggZiG gZVsZGFwOi8vbGRhcC50aXBraS5pdC9jbiUzZENDUkwxNixvdSUzZENBJTIwQ2l0 dGFkaW5pLG8lM2RSZWdpb25lJTIwTG9tYmFyZGlhLGMlM2RJVD9jZXJ0aWZpY2F0 ZVJldm9jYXRpb25MaXN0P2Jhc2U/KG9iamVjdENsYXNzPWNSTERpc3RyaWJ1dGlv blBvaW50KTAdBgNVHQ4EFgQUDqp65vI6Bkn9EB/AVVfPBswsRdIwDQYJKoZIhvcN AQEFBQADggEBADWbuwUN+Eqllugt6JbmneIRSmH7/5MVeo3YYXzShqECcXmjZ/Og K6J7WzV7Pam7PHzNqKu9CDyqa2hLVA8QnCkHkDNjJSVAML3z2q3gKH7l6Sfssr9H jG6yoGNQRwXmyoXaq32L/WiHep7NNITPpzxo5NHChLh81crUqP6aC/wFd66yJhgm 5bobcAdqJf41TWSoYSZPPJWxzhSs1DKGbn+bJ1wzoFIJBLFYj9ilmtCBFwi498za LS1oYFIvMydUGvvfF+OD7Vij9JHGbGm2FZPodDAgNHTxiLg0kNgHNNYu51GkB4Zy EmZNoul9bzffxW35PFJTbEgRkCWU5h0msbY= -----END CERTIFICATE-----
signerInfos
0
version
phpseclib4\File\ASN1\Types\Integer
sid
issuerAndSerialNumber
issuer
rdnSequence
0
0
type
phpseclib4\File\ASN1\Types\OID
value
phpseclib4\File\ASN1\Types\PrintableString
1
0
type
phpseclib4\File\ASN1\Types\OID
value
phpseclib4\File\ASN1\Types\PrintableString
2
0
type
phpseclib4\File\ASN1\Types\OID
value
phpseclib4\File\ASN1\Types\PrintableString
3
0
type
phpseclib4\File\ASN1\Types\OID
value
phpseclib4\File\ASN1\Types\PrintableString
serialNumber
phpseclib4\File\ASN1\Types\Integer
digestAlgorithm
algorithm
phpseclib4\File\ASN1\Types\OID
signedAttrs
0
type
phpseclib4\File\ASN1\Types\OID
value
0
phpseclib4\File\ASN1\Types\OID
1
type
phpseclib4\File\ASN1\Types\OID
value
0
phpseclib4\File\ASN1\Types\UTCTime
2
type
phpseclib4\File\ASN1\Types\OID
value
0
phpseclib4\File\ASN1\Types\OctetString
3
type
phpseclib4\File\ASN1\Types\OID
value
0
certs
0
hashAlgorithm
algorithm
phpseclib4\File\ASN1\Types\OID
certHash
phpseclib4\File\ASN1\Types\OctetString
issuerSerial
issuer
0
directoryName
rdnSequence
0
0
type
phpseclib4\File\ASN1\Types\OID
value
phpseclib4\File\ASN1\Types\PrintableString
1
0
type
phpseclib4\File\ASN1\Types\OID
value
phpseclib4\File\ASN1\Types\PrintableString
2
0
type
phpseclib4\File\ASN1\Types\OID
value
phpseclib4\File\ASN1\Types\PrintableString
3
0
type
phpseclib4\File\ASN1\Types\OID
value
phpseclib4\File\ASN1\Types\PrintableString
serialNumber
phpseclib4\File\ASN1\Types\Integer
signatureAlgorithm
algorithm
phpseclib4\File\ASN1\Types\OID
signature
phpseclib4\File\ASN1\Types\OctetString
Extracting file contents
If the signed file is embedded it can be extracted thusly:
file_put_contents(
'filename.ext',
(string) $cms['content']['encapContentInfo']['eContent']
);
Extracting Signers
Signers can be extracted thusly:
$signers = $cms->getSigners();
foreach ($signers as $signer) {
// do stuff
}
You can also find the signer that corresponds to an X.509 certificate by doing the following:
$signer = $cms->findSigner($x509);
Note that even if the distinguished names match, if the key usage extension isn't set appropriately (eg. if neither digitalSignature or nonRepudiation are set), findSigner() will fail. This key usage check can be disabled by doing calling X509::ignoreKeyUsage().
Additionally, if an Enhanced Security Service (ESS) attribute is present then both that AND the SignerIdentifier will need to match the certificate.
getSigners() returns an array of Signer objects and findSigner() returns either a single Signer object or null. The full namespace for the Signer object is \phpseclib4\File\CMS\SignedData\Signer.
Extracting X509 certs
Certificates embedded within the CMS can be extracted thusly:
$cms->getCertificates();
Likewise CRLs embedded within the CMS can be extracted as follows:
$cms->getCRLs();
To see the certificate associated with a Signer one need simply do the following:
$signer->getCertificate();
That method returns an X.509 certificate object or a null if no matching certificates are found.
The same stipulations discussed above for $cms->findSigner() also apply to $signer->getCertificate().
Whereas $cms->findSigner() can take in an externally loaded X.509 certificate, $signer->getCertificate() looks exclusively at certificates embedded within the CMS itself. If no corresponding certificate can be found then the signature will fail validation. If you have the certificate but it just does not happen to be embedded within the CMS you can add it to the CMS thusly:
$x509 = \phpseclib4\File\X509::load('...');
$cms->addCertificate($x509);
Signer Attributes
Signed or unsigned attributes can be listed by calling $signer->listSignedAttrs() and $signer->listUnsignedAttrs(), respectively.
Example:
0
1
2
3
Individual attributes can be accessed by calling $signer->getSignedAttr('whatever') or $signer->getUnsignedAttr('whatever'). If an attribute appears multiple times the first one will be the one that's returned.
Example:
0
phpseclib4\File\ASN1\Types\OID
The existance of an attribute can be tested for by doing $signer->hasSignedAttr('whatever') or $signer->hasUnsignedAttr('whatever').
Creating SignedData
Minimalistic Example
use phpseclib4\File\CMS;
$cms = new CMS\SignedData('');
echo $cms;
That'll produce the following:
-----BEGIN CMS-----
MCcGCSqGSIb3DQEHAqAaMBgCAQExADAPBgkqhkiG9w0BBwGgAgQAMQA=
-----END CMS-----
There are no signers present and no certificates.
If you saved this CMS as test.p7m and then did openssl cms -in test.p7m -inform PEM -cmsout -print -noout you'd get this:
CMS_ContentInfo:
contentType: pkcs7-signedData (1.2.840.113549.1.7.2)
d.signedData:
version: 1
digestAlgorithms:
<EMPTY>
encapContentInfo:
eContentType: pkcs7-data (1.2.840.113549.1.7.1)
eContent:
certificates:
<ABSENT>
crls:
<ABSENT>
signerInfos:
<EMPTY>
If you did print_r($cms) on that CMS you'd get this:
contentType
phpseclib4\File\ASN1\Types\OID
content
version
phpseclib4\File\ASN1\Types\Integer
digestAlgorithms
encapContentInfo
eContentType
phpseclib4\File\ASN1\Types\OID
eContent
phpseclib4\File\ASN1\Types\OctetString
signerInfos
Detached Signatures
To create a detached signature the first thing that needs to be done is that a resource needs to be passed to the CMS\SignedData constructor:
$fp = fopen('filename.pdf', 'r');
$cms = new CMS\SignedData($fp);
Technically, one could pass a string to the constructor and then do this:
unset($cms['content']['encapContentInfo']['eContent']);
That, however, doesn't scale well. Like what if you wanted to sign a 1GB file? Passing a resource to the constructor is significantly more performant than loading a string of that size. Although I suppose one could use unset in this manner to turn an attached signature into a detached signature. Due to the way SignedData works resigning wouldn't be necessary if one removed the file in that manner.
A cleaner way to turn an attached signature into a detached one is $cms->detach(). That removes the embedded content from the structure without invalidating the signatures - the signers' message digest attributes still refer to the original content, so re-attaching the same content later will validate just fine.
To validate a detached signature you need to re-attach the content first. That is done by calling $cms->attach($content). $content can be either a string (for small content) or a resource (for large content - streamed during hashing, never loaded into memory):
$cms = CMS::load(file_get_contents('document.p7s'));
// Small content - pass as a string:
$cms->attach(file_get_contents('document.pdf'));
// Large content - pass as a resource:
$cms->attach(fopen('document.pdf', 'r'));
foreach ($cms->getSigners() as $signer) {
var_dump($signer->validateSignature());
}
Anything other than a string or resource passed to attach() will throw \phpseclib4\Exception\UnexpectedValueException. If attach() isn't called before validating a detached signature, all signers will fail since they're hashing against missing content.
Adding Signers
The simplest way to create a signer - and to add a signature - is to do this:
$pfx->sign($cms);
$pfx, in this case, is an instance of \phpseclib4\File\PFX.
Doing that is exactly the same thing as doing this:
$signer = $cms->addESSSigner($x509);
$private->sign($signer);
Where $x509 and $private could correspond to $pfx->getCertificates()[0] and $pfx->getPrivateKeys()[0], respectively, or just independently loaded / generated certificates and keys (well, independent of a PFX - not of each other).
ESS signatures have an extra signed attribute - id-aa-signingCertificateV2 - that non-ESS signatures do not have. This attribute "allows for a verifier to check that the certificate used in the verification process was the same certificate the signer intended" (RFC5035 § 4)
Non-ESS signatures can be added by doing $cms->addSigner($x509). Those still make use of signed attributes - just not id-aa-signingCertificateV2. A third type of signature - one that doesn't make use of signed attributes at all - can be added by doing $cms->addNakedSigner($x509).
All three of addSigner(), addESSSigner(), and addNakedSigner() take an optional second parameter that controls how the signer identifies its certificate in the resulting CMS:
CMS::ISSUER_AND_DN(the default) - identify the signing cert by its issuer DN and serial number. This is the conventional choice and what most verifiers expect.CMS::KEY_ID- identify the signing cert by itssubjectKeyIdentifierextension. Smaller and unambiguous, but requires that the cert actually has an SKI extension.
$signer = $cms->addESSSigner($x509, CMS::KEY_ID);
To set the message digest you need to update the private key to use that digest. So like by default phpseclib uses sha256 for most everything but if you wanted to use sha512 you'd need to do this:
$private->withHash('sha512')->sign($signer)
Also note that Ed25519 / Ed448 signed CMSs are not well supported. RFC8419 talks about them but OpenSSL doesn't support them.
Another way to add a signer:
$private->sign($cms);
Where $private is an instance of \phpseclib4\Crypt\Common\PrivateKey.
Doing that is basically the same thing as doing this:
$x509 = new X509($private->getPublicKey());
$x509->setExtension('id-ce-keyUsage', ['digitalSignature']);
$private->sign($x509);
$signer = $cms->addESSSigner($x509);
$private->sign($signer);
ie. it's signed by a self signed X509 with an empty DN.
Additionally, individual signers can be re-signed with PFXs and private keys. eg.
$pfx->sign($signer);
Adding Signer Attributes
Signer attributes can be added by calling either $signer->setSignedAttr($type, $value) or $signer->setUnsignedAttr($type, $value).
Here's an example:
$signer->setSignedAttr('2.9999', 'blah blah');
That is equivalent to doing this:
$signer['signedAttrs'][] = [
'type' => '2.9999',
'value' => ['blah blah']
];
One quirky thing about adding signers and signer attributes is that they're not necessarily going to show up where you expect them to. Like an ESS signer comes with four signed attributes out of the box:
- id-contentType (required if signedAttrs is going to be present)
- id-messageDigest (required if signedAttrs is going to be present)
- id-aa-signingCertificateV2 (required for ESS signers)
- id-aa-signingTime (which isn't required but, none-the-less, phpseclib adds it)
Consequently, you might think that, once you've added a signer, you'd access it by doing $signer['signedAttrs'][4], but that isn't necessarily going to be the case - it could wind up being added at $signer['signedAttrs'][0] because of this blurb from ITU-T Recommendation X.690 § 11.6 Set-of components:
The encodings of the component values of a set-of value shall appear in ascending order, the encodings being compared as octet strings with the shorter components being padded at their trailing end with 0-octets. NOTE - The padding octets are for comparison purposes only and do not appear in the encodings.
Note that adding signed attributes after a signer has been signed will require the signer resign.
Signatures in Parallel
Let's say you have a file that you wanted multiple people to sign but that they have to do it separately. You could email the file to be signed to one party after another party has signed it, however, the time that it would take for all signatures to be received in that manner would be array_sum($timesToSign), where $timesToSign is an array containing the time to sign for each signer.
What would be much faster is if you sent the file to each of the parties at the same time and let each party sign it independent of the other. Using that approach the amount of time that it'd take for the file to be signed by all parties would be max($timesToSign).
If everyone signed the file independently you could take the CMS that each of them generated and then copy their signature to a master list of signatures. eg.
$cms1 = CMS::load('signer1.p7m');
$cms2 = CMS::load('signer2.p7m');
$master = CMS::load('master.p7m');
$master->addSignature($cms1->getSigners()[0]);
$master->addSignature($cms2->getSigners()[0]);
Output Format
By default, CMS objects, when cast to a string, are converted to base64-encoded PEMs. To make it so that casting to a string produces a binary encoded DERs one need simply call CMS::enableBinaryOutput(). To go back to PEMs calling CMS::disableBinaryOutput() is sufficient.
You can also convert a CMS object to binary by doing $cms->toString(['binary' => true]).
Validating Signatures
To validate the signature of a signer one need simply do this:
$signer->validateSignature();
If you want to validate all signatures at once you can do this:
$cms->validateSignature();
validateSignature() will return false if the certificate chain doesn't ultimately trace back to a CA cert. The CA requirement can be removed by passing bool(false) to the validateSignature() method:
$signer->validateSignature(false);
validateSignature() will also return false if the key usage extension on the signing certificate isn't set appropriately (eg. if neither digitalSignature or nonRepudiation are set). This key usage check can be disabled by calling X509::ignoreKeyUsage().
Note that although there are multiple methods to add signers there's only one method to verify signatures. phpseclib's validateSignature() method is ESS aware whereas openssl_cms_verify() isn't. What that means is that if an ESS signature is detected and the ESS signature isn't valid whereas the regular signature is then phpseclib's validateSignature() will fail whereas openssl_cms_verify() won't. The reason for this failure, in this scenario, isn't because the signature is actually invalid but because the signing certificate wouldn't be locatable. eg. $signer->getCertificate() would return nothing because it's looking for a certificate that matches the ESS criteria and it wouldn't be able to find such a certificate.