Skip to main content
Version: 4.0

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);
(download sample.p7m)

$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
id-signedData
content
version
phpseclib4\File\ASN1\Types\Integer
v1
digestAlgorithms
0
algorithm
phpseclib4\File\ASN1\Types\OID
id-sha256
encapContentInfo
eContentType
phpseclib4\File\ASN1\Types\OID
id-data
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
v1
sid
issuerAndSerialNumber
issuer
rdnSequence
0
0
type
phpseclib4\File\ASN1\Types\OID
id-at-countryName
value
phpseclib4\File\ASN1\Types\PrintableString
IT
1
0
type
phpseclib4\File\ASN1\Types\OID
id-at-organizationName
value
phpseclib4\File\ASN1\Types\PrintableString
I.T. Telecom S.R.L.
2
0
type
phpseclib4\File\ASN1\Types\OID
id-at-organizationalUnitName
value
phpseclib4\File\ASN1\Types\PrintableString
Servizi di certificazione
3
0
type
phpseclib4\File\ASN1\Types\OID
id-at-commonName
value
phpseclib4\File\ASN1\Types\PrintableString
Regione Lombardia Certification Authority Cittadini
serialNumber
phpseclib4\File\ASN1\Types\Integer
2764036
digestAlgorithm
algorithm
phpseclib4\File\ASN1\Types\OID
id-sha256
signedAttrs
0
type
phpseclib4\File\ASN1\Types\OID
id-contentType
value
0
phpseclib4\File\ASN1\Types\OID
id-data
1
type
phpseclib4\File\ASN1\Types\OID
id-signingTime
value
0
phpseclib4\File\ASN1\Types\UTCTime
2014-08-19 16:21:30
2
type
phpseclib4\File\ASN1\Types\OID
id-messageDigest
value
0
phpseclib4\File\ASN1\Types\OctetString
ee180eea75b4a945a160c0781b9ebdbc144365615c0f5604411b9b2175e791e9
3
type
phpseclib4\File\ASN1\Types\OID
id-aa-signingCertificateV2
value
0
certs
0
hashAlgorithm
algorithm
phpseclib4\File\ASN1\Types\OID
id-sha256
certHash
phpseclib4\File\ASN1\Types\OctetString
a247da372573393067cbbba7a535c804861b94a2575ef8ad5adf30b13a0638a8
issuerSerial
issuer
0
directoryName
rdnSequence
0
0
type
phpseclib4\File\ASN1\Types\OID
id-at-countryName
value
phpseclib4\File\ASN1\Types\PrintableString
IT
1
0
type
phpseclib4\File\ASN1\Types\OID
id-at-organizationName
value
phpseclib4\File\ASN1\Types\PrintableString
I.T. Telecom S.R.L.
2
0
type
phpseclib4\File\ASN1\Types\OID
id-at-organizationalUnitName
value
phpseclib4\File\ASN1\Types\PrintableString
Servizi di certificazione
3
0
type
phpseclib4\File\ASN1\Types\OID
id-at-commonName
value
phpseclib4\File\ASN1\Types\PrintableString
Regione Lombardia Certification Authority Cittadini
serialNumber
phpseclib4\File\ASN1\Types\Integer
2764036
signatureAlgorithm
algorithm
phpseclib4\File\ASN1\Types\OID
rsaEncryption
signature
phpseclib4\File\ASN1\Types\OctetString
0f7c375e5ff7306c280912b76f9eb11470d8d0731c4dc0f013e605e3cea74376a7474664e6095c72d2ef2a5bb356a05bbec1a2364008846aa101a9160676ca6bdb989e60c012266fe25d51ead5c8d71926e0504f3cbabca9ae6e9e05318905597ed0230adbb3fe0f3f19b96050fcf05def9fab3bd1e575cc8a33ab1a16fcce25

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
id-contentType
1
id-signingTime
2
id-messageDigest
3
id-aa-signingCertificateV2

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
id-data

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
id-signedData
content
version
phpseclib4\File\ASN1\Types\Integer
v1
digestAlgorithms
encapContentInfo
eContentType
phpseclib4\File\ASN1\Types\OID
id-data
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 its subjectKeyIdentifier extension. 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.