added composer files and installed vendor packages. Also modified xsls to display base project
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Asn1Test.php
|
||||
*
|
||||
* @since 2026-07-15
|
||||
* @category Library
|
||||
* @package PdfSign
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-pdf-sign
|
||||
*
|
||||
* This file is part of tc-lib-pdf-sign software library.
|
||||
*/
|
||||
|
||||
namespace Test\Cms;
|
||||
|
||||
use Com\Tecnick\Pdf\Sign\Cms\Asn1;
|
||||
use Com\Tecnick\Pdf\Sign\Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Asn1 Test
|
||||
*
|
||||
* @since 2026-07-15
|
||||
* @category Library
|
||||
* @package PdfSign
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-pdf-sign
|
||||
*/
|
||||
class Asn1Test extends TestCase
|
||||
{
|
||||
private Asn1 $asn1;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->asn1 = new Asn1();
|
||||
}
|
||||
|
||||
public function testEncodeLengthShortForm(): void
|
||||
{
|
||||
$this->assertSame("\x05", $this->asn1->encodeLength(5));
|
||||
$this->assertSame("\x7F", $this->asn1->encodeLength(127));
|
||||
}
|
||||
|
||||
public function testEncodeLengthLongForm(): void
|
||||
{
|
||||
$this->assertSame("\x81\x80", $this->asn1->encodeLength(128));
|
||||
$this->assertSame("\x82\x01\x00", $this->asn1->encodeLength(256));
|
||||
}
|
||||
|
||||
public function testEncodeInteger(): void
|
||||
{
|
||||
$this->assertSame("\x02\x01\x00", $this->asn1->encodeInteger(0));
|
||||
$this->assertSame("\x02\x01\x7F", $this->asn1->encodeInteger(127));
|
||||
$this->assertSame("\x02\x02\x00\xFF", $this->asn1->encodeInteger(255));
|
||||
$this->assertSame("\x02\x02\x01\x00", $this->asn1->encodeInteger(256));
|
||||
}
|
||||
|
||||
public function testEncodeIntegerBytesTrimsAndPads(): void
|
||||
{
|
||||
$this->assertSame("\x02\x01\x7F", $this->asn1->encodeIntegerBytes("\x00\x7F"));
|
||||
$this->assertSame("\x02\x02\x00\x80", $this->asn1->encodeIntegerBytes("\x80"));
|
||||
}
|
||||
|
||||
public function testEncodeBoolean(): void
|
||||
{
|
||||
$this->assertSame("\x01\x01\xFF", $this->asn1->encodeBoolean(true));
|
||||
$this->assertSame("\x01\x01\x00", $this->asn1->encodeBoolean(false));
|
||||
}
|
||||
|
||||
public function testEncodeNull(): void
|
||||
{
|
||||
$this->assertSame("\x05\x00", $this->asn1->encodeNull());
|
||||
}
|
||||
|
||||
public function testEncodeOctetStringSequenceSet(): void
|
||||
{
|
||||
$this->assertSame("\x04\x02AB", $this->asn1->encodeOctetString('AB'));
|
||||
$this->assertSame("\x30\x02AB", $this->asn1->encodeSequence('AB'));
|
||||
$this->assertSame("\x31\x02AB", $this->asn1->encodeSet('AB'));
|
||||
}
|
||||
|
||||
public function testEncodeContext(): void
|
||||
{
|
||||
$this->assertSame("\xA0\x02AB", $this->asn1->encodeContext(0, 'AB'));
|
||||
$this->assertSame("\xA3\x02AB", $this->asn1->encodeContext(3, 'AB'));
|
||||
}
|
||||
|
||||
public function testEncodeObjectIdentifier(): void
|
||||
{
|
||||
// sha256WithRSAEncryption: 1.2.840.113549.1.1.11
|
||||
$this->assertSame(
|
||||
'06092a864886f70d01010b',
|
||||
\bin2hex($this->asn1->encodeObjectIdentifier('1.2.840.113549.1.1.11')),
|
||||
);
|
||||
}
|
||||
|
||||
public function testReadTlvRoundTrip(): void
|
||||
{
|
||||
$der = $this->asn1->encodeSequence($this->asn1->encodeInteger(256));
|
||||
$offset = 0;
|
||||
$tlv = $this->asn1->readTlv($der, $offset);
|
||||
$this->assertSame(0x30, $tlv['tag']);
|
||||
$this->assertSame(\strlen($der), $offset);
|
||||
$this->assertSame($der, $tlv['raw']);
|
||||
|
||||
$inner = 0;
|
||||
$intTlv = $this->asn1->readTlv($tlv['value'], $inner);
|
||||
$this->assertSame(0x02, $intTlv['tag']);
|
||||
$this->assertSame(256, $this->asn1->decodeInteger($intTlv['value']));
|
||||
}
|
||||
|
||||
public function testReadTlvRejectsTruncatedData(): void
|
||||
{
|
||||
$this->expectException(Exception::class);
|
||||
$offset = 0;
|
||||
$this->asn1->readTlv("\x30\x05\x00", $offset);
|
||||
}
|
||||
|
||||
public function testDecodeIntegerRejectsEmpty(): void
|
||||
{
|
||||
$this->expectException(Exception::class);
|
||||
$this->asn1->decodeInteger('');
|
||||
}
|
||||
|
||||
public function testEncodeIntegerBytesEmptyInputYieldsZero(): void
|
||||
{
|
||||
$this->assertSame("\x02\x01\x00", $this->asn1->encodeIntegerBytes(''));
|
||||
}
|
||||
|
||||
public function testEncodeObjectIdentifierRejectsSingleArc(): void
|
||||
{
|
||||
$this->expectException(Exception::class);
|
||||
$this->asn1->encodeObjectIdentifier('1');
|
||||
}
|
||||
|
||||
public function testEncodeObjectIdentifierClampsNegativeArc(): void
|
||||
{
|
||||
$this->assertSame('06022a00', \bin2hex($this->asn1->encodeObjectIdentifier('1.2.-1')));
|
||||
}
|
||||
|
||||
public function testReadTlvRejectsEmptyData(): void
|
||||
{
|
||||
$this->expectException(Exception::class);
|
||||
$offset = 0;
|
||||
$this->asn1->readTlv('', $offset);
|
||||
}
|
||||
|
||||
public function testReadTlvRejectsMissingLength(): void
|
||||
{
|
||||
$this->expectException(Exception::class);
|
||||
$offset = 0;
|
||||
$this->asn1->readTlv("\x30", $offset);
|
||||
}
|
||||
|
||||
public function testReadTlvRejectsUnsupportedLongFormLength(): void
|
||||
{
|
||||
$this->expectException(Exception::class);
|
||||
$offset = 0;
|
||||
// 0x85 announces a 5-octet length, which exceeds the supported 4 octets.
|
||||
$this->asn1->readTlv("\x04\x85\x00\x00\x00\x00\x00", $offset);
|
||||
}
|
||||
|
||||
public function testReadTlvHandlesLongFormLength(): void
|
||||
{
|
||||
// A 200-byte payload forces a multi-octet (long-form) DER length.
|
||||
$payload = \str_repeat("\x41", 200);
|
||||
$der = $this->asn1->encodeOctetString($payload);
|
||||
$offset = 0;
|
||||
$tlv = $this->asn1->readTlv($der, $offset);
|
||||
$this->assertSame(0x04, $tlv['tag']);
|
||||
$this->assertSame($payload, $tlv['value']);
|
||||
$this->assertSame(\strlen($der), $offset);
|
||||
}
|
||||
|
||||
// Coverage note: Asn1::encodeLength() throws on a length needing more than
|
||||
// 127 octets to represent. That requires content larger than 2^1016 bytes,
|
||||
// which is unrepresentable by a PHP int and unallocatable, so the guard is
|
||||
// defensive and cannot be exercised in a unit test.
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* BuilderTest.php
|
||||
*
|
||||
* @since 2026-07-15
|
||||
* @category Library
|
||||
* @package PdfSign
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-pdf-sign
|
||||
*
|
||||
* This file is part of tc-lib-pdf-sign software library.
|
||||
*/
|
||||
|
||||
namespace Test\Cms;
|
||||
|
||||
use Com\Tecnick\Pdf\Sign\Cms\Asn1;
|
||||
use Com\Tecnick\Pdf\Sign\Cms\Builder;
|
||||
use Com\Tecnick\Pdf\Sign\Exception;
|
||||
use OpenSSLAsymmetricKey;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* CMS Builder Test
|
||||
*
|
||||
* @since 2026-07-15
|
||||
* @category Library
|
||||
* @package PdfSign
|
||||
* @author Nicola Asuni <info@tecnick.com>
|
||||
* @copyright 2026 Nicola Asuni - Tecnick.com LTD
|
||||
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||||
* @link https://github.com/tecnickcom/tc-lib-pdf-sign
|
||||
*/
|
||||
class BuilderTest extends TestCase
|
||||
{
|
||||
private const SIGNING_TIME = 1_700_000_000;
|
||||
|
||||
private Asn1 $asn1;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->asn1 = new Asn1();
|
||||
}
|
||||
|
||||
public function testSignRsaSha256ProducesVerifiableCms(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$data = 'The quick brown fox jumps over the lazy dog.';
|
||||
|
||||
$builder = new Builder($this->asn1);
|
||||
$cms = $builder->sign($data, $cred['cert_der'], $cred['key'], [], 'sha256', self::SIGNING_TIME);
|
||||
|
||||
$parts = $this->parseSignerInfo($cms);
|
||||
$this->assertSame(0xA0, $parts['signed_attrs']['tag']);
|
||||
$this->assertSame(0x04, $parts['signature']['tag']);
|
||||
$this->assertSame(0xA0, $parts['certificates']['tag']);
|
||||
$this->assertStringContainsString($cred['cert_der'], $parts['certificates']['value']);
|
||||
|
||||
// Cryptographically verify the signature over the DER SET OF signed attributes.
|
||||
$this->assertVerifies($parts, $cred['cert_pem'], OPENSSL_ALGO_SHA256);
|
||||
|
||||
// content-type present and equal to id-data.
|
||||
$contentType = $this->attributeValue($parts['signed_attrs']['value'], '1.2.840.113549.1.9.3');
|
||||
$this->assertNotNull($contentType);
|
||||
$this->assertSame($this->asn1->encodeObjectIdentifier('1.2.840.113549.1.7.1'), $contentType['raw']);
|
||||
|
||||
// signing-time is a UTCTime for a 2023 timestamp.
|
||||
$signingTime = $this->attributeValue($parts['signed_attrs']['value'], '1.2.840.113549.1.9.5');
|
||||
$this->assertNotNull($signingTime);
|
||||
$this->assertSame(0x17, $signingTime['tag']);
|
||||
|
||||
// message-digest equals SHA-256 of the content.
|
||||
$messageDigest = $this->attributeValue($parts['signed_attrs']['value'], '1.2.840.113549.1.9.4');
|
||||
$this->assertNotNull($messageDigest);
|
||||
$this->assertSame(\hash('sha256', $data, true), $messageDigest['value']);
|
||||
|
||||
// signing-certificate-v2 carries the SHA-256 hash of the signer certificate;
|
||||
// for SHA-256 the ESSCertIDv2 hashAlgorithm is omitted so certHash is first.
|
||||
$certHash = $this->firstCertHash('1.2.840.113549.1.9.16.2.47', $parts['signed_attrs']['value']);
|
||||
$this->assertSame(0x04, $certHash['tag']);
|
||||
$this->assertSame(\hash('sha256', $cred['cert_der'], true), $certHash['value']);
|
||||
}
|
||||
|
||||
public function testSignOmitsSigningTimeForPadesBaseline(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$data = 'PAdES-BASELINE forbids the CMS signing-time attribute.';
|
||||
|
||||
$builder = new Builder($this->asn1);
|
||||
// includeSigningTime = false: the PAdES-BASELINE case, where the signing time
|
||||
// is carried by the /M signature dictionary entry rather than the CMS.
|
||||
$cms = $builder->sign($data, $cred['cert_der'], $cred['key'], [], 'sha256', self::SIGNING_TIME, null, false);
|
||||
|
||||
$parts = $this->parseSignerInfo($cms);
|
||||
// The signature still verifies over the (smaller) DER SET OF signed attributes.
|
||||
$this->assertVerifies($parts, $cred['cert_pem'], OPENSSL_ALGO_SHA256);
|
||||
|
||||
// signing-time (1.2.840.113549.1.9.5) is absent.
|
||||
$this->assertNull($this->attributeValue($parts['signed_attrs']['value'], '1.2.840.113549.1.9.5'));
|
||||
|
||||
// The other mandatory signed attributes remain present.
|
||||
$this->assertNotNull($this->attributeValue($parts['signed_attrs']['value'], '1.2.840.113549.1.9.3'));
|
||||
$this->assertNotNull($this->attributeValue($parts['signed_attrs']['value'], '1.2.840.113549.1.9.4'));
|
||||
$this->assertNotNull($this->attributeValue($parts['signed_attrs']['value'], '1.2.840.113549.1.9.16.2.47'));
|
||||
}
|
||||
|
||||
public function testSignEcSha256ProducesVerifiableCms(): void
|
||||
{
|
||||
$cred = $this->makeCredential('ec');
|
||||
$data = 'elliptic-curve payload';
|
||||
|
||||
$builder = new Builder($this->asn1);
|
||||
$cms = $builder->sign($data, $cred['cert_der'], $cred['key'], [], 'sha256', self::SIGNING_TIME);
|
||||
|
||||
$parts = $this->parseSignerInfo($cms);
|
||||
$this->assertVerifies($parts, $cred['cert_pem'], OPENSSL_ALGO_SHA256);
|
||||
}
|
||||
|
||||
public function testSignRsaSha384IncludesEssCertHashAlgorithm(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$builder = new Builder($this->asn1);
|
||||
$cms = $builder->sign('data', $cred['cert_der'], $cred['key'], [], 'sha384', self::SIGNING_TIME);
|
||||
|
||||
$parts = $this->parseSignerInfo($cms);
|
||||
$this->assertVerifies($parts, $cred['cert_pem'], OPENSSL_ALGO_SHA384);
|
||||
|
||||
// For a non-default digest, ESSCertIDv2 begins with the hashAlgorithm SEQUENCE.
|
||||
$scv2 = $this->attributeValue($parts['signed_attrs']['value'], '1.2.840.113549.1.9.16.2.47');
|
||||
$this->assertNotNull($scv2);
|
||||
$certsOffset = 0;
|
||||
$certs = $this->asn1->readTlv($scv2['value'], $certsOffset);
|
||||
$essOffset = 0;
|
||||
$ess = $this->asn1->readTlv($certs['value'], $essOffset);
|
||||
$firstOffset = 0;
|
||||
$first = $this->asn1->readTlv($ess['value'], $firstOffset);
|
||||
$this->assertSame(0x30, $first['tag']);
|
||||
}
|
||||
|
||||
public function testSignRsaSha512ProducesVerifiableCms(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$builder = new Builder($this->asn1);
|
||||
$cms = $builder->sign('data', $cred['cert_der'], $cred['key'], [], 'sha512', self::SIGNING_TIME);
|
||||
|
||||
$parts = $this->parseSignerInfo($cms);
|
||||
$this->assertVerifies($parts, $cred['cert_pem'], OPENSSL_ALGO_SHA512);
|
||||
}
|
||||
|
||||
public function testSignEmbedsChainCertificates(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$chainDer = $this->pemToDer((string) \file_get_contents(__DIR__ . '/../data/ocsp_ca.pem'));
|
||||
|
||||
$builder = new Builder($this->asn1);
|
||||
$cms = $builder->sign('data', $cred['cert_der'], $cred['key'], [$chainDer], 'sha256', self::SIGNING_TIME);
|
||||
|
||||
$parts = $this->parseSignerInfo($cms);
|
||||
$this->assertStringContainsString($cred['cert_der'], $parts['certificates']['value']);
|
||||
$this->assertStringContainsString($chainDer, $parts['certificates']['value']);
|
||||
}
|
||||
|
||||
public function testSignWithoutTimestampHasNoUnsignedAttributes(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$builder = new Builder($this->asn1);
|
||||
$cms = $builder->sign('data', $cred['cert_der'], $cred['key'], [], 'sha256', self::SIGNING_TIME);
|
||||
|
||||
$parts = $this->parseSignerInfo($cms);
|
||||
$this->assertNull($parts['unsigned_attrs']);
|
||||
}
|
||||
|
||||
public function testSignEmbedsSignatureTimestampUnsignedAttribute(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$token = $this->asn1->encodeSequence($this->asn1->encodeOctetString('fake-rfc3161-token'));
|
||||
|
||||
$captured = '';
|
||||
$provider = static function (string $signature) use (&$captured, $token): string {
|
||||
$captured = $signature;
|
||||
return $token;
|
||||
};
|
||||
|
||||
$builder = new Builder($this->asn1);
|
||||
$cms = $builder->sign('data', $cred['cert_der'], $cred['key'], [], 'sha256', self::SIGNING_TIME, $provider);
|
||||
|
||||
$parts = $this->parseSignerInfo($cms);
|
||||
// The signature is cryptographically unchanged by the added unsigned attribute.
|
||||
$this->assertVerifies($parts, $cred['cert_pem'], OPENSSL_ALGO_SHA256);
|
||||
|
||||
// The provider timestamps the raw SignerInfo signature bytes.
|
||||
$this->assertSame($parts['signature']['value'], $captured);
|
||||
|
||||
// unsignedAttrs is a [1] IMPLICIT context tag carrying id-aa-signatureTimeStampToken.
|
||||
$this->assertNotNull($parts['unsigned_attrs']);
|
||||
$this->assertSame(0xA1, $parts['unsigned_attrs']['tag']);
|
||||
|
||||
$tstValue = $this->attributeValue($parts['unsigned_attrs']['value'], '1.2.840.113549.1.9.16.2.14');
|
||||
$this->assertNotNull($tstValue);
|
||||
$this->assertSame($token, $tstValue['raw']);
|
||||
}
|
||||
|
||||
public function testSignRejectsEmptySignatureTimestampToken(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$provider = static fn(): string => '';
|
||||
|
||||
$builder = new Builder($this->asn1);
|
||||
$this->expectException(Exception::class);
|
||||
$builder->sign('data', $cred['cert_der'], $cred['key'], [], 'sha256', self::SIGNING_TIME, $provider);
|
||||
}
|
||||
|
||||
public function testSignUsesGeneralizedTimeForFarFuture(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$builder = new Builder($this->asn1);
|
||||
// 2100-01-01T00:00:00Z is outside the UTCTime range (1950-2049).
|
||||
$cms = $builder->sign('data', $cred['cert_der'], $cred['key'], [], 'sha256', 4_102_444_800);
|
||||
|
||||
$parts = $this->parseSignerInfo($cms);
|
||||
$signingTime = $this->attributeValue($parts['signed_attrs']['value'], '1.2.840.113549.1.9.5');
|
||||
$this->assertNotNull($signingTime);
|
||||
$this->assertSame(0x18, $signingTime['tag']);
|
||||
}
|
||||
|
||||
public function testSignRejectsUnsupportedDigest(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$builder = new Builder($this->asn1);
|
||||
$this->expectException(Exception::class);
|
||||
$builder->sign('data', $cred['cert_der'], $cred['key'], [], 'md5', self::SIGNING_TIME);
|
||||
}
|
||||
|
||||
public function testSignFailsWithNonSigningKey(): void
|
||||
{
|
||||
$cred = $this->makeCredential('rsa');
|
||||
$publicKey = \openssl_pkey_get_public($cred['cert_pem']);
|
||||
if ($publicKey === false) {
|
||||
$this->fail('Unable to load public key');
|
||||
}
|
||||
|
||||
$builder = new Builder($this->asn1);
|
||||
$this->expectException(Exception::class);
|
||||
\set_error_handler(static fn(): bool => true);
|
||||
try {
|
||||
$builder->sign('data', $cred['cert_der'], $publicKey, [], 'sha256', self::SIGNING_TIME);
|
||||
} finally {
|
||||
\restore_error_handler();
|
||||
}
|
||||
}
|
||||
|
||||
public function testSignRejectsUnsupportedKeyType(): void
|
||||
{
|
||||
$cred = $this->makeCredential('dsa');
|
||||
$builder = new Builder($this->asn1);
|
||||
$this->expectException(Exception::class);
|
||||
$builder->sign('data', $cred['cert_der'], $cred['key'], [], 'sha256', self::SIGNING_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a private key and a matching self-signed certificate.
|
||||
*
|
||||
* @return array{key: OpenSSLAsymmetricKey, cert_pem: string, cert_der: string}
|
||||
*/
|
||||
private function makeCredential(string $keyType): array
|
||||
{
|
||||
$config = [
|
||||
'config' => __DIR__ . '/../../openssl.cnf',
|
||||
'digest_alg' => 'sha256',
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
];
|
||||
if ($keyType === 'ec') {
|
||||
$config['private_key_type'] = OPENSSL_KEYTYPE_EC;
|
||||
$config['curve_name'] = 'prime256v1';
|
||||
} elseif ($keyType === 'dsa') {
|
||||
$config['private_key_type'] = OPENSSL_KEYTYPE_DSA;
|
||||
$config['private_key_bits'] = 1024;
|
||||
}
|
||||
|
||||
$key = \openssl_pkey_new($config);
|
||||
if (!$key instanceof OpenSSLAsymmetricKey) {
|
||||
$this->markTestSkipped($keyType . ' key generation is not available');
|
||||
}
|
||||
|
||||
$csr = \openssl_csr_new(['commonName' => 'tc-lib-pdf-sign signer'], $key, $config);
|
||||
if (!$csr instanceof \OpenSSLCertificateSigningRequest) {
|
||||
$this->markTestSkipped('CSR generation failed for ' . $keyType);
|
||||
}
|
||||
|
||||
$cert = \openssl_csr_sign($csr, null, $key, 365, $config);
|
||||
if (!$cert instanceof \OpenSSLCertificate) {
|
||||
$this->markTestSkipped('Certificate signing failed for ' . $keyType);
|
||||
}
|
||||
|
||||
$certPem = '';
|
||||
\openssl_x509_export($cert, $certPem);
|
||||
|
||||
return ['key' => $key, 'cert_pem' => $certPem, 'cert_der' => $this->pemToDer($certPem)];
|
||||
}
|
||||
|
||||
private function pemToDer(string $pem): string
|
||||
{
|
||||
$stripped = (string) \preg_replace('/-----[^-]+-----|\s+/', '', $pem);
|
||||
$der = \base64_decode($stripped, true);
|
||||
if ($der === false) {
|
||||
$this->fail('Invalid PEM');
|
||||
}
|
||||
|
||||
return $der;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the SignerInfo signature over the reconstructed DER SET OF signed attributes.
|
||||
*
|
||||
* @param array{signed_attrs: array{tag:int,value:string,raw:string}, signature: array{tag:int,value:string,raw:string}, certificates: array{tag:int,value:string,raw:string}, unsigned_attrs: array{tag:int,value:string,raw:string}|null} $parts
|
||||
*/
|
||||
private function assertVerifies(array $parts, string $certPem, int $opensslAlgo): void
|
||||
{
|
||||
$publicKey = \openssl_pkey_get_public($certPem);
|
||||
if ($publicKey === false) {
|
||||
$this->fail('Unable to load public key');
|
||||
}
|
||||
|
||||
$signedAttrsSet = $this->asn1->encodeSet($parts['signed_attrs']['value']);
|
||||
$result = \openssl_verify($signedAttrsSet, $parts['signature']['value'], $publicKey, $opensslAlgo);
|
||||
$this->assertSame(1, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Descend into a SigningCertificate attribute and return the ESSCertID certHash TLV.
|
||||
*
|
||||
* @return array{tag: int, value: string, raw: string}
|
||||
*/
|
||||
private function firstCertHash(string $oid, string $attrsDer): array
|
||||
{
|
||||
$value = $this->attributeValue($attrsDer, $oid);
|
||||
$this->assertNotNull($value);
|
||||
$certsOffset = 0;
|
||||
$certs = $this->asn1->readTlv($value['value'], $certsOffset);
|
||||
$essOffset = 0;
|
||||
$ess = $this->asn1->readTlv($certs['value'], $essOffset);
|
||||
$hashOffset = 0;
|
||||
return $this->asn1->readTlv($ess['value'], $hashOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an Attribute by OID and return the first value TLV of its value SET.
|
||||
*
|
||||
* @return array{tag: int, value: string, raw: string}|null
|
||||
*/
|
||||
private function attributeValue(string $attrsDer, string $oid): ?array
|
||||
{
|
||||
$oidDer = $this->asn1->encodeObjectIdentifier($oid);
|
||||
$offset = 0;
|
||||
$length = \strlen($attrsDer);
|
||||
while ($offset < $length) {
|
||||
$attribute = $this->asn1->readTlv($attrsDer, $offset);
|
||||
$inner = 0;
|
||||
$attrOid = $this->asn1->readTlv($attribute['value'], $inner);
|
||||
if ($attrOid['raw'] === $oidDer) {
|
||||
$set = $this->asn1->readTlv($attribute['value'], $inner);
|
||||
$valueOffset = 0;
|
||||
return $this->asn1->readTlv($set['value'], $valueOffset);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate a CMS ContentInfo to the SignerInfo fields under test.
|
||||
*
|
||||
* @return array{signed_attrs: array{tag:int,value:string,raw:string}, signature: array{tag:int,value:string,raw:string}, certificates: array{tag:int,value:string,raw:string}, unsigned_attrs: array{tag:int,value:string,raw:string}|null}
|
||||
*/
|
||||
private function parseSignerInfo(string $cms): array
|
||||
{
|
||||
$offset = 0;
|
||||
$contentInfo = $this->asn1->readTlv($cms, $offset);
|
||||
|
||||
$ciOffset = 0;
|
||||
$this->asn1->readTlv($contentInfo['value'], $ciOffset); // contentType OID
|
||||
$explicit = $this->asn1->readTlv($contentInfo['value'], $ciOffset); // [0] EXPLICIT
|
||||
|
||||
$sdOffset = 0;
|
||||
$signedData = $this->asn1->readTlv($explicit['value'], $sdOffset);
|
||||
|
||||
$sdInner = 0;
|
||||
$this->asn1->readTlv($signedData['value'], $sdInner); // version
|
||||
$this->asn1->readTlv($signedData['value'], $sdInner); // digestAlgorithms
|
||||
$this->asn1->readTlv($signedData['value'], $sdInner); // encapContentInfo
|
||||
$certificates = $this->asn1->readTlv($signedData['value'], $sdInner); // certificates [0]
|
||||
$signerInfos = $this->asn1->readTlv($signedData['value'], $sdInner); // signerInfos SET
|
||||
|
||||
$siOffset = 0;
|
||||
$signerInfo = $this->asn1->readTlv($signerInfos['value'], $siOffset);
|
||||
|
||||
$siInner = 0;
|
||||
$this->asn1->readTlv($signerInfo['value'], $siInner); // version
|
||||
$this->asn1->readTlv($signerInfo['value'], $siInner); // sid
|
||||
$this->asn1->readTlv($signerInfo['value'], $siInner); // digestAlgorithm
|
||||
$signedAttrs = $this->asn1->readTlv($signerInfo['value'], $siInner); // [0] IMPLICIT
|
||||
$this->asn1->readTlv($signerInfo['value'], $siInner); // signatureAlgorithm
|
||||
$signature = $this->asn1->readTlv($signerInfo['value'], $siInner); // signature
|
||||
|
||||
$unsignedAttrs = null;
|
||||
if ($siInner < \strlen($signerInfo['value'])) {
|
||||
$unsignedAttrs = $this->asn1->readTlv($signerInfo['value'], $siInner); // [1] IMPLICIT
|
||||
}
|
||||
|
||||
return [
|
||||
'signed_attrs' => $signedAttrs,
|
||||
'signature' => $signature,
|
||||
'certificates' => $certificates,
|
||||
'unsigned_attrs' => $unsignedAttrs,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user