diff --git a/phpseclib/File/X509.php b/phpseclib/File/X509.php index ba6a0481..639a0233 100644 --- a/phpseclib/File/X509.php +++ b/phpseclib/File/X509.php @@ -267,6 +267,12 @@ class X509 */ private $challenge; + /** + * @var array + * @access private + */ + private $extensionValues = []; + /** * OIDs loaded * @@ -291,6 +297,12 @@ class X509 */ private static $disable_url_fetch = false; + /** + * @var array + * @access private + */ + private static $extensions = []; + /** * Default Constructor. * @@ -558,6 +570,10 @@ class X509 $filters['distributionPoint']['fullName']['directoryName']['rdnSequence']['value'] = $type_utf8_string; $filters['directoryName']['rdnSequence']['value'] = $type_utf8_string; + foreach (self::$extensions as $extension) { + $filters['tbsCertificate']['extensions'][] = $extension; + } + /* in the case of policyQualifiers/qualifier, the type has to be \phpseclib3\File\ASN1::TYPE_IA5_STRING. \phpseclib3\File\ASN1::TYPE_PRINTABLE_STRING will cause OpenSSL's X.509 parser to spit out random characters. @@ -641,6 +657,13 @@ class X509 */ private function mapOutExtensions(&$root, $path) { + foreach ($this->extensionValues as $id => $value) { + $root['tbsCertificate']['extensions'][] = [ + 'extnId' => $id, + 'extnValue' => $value, + ]; + } + $extensions = &$this->subArray($root, $path); if (is_array($extensions)) { @@ -850,6 +873,10 @@ class X509 return true; } + if (isset(self::$extensions[$extnId])) { + return self::$extensions[$extnId]; + } + switch ($extnId) { case 'id-ce-keyUsage': return Maps\KeyUsage::MAP; @@ -3996,4 +4023,44 @@ class X509 return false; } + + /** + * Register the mapping for a custom/unsupported extension. + * + * @param string $id + * @param array $mapping + */ + public static function registerExtension($id, array $mapping) + { + if (isset(self::$extensions[$id]) && self::$extensions[$id] !== $mapping) { + throw new \RuntimeException( + 'Extension ' . $id . ' has already been defined with a different mapping.' + ); + } + + self::$extensions[$id] = $mapping; + } + + /** + * Register the mapping for a custom/unsupported extension. + * + * @param string $id + * + * @return array|null + */ + public static function getRegisteredExtension($id) + { + return isset(self::$extensions[$id]) ? self::$extensions[$id] : null; + } + + /** + * Register the mapping for a custom/unsupported extension. + * + * @param string $id + * @param mixed $value + */ + public function setExtensionValue($id, $value) + { + $this->extensionValues[$id] = $value; + } } diff --git a/tests/Unit/File/X509/X509ExtensionTest.php b/tests/Unit/File/X509/X509ExtensionTest.php new file mode 100644 index 00000000..4613d9e3 --- /dev/null +++ b/tests/Unit/File/X509/X509ExtensionTest.php @@ -0,0 +1,117 @@ + + * @copyright 2014 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + */ + +use phpseclib3\Crypt\RSA; +use phpseclib3\File\ASN1; +use phpseclib3\File\X509; + +class Unit_File_X509_X509ExtensionTest extends PhpseclibTestCase +{ + public function testCustomExtension() + { + $customExtensionData = [ + 'toggle' => true, + 'num' => 3, + 'name' => 'Johnny', + 'list' => ['foo', 'bar'], + ]; + $customExtensionName = 'cust'; + $customExtensionNumber = '2.16.840.1.101.3.4.2.99'; + $customExtensionMapping = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'toggle' => ['type' => ASN1::TYPE_BOOLEAN], + 'num' => ['type' => ASN1::TYPE_INTEGER], + 'name' => ['type' => ASN1::TYPE_OCTET_STRING], + 'list' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 0, + 'max' => -1, + 'children' => ['type' => ASN1::TYPE_OCTET_STRING], + ], + ], + ]; + + ASN1::loadOIDs([ + $customExtensionName => $customExtensionNumber, + ]); + + X509::registerExtension($customExtensionName, $customExtensionMapping); + + $privateKey = RSA::createKey(); + + $publicKey = $privateKey->getPublicKey(); + + $subject = new X509(); + $subject->setDNProp('id-at-organizationName', 'phpseclib CA cert'); + $subject->setPublicKey($publicKey); + + $issuer = new X509(); + $issuer->setPrivateKey($privateKey); + $issuer->setDN($subject->getDN()); + + $x509 = new X509(); + $x509->setExtensionValue($customExtensionName, $customExtensionData); + $x509->makeCA(); + + $result = $x509->sign($issuer, $subject); + + $certificate = $x509->saveX509($result); + + $x509 = new X509(); + + $decodedData = $x509->loadX509($certificate); + $customExtensionDecodedData = null; + + foreach ($decodedData['tbsCertificate']['extensions'] as $extension) { + if ($extension['extnId'] === $customExtensionName) { + $customExtensionDecodedData = $extension['extnValue']; + + break; + } + } + + $this->assertTrue($customExtensionDecodedData['toggle']); + $this->assertInstanceOf('phpseclib3\Math\BigInteger', $customExtensionDecodedData['num']); + $this->assertSame('3', (string) $customExtensionDecodedData['num']); + $this->assertSame('Johnny', $customExtensionDecodedData['name']); + $this->assertSame(['foo', 'bar'], $customExtensionDecodedData['list']); + $this->assertSame($customExtensionMapping, X509::getRegisteredExtension($customExtensionName)); + } + + public function testCustomExtensionRegisterTwiceTheSame() + { + $customExtensionMapping = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'toggle' => ['type' => ASN1::TYPE_BOOLEAN], + 'num' => ['type' => ASN1::TYPE_INTEGER], + 'name' => ['type' => ASN1::TYPE_OCTET_STRING], + 'list' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 0, + 'max' => -1, + 'children' => ['type' => ASN1::TYPE_OCTET_STRING], + ], + ], + ]; + + X509::registerExtension('foo', $customExtensionMapping); + X509::registerExtension('foo', $customExtensionMapping); + + $this->assertSame($customExtensionMapping, X509::getRegisteredExtension('foo')); + } + + public function testCustomExtensionRegisterConflict() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Extension bar has already been defined with a different mapping.'); + + X509::registerExtension('bar', ['type' => ASN1::TYPE_OCTET_STRING]); + X509::registerExtension('bar', ['type' => ASN1::TYPE_ANY]); + } +}