diff --git a/.gitignore b/.gitignore index 6c7bb70..ac92ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,4 @@ composer.lock vendor -# Testing env variables. -envs.local.php - -.php_cs.cache -.phpunit.result.cache +.phpunit.result.cache diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index a0d3292..0000000 --- a/.php_cs.dist +++ /dev/null @@ -1,21 +0,0 @@ -in(__DIR__.'/src') - ->in(__DIR__.'/tests'); - -return PhpCsFixer\Config::create() - ->setRules([ - '@Symfony' => true, - '@Symfony:risky' => true, - '@DoctrineAnnotation' => true, - 'array_syntax' => ['syntax' => 'short'], - 'phpdoc_summary' => false, - 'no_superfluous_phpdoc_tags' => true, - 'concat_space' => ['spacing' => 'one'], - 'native_constant_invocation' => true, - 'native_function_invocation' => ['include' => ['@compiler_optimized']], - 'ordered_imports' => ['sort_algorithm' => 'alpha'], - ]) - ->setRiskyAllowed(true) - ->setFinder($finder); diff --git a/composer.json b/composer.json index b0ebd0f..4021c0d 100644 --- a/composer.json +++ b/composer.json @@ -22,9 +22,11 @@ "license": "MIT", "require": { "php": "^7.2 || ^8", + "ext-bcmath": "*", "ext-curl": "*", "ext-dom": "*", "ext-openssl": "*", + "ext-zip": "*", "ext-zlib": "*", "ext-json": "*", "phpseclib/phpseclib": "~2.0" diff --git a/envs-example.local.php b/envs-example.local.php deleted file mode 100644 index 4d1f0e5..0000000 --- a/envs-example.local.php +++ /dev/null @@ -1,15 +0,0 @@ -dom->createElement('SegmentNumber'); + $xmlSegmentNumber->setAttribute('lastSegment', 'true'); + $xmlSegmentNumber->nodeValue = (string)$segmentNumber; + + $this->instance->appendChild($xmlSegmentNumber); + + return $this; + } + public function getInstance(): DOMElement { return $this->instance; diff --git a/src/Builders/OrderDetailsBuilder.php b/src/Builders/OrderDetailsBuilder.php index c4ea7ba..7d6f34e 100644 --- a/src/Builders/OrderDetailsBuilder.php +++ b/src/Builders/OrderDetailsBuilder.php @@ -17,6 +17,7 @@ class OrderDetailsBuilder const ORDER_ATTRIBUTE_DZNNN = 'DZNNN'; const ORDER_ATTRIBUTE_DZHNN = 'DZHNN'; + const ORDER_ATTRIBUTE_OZHNN = 'OZHNN'; /** * @var DOMElement diff --git a/src/Builders/StaticBuilder.php b/src/Builders/StaticBuilder.php index d6183f3..42570a9 100644 --- a/src/Builders/StaticBuilder.php +++ b/src/Builders/StaticBuilder.php @@ -138,7 +138,7 @@ public function addBank(KeyRing $keyRing, string $algorithm = 'sha256'): StaticB $xmlAuthentication = $this->dom->createElement('Authentication'); $xmlAuthentication->setAttribute('Version', $keyRing->getBankSignatureXVersion()); $xmlAuthentication->setAttribute('Algorithm', sprintf('http://www.w3.org/2001/04/xmlenc#%s', $algorithm)); - $certificateXDigest = $this->cryptService->calculateDigest($signatureX, $algorithm); + $certificateXDigest = $this->cryptService->calculateDigest($signatureX, $algorithm, true); $xmlAuthentication->nodeValue = base64_encode($certificateXDigest); $xmlBankPubKeyDigests->appendChild($xmlAuthentication); @@ -146,7 +146,7 @@ public function addBank(KeyRing $keyRing, string $algorithm = 'sha256'): StaticB $xmlEncryption = $this->dom->createElement('Encryption'); $xmlEncryption->setAttribute('Version', $keyRing->getBankSignatureEVersion()); $xmlEncryption->setAttribute('Algorithm', sprintf('http://www.w3.org/2001/04/xmlenc#%s', $algorithm)); - $certificateEDigest = $this->cryptService->calculateDigest($signatureE, $algorithm); + $certificateEDigest = $this->cryptService->calculateDigest($signatureE, $algorithm, true); $xmlEncryption->nodeValue = base64_encode($certificateEDigest); $xmlBankPubKeyDigests->appendChild($xmlEncryption); diff --git a/src/Contexts/RequestContext.php b/src/Contexts/RequestContext.php index c4962ad..4c71214 100644 --- a/src/Contexts/RequestContext.php +++ b/src/Contexts/RequestContext.php @@ -76,6 +76,11 @@ class RequestContext */ private $receiptCode; + /** + * @var int + */ + private $segmentNumber; + /** * @var string */ @@ -225,6 +230,18 @@ public function getReceiptCode(): string return $this->receiptCode; } + public function setSegmentNumber(int $segmentNumber): RequestContext + { + $this->segmentNumber = $segmentNumber; + + return $this; + } + + public function getSegmentNumber(): int + { + return $this->segmentNumber; + } + public function setTransactionId(string $transactionId): RequestContext { $this->transactionId = $transactionId; diff --git a/src/Contracts/Crypt/AESInterface.php b/src/Contracts/Crypt/AESInterface.php index 8e32eed..943760f 100644 --- a/src/Contracts/Crypt/AESInterface.php +++ b/src/Contracts/Crypt/AESInterface.php @@ -12,30 +12,55 @@ interface AESInterface { /** + * Sets the key length + * + * Valid key lengths are 128, 192, and 256. If the length is less than 128, it will be rounded up to + * 128. If the length is greater than 128 and invalid, it will be rounded down to the closest valid amount. + * * @param int $length * * @return void - * @see \phpseclib\Crypt\AES::setKeyLength() */ - public function setKeyLength($length); + public function setKeyLength(int $length); /** + * Sets the key. + * + * Rijndael supports five different key lengths, AES only supports three. + * * @param string $key * * @return void - * @see \phpseclib\Crypt\AES::setKey() */ - public function setKey($key); + public function setKey(string $key); + + /** + * Sets the initialization vector. (optional) + * + * SetIV is not required when self::MODE_ECB (or ie for AES: AES::MODE_ECB) is being used. + * If not explicitly set, it'll be assumed to be all zero's. + * + * @param string $iv + * + * @return void + */ + public function setIV(string $iv); /** + * Decrypts a message. + * + * If strlen($ciphertext) is not a multiple of the block size, null bytes will be added + * to the end of the string until it is. + * * @param string $ciphertext * * @return string $plaintext - * @see \phpseclib\Crypt\AES::decrypt() */ - public function decrypt($ciphertext); + public function decrypt(string $ciphertext); /** + * Set options. + * * @param mixed $options * * @return void diff --git a/src/Contracts/Crypt/ASN1Interface.php b/src/Contracts/Crypt/ASN1Interface.php new file mode 100644 index 0000000..d6dfb58 --- /dev/null +++ b/src/Contracts/Crypt/ASN1Interface.php @@ -0,0 +1,104 @@ +compare($y) means $x != $y, it, in fact, means the opposite. + * The reason for this is demonstrated thusly: + * + * $x > $y: $x->compare($y) > 0 + * $x < $y: $x->compare($y) < 0 + * $x == $y: $x->compare($y) == 0 + * + * Note how the same comparison operator is used. If you want to test for equality, use $x->equals($y). + * + * @param BigIntegerInterface $y + * + * @return int that is < 0 if $this is less than $y; > 0 if $this is greater than $y, and 0 if they are equal. + * @internal Could return $this->subtract($x), but that's not as fast as what we do do. + */ + public function compare(BigIntegerInterface $y); + + /** + * Performs modular exponentiation. + * + * @param BigIntegerInterface $e + * @param BigIntegerInterface $n + * + * @return BigIntegerInterface + * + * @internal The most naive approach to modular exponentiation has very unreasonable requirements, and + * and although the approach involving repeated squaring does vastly better, it, too, is impractical + * for our purposes. The reason being that division - by far the most complicated and time-consuming + * of the basic operations (eg. +,-,*,/) - occurs multiple times within it. + * + * Modular reductions resolve this issue. Although an individual modular reduction takes more time + * then an individual division, when performed in succession (with the same modulo), they're a lot faster. + * + * The two most commonly used modular reductions are Barrett and Montgomery reduction. Montgomery reduction, + * although faster, only works when the gcd of the modulo and of the base being used is 1. In RSA, when the + * base is a power of two, the modulo - a product of two primes - is always going to have a gcd of 1 (because + * the product of two odd numbers is odd), but what about when RSA isn't used? + * + * In contrast, Barrett reduction has no such constraint. As such, some bigint implementations perform a + * Barrett reduction after every operation in the modpow function. Others perform Barrett reductions when the + * modulo is even and Montgomery reductions when the modulo is odd. BigInteger.java's modPow method, however, + * uses a trick involving the Chinese Remainder Theorem to factor the even modulo into two numbers - one odd and + * the other, a power of two - and recombine them, later. This is the method that this modPow function uses. + * {@link http://islab.oregonstate.edu/papers/j34monex.pdf Montgomery Reduction with Even Modulus} elaborates. + */ + public function modPow(BigIntegerInterface $e, BigIntegerInterface $n); + + /** + * Multiplies two BigIntegers + * + * @param BigIntegerInterface $x + * + * @return BigIntegerInterface + */ + public function multiply(BigIntegerInterface $x); + + /** + * Calculates modular inverses. + * + * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. + * + * @param BigIntegerInterface $n + * + * @return BigIntegerInterface + * @internal See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=21 HAC 14.64} + * for more information. + */ + public function modInverse(BigIntegerInterface $n); + + /** + * Divides two BigIntegers. + * + * Returns an array whose first element contains the quotient and whose second element contains the + * "common residue". If the remainder would be positive, the "common residue" and the remainder are the + * same. If the remainder would be negative, the "common residue" is equal to the sum of the remainder + * and the divisor (basically, the "common residue" is the first positive modulo). + * + * @param BigIntegerInterface $y + * + * @return array + * @internal This function is based off of + * {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=9 HAC 14.20}. + */ + public function divide(BigIntegerInterface $y); + + /** + * Subtracts two BigIntegers. + * + * @param BigIntegerInterface $y + * + * @return BigIntegerInterface + * @internal Performs base-2**52 subtraction + */ + public function subtract(BigIntegerInterface $y); + + /** + * Adds two BigIntegers. + * + * @param BigIntegerInterface $y + * + * @return BigIntegerInterface + * @internal Performs base-2**52 addition + */ + public function add(BigIntegerInterface $y); + + /** + * Generate a random number + * + * Returns a random number between $min and $max where $min and $max + * can be defined using one of the two methods: + * + * @param BigIntegerInterface $arg1 + * @param BigIntegerInterface|false $arg2 + * + * @return BigIntegerInterface + * @internal The API for creating random numbers used to be $a->random($min, $max), + * where $a was a BigInteger object. That method is still supported for BC purposes. + */ + public function random(BigIntegerInterface $arg1, $arg2 = false); + + /** + * Absolute value. + * + * @return BigIntegerInterface + */ + public function abs(); + + /** + * Logical Left Shift + * + * Shifts BigInteger's by $shift bits, effectively multiplying by 2**$shift. + * + * @param int $shift + * + * @return BigIntegerInterface + * @internal The only version that yields any speed increases is the internal version. + */ + public function bitwiseLeftShift(int $shift); + + /** + * Logical Right Shift + * + * Shifts BigInteger's by $shift bits, effectively dividing by 2**$shift. + * + * @param int $shift + * + * @return BigIntegerInterface + * @internal The only version that yields any speed increases is the internal version. + */ + public function bitwiseRightShift(int $shift); + + /** + * Logical Or + * + * @param BigIntegerInterface $x + * + * @return BigIntegerInterface + * @internal Implemented per a request by Lluis Pamies i Juarez + */ + public function bitwiseOr(BigIntegerInterface $x); + + /** + * Logical And + * + * @param BigIntegerInterface $x + * + * @return BigIntegerInterface + * @internal Implemented per a request by Lluis Pamies i Juarez + */ + public function bitwiseAnd(BigIntegerInterface $x); + + /** + * Setup Precision + * + * Some bitwise operations give different results depending on the precision being used. Examples include left + * shift, not, and rotates. + * + * @param int $bits + * + * @return void + */ + public function setupPrecision(int $bits); + + /** + * Get Value. + * + * @return mixed + */ + public function getValue(); + + /** + * Set Value. + * + * @param mixed $value + * + * @return void + */ + public function setValue($value); + + /** + * Set Bitmask. + * + * @param BigIntegerInterface|false $bitmask + * + * @return void + */ + public function setBitmask($bitmask); + + /** + * Set Precision. + * + * @param int $precision + * + * @return void + */ + public function setPrecision(int $precision); + + /** + * Is negative? + * + * @return bool + */ + public function isNegative(); } diff --git a/src/Contracts/Crypt/HashInterface.php b/src/Contracts/Crypt/HashInterface.php new file mode 100644 index 0000000..ac1860f --- /dev/null +++ b/src/Contracts/Crypt/HashInterface.php @@ -0,0 +1,22 @@ + '', * 'partialkey' => '', * ] - * @see \phpseclib\Crypt\RSA::createKey() */ public function createKey($bits = 1024, $timeout = false, $partial = array()); /** + * Returns the public key + * + * The public key is only returned under two circumstances - if the private key + * had the public key embedded within it or if the public key was set via setPublicKey(). + * If the currently loaded key is supposed to be the public key this function won't return + * it since this library, for the most part, doesn't distinguish between public and private keys. + * * @param int $type optional * - * @return string - * @see \phpseclib\Crypt\RSA::getPublicKey() + * @return string|null */ public function getPublicKey($type = RSA::PUBLIC_FORMAT_PKCS8); /** + * Returns the private key + * + * The private key is only returned if the currently loaded key contains the constituent prime numbers. + * * @param int $type optional * - * @return string - * @see \phpseclib\Crypt\RSA::getPrivateKey() + * @return mixed */ public function getPrivateKey($type = RSA::PUBLIC_FORMAT_PKCS1); + + /** + * Create a signature + * + * @param string $message + * + * @return string|null + */ + public function sign(string $message); } diff --git a/src/Contracts/Crypt/TripleDESInterface.php b/src/Contracts/Crypt/TripleDESInterface.php new file mode 100644 index 0000000..8486a09 --- /dev/null +++ b/src/Contracts/Crypt/TripleDESInterface.php @@ -0,0 +1,59 @@ +signatureFactory->createSignatureAFromKeys( $this->cryptService->generateKeys($this->keyRing), + $this->keyRing->getPassword(), $this->bank->isCertified() ? $this->x509Generator : null ); $request = $this->requestFactory->createINI($signatureA, $dateTime); @@ -144,17 +146,19 @@ public function INI(DateTime $dateTime = null): Response * @inheritDoc * @throws EbicsException */ - public function HIA(DateTime $dateTime = null): Response + public function HIA(DateTimeInterface $dateTime = null): Response { if (null === $dateTime) { $dateTime = new DateTime(); } $signatureE = $this->signatureFactory->createSignatureEFromKeys( $this->cryptService->generateKeys($this->keyRing), + $this->keyRing->getPassword(), $this->bank->isCertified() ? $this->x509Generator : null ); $signatureX = $this->signatureFactory->createSignatureXFromKeys( $this->cryptService->generateKeys($this->keyRing), + $this->keyRing->getPassword(), $this->bank->isCertified() ? $this->x509Generator : null ); $request = $this->requestFactory->createHIA($signatureE, $signatureX, $dateTime); @@ -171,7 +175,7 @@ public function HIA(DateTime $dateTime = null): Response * @inheritDoc * @throws Exceptions\EbicsException */ - public function HPB(DateTime $dateTime = null): Response + public function HPB(DateTimeInterface $dateTime = null): Response { if (null === $dateTime) { $dateTime = new DateTime(); @@ -196,7 +200,7 @@ public function HPB(DateTime $dateTime = null): Response * @inheritDoc * @throws Exceptions\EbicsException */ - public function HPD(DateTime $dateTime = null): Response + public function HPD(DateTimeInterface $dateTime = null): Response { if (null === $dateTime) { $dateTime = new DateTime(); @@ -219,7 +223,7 @@ public function HPD(DateTime $dateTime = null): Response * @inheritDoc * @throws Exceptions\EbicsException */ - public function HKD(DateTime $dateTime = null): Response + public function HKD(DateTimeInterface $dateTime = null): Response { if (null === $dateTime) { $dateTime = new DateTime(); @@ -242,7 +246,7 @@ public function HKD(DateTime $dateTime = null): Response * @inheritDoc * @throws Exceptions\EbicsException */ - public function HTD(DateTime $dateTime = null): Response + public function HTD(DateTimeInterface $dateTime = null): Response { if (null === $dateTime) { $dateTime = new DateTime(); @@ -270,9 +274,9 @@ public function FDL( $fileInfo, $format = 'plain', $countryCode = 'FR', - DateTime $dateTime = null, - DateTime $startDateTime = null, - DateTime $endDateTime = null + DateTimeInterface $dateTime = null, + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null ): Response { if (null === $dateTime) { $dateTime = new DateTime(); @@ -286,7 +290,7 @@ public function FDL( $transaction = $this->responseHandler->retrieveTransaction($response); $response->addTransaction($transaction); - // Prepare decrypted datas. + // Prepare decrypted data. $orderDataEncrypted = $this->responseHandler->retrieveOrderData($response); switch ($format) { case 'plain': @@ -310,7 +314,7 @@ public function transferReceipt(Response $response, bool $acknowledged = true): { $lastTransaction = $response->getLastTransaction(); if (null === $lastTransaction) { - throw new EbicsException('There is no transactions to mark as received'); + throw new EbicsException('There is no transactions to mark as received.'); } $request = $this->requestFactory->createTransferReceipt($lastTransaction->getId(), $acknowledged); @@ -323,10 +327,32 @@ public function transferReceipt(Response $response, bool $acknowledged = true): /** * @inheritDoc - * @throws Exceptions\EbicsException */ - public function HAA(DateTime $dateTime = null, string $phase = null, string $transactionId = null): Response + public function transferTransfer(Response $response, int $segmentNumber = 1): Response { + $lastTransaction = $response->getLastTransaction(); + if (null === $lastTransaction) { + throw new EbicsException('There is no transactions to mark as transferred.'); + } + + // TODO: Add order data. + $request = $this->requestFactory->createTransferTransfer($lastTransaction->getId(), $segmentNumber); + $response = $this->httpClient->post($this->bank->getUrl(), $request); + + $this->checkH004ReturnCode($request, $response); + + return $response; + } + + /** + * @inheritDoc + * @throws Exceptions\EbicsException + */ + public function HAA( + DateTimeInterface $dateTime = null, + string $phase = null, + string $transactionId = null + ): Response { if (null === $dateTime) { $dateTime = new DateTime(); } @@ -349,9 +375,9 @@ public function HAA(DateTime $dateTime = null, string $phase = null, string $tra * @throws Exceptions\EbicsException */ public function VMK( - DateTime $dateTime = null, - DateTime $startDateTime = null, - DateTime $endDateTime = null + DateTimeInterface $dateTime = null, + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null ): Response { if (null === $dateTime) { $dateTime = new DateTime(); @@ -360,6 +386,12 @@ public function VMK( $response = $this->httpClient->post($this->bank->getUrl(), $request); $this->checkH004ReturnCode($request, $response); + $transaction = $this->responseHandler->retrieveTransaction($response); + $response->addTransaction($transaction); + // Prepare decrypted OrderData. + $orderDataEncrypted = $this->responseHandler->retrieveOrderData($response); + $orderDataContent = $this->cryptService->decryptOrderDataContent($this->keyRing, $orderDataEncrypted); + $transaction->setPlainOrderData($orderDataContent); return $response; } @@ -369,9 +401,9 @@ public function VMK( * @throws Exceptions\EbicsException */ public function STA( - DateTime $dateTime = null, - DateTime $startDateTime = null, - DateTime $endDateTime = null + DateTimeInterface $dateTime = null, + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null ): Response { if (null === $dateTime) { $dateTime = new DateTime(); @@ -380,6 +412,12 @@ public function STA( $response = $this->httpClient->post($this->bank->getUrl(), $request); $this->checkH004ReturnCode($request, $response); + $transaction = $this->responseHandler->retrieveTransaction($response); + $response->addTransaction($transaction); + // Prepare decrypted OrderData. + $orderDataEncrypted = $this->responseHandler->retrieveOrderData($response); + $orderDataContent = $this->cryptService->decryptOrderDataContent($this->keyRing, $orderDataEncrypted); + $transaction->setPlainOrderData($orderDataContent); return $response; } @@ -390,9 +428,9 @@ public function STA( */ // @codingStandardsIgnoreStart public function C53( - DateTime $dateTime = null, - DateTime $startDateTime = null, - DateTime $endDateTime = null + DateTimeInterface $dateTime = null, + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null ): Response { // @codingStandardsIgnoreEnd if (null === $dateTime) { @@ -403,6 +441,12 @@ public function C53( $this->checkH004ReturnCode($request, $response); + $transaction = $this->responseHandler->retrieveTransaction($response); + $response->addTransaction($transaction); + $orderDataEncrypted = $this->responseHandler->retrieveOrderData($response); + $orderDataItems = $this->cryptService->decryptOrderDataItems($this->keyRing, $orderDataEncrypted); + $transaction->setOrderDataItems($orderDataItems); + return $response; } @@ -412,9 +456,9 @@ public function C53( */ // @codingStandardsIgnoreStart public function Z53( - DateTime $dateTime = null, - DateTime $startDateTime = null, - DateTime $endDateTime = null + DateTimeInterface $dateTime = null, + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null ): Response { // @codingStandardsIgnoreEnd if (null === $dateTime) { @@ -428,6 +472,36 @@ public function Z53( return $response; } + public function CCT(DateTimeInterface $dateTime = null): Response + { + if (null === $dateTime) { + $dateTime = new DateTime(); + } + $request = $this->requestFactory->createCCT($dateTime); + $response = $this->httpClient->post($this->bank->getUrl(), $request); + + $this->checkH004ReturnCode($request, $response); + $transaction = $this->responseHandler->retrieveTransaction($response); + $response->addTransaction($transaction); + + return $response; + } + + public function CDD(DateTimeInterface $dateTime = null): Response + { + if (null === $dateTime) { + $dateTime = new DateTime(); + } + $request = $this->requestFactory->createCDD($dateTime); + $response = $this->httpClient->post($this->bank->getUrl(), $request); + + $this->checkH004ReturnCode($request, $response); + $transaction = $this->responseHandler->retrieveTransaction($response); + $response->addTransaction($transaction); + + return $response; + } + /** * @param Request $request * @param Response $response diff --git a/src/Factories/Crypt/AESFactory.php b/src/Factories/Crypt/AESFactory.php index 4c76dcb..51b8526 100644 --- a/src/Factories/Crypt/AESFactory.php +++ b/src/Factories/Crypt/AESFactory.php @@ -15,12 +15,10 @@ class AESFactory { /** - * @param int $mode - * * @return AESInterface */ - public function create($mode = AES::MODE_CBC): AESInterface + public function create(): AESInterface { - return new AES($mode); + return new AES(); } } diff --git a/src/Factories/Crypt/BigIntegerFactory.php b/src/Factories/Crypt/BigIntegerFactory.php index 3882a98..be6dfb7 100644 --- a/src/Factories/Crypt/BigIntegerFactory.php +++ b/src/Factories/Crypt/BigIntegerFactory.php @@ -15,7 +15,7 @@ class BigIntegerFactory { /** - * @param int|string|resource $x base-10 number or base-$base number if $base set. + * @param int|string $x base-10 number or base-$base number if $base set. * @param int $base * * @return BigIntegerInterface @@ -24,20 +24,4 @@ public function create($x = 0, int $base = 10): BigIntegerInterface { return new BigInteger($x, $base); } - - /** - * Cast Big integer from phpseclib. - * - * @param \phpseclib\Math\BigInteger $bigInteger - * - * @return BigIntegerInterface - */ - public static function createFromPhpSecLib(\phpseclib\Math\BigInteger $bigInteger): BigIntegerInterface - { - $obj = new BigInteger(); - foreach (get_object_vars($bigInteger) as $key => $name) { - $obj->$key = $name; - } - return $obj; - } } diff --git a/src/Factories/RequestFactory.php b/src/Factories/RequestFactory.php index 2fb5a37..d21ab9f 100644 --- a/src/Factories/RequestFactory.php +++ b/src/Factories/RequestFactory.php @@ -21,7 +21,7 @@ use AndrewSvirin\Ebics\Models\KeyRing; use AndrewSvirin\Ebics\Models\OrderData; use AndrewSvirin\Ebics\Models\User; -use DateTime; +use DateTimeInterface; /** * Class RequestFactory represents producers for the @see Request. @@ -79,7 +79,7 @@ public function __construct(Bank $bank, User $user, KeyRing $keyRing) $this->keyRing = $keyRing; } - public function createINI(SignatureInterface $certificateA, DateTime $dateTime): Request + public function createINI(SignatureInterface $certificateA, DateTimeInterface $dateTime): Request { $context = (new RequestContext()) ->setBank($this->bank) @@ -138,7 +138,7 @@ public function createHEV(): Request public function createHIA( SignatureInterface $certificateE, SignatureInterface $certificateX, - DateTime $dateTime + DateTimeInterface $dateTime ): Request { $context = (new RequestContext()) ->setBank($this->bank) @@ -183,12 +183,12 @@ public function createHIA( } /** - * @param DateTime $dateTime + * @param DateTimeInterface $dateTime * * @return Request * @throws EbicsException */ - public function createHPB(DateTime $dateTime): Request + public function createHPB(DateTimeInterface $dateTime): Request { $context = (new RequestContext()) ->setBank($this->bank) @@ -224,12 +224,12 @@ public function createHPB(DateTime $dateTime): Request } /** - * @param DateTime $dateTime + * @param DateTimeInterface $dateTime * * @return Request * @throws EbicsException */ - public function createHPD(DateTime $dateTime): Request + public function createHPD(DateTimeInterface $dateTime): Request { $context = (new RequestContext()) ->setBank($this->bank) @@ -270,12 +270,12 @@ public function createHPD(DateTime $dateTime): Request } /** - * @param DateTime $dateTime + * @param DateTimeInterface $dateTime * * @return Request * @throws EbicsException */ - public function createHKD(DateTime $dateTime): Request + public function createHKD(DateTimeInterface $dateTime): Request { $context = (new RequestContext()) ->setBank($this->bank) @@ -316,12 +316,12 @@ public function createHKD(DateTime $dateTime): Request } /** - * @param DateTime $dateTime + * @param DateTimeInterface $dateTime * * @return Request * @throws EbicsException */ - public function createHTD(DateTime $dateTime): Request + public function createHTD(DateTimeInterface $dateTime): Request { $context = (new RequestContext()) ->setBank($this->bank) @@ -362,21 +362,21 @@ public function createHTD(DateTime $dateTime): Request } /** - * @param DateTime $dateTime + * @param DateTimeInterface $dateTime * @param string $fileFormat * @param string $countryCode - * @param DateTime|null $startDateTime - * @param DateTime|null $endDateTime + * @param DateTimeInterface|null $startDateTime + * @param DateTimeInterface|null $endDateTime * * @return Request * @throws EbicsException */ public function createFDL( - DateTime $dateTime, + DateTimeInterface $dateTime, string $fileFormat, string $countryCode = 'FR', - DateTime $startDateTime = null, - DateTime $endDateTime = null + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null ): Request { $context = (new RequestContext()) ->setBank($this->bank) @@ -426,12 +426,12 @@ public function createFDL( } /** - * @param DateTime $dateTime + * @param DateTimeInterface $dateTime * * @return Request * @throws EbicsException */ - public function createHAA(DateTime $dateTime): Request + public function createHAA(DateTimeInterface $dateTime): Request { $context = (new RequestContext()) ->setBank($this->bank) @@ -511,15 +511,60 @@ public function createTransferReceipt(string $transactionId, bool $acknowledged) } /** - * @param DateTime $dateTime - * @param DateTime|null $startDateTime - * @param DateTime|null $endDateTime + * @param string $transactionId + * @param int $segmentNumber * * @return Request * @throws EbicsException */ - public function createVMK(DateTime $dateTime, DateTime $startDateTime = null, DateTime $endDateTime = null): Request + public function createTransferTransfer(string $transactionId, int $segmentNumber): Request { + $context = (new RequestContext()) + ->setBank($this->bank) + ->setTransactionId($transactionId) + ->setSegmentNumber($segmentNumber); + + $request = $this->requestBuilder + ->createInstance() + ->addContainerSecured(function (XmlBuilder $builder) use ($context) { + $builder->addHeader(function (HeaderBuilder $builder) use ($context) { + $builder->addStatic(function (StaticBuilder $builder) use ($context) { + $builder + ->addHostId($context->getBank()->getHostId()) + ->addTransactionId($context->getTransactionId()); + })->addMutable(function (MutableBuilder $builder) use ($context) { + $builder + ->addTransactionPhase(MutableBuilder::PHASE_TRANSFER) + ->addSegmentNumber($context->getSegmentNumber()); + }); + })->addBody(function (BodyBuilder $builder) { + $builder->addDataTransfer(function (DataTransferBuilder $builder) { + $orderData = new OrderData(); + // TODO: Add order data. + $builder->addOrderData($orderData->getContent()); + }); + }); + }) + ->popInstance(); + + $this->authSignatureHandler->handle($request); + + return $request; + } + + /** + * @param DateTimeInterface $dateTime + * @param DateTimeInterface|null $startDateTime + * @param DateTimeInterface|null $endDateTime + * + * @return Request + * @throws EbicsException + */ + public function createVMK( + DateTimeInterface $dateTime, + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null + ): Request { $context = (new RequestContext()) ->setBank($this->bank) ->setUser($this->user) @@ -561,15 +606,18 @@ public function createVMK(DateTime $dateTime, DateTime $startDateTime = null, Da } /** - * @param DateTime $dateTime - * @param DateTime|null $startDateTime - * @param DateTime|null $endDateTime + * @param DateTimeInterface $dateTime + * @param DateTimeInterface|null $startDateTime + * @param DateTimeInterface|null $endDateTime * * @return Request * @throws EbicsException */ - public function createSTA(DateTime $dateTime, DateTime $startDateTime = null, DateTime $endDateTime = null): Request - { + public function createSTA( + DateTimeInterface $dateTime, + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null + ): Request { $context = (new RequestContext()) ->setBank($this->bank) ->setUser($this->user) @@ -611,15 +659,18 @@ public function createSTA(DateTime $dateTime, DateTime $startDateTime = null, Da } /** - * @param DateTime $dateTime - * @param DateTime|null $startDateTime - * @param DateTime|null $endDateTime + * @param DateTimeInterface $dateTime + * @param DateTimeInterface|null $startDateTime + * @param DateTimeInterface|null $endDateTime * * @return Request * @throws EbicsException */ - public function createC53(DateTime $dateTime, DateTime $startDateTime = null, DateTime $endDateTime = null): Request - { + public function createC53( + DateTimeInterface $dateTime, + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null + ): Request { $context = (new RequestContext()) ->setBank($this->bank) ->setUser($this->user) @@ -661,15 +712,18 @@ public function createC53(DateTime $dateTime, DateTime $startDateTime = null, Da } /** - * @param DateTime $dateTime - * @param DateTime|null $startDateTime - * @param DateTime|null $endDateTime + * @param DateTimeInterface $dateTime + * @param DateTimeInterface|null $startDateTime + * @param DateTimeInterface|null $endDateTime * * @return Request * @throws EbicsException */ - public function createZ53(DateTime $dateTime, DateTime $startDateTime = null, DateTime $endDateTime = null): Request - { + public function createZ53( + DateTimeInterface $dateTime, + DateTimeInterface $startDateTime = null, + DateTimeInterface $endDateTime = null + ): Request { $context = (new RequestContext()) ->setBank($this->bank) ->setUser($this->user) @@ -709,4 +763,84 @@ public function createZ53(DateTime $dateTime, DateTime $startDateTime = null, Da return $request; } + + public function createCCT(DateTimeInterface $dateTime): Request + { + $context = (new RequestContext()) + ->setBank($this->bank) + ->setUser($this->user) + ->setKeyRing($this->keyRing) + ->setDateTime($dateTime); + + $request = $this->requestBuilder + ->createInstance() + ->addContainerSecured(function (XmlBuilder $builder) use ($context) { + $builder->addHeader(function (HeaderBuilder $builder) use ($context) { + $builder->addStatic(function (StaticBuilder $builder) use ($context) { + $builder + ->addHostId($context->getBank()->getHostId()) + ->addRandomNonce() + ->addTimestamp($context->getDateTime()) + ->addPartnerId($context->getUser()->getPartnerId()) + ->addUserId($context->getUser()->getUserId()) + ->addProduct('Ebics client PHP', 'de') + ->addOrderDetails(function (OrderDetailsBuilder $orderDetailsBuilder) { + $orderDetailsBuilder + ->addOrderType('CCT') + ->addOrderAttribute(OrderDetailsBuilder::ORDER_ATTRIBUTE_OZHNN) + ->addStandardOrderParams(); + }) + ->addBank($context->getKeyRing()) + ->addSecurityMedium(StaticBuilder::SECURITY_MEDIUM_0000); + })->addMutable(function (MutableBuilder $builder) { + $builder->addTransactionPhase(MutableBuilder::PHASE_INITIALIZATION); + }); + })->addBody(); + }) + ->popInstance(); + + $this->authSignatureHandler->handle($request); + + return $request; + } + + public function createCDD(DateTimeInterface $dateTime): Request + { + $context = (new RequestContext()) + ->setBank($this->bank) + ->setUser($this->user) + ->setKeyRing($this->keyRing) + ->setDateTime($dateTime); + + $request = $this->requestBuilder + ->createInstance() + ->addContainerSecured(function (XmlBuilder $builder) use ($context) { + $builder->addHeader(function (HeaderBuilder $builder) use ($context) { + $builder->addStatic(function (StaticBuilder $builder) use ($context) { + $builder + ->addHostId($context->getBank()->getHostId()) + ->addRandomNonce() + ->addTimestamp($context->getDateTime()) + ->addPartnerId($context->getUser()->getPartnerId()) + ->addUserId($context->getUser()->getUserId()) + ->addProduct('Ebics client PHP', 'de') + ->addOrderDetails(function (OrderDetailsBuilder $orderDetailsBuilder) { + $orderDetailsBuilder + ->addOrderType('CDD') + ->addOrderAttribute(OrderDetailsBuilder::ORDER_ATTRIBUTE_OZHNN) + ->addStandardOrderParams(); + }) + ->addBank($context->getKeyRing()) + ->addSecurityMedium(StaticBuilder::SECURITY_MEDIUM_0000); + })->addMutable(function (MutableBuilder $builder) { + $builder->addTransactionPhase(MutableBuilder::PHASE_INITIALIZATION); + }); + })->addBody(); + }) + ->popInstance(); + + $this->authSignatureHandler->handle($request); + + return $request; + } } diff --git a/src/Factories/SignatureBankLetterFactory.php b/src/Factories/SignatureBankLetterFactory.php index 23a68ea..7f4d874 100644 --- a/src/Factories/SignatureBankLetterFactory.php +++ b/src/Factories/SignatureBankLetterFactory.php @@ -19,6 +19,7 @@ class SignatureBankLetterFactory * @param string $exponent * @param string $modulus * @param string $keyHash + * @param int $modulusSize * * @return SignatureBankLetter */ @@ -27,14 +28,16 @@ public function create( string $version, string $exponent, string $modulus, - string $keyHash + string $keyHash, + int $modulusSize ): SignatureBankLetter { return new SignatureBankLetter( $type, $version, $exponent, $modulus, - $keyHash + $keyHash, + $modulusSize ); } } diff --git a/src/Factories/SignatureFactory.php b/src/Factories/SignatureFactory.php index 6c19fb8..fdb3a91 100644 --- a/src/Factories/SignatureFactory.php +++ b/src/Factories/SignatureFactory.php @@ -100,15 +100,17 @@ public function createSignatureX(string $publicKey, string $privateKey = null): * 'publickey' => '', * 'privatekey' => '', * ] + * @param string $password * @param X509GeneratorInterface|null $x509Generator * * @return SignatureInterface */ public function createSignatureAFromKeys( array $keys, + string $password, X509GeneratorInterface $x509Generator = null ): SignatureInterface { - return $this->createSignatureFromKeys($keys, Signature::TYPE_A, $x509Generator); + return $this->createSignatureFromKeys($keys, $password, Signature::TYPE_A, $x509Generator); } /** @@ -116,15 +118,17 @@ public function createSignatureAFromKeys( * 'publickey' => '', * 'privatekey' => '', * ] + * @param string $password * @param X509GeneratorInterface|null $x509Generator * * @return SignatureInterface */ public function createSignatureEFromKeys( array $keys, + string $password, X509GeneratorInterface $x509Generator = null ): SignatureInterface { - return $this->createSignatureFromKeys($keys, Signature::TYPE_E, $x509Generator); + return $this->createSignatureFromKeys($keys, $password, Signature::TYPE_E, $x509Generator); } /** @@ -132,15 +136,17 @@ public function createSignatureEFromKeys( * 'publickey' => '', * 'privatekey' => '', * ] + * @param string $password * @param X509GeneratorInterface|null $x509Generator * * @return SignatureInterface */ public function createSignatureXFromKeys( array $keys, + string $password, X509GeneratorInterface $x509Generator = null ): SignatureInterface { - return $this->createSignatureFromKeys($keys, Signature::TYPE_X, $x509Generator); + return $this->createSignatureFromKeys($keys, $password, Signature::TYPE_X, $x509Generator); } /** @@ -170,6 +176,7 @@ public function createCertificateXFromDetails(string $exponent, string $modulus) * 'publickey' => '', * 'privatekey' => '', * ] + * @param string $password * @param string $type * @param X509GeneratorInterface|null $x509Generator * @@ -177,13 +184,14 @@ public function createCertificateXFromDetails(string $exponent, string $modulus) */ private function createSignatureFromKeys( array $keys, + string $password, string $type, X509GeneratorInterface $x509Generator = null ): SignatureInterface { $signature = new Signature($type, $keys['publickey'], $keys['privatekey']); if (null !== $x509Generator) { - $certificateContent = $this->generateCertificateContent($keys, $type, $x509Generator); + $certificateContent = $this->generateCertificateContent($keys, $password, $type, $x509Generator); $signature->setCertificateContent($certificateContent); } @@ -195,6 +203,7 @@ private function createSignatureFromKeys( * 'publickey' => '', * 'privatekey' => '', * ] + * @param string $password * @param string $type * @param X509GeneratorInterface $x509Generator * @@ -202,10 +211,12 @@ private function createSignatureFromKeys( */ private function generateCertificateContent( array $keys, + string $password, string $type, X509GeneratorInterface $x509Generator ): string { $privateKey = $this->rsaFactory->create(); + $privateKey->setPassword($password); $privateKey->loadKey($keys['privatekey']); $publicKey = $this->rsaFactory->create(); @@ -226,7 +237,11 @@ private function generateCertificateContent( throw new RuntimeException('Unpredictable type.'); } - return $x509->saveX509CurrentCert(); + if (!($currentCert = $x509->saveX509CurrentCert())) { + throw new RuntimeException('Can not save current certificate.'); + } + + return $currentCert; } /** diff --git a/src/Models/CertificateX509.php b/src/Models/CertificateX509.php index 2f2e031..d397e16 100644 --- a/src/Models/CertificateX509.php +++ b/src/Models/CertificateX509.php @@ -2,8 +2,6 @@ namespace AndrewSvirin\Ebics\Models; -use AndrewSvirin\Ebics\Contracts\Crypt\RSAInterface; -use AndrewSvirin\Ebics\Factories\Crypt\BigIntegerFactory; use AndrewSvirin\Ebics\Models\Crypt\X509; use DateTime; @@ -12,8 +10,6 @@ * * @license http://www.opensource.org/licenses/mit-license.html MIT License * @author Andrew Svirin - * - * @method RSAInterface getPublicKey() */ class CertificateX509 extends X509 { @@ -24,9 +20,7 @@ class CertificateX509 extends X509 */ public function getSerialNumber(): string { - $certificateSerialNumber = BigIntegerFactory::createFromPhpSecLib( - $this->currentCert['tbsCertificate']['serialNumber'] - ); + $certificateSerialNumber = $this->currentCert['tbsCertificate']['serialNumber']; return $certificateSerialNumber->toString(); } diff --git a/src/Models/Crypt/AES.php b/src/Models/Crypt/AES.php index 5230ec6..77969a0 100644 --- a/src/Models/Crypt/AES.php +++ b/src/Models/Crypt/AES.php @@ -3,23 +3,358 @@ namespace AndrewSvirin\Ebics\Models\Crypt; use AndrewSvirin\Ebics\Contracts\Crypt\AESInterface; +use LogicException; /** - * Crypt RSA model. - * - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @author Andrew Svirin + * Pure-PHP implementation of AES. */ -class AES extends \phpseclib\Crypt\AES implements AESInterface +class AES implements AESInterface { + /**#@+*/ /** - * @see \phpseclib\Crypt\AES::MODE_CBC + * Base value for the mcrypt implementation $engine switch */ - const MODE_CBC = 2; + const ENGINE_OPENSSL = 3; + /**#@-*/ + + /** + * The Key Length (in bytes) + * + * @var int + */ + protected $key_length = 16; + + /** + * Padding status + * + * @var bool + */ + protected $padding = true; + + /** + * Is the mode one that is paddable? + * + * @var bool + */ + protected $paddable = false; + + /** + * Has the key length explicitly been set or should it be derived from the key, itself? + * + * @var bool + */ + protected $explicit_key_length = false; + + /** + * The Block Length of the block cipher + * + * @var int + */ + protected $block_size = 16; + + /** + * Holds which crypt engine internaly should be use, + * which will be determined automatically on __construct() + * + * Currently available $engines are: + * - self::ENGINE_OPENSSL (very fast, php-extension: openssl, extension_loaded('openssl') required) + * + * @var int|null + */ + protected $engine; + + /** + * Does internal cipher state need to be (re)initialized? + * + * @var bool + */ + protected $changed = true; + + /** + * The Key + * + * @var string + */ + protected $key = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + + /** + * The Initialization Vector + * + * @var string + */ + protected $iv; + + /** + * A "sliding" Initialization Vector + * + * @var string + */ + protected $encryptIV; + + /** + * A "sliding" Initialization Vector + * + * @var string + */ + protected $decryptIV; + + /** + * The openssl specific name of the cipher in ECB mode + * + * If OpenSSL does not support the mode we're trying to use (CTR) + * it can still be emulated with ECB mode. + * + * @link http://www.php.net/openssl-get-cipher-methods + * @var string + */ + protected $cipherNameOpensslEcb; + + /** + * The openssl specific name of the cipher + * + * Only used if $engine == self::ENGINE_OPENSSL + * + * @link http://www.php.net/openssl-get-cipher-methods + * @var string + */ + protected $cipherNameOpenssl; + + /** + * Determines what options are passed to openssl_encrypt/decrypt + * + * @var mixed + */ + protected $opensslOptions; + + /** + * Default Constructor. + * + * Determines whether or not the mcrypt extension should be used. + */ + public function __construct() + { + // $mode dependent settings + $this->paddable = true; + + $this->setEngine(); + } + + public function setKeyLength($length) + { + switch (true) { + case $length === 128: + $this->key_length = 16; + break; + case $length === 224: + case $length === 256: + $this->key_length = 32; + break; + default: + throw new LogicException('Unhandled length.'); + } + + $this->explicit_key_length = true; + $this->changed = true; + $this->setEngine(); + } + + public function setKey($key) + { + if (!$this->explicit_key_length) { + $this->setKeyLength(strlen($key) << 3); + $this->explicit_key_length = false; + } + + $this->key = $key; + $this->changed = true; + $this->setEngine(); + + if (!$this->explicit_key_length) { + $length = strlen($key); + switch (true) { + case $length <= 16: + $this->key_length = 16; + break; + case $length <= 24: + $this->key_length = 24; + break; + default: + $this->key_length = 32; + } + $this->setEngine(); + } + } + + public function setIV($iv) + { + $this->iv = $iv; + $this->changed = true; + } + + public function decrypt($ciphertext) + { + if ($this->paddable) { + // we pad with chr(0) since that's what mcrypt_generic does. to quote from + // {@link http://www.php.net/function.mcrypt-generic}: "The data is padded with "\0" + // to make sure the length of the data is n * blocksize." + $ciphertext = str_pad( + $ciphertext, + strlen($ciphertext) + ($this->block_size - strlen($ciphertext) % $this->block_size) % $this->block_size, + chr(0) + ); + } + + if ($this->changed) { + $this->clearBuffers(); + $this->changed = false; + } + + if (!defined('OPENSSL_RAW_DATA')) { + $padding = str_repeat(chr($this->block_size), $this->block_size) ^ + substr($ciphertext, -$this->block_size); + + if (!($encrypted = openssl_encrypt( + $padding, + $this->cipherNameOpensslEcb, + $this->key, + $this->opensslOptions + ))) { + throw new LogicException('Encryption failed.'); + } + $ciphertext .= substr( + $encrypted, + 0, + $this->block_size + ); + } + if (!($plaintext = openssl_decrypt( + $ciphertext, + $this->cipherNameOpenssl, + $this->key, + $this->opensslOptions, + $this->decryptIV + ))) { + throw new LogicException('Decryption failed.'); + } + + return $this->paddable ? $this->unpad($plaintext) : $plaintext; + } + + /** + * Sets the engine as appropriate + * + * @return void + */ + private function setEngine() + { + $this->engine = null; + + $engine = self::ENGINE_OPENSSL; + + if ($this->isValidEngine($engine)) { + $this->engine = $engine; + } + + $this->changed = true; + } public function setOpenSSLOptions($options): void { - $this->openssl_options = $options; + $this->opensslOptions = $options; + } + + /** + * Clears internal buffers + * + * Clearing/resetting the internal buffers is done everytime + * after disableContinuousBuffer() or on cipher $engine (re)init + * ie after setKey() or setIV() + * + * @return void + */ + private function clearBuffers() + { + // mcrypt's handling of invalid's $iv: + $this->encryptIV = $this->decryptIV = str_pad(substr($this->iv, 0, $this->block_size), $this->block_size, "\0"); + + $this->key = str_pad(substr($this->key, 0, $this->key_length), $this->key_length, "\0"); + } + + /** + * Test for engine validity + * + * @param int $engine + * + * @return bool + */ + private function isValidEngine(int $engine) + { + if (empty($engine)) { + return false; + } + + switch ($engine) { + case self::ENGINE_OPENSSL: + if ($this->block_size != 16) { + return false; + } + $this->cipherNameOpensslEcb = 'aes-' . ($this->key_length << 3) . '-ecb'; + $this->cipherNameOpenssl = 'aes-' . ($this->key_length << 3) . '-' . $this->opensslTranslateMode(); + break; + default: + throw new LogicException('Unhandled engine.'); + } + + // prior to PHP 5.4.0 OPENSSL_RAW_DATA and OPENSSL_ZERO_PADDING were not defined. + // instead of expecting an integer $options openssl_encrypt expected a boolean $raw_data. + if (!defined('OPENSSL_RAW_DATA')) { + $this->opensslOptions = true; + } else { + $this->opensslOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING; + } + + $methods = openssl_get_cipher_methods(); + if (in_array($this->cipherNameOpenssl, $methods)) { + return true; + } + return false; + } + + /** + * Unpads a string. + * + * If padding is enabled and the reported padding length is invalid the encryption key will be assumed to be wrong + * and false will be returned. + * + * @param string $text + * + * @return string + */ + private function unpad(string $text) + { + if (!$this->padding) { + return $text; + } + + $length = ord($text[strlen($text) - 1]); + + if (!$length || $length > $this->block_size) { + throw new LogicException('Length incorrect.'); + } + + return substr($text, 0, -$length); + } + + /** + * OpenSSL Mode Mapper + * + * May need to be overwritten by classes extending this one in some cases + * + * @return string + */ + private function opensslTranslateMode() + { + return 'cbc'; } } diff --git a/src/Models/Crypt/ASN1.php b/src/Models/Crypt/ASN1.php new file mode 100644 index 0000000..9867017 --- /dev/null +++ b/src/Models/Crypt/ASN1.php @@ -0,0 +1,1327 @@ + true, + self::TYPE_INTEGER => true, + self::TYPE_BIT_STRING => 'bitString', + self::TYPE_OCTET_STRING => 'octetString', + self::TYPE_NULL => 'null', + self::TYPE_OBJECT_IDENTIFIER => 'objectIdentifier', + self::TYPE_REAL => true, + self::TYPE_ENUMERATED => 'enumerated', + self::TYPE_UTF8_STRING => 'utf8String', + self::TYPE_NUMERIC_STRING => 'numericString', + self::TYPE_PRINTABLE_STRING => 'printableString', + self::TYPE_TELETEX_STRING => 'teletexString', + self::TYPE_VIDEOTEX_STRING => 'videotexString', + self::TYPE_IA5_STRING => 'ia5String', + self::TYPE_UTC_TIME => 'utcTime', + self::TYPE_GENERALIZED_TIME => 'generalTime', + self::TYPE_GRAPHIC_STRING => 'graphicString', + self::TYPE_VISIBLE_STRING => 'visibleString', + self::TYPE_GENERAL_STRING => 'generalString', + self::TYPE_UNIVERSAL_STRING => 'universalString', + //self::TYPE_CHARACTER_STRING => 'characterString', + self::TYPE_BMP_STRING => 'bmpString' + ]; + + /** + * String type to character size mapping table. + * + * Non-convertable types are absent from this table. + * size == 0 indicates variable length encoding. + * + * @var array + */ + protected $stringTypeSize = [ + self::TYPE_UTF8_STRING => 0, + self::TYPE_BMP_STRING => 2, + self::TYPE_UNIVERSAL_STRING => 4, + self::TYPE_PRINTABLE_STRING => 1, + self::TYPE_TELETEX_STRING => 1, + self::TYPE_IA5_STRING => 1, + self::TYPE_VISIBLE_STRING => 1, + ]; + + /** + * @var array + */ + protected $location; + + public function loadOIDs($oids) + { + $this->oids = $oids; + } + + public function decodeBER($encoded) + { + // encapsulate in an array for BC with the old decodeBER + return [$this->decodeBERInternal($encoded)]; + } + + /** + * Parse BER-encoding (Helper function) + * + * Sometimes we want to get the BER encoding of a particular tag. $start lets us do that + * without having to reencode. $encoded is passed by reference for the recursive calls done + * for self::TYPE_BIT_STRING and self::TYPE_OCTET_STRING. In those cases, the indefinite length is used. + * + * @param string $encoded + * @param int $start + * @param int $encoded_pos + * + * @return array|false + */ + private function decodeBERInternal(string $encoded, int $start = 0, int $encoded_pos = 0) + { + $current = ['start' => $start]; + + $type = ord($encoded[$encoded_pos++]); + $start++; + + $constructed = ($type >> 5) & 1; + + $tag = $type & 0x1F; + if ($tag == 0x1F) { + $tag = 0; + // process septets (since the eighth bit is ignored, it's not an octet) + do { + $temp = ord($encoded[$encoded_pos++]); + $loop = $temp >> 7; + $tag <<= 7; + $tag |= $temp & 0x7F; + $start++; + } while ($loop); + } + + // Length, as discussed in paragraph 8.1.3 of X.690-0207.pdf#page=13 + $length = ord($encoded[$encoded_pos++]); + $start++; + if ($length == 0x80) { // indefinite length + // "[A sender shall] use the indefinite form (see 8.1.3.6) if the encoding is constructed and is not all + // immediately available." -- paragraph 8.1.3.2.c + $length = strlen($encoded) - $encoded_pos; + } elseif ($length & 0x80) { // definite length, long form + // technically, the long form of the length can be represented by up to 126 octets (bytes), but we'll only + // support it up to four. + $length &= 0x7F; + $temp = substr($encoded, $encoded_pos, $length); + $encoded_pos += $length; + // tags of indefinte length don't really have a header length; this length includes the tag + $current += ['headerlength' => $length + 2]; + $start += $length; + + if (!($unpacked = unpack('Nlength', substr(str_pad($temp, 4, chr(0), STR_PAD_LEFT), -4)))) { + throw new LogicException('Unpack failed.'); + } + extract($unpacked); + } else { + $current += ['headerlength' => 2]; + } + + if ($length > (strlen($encoded) - $encoded_pos)) { + return false; + } + + $content = substr($encoded, $encoded_pos, $length); + $content_pos = 0; + + // at this point $length can be overwritten. it's only accurate for definite length things as is + + /* Class is UNIVERSAL, APPLICATION, PRIVATE, or CONTEXT-SPECIFIC. The UNIVERSAL class is restricted to the ASN.1 + built-in types. It defines an application-independent data type that must be distinguishable from all other + data types. The other three classes are user defined. The APPLICATION class distinguishes data types that + have a wide, scattered use within a particular presentation context. PRIVATE distinguishes data types within + a particular organization or country. CONTEXT-SPECIFIC distinguishes members of a sequence or set, the + alternatives of a CHOICE, or universally tagged set members. Only the class number appears in braces for this + data type; the term CONTEXT-SPECIFIC does not appear. + + -- http://www.obj-sys.com/asn1tutorial/node12.html */ + $class = ($type >> 6) & 3; + switch ($class) { + case self::CLASS_APPLICATION: + case self::CLASS_PRIVATE: + case self::CLASS_CONTEXT_SPECIFIC: + if (!$constructed) { + return [ + 'type' => $class, + 'constant' => $tag, + 'content' => $content, + 'length' => $length + $start - $current['start'] + ]; + } + + $newcontent = []; + $remainingLength = $length; + while ($remainingLength > 0) { + $temp = $this->decodeBERInternal($content, $start, $content_pos); + if ($temp === false) { + break; + } + $length = $temp['length']; + // end-of-content octets - see paragraph 8.1.5 + if (substr($content, $content_pos + $length, 2) == "\0\0") { + $length += 2; + $start += $length; + $newcontent[] = $temp; + break; + } + $start += $length; + $remainingLength -= $length; + $newcontent[] = $temp; + $content_pos += $length; + } + + return [ + 'type' => $class, + 'constant' => $tag, + // the array encapsulation is for BC with the old format + 'content' => $newcontent, + // the only time when $content['headerlength'] isn't defined is when + // the length is indefinite. the absence of $content['headerlength'] is + // how we know if something is indefinite or not. technically, it could + // be defined to be 2 and then another indicator could be used but whatever. + 'length' => $start - $current['start'] + ] + $current; + } + + $current += ['type' => $tag]; + + // decode UNIVERSAL tags + switch ($tag) { + case self::TYPE_BOOLEAN: + // "The contents octets shall consist of a single octet." -- paragraph 8.2.1 + //if (strlen($content) != 1) { + // return false; + //} + $current['content'] = (bool)ord($content[$content_pos]); + break; + case self::TYPE_INTEGER: + case self::TYPE_ENUMERATED: + $current['content'] = new BigInteger(substr($content, $content_pos), -256); + break; + case self::TYPE_REAL: // not currently supported + return false; + case self::TYPE_BIT_STRING: + // The initial octet shall encode, as an unsigned binary integer with bit 1 + // as the least significant bit, the number of unused bits in the final subsequent octet. + // The number shall be in the range zero to seven. + if (!$constructed) { + $current['content'] = substr($content, $content_pos); + } else { + $temp = $this->decodeBERInternal($content, $start, $content_pos); + if ($temp === false) { + return false; + } + $length -= (strlen($content) - $content_pos); + } + break; + case self::TYPE_OCTET_STRING: + if (!$constructed) { + $current['content'] = substr($content, $content_pos); + } else { + $current['content'] = ''; + $length = 0; + while (substr($content, $content_pos, 2) != "\0\0") { + $temp = $this->decodeBERInternal($content, $length + $start, $content_pos); + if ($temp === false) { + return false; + } + $content_pos += $temp['length']; + // all subtags should be octet strings + //if ($temp['type'] != self::TYPE_OCTET_STRING) { + // return false; + //} + $current['content'] .= $temp['content']; + $length += $temp['length']; + } + if (substr($content, $content_pos, 2) == "\0\0") { + $length += 2; // +2 for the EOC + } + } + break; + case self::TYPE_NULL: + // "The contents octets shall not contain any octets." -- paragraph 8.8.2 + //if (strlen($content)) { + // return false; + //} + break; + case self::TYPE_SEQUENCE: + case self::TYPE_SET: + $offset = 0; + $current['content'] = []; + $content_len = strlen($content); + while ($content_pos < $content_len) { + // if indefinite length construction was used and we have an end-of-content string next + // see paragraphs 8.1.1.3, 8.1.3.2, 8.1.3.6, 8.1.5, and (for an example) 8.6.4.2 + if (!isset($current['headerlength']) && substr($content, $content_pos, 2) == "\0\0") { + $length = $offset + 2; // +2 for the EOC + break 2; + } + $temp = $this->decodeBERInternal($content, $start + $offset, $content_pos); + if ($temp === false) { + return false; + } + $content_pos += $temp['length']; + $current['content'][] = $temp; + $offset += $temp['length']; + } + break; + case self::TYPE_OBJECT_IDENTIFIER: + $current['content'] = $this->decodeOID(substr($content, $content_pos)); + break; + /* Each character string type shall be encoded as if it had been declared: + [UNIVERSAL x] IMPLICIT OCTET STRING + + -- X.690-0207.pdf#page=23 (paragraph 8.21.3) + + Per that, we're not going to do any validation. If there are any illegal characters in the string, + we don't really care */ + case self::TYPE_NUMERIC_STRING: + // 0,1,2,3,4,5,6,7,8,9, and space + case self::TYPE_PRINTABLE_STRING: + // Upper and lower case letters, digits, space, apostrophe, left/right parenthesis, plus sign, comma, + // hyphen, full stop, solidus, colon, equal sign, question mark + case self::TYPE_TELETEX_STRING: + // The Teletex character set in CCITT's T61, space, and delete + // see http://en.wikipedia.org/wiki/Teletex#Character_sets + case self::TYPE_VIDEOTEX_STRING: + // The Videotex character set in CCITT's T.100 and T.101, space, and delete + case self::TYPE_VISIBLE_STRING: + // Printing character sets of international ASCII, and space + case self::TYPE_IA5_STRING: + // International Alphabet 5 (International ASCII) + case self::TYPE_GRAPHIC_STRING: + // All registered G sets, and space + case self::TYPE_GENERAL_STRING: + // All registered C and G sets, space and delete + case self::TYPE_UTF8_STRING: + // ???? + case self::TYPE_BMP_STRING: + $current['content'] = substr($content, $content_pos); + break; + case self::TYPE_UTC_TIME: + case self::TYPE_GENERALIZED_TIME: + $current['content'] = $this->decodeTime(substr($content, $content_pos), $tag); + } + + $start += $length; + + // ie. length is the length of the full TLV encoding - it's not just the length of the value + return $current + ['length' => $start - $current['start']]; + } + + /** + * BER-decode the OID + * + * Called by _decode_ber() + * + * @param string $content + * + * @return string + */ + private function decodeOID(string $content) + { + static $eighty; + if (!$eighty) { + $eighty = new BigInteger(80); + } + + $oid = []; + $pos = 0; + $len = strlen($content); + $n = new BigInteger(); + while ($pos < $len) { + $temp = ord($content[$pos++]); + $n = $n->bitwiseLeftShift(7); + $n = $n->bitwiseOr(new BigInteger($temp & 0x7F)); + if (~$temp & 0x80) { + $oid[] = $n; + $n = new BigInteger(); + } + } + $part1 = array_shift($oid); + $first = floor(ord($content[0]) / 40); + /* + "This packing of the first two object identifier components recognizes that only + three values are allocated from the root node, and at most 39 subsequent values + from nodes reached by X = 0 and X = 1." + + -- https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=22 + */ + if ($first <= 2) { // ie. 0 <= ord($content[0]) < 120 (0x78) + array_unshift($oid, ord($content[0]) % 40); + array_unshift($oid, $first); + } else { + array_unshift($oid, $part1->subtract($eighty)); + array_unshift($oid, 2); + } + + return implode('.', $oid); + } + + /** + * BER-decode the time + * + * Called by _decode_ber() and in the case of implicit tags asn1map(). + * + * @param string $content + * @param int $tag + * + * @return DateTime|false + */ + private function decodeTime(string $content, int $tag) + { + /* UTCTime: + http://tools.ietf.org/html/rfc5280#section-4.1.2.5.1 + http://www.obj-sys.com/asn1tutorial/node15.html + + GeneralizedTime: + http://tools.ietf.org/html/rfc5280#section-4.1.2.5.2 + http://www.obj-sys.com/asn1tutorial/node14.html */ + + $format = 'YmdHis'; + + if ($tag == self::TYPE_UTC_TIME) { + // https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=28 says "the seconds + // element shall always be present" but none-the-less I've seen X509 certs where it isn't and if the + // browsers parse it lib ought to too + if (preg_match('#^(\d{10})(Z|[+-]\d{4})$#', $content, $matches)) { + $content = $matches[1] . '00' . $matches[2]; + } + $prefix = substr($content, 0, 2) >= 50 ? '19' : '20'; + $content = $prefix . $content; + } elseif (strpos($content, '.') !== false) { + $format .= '.u'; + } + + if ($content[strlen($content) - 1] == 'Z') { + $content = substr($content, 0, -1) . '+0000'; + } + + if (strpos($content, '-') !== false || strpos($content, '+') !== false) { + $format .= 'O'; + } + + // error supression isn't necessary as of PHP 7.0: + // http://php.net/manual/en/migration70.other-changes.php + return DateTime::createFromFormat($format, $content); + } + + public function asn1map($decoded, $mapping, $special = []) + { + if (!is_array($decoded)) { + return false; + } + + if (isset($mapping['explicit']) && is_array($decoded['content'])) { + $decoded = $decoded['content'][0]; + } + + switch (true) { + case $mapping['type'] == self::TYPE_ANY: + $intype = $decoded['type']; + $inmap = $this->ANYmap[$intype]; + if (is_string($inmap)) { + return [$inmap => $this->asn1map($decoded, ['type' => $intype] + $mapping, $special)]; + } + break; + case $mapping['type'] == self::TYPE_CHOICE: + foreach ($mapping['children'] as $key => $option) { + switch (true) { + case isset($option['constant']) && $option['constant'] == $decoded['constant']: + case !isset($option['constant']) && $option['type'] == $decoded['type']: + $value = $this->asn1map($decoded, $option, $special); + break; + case !isset($option['constant']) && $option['type'] == self::TYPE_CHOICE: + $v = $this->asn1map($decoded, $option, $special); + if (isset($v)) { + $value = $v; + } + } + if (isset($value)) { + if (isset($special[$key])) { + $value = call_user_func($special[$key], $value); + } + return [$key => $value]; + } + } + return null; + case isset($mapping['implicit']): + case isset($mapping['explicit']): + case $decoded['type'] == $mapping['type']: + break; + default: + // if $decoded['type'] and $mapping['type'] are both strings, but different types of strings, + // let it through + switch (true) { + case $decoded['type'] < 18: // self::TYPE_NUMERIC_STRING == 18 + case $decoded['type'] > 30: // self::TYPE_BMP_STRING == 30 + case $mapping['type'] < 18: + case $mapping['type'] > 30: + return null; + } + } + + if (isset($mapping['implicit'])) { + $decoded['type'] = $mapping['type']; + } + + switch ($decoded['type']) { + case self::TYPE_SEQUENCE: + $map = []; + + // ignore the min and max + if (isset($mapping['min']) && isset($mapping['max'])) { + $child = $mapping['children']; + foreach ($decoded['content'] as $content) { + if (($map[] = $this->asn1map($content, $child, $special)) === null) { + return null; + } + } + + return $map; + } + + $n = count($decoded['content']); + $i = 0; + + foreach ($mapping['children'] as $key => $child) { + $maymatch = $i < $n; // Match only existing input. + if ($maymatch) { + $temp = $decoded['content'][$i]; + + if ($child['type'] != self::TYPE_CHOICE) { + // Get the mapping and input class & constant. + $childClass = $tempClass = self::CLASS_UNIVERSAL; + $constant = null; + if (isset($temp['constant'])) { + $tempClass = $temp['type']; + } + if (isset($child['class'])) { + $childClass = $child['class']; + $constant = $child['cast']; + } elseif (isset($child['constant'])) { + $childClass = self::CLASS_CONTEXT_SPECIFIC; + $constant = $child['constant']; + } + + if (isset($constant) && isset($temp['constant'])) { + // Can only match if constants and class match. + $maymatch = $constant == $temp['constant'] && $childClass == $tempClass; + } else { + // Can only match if no constant expected and type matches or is generic. + $maymatch = !isset($child['constant']) && + array_search( + $child['type'], + [$temp['type'], self::TYPE_ANY, self::TYPE_CHOICE] + ) !== false; + } + } + } + + if ($maymatch && isset($temp)) { + // Attempt submapping. + $candidate = $this->asn1map($temp, $child, $special); + $maymatch = $candidate !== null; + } + + if ($maymatch) { + // Got the match: use it. + if (isset($special[$key])) { + $candidate = call_user_func($special[$key], $candidate ?? null); + } + $map[$key] = $candidate ?? null; + $i++; + } elseif (isset($child['default'])) { + $map[$key] = $child['default']; // Use default. + } elseif (!isset($child['optional'])) { + return null; // Syntax error. + } + } + + // Fail mapping if all input items have not been consumed. + return $i < $n ? null : $map; + + // the main diff between sets and sequences is the encapsulation of the foreach in another for loop + case self::TYPE_SET: + $map = []; + + // ignore the min and max + if (isset($mapping['min']) && isset($mapping['max'])) { + $child = $mapping['children']; + foreach ($decoded['content'] as $content) { + if (($map[] = $this->asn1map($content, $child, $special)) === null) { + return null; + } + } + + return $map; + } + + for ($i = 0; $i < count($decoded['content']); $i++) { + $temp = $decoded['content'][$i]; + $tempClass = self::CLASS_UNIVERSAL; + if (isset($temp['constant'])) { + $tempClass = $temp['type']; + } + + foreach ($mapping['children'] as $key => $child) { + if (isset($map[$key])) { + continue; + } + $maymatch = true; + if ($child['type'] != self::TYPE_CHOICE) { + $childClass = self::CLASS_UNIVERSAL; + $constant = null; + if (isset($child['class'])) { + $childClass = $child['class']; + $constant = $child['cast']; + } elseif (isset($child['constant'])) { + $childClass = self::CLASS_CONTEXT_SPECIFIC; + $constant = $child['constant']; + } + + if (isset($constant) && isset($temp['constant'])) { + // Can only match if constants and class match. + $maymatch = $constant == $temp['constant'] && $childClass == $tempClass; + } else { + // Can only match if no constant expected and type matches or is generic. + $maymatch = !isset($child['constant']) && + array_search( + $child['type'], + [$temp['type'], self::TYPE_ANY, self::TYPE_CHOICE] + ) !== false; + } + } + + if ($maymatch) { + // Attempt submapping. + $candidate = $this->asn1map($temp, $child, $special); + $maymatch = $candidate !== null; + } + + if (!$maymatch) { + break; + } + + // Got the match: use it. + if (isset($special[$key])) { + $candidate = call_user_func($special[$key], $candidate ?? null); + } + $map[$key] = $candidate ?? null; + break; + } + } + + foreach ($mapping['children'] as $key => $child) { + if (!isset($map[$key])) { + if (isset($child['default'])) { + $map[$key] = $child['default']; + } elseif (!isset($child['optional'])) { + return null; + } + } + } + return $map; + case self::TYPE_OBJECT_IDENTIFIER: + return isset($this->oids[$decoded['content']]) ? $this->oids[$decoded['content']] : $decoded['content']; + case self::TYPE_UTC_TIME: + case self::TYPE_GENERALIZED_TIME: + // for explicitly tagged optional stuff + if (is_array($decoded['content'])) { + $decoded['content'] = $decoded['content'][0]['content']; + } + // for implicitly tagged optional stuff + // in theory, doing isset($mapping['implicit']) would work but malformed certs do exist + // in the wild that OpenSSL decodes without issue so we'll support them as well + if (!is_object($decoded['content'])) { + $decoded['content'] = $this->decodeTime($decoded['content'], $decoded['type']); + } + return $decoded['content'] ? $decoded['content']->format($this->format) : false; + case self::TYPE_BIT_STRING: + if (isset($mapping['mapping'])) { + $offset = ord($decoded['content'][0]); + $size = (strlen($decoded['content']) - 1) * 8 - $offset; + /* + From X.680-0207.pdf#page=46 (21.7): + + "When a "NamedBitList" is used in defining a bitstring type ASN.1 encoding + rules are free to add (or remove) arbitrarily any trailing 0 bits to (or from) + values that are being encoded or decoded. Application designers should therefore + ensure that different semantics are not associated with such values which differ + only in the number of trailing 0 bits." + */ + $bits = count($mapping['mapping']) == $size ? [] : + array_fill(0, count($mapping['mapping']) - $size, false); + for ($i = strlen($decoded['content']) - 1; $i > 0; $i--) { + $current = ord($decoded['content'][$i]); + for ($j = $offset; $j < 8; $j++) { + $bits[] = (bool)($current & (1 << $j)); + } + $offset = 0; + } + $values = []; + $map = array_reverse($mapping['mapping']); + foreach ($map as $i => $value) { + if ($bits[$i]) { + $values[] = $value; + } + } + return $values; + } + // no break + case self::TYPE_OCTET_STRING: + return base64_encode($decoded['content']); + case self::TYPE_NULL: + return ''; + case self::TYPE_BOOLEAN: + case self::TYPE_NUMERIC_STRING: + case self::TYPE_PRINTABLE_STRING: + case self::TYPE_TELETEX_STRING: + case self::TYPE_VIDEOTEX_STRING: + case self::TYPE_IA5_STRING: + case self::TYPE_GRAPHIC_STRING: + case self::TYPE_VISIBLE_STRING: + case self::TYPE_GENERAL_STRING: + case self::TYPE_UNIVERSAL_STRING: + case self::TYPE_UTF8_STRING: + case self::TYPE_BMP_STRING: + return $decoded['content']; + case self::TYPE_INTEGER: + case self::TYPE_ENUMERATED: + $temp = $decoded['content']; + if (isset($mapping['implicit'])) { + $temp = new BigInteger($decoded['content'], -256); + } + if (isset($mapping['mapping'])) { + $temp = (int)$temp->toString(); + return isset($mapping['mapping'][$temp]) ? + $mapping['mapping'][$temp] : + false; + } + return $temp; + default: + throw new LogicException('Decoded type not handled.'); + } + } + + public function loadFilters($filters) + { + $this->filters = $filters; + } + + public function encodeDER($source, $mapping, $special = []) + { + $this->location = []; + if (!($encodedDer = $this->encodeDERInternal($source, $mapping, null, $special))) { + throw new LogicException('DER was not encoded.'); + } + return $encodedDer; + } + + /** + * ASN.1 Encode (Helper function) + * + * @param mixed $source + * @param array $mapping + * @param string|null $idx + * @param array $special + * + * @return string|false + */ + private function encodeDERInternal($source, array $mapping, string $idx = null, array $special = []) + { + // do not encode (implicitly optional) fields with value set to default + if (isset($mapping['default']) && $source === $mapping['default']) { + return ''; + } + + if (isset($idx)) { + if (isset($special[$idx])) { + $source = call_user_func($special[$idx], $source); + } + $this->location[] = $idx; + } + + $tag = $mapping['type']; + + switch ($tag) { + case self::TYPE_SET: // Children order is not important, thus process in sequence. + case self::TYPE_SEQUENCE: + $tag |= 0x20; // set the constructed bit + + // ignore the min and max + if (isset($mapping['min']) && isset($mapping['max'])) { + $value = []; + $child = $mapping['children']; + + foreach ($source as $content) { + $temp = $this->encodeDERInternal($content, $child, null, $special); + if ($temp === false) { + return false; + } + $value[] = $temp; + } + /* "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." + + -- sec 11.6 of http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf */ + if ($mapping['type'] == self::TYPE_SET) { + sort($value); + } + $value = implode('', $value); + break; + } + + $value = ''; + foreach ($mapping['children'] as $key => $child) { + if (!array_key_exists($key, $source)) { + if (!isset($child['optional'])) { + return false; + } + continue; + } + + $temp = $this->encodeDERInternal($source[$key], $child, $key, $special); + if ($temp === false) { + return false; + } + + // An empty child encoding means it has been optimized out. + // Else we should have at least one tag byte. + if ($temp === '') { + continue; + } + + // if isset($child['constant']) is true then isset($child['optional']) should be true as well + if (isset($child['constant'])) { + /* + From X.680-0207.pdf#page=58 (30.6): + + "The tagging construction specifies explicit tagging if any of the following holds: + ... + c) the "Tag Type" alternative is used and the value of "TagDefault" for the + module is IMPLICIT TAGS or AUTOMATIC TAGS, but the type defined by "Type" is + an untagged choice type, an untagged open type, or an untagged "DummyReference" + (see ITU-T Rec. X.683 | ISO/IEC 8824-4, 8.3)." + */ + if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { + $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']); + $temp = $subtag . $this->encodeLength(strlen($temp)) . $temp; + } else { + $subtag = chr( + (self::CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant'] + ); + $temp = $subtag . substr($temp, 1); + } + } + $value .= $temp; + } + break; + case self::TYPE_CHOICE: + $temp = false; + + foreach ($mapping['children'] as $key => $child) { + if (!isset($source[$key])) { + continue; + } + + $temp = $this->encodeDERInternal($source[$key], $child, $key, $special); + if ($temp === false) { + return false; + } + + // An empty child encoding means it has been optimized out. + // Else we should have at least one tag byte. + if ($temp === '') { + continue; + } + + $tag = ord($temp[0]); + + // if isset($child['constant']) is true then isset($child['optional']) should be true as well + if (isset($child['constant'])) { + if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { + $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']); + $temp = $subtag . $this->encodeLength(strlen($temp)) . $temp; + } else { + $subtag = chr( + (self::CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant'] + ); + $temp = $subtag . substr($temp, 1); + } + } + } + + if (isset($idx)) { + array_pop($this->location); + } + + if ($temp && isset($mapping['cast'])) { + $temp[0] = chr(($mapping['class'] << 6) | ($tag & 0x20) | $mapping['cast']); + } + + return $temp; + case self::TYPE_INTEGER: + case self::TYPE_ENUMERATED: + if (!isset($mapping['mapping'])) { + if (is_numeric($source)) { + $source = new BigInteger((string)$source); + } + $value = $source->toBytes(true); + } else { + $value = array_search($source, $mapping['mapping']); + if ($value === false) { + return false; + } + $value = new BigInteger($value); + $value = $value->toBytes(true); + } + if (!strlen($value)) { + $value = chr(0); + } + break; + case self::TYPE_UTC_TIME: + case self::TYPE_GENERALIZED_TIME: + $format = $mapping['type'] == self::TYPE_UTC_TIME ? 'y' : 'Y'; + $format .= 'mdHis'; + $date = new DateTime($source, new DateTimeZone('GMT')); + $value = $date->format($format) . 'Z'; + break; + case self::TYPE_BIT_STRING: + if (isset($mapping['mapping'])) { + $bits = array_fill(0, count($mapping['mapping']), 0); + $size = 0; + for ($i = 0; $i < count($mapping['mapping']); $i++) { + if (in_array($mapping['mapping'][$i], $source)) { + $bits[$i] = 1; + $size = $i; + } + } + + if (isset($mapping['min']) && $mapping['min'] >= 1 && $size < $mapping['min']) { + $size = $mapping['min'] - 1; + } + + $offset = 8 - (($size + 1) & 7); + $offset = $offset !== 8 ? $offset : 0; + + $value = chr($offset); + + for ($i = $size + 1; $i < count($mapping['mapping']); $i++) { + unset($bits[$i]); + } + + $bits = implode('', array_pad($bits, $size + $offset + 1, 0)); + $bytes = explode(' ', rtrim(chunk_split($bits, 8, ' '))); + foreach ($bytes as $byte) { + $value .= chr((int)bindec($byte)); + } + + break; + } + // no break + case self::TYPE_OCTET_STRING: + /* The initial octet shall encode, as an unsigned binary integer with bit 1 + as the least significant bit, the number of unused bits in the final subsequent + octet. The number shall be in the range zero to seven. + + -- http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=16 */ + $value = base64_decode($source); + break; + case self::TYPE_OBJECT_IDENTIFIER: + $value = $this->encodeOID($source); + break; + case self::TYPE_ANY: + $loc = $this->location; + if (isset($idx)) { + array_pop($this->location); + } + + switch (true) { + case !isset($source): + return $this->encodeDERInternal( + null, + ['type' => self::TYPE_NULL] + $mapping, + null, + $special + ); + case is_int($source): + case $source instanceof BigInteger: + return $this->encodeDERInternal( + $source, + ['type' => self::TYPE_INTEGER] + $mapping, + null, + $special + ); + case is_float($source): + return $this->encodeDERInternal( + $source, + ['type' => self::TYPE_REAL] + $mapping, + null, + $special + ); + case is_bool($source): + return $this->encodeDERInternal( + $source, + ['type' => self::TYPE_BOOLEAN] + $mapping, + null, + $special + ); + case is_array($source) && count($source) == 1: + $typename = implode('', array_keys($source)); + $outtype = array_search($typename, $this->ANYmap, true); + if ($outtype !== false) { + return $this->encodeDERInternal( + $source[$typename], + ['type' => $outtype] + $mapping, + null, + $special + ); + } + } + + $filters = $this->filters; + foreach ($loc as $part) { + if (!isset($filters[$part])) { + $filters = false; + break; + } + $filters = $filters[$part]; + } + if ($filters === false) { + throw new LogicException('No filters defined for ' . implode('/', $loc)); + } + return $this->encodeDERInternal($source, $filters + $mapping, null, $special); + case self::TYPE_NULL: + $value = ''; + break; + case self::TYPE_NUMERIC_STRING: + case self::TYPE_TELETEX_STRING: + case self::TYPE_PRINTABLE_STRING: + case self::TYPE_UNIVERSAL_STRING: + case self::TYPE_UTF8_STRING: + case self::TYPE_BMP_STRING: + case self::TYPE_IA5_STRING: + case self::TYPE_VISIBLE_STRING: + case self::TYPE_VIDEOTEX_STRING: + case self::TYPE_GRAPHIC_STRING: + case self::TYPE_GENERAL_STRING: + $value = $source; + break; + case self::TYPE_BOOLEAN: + $value = $source ? "\xFF" : "\x00"; + break; + default: + throw new LogicException( + 'Mapping provides no type definition for ' . implode('/', $this->location) + ); + } + + if (isset($idx)) { + array_pop($this->location); + } + + if (isset($mapping['cast'])) { + if (isset($mapping['explicit']) || $mapping['type'] == self::TYPE_CHOICE) { + $value = chr($tag) . $this->encodeLength(strlen($value)) . $value; + $tag = ($mapping['class'] << 6) | 0x20 | $mapping['cast']; + } elseif (isset($temp)) { + $tag = ($mapping['class'] << 6) | (ord($temp[0]) & 0x20) | $mapping['cast']; + } + } + + return chr($tag) . $this->encodeLength(strlen($value)) . $value; + } + + /** + * DER-encode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} + * for more information. + * + * @param int $length + * + * @return string + */ + private function encodeLength(int $length) + { + if ($length <= 0x7F) { + return chr($length); + } + + $temp = ltrim(pack('N', $length), chr(0)); + return pack('Ca*', 0x80 | strlen($temp), $temp); + } + + /** + * DER-encode the OID + * + * Called by _encode_der() + * + * @param string $source + * + * @return string + */ + private function encodeOID(string $source) + { + static $mask, $zero, $forty; + if (!$mask) { + $mask = new BigInteger(0x7F); + $zero = new BigInteger(); + $forty = new BigInteger(40); + } + + $oid = preg_match('#(?:\d+\.)+#', $source) ? $source : array_search($source, $this->oids); + if ($oid === false) { + throw new LogicException('Invalid OID'); + } + $parts = explode('.', (string)$oid); + $part1 = array_shift($parts); + $part2 = array_shift($parts); + + $first = new BigInteger($part1); + $first = $first->multiply($forty); + $first = $first->add(new BigInteger($part2)); + + array_unshift($parts, $first->toString()); + + $value = ''; + foreach ($parts as $part) { + if (!$part) { + $temp = "\0"; + } else { + $temp = ''; + $part = new BigInteger($part); + while (!$part->equals($zero)) { + $submask = $part->bitwiseAnd($mask); + $submask->setupPrecision(8); + $temp = (chr(0x80) | $submask->toBytes()) . $temp; + $part = $part->bitwiseRightShift(7); + } + $temp[strlen($temp) - 1] = $temp[strlen($temp) - 1] & chr(0x7F); + } + $value .= $temp; + } + + return $value; + } + + public function convert($in, $from = self::TYPE_UTF8_STRING, $to = self::TYPE_UTF8_STRING) + { + if (!isset($this->stringTypeSize[$from]) || !isset($this->stringTypeSize[$to])) { + return false; + } + $insize = $this->stringTypeSize[$from]; + $outsize = $this->stringTypeSize[$to]; + $inlength = strlen($in); + $out = ''; + + for ($i = 0; $i < $inlength;) { + if ($inlength - $i < $insize) { + return false; + } + + // Get an input character as a 32-bit value. + $c = ord($in[$i++]); + switch (true) { + case $insize == 4: + $c = ($c << 8) | ord($in[$i++]); + $c = ($c << 8) | ord($in[$i++]); + // no break + case $insize == 2: + $c = ($c << 8) | ord($in[$i++]); + // no break + case $insize == 1: + break; + case ($c & 0x80) == 0x00: + break; + case ($c & 0x40) == 0x00: + return false; + default: + $bit = 6; + do { + if ($bit > 25 || $i >= $inlength || (ord($in[$i]) & 0xC0) != 0x80) { + return false; + } + $c = ($c << 6) | (ord($in[$i++]) & 0x3F); + $bit += 5; + $mask = 1 << $bit; + } while ($c & $bit); + $c &= $mask - 1; + break; + } + + // Convert and append the character to output string. + $v = ''; + switch (true) { + case $outsize == 4: + $v .= chr($c & 0xFF); + $c >>= 8; + $v .= chr($c & 0xFF); + $c >>= 8; + // no break + case $outsize == 2: + $v .= chr($c & 0xFF); + $c >>= 8; + // no break + case $outsize == 1: + $v .= chr($c & 0xFF); + $c >>= 8; + if ($c) { + return false; + } + break; + case ($c & 0x80000000) != 0: + return false; + case $c >= 0x04000000: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x04000000; + // no break + case $c >= 0x00200000: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x00200000; + // no break + case $c >= 0x00010000: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x00010000; + // no break + case $c >= 0x00000800: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x00000800; + // no break + case $c >= 0x00000080: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x000000C0; + // no break + default: + $v .= chr($c); + break; + } + $out .= strrev($v); + } + return $out; + } + + public function getANYmap() + { + return $this->ANYmap; + } + + public function getStringTypeSize() + { + return $this->stringTypeSize; + } +} diff --git a/src/Models/Crypt/BigInteger.php b/src/Models/Crypt/BigInteger.php index e1cffcd..8275392 100644 --- a/src/Models/Crypt/BigInteger.php +++ b/src/Models/Crypt/BigInteger.php @@ -3,16 +3,612 @@ namespace AndrewSvirin\Ebics\Models\Crypt; use AndrewSvirin\Ebics\Contracts\Crypt\BigIntegerInterface; +use LogicException; /** - * Crypt Big Integer model. + * Pure-PHP arbitrary precision integer arithmetic library. * - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @author Andrew Svirin - * - * @property BigIntegerInterface $exponent - * @property BigIntegerInterface $modulus + * Supports base-10 and base-256 numbers. Uses the BCMath extension. */ -class BigInteger extends \phpseclib\Math\BigInteger implements BigIntegerInterface +class BigInteger implements BigIntegerInterface { + + /** + * Holds the BigInteger's value. + * + * @var mixed + */ + protected $value; + + /** + * Holds the BigInteger's magnitude. + * + * @var bool + */ + protected $is_negative = false; + + /** + * Precision + * + * @var int + */ + protected $precision = -1; + + /** + * Precision Bitmask + * + * @var BigIntegerInterface|false + */ + protected $bitmask = false; + + /** + * Converts base-10, and binary strings (base-256) to BigIntegers. + * + * If the second parameter - $base - is negative, then it will be assumed that the number's are encoded using + * two's compliment. The sole exception to this is -10, which is treated the same as 10 is. + * + * @param int|string $x base-10 number or base-$base number if $base set. + * @param int $base + */ + public function __construct($x = 0, $base = 10) + { + if (!defined('PHP_INT_SIZE')) { + define('PHP_INT_SIZE', 4); + } + + $this->value = '0'; + + // '0' counts as empty() but when the base is 256 '0' is equal to ord('0') or 48 + // '0' is the only value like this per http://php.net/empty + if (empty($x) && (abs($base) != 256 || $x !== '0')) { + return; + } + + switch ($base) { + case -256: + if (ord(((string)$x)[0]) & 0x80) { + $x = ~$x; + $this->is_negative = true; + } + // no break + case 256: + // round $len to the nearest 4 (thanks, DavidMJ!) + $len = (strlen((string)$x) + 3) & 0xFFFFFFFC; + + $x = str_pad((string)$x, $len, chr(0), STR_PAD_LEFT); + + for ($i = 0; $i < $len; $i += 4) { + $this->value = bcmul($this->value, '4294967296', 0); // 4294967296 == 2**32 + $this->value = bcadd( + $this->value, + (string)(0x1000000 * ord($x[$i]) + + ((ord($x[$i + 1]) << 16) | (ord($x[$i + 2]) << 8) | ord($x[$i + 3]))), + 0 + ); + } + + if ($this->is_negative) { + $this->value = '-' . $this->value; + } + + + if ($this->is_negative) { + $this->is_negative = false; + + $temp = $this->add(new self('-1')); + $this->value = $temp->getValue(); + } + break; + case 10: + case -10: + // (?value = $x === '-' ? '0' : (string)$x; + break; + default: + throw new LogicException('Base is not supported'); + } + } + + public function toBytes($twosCompliment = false) + { + if ($twosCompliment) { + $comparison = $this->compare(new self()); + if ($comparison == 0) { + return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : ''; + } + + $temp = $comparison < 0 ? $this->add(new self(1)) : $this->copy(); + $bytes = $temp->toBytes(); + + if (!strlen($bytes)) { // eg. if the number we're trying to convert is -1 + $bytes = chr(0); + } + + if ($this->precision <= 0 && (ord($bytes[0]) & 0x80)) { + $bytes = chr(0) . $bytes; + } + + return $comparison < 0 ? ~$bytes : $bytes; + } + + if ($this->value === '0') { + return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : ''; + } + + $value = ''; + $current = $this->value; + + if ($current[0] == '-') { + $current = substr($current, 1); + } + + while (bccomp($current, '0', 0) > 0) { + $temp = bcmod($current, '16777216'); + $value = chr($temp >> 16) . chr($temp >> 8) . chr((int)$temp) . $value; + $current = bcdiv($current, '16777216', 0); + } + + return $this->precision > 0 ? + substr(str_pad($value, $this->precision >> 3, chr(0), STR_PAD_LEFT), -($this->precision >> 3)) : + ltrim($value, chr(0)); + } + + public function toHex($twosCompliment = false) + { + return bin2hex($this->toBytes($twosCompliment)); + } + + public function toString() + { + if ($this->value === '0') { + return '0'; + } + + return ltrim($this->value, '0'); + } + + /** + * __toString() magic method + * + * Will be called, automatically, if you're supporting just PHP5. If you're supporting PHP4, you'll need to call + * toString(). + */ + public function __toString() + { + return $this->toString(); + } + + public function equals($x) + { + return $this->value === $x->getValue() && $this->is_negative == $x->isNegative(); + } + + public function copy() + { + $temp = new self(); + $temp->value = $this->value; + $temp->is_negative = $this->is_negative; + $temp->precision = $this->precision; + $temp->bitmask = $this->bitmask; + return $temp; + } + + public function compare($y) + { + return bccomp($this->value, $y->getValue(), 0); + } + + public function modPow($e, $n) + { + $n = $this->bitmask !== false && $this->bitmask->compare($n) < 0 ? $this->bitmask : $n->abs(); + + if ($e->compare(new self()) < 0) { + $e = $e->abs(); + + $temp = $this->modInverse($n); + + return $this->normalize($temp->modPow($e, $n)); + } + + if ($this->compare(new self()) < 0 || $this->compare($n) > 0) { + [, $temp] = $this->divide($n); + return $temp->modPow($e, $n); + } + + $components = [ + 'modulus' => $n->toBytes(true), + 'publicExponent' => $e->toBytes(true) + ]; + + $components = [ + 'modulus' => pack( + 'Ca*a*', + 2, + $this->encodeASN1Length(strlen($components['modulus'])), + $components['modulus'] + ), + 'publicExponent' => pack( + 'Ca*a*', + 2, + $this->encodeASN1Length(strlen($components['publicExponent'])), + $components['publicExponent'] + ) + ]; + + $RSAPublicKey = pack( + 'Ca*a*a*', + 48, + $this->encodeASN1Length(strlen($components['modulus']) + strlen($components['publicExponent'])), + $components['modulus'], + $components['publicExponent'] + ); + + $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $RSAPublicKey = chr(0) . $RSAPublicKey; + $RSAPublicKey = chr(3) . $this->encodeASN1Length(strlen($RSAPublicKey)) . $RSAPublicKey; + + $encapsulated = pack( + 'Ca*a*', + 48, + $this->encodeASN1Length(strlen($rsaOID . $RSAPublicKey)), + $rsaOID . $RSAPublicKey + ); + + $RSAPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . + chunk_split(base64_encode($encapsulated)) . + '-----END PUBLIC KEY-----'; + + $plaintext = str_pad($this->toBytes(), strlen($n->toBytes(true)) - 1, "\0", STR_PAD_LEFT); + + if (!openssl_public_encrypt($plaintext, $result, $RSAPublicKey, OPENSSL_NO_PADDING)) { + throw new LogicException('Public encrypt failed.'); + } + + return new self($result, 256); + } + + public function multiply($x) + { + $temp = new self(); + $temp->setValue(bcmul($this->value, $x->getValue(), 0)); + + return $this->normalize($temp); + } + + public function modInverse($n) + { + static $zero, $one; + if (!isset($zero)) { + $zero = new self(); + $one = new self(1); + } + + // $x mod -$n == $x mod $n. + $n = $n->abs(); + + if ($this->compare($zero) < 0) { + $temp = $this->abs(); + $temp = $temp->modInverse($n); + return $this->normalize($n->subtract($temp)); + } + + $extendedGcd = $this->extendedGCD($n); + + if (!$extendedGcd['gcd']->equals($one)) { + throw new LogicException('Values are not equals'); + } + + $x = $extendedGcd['x']->compare($zero) < 0 ? $extendedGcd['x']->add($n) : $extendedGcd['x']; + + return $this->normalize($x); + } + + public function divide($y) + { + $quotient = new self(); + $remainder = new self(); + + $quotient->setValue(bcdiv($this->value, $y->getValue(), 0)); + $remainder->setValue(bcmod($this->value, $y->getValue())); + + if ($remainder->value[0] == '-') { + $remainder->setValue(bcadd( + $remainder->value, + $y->getValue()[0] == '-' ? substr($y->getValue(), 1) : $y->getValue(), + 0 + )); + } + + return [$this->normalize($quotient), $this->normalize($remainder)]; + } + + public function subtract($y) + { + $temp = new self(); + $temp->setValue(bcsub($this->value, $y->getValue(), 0)); + + return $this->normalize($temp); + } + + public function add($y) + { + $temp = new self(); + $temp->setValue(bcadd($this->value, $y->getValue(), 0)); + + return $this->normalize($temp); + } + + public function random($arg1, $arg2 = false) + { + if ($arg2 === false) { + $max = $arg1; + $min = $this; + } else { + $min = $arg1; + $max = $arg2; + } + + $compare = $max->compare($min); + + if (!$compare) { + return $this->normalize($min); + } elseif ($compare < 0) { + // if $min is bigger then $max, swap $min and $max + $temp = $max; + $max = $min; + $min = $temp; + } + + static $one; + if (!isset($one)) { + $one = new self(1); + } + + $max = $max->subtract($min->subtract($one)); + $size = strlen(ltrim($max->toBytes(), chr(0))); + + /* + doing $random % $max doesn't work because some numbers will be more likely to occur than others. + eg. if $max is 140 and $random's max is 255 then that'd mean both $random = 5 and $random = 145 + would produce 5 whereas the only value of random that could produce 139 would be 139. ie. + not all numbers would be equally likely. some would be more likely than others. + + creating a whole new random number until you find one that is within the range doesn't work + because, for sufficiently small ranges, the likelihood that you'd get a number within that range + would be pretty small. eg. with $random's max being 255 and if your $max being 1 the probability + would be pretty high that $random would be greater than $max. + */ + $random_max = new self(chr(1) . str_repeat("\0", $size), 256); + $random = $this->randomNumberHelper($size); + + [$max_multiple] = $random_max->divide($max); + $max_multiple = $max_multiple->multiply($max); + + while ($random->compare($max_multiple) >= 0) { + $random = $random->subtract($max_multiple); + $random_max = $random_max->subtract($max_multiple); + $random = $random->bitwiseLeftShift(8); + $random = $random->add($this->randomNumberHelper(1)); + $random_max = $random_max->bitwiseLeftShift(8); + [$max_multiple] = $random_max->divide($max); + $max_multiple = $max_multiple->multiply($max); + } + [, $random] = $random->divide($max); + + return $this->normalize($random->add($min)); + } + + public function abs() + { + $temp = new self(); + + $temp->value = (bccomp($this->value, '0', 0) < 0) ? substr($this->value, 1) : $this->value; + + return $temp; + } + + /** + * Normalize + * + * Removes leading zeros and truncates (if necessary) to maintain the appropriate precision + * + * @param BigIntegerInterface $result + * + * @return BigIntegerInterface + */ + private function normalize($result) + { + $result->setPrecision($this->precision); + $result->setBitmask($this->bitmask); + + if (!empty($result->bitmask) && !empty($result->bitmask->getValue())) { + $result->setValue(bcmod($result->getValue(), $result->bitmask->getValue())); + } + + return $result; + } + + /** + * DER-encode an integer + * + * The ability to DER-encode integers is needed to create RSA public keys for use with OpenSSL + * + * @param int $length + * + * @return string + */ + private function encodeASN1Length($length) + { + if ($length <= 0x7F) { + return chr($length); + } + + $temp = ltrim(pack('N', $length), chr(0)); + return pack('Ca*', 0x80 | strlen($temp), $temp); + } + + /** + * Generates a random BigInteger + * + * Byte length is equal to $length. + * + * @param int $size + * + * @return BigIntegerInterface + */ + private function randomNumberHelper(int $size) + { + $random = ''; + + if ($size & 1) { + $random .= chr(mt_rand(0, 255)); + } + + $blocks = $size >> 1; + for ($i = 0; $i < $blocks; ++$i) { + // mt_rand(-2147483648, 0x7FFFFFFF) always produces -2147483648 on some systems + $random .= pack('n', mt_rand(0, 0xFFFF)); + } + + return new self($random, 256); + } + + /** + * Calculates the greatest common divisor and Bezout's identity. + * + * Say you have 693 and 609. The GCD is 21. Bezout's identity states that there exist integers x and y such that + * 693*x + 609*y == 21. In point of fact, there are actually an infinite number of x and y combinations and which + * combination is returned is dependent upon which mode is in use. See + * {@link http://en.wikipedia.org/wiki/B%C3%A9zout%27s_identity Bezout's identity - Wikipedia} for more information. + * + * @param BigIntegerInterface $n + * + * @return array = [ + * 'gcd' => '', + * 'x' => '', + * 'y' => '', + * ] + */ + private function extendedGCD($n) + { + // it might be faster to use the binary xGCD algorithim here, as well, but (1) that algorithim works + // best when the base is a power of 2 and (2) i don't think it'd make much difference, anyway. as is, + // the basic extended euclidean algorithim is what we're using. + + $u = $this->value; + $v = $n->getValue(); + + $a = '1'; + $b = '0'; + $c = '0'; + $d = '1'; + + while (bccomp($v, '0', 0) != 0) { + $q = bcdiv($u, $v, 0); + + $temp = $u; + $u = $v; + $v = bcsub($temp, bcmul($v, $q, 0), 0); + + $temp = $a; + $a = $c; + $c = bcsub($temp, bcmul($a, $q, 0), 0); + + $temp = $b; + $b = $d; + $d = bcsub($temp, bcmul($b, $q, 0), 0); + } + + return [ + 'gcd' => $this->normalize(new self($u)), + 'x' => $this->normalize(new self($a)), + 'y' => $this->normalize(new self($b)) + ]; + } + + public function bitwiseLeftShift($shift) + { + $temp = new self(); + + $temp->setValue(bcmul($this->value, bcpow('2', (string)$shift, 0), 0)); + + return $this->normalize($temp); + } + + public function bitwiseOr($x) + { + $left = $this->toBytes(); + $right = $x->toBytes(); + + $length = max(strlen($left), strlen($right)); + + $left = str_pad($left, $length, chr(0), STR_PAD_LEFT); + $right = str_pad($right, $length, chr(0), STR_PAD_LEFT); + + return $this->normalize(new self($left | $right, 256)); + } + + public function bitwiseAnd($x) + { + $left = $this->toBytes(); + $right = $x->toBytes(); + + $length = max(strlen($left), strlen($right)); + + $left = str_pad($left, $length, chr(0), STR_PAD_LEFT); + $right = str_pad($right, $length, chr(0), STR_PAD_LEFT); + + return $this->normalize(new self($left & $right, 256)); + } + + public function setupPrecision($bits) + { + $this->precision = $bits; + + $this->bitmask = new self(bcpow('2', (string)$bits, 0)); + + $temp = $this->normalize($this); + $this->value = $temp->getValue(); + } + + public function bitwiseRightShift($shift) + { + $temp = new self(); + + $temp->value = bcdiv($this->value, bcpow('2', (string)$shift, 0), 0); + + return $this->normalize($temp); + } + + public function getValue() + { + return $this->value; + } + + public function setValue($value) + { + $this->value = $value; + } + + public function setBitmask($bitmask) + { + $this->bitmask = $bitmask; + } + + public function setPrecision($precision) + { + $this->precision = $precision; + } + + public function isNegative() + { + return $this->is_negative; + } } diff --git a/src/Models/Crypt/Hash.php b/src/Models/Crypt/Hash.php new file mode 100644 index 0000000..1d19173 --- /dev/null +++ b/src/Models/Crypt/Hash.php @@ -0,0 +1,144 @@ +setHash($hash); + } + + public function hash($text) + { + if (!empty($this->key) || is_string($this->key)) { + $output = hash_hmac($this->hash, $text, $this->computedKey, true); + } else { + $output = hash($this->hash, $text, true); + } + + if (!($hash = substr($output, 0, $this->l))) { + throw new LogicException('Hash can not be empty.'); + } + + return $hash; + } + + /** + * Sets the hash function. + * + * @param string $hash + * + * @return void + */ + private function setHash($hash) + { + switch ($hash) { + case 'sha1': + $this->l = 20; + break; + case 'sha256': + $this->l = 32; + break; + default: + throw new LogicException('Hash is not supported'); + } + + switch ($hash) { + case 'sha1': + case 'sha256': + $this->b = 64; + break; + default: + throw new LogicException('Hash is not supported'); + } + + switch ($hash) { + case 'sha256': + $this->hash = $hash; + return; + case 'sha1': + default: + $this->hash = 'sha1'; + } + $this->computeKey(); + } + + /** + * Pre-compute the key used by the HMAC + * + * Quoting http://tools.ietf.org/html/rfc2104#section-2, "Applications that use keys longer than B bytes + * will first hash the key using H and then use the resultant L byte string as the actual key to HMAC." + * + * As documented in https://www.reddit.com/r/PHP/comments/9nct2l/symfonypolyfill_hash_pbkdf2_correct_fix_for/ + * when doing an HMAC multiple times it's faster to compute the hash once instead of computing it during + * every call + * + * @return void + */ + private function computeKey() + { + if ($this->key === null) { + $this->computedKey = null; + return; + } + + if (strlen($this->key) <= $this->b) { + $this->computedKey = $this->key; + return; + } + + $this->computedKey = hash($this->hash, $this->key, true); + } +} diff --git a/src/Models/Crypt/RSA.php b/src/Models/Crypt/RSA.php index 24b6280..71a548c 100644 --- a/src/Models/Crypt/RSA.php +++ b/src/Models/Crypt/RSA.php @@ -3,42 +3,1247 @@ namespace AndrewSvirin\Ebics\Models\Crypt; use AndrewSvirin\Ebics\Contracts\Crypt\BigIntegerInterface; +use AndrewSvirin\Ebics\Contracts\Crypt\HashInterface; use AndrewSvirin\Ebics\Contracts\Crypt\RSAInterface; -use AndrewSvirin\Ebics\Factories\Crypt\BigIntegerFactory; +use LogicException; /** - * Crypt RSA model. - * - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @author Andrew Svirin - * - * @property \phpseclib\Math\BigInteger $exponent - * @property \phpseclib\Math\BigInteger $modulus + * Pure-PHP PKCS#1 (v2.1) compliant implementation of RSA. + * Uses encryption mode PKCS1. */ -class RSA extends \phpseclib\Crypt\RSA implements RSAInterface +class RSA implements RSAInterface { + + /**#@+*/ /** - * @see \phpseclib\Crypt\RSA::ENCRYPTION_PKCS1 + * PKCS#1 formatted private key + * + * Used by OpenSSH */ - const ENCRYPTION_PKCS1 = 2; + const PRIVATE_FORMAT_PKCS1 = 0; + /**#@-*/ + /**#@+*/ /** - * @see \phpseclib\Crypt\RSA::PRIVATE_FORMAT_PKCS1 + * Raw public key + * + * An array containing two BigInteger objects. + * + * The exponent can be indexed with any of the following: + * + * 0, e, exponent, publicExponent + * + * The modulus can be indexed with any of the following: + * + * 1, n, modulo, modulus */ - const PRIVATE_FORMAT_PKCS1 = 0; + const PUBLIC_FORMAT_RAW = 3; /** - * @see \phpseclib\Crypt\RSA::PUBLIC_FORMAT_PKCS1 + * @see self::PUBLIC_FORMAT_PKCS1 */ const PUBLIC_FORMAT_PKCS1 = 4; + const PUBLIC_FORMAT_PKCS1_RAW = 4; + + /** + * @see self::PUBLIC_FORMAT_PKCS8 + */ + const PUBLIC_FORMAT_PKCS8 = 7; + + /** + * ASN1 Sequence (with the constucted bit set) + * + * @see self::ASN1_SEQUENCE + */ + const ASN1_SEQUENCE = 48; + + /** + * ASN1 Integer + */ + const ASN1_INTEGER = 2; + + /** + * ASN1 Bit String + */ + const ASN1_BITSTRING = 3; + + /** + * ASN1 Object Identifier + */ + const ASN1_OBJECT = 6; + + /** + * Modulus length + * + * @var int|null + */ + protected $k; + + /** + * Modulus (ie. n) + * + * @var BigIntegerInterface|null + */ + protected $modulus; + + /** + * Exponent (ie. e or d) + * + * @var BigIntegerInterface|null + */ + protected $exponent; + + /** + * Primes for Chinese Remainder Theorem (ie. p and q) + * + * @var array|null + */ + protected $primes; + + /** + * Exponents for Chinese Remainder Theorem (ie. dP and dQ) + * + * @var array|null + */ + protected $exponents; + + /** + * Coefficients for Chinese Remainder Theorem (ie. qInv) + * + * @var array|null + */ + protected $coefficients; + + /** + * Public Exponent + * + * @var mixed + */ + protected $publicExponent = false; + + /** + * Password. + * + * @var string|null + */ + protected $password = null; + + /** + * Public Key Format. + * + * @var int + */ + protected $publicKeyFormat; + + /** + * Private Key Format. + * + * @var int + */ + protected $privateKeyFormat; + + /** + * Hash name. + * + * @var string + */ + protected $hashName; + + /** + * Hash function + * + * @var HashInterface + */ + protected $hash; + + /** + * Precomputed Zero + * + * @var BigInteger + */ + protected $zero; + + public function __construct() + { + $this->zero = new BigInteger(); + } public function getExponent(): BigIntegerInterface { - return BigIntegerFactory::createFromPhpSecLib($this->exponent); + return $this->exponent; } public function getModulus(): BigIntegerInterface { - return BigIntegerFactory::createFromPhpSecLib($this->modulus); + return $this->modulus; + } + + public function setPassword($password = null) + { + $this->password = $password; + } + + public function setPublicKeyFormat($format) + { + $this->publicKeyFormat = $format; + } + + public function setPrivateKeyFormat($format) + { + $this->privateKeyFormat = $format; + } + + public function setHash($hash) + { + $this->hash = new Hash($hash); + $this->hashName = $hash; + } + + public function createKey($bits = 1024, $timeout = false, $partial = []) + { + if (!defined('CRYPT_RSA_EXPONENT')) { + // http://en.wikipedia.org/wiki/65537_%28number%29 + define('CRYPT_RSA_EXPONENT', 65537); + } + // per , this number ought not result + // in primes smaller than 256 bits. as a consequence if the key you're trying to create is + // 1024 bits and you've set CRYPT_RSA_SMALLEST_PRIME to 384 bits then you're going to get a + // 384 bit prime and a 640 bit prime (384 + 1024 % 384). at least if CRYPT_RSA_MODE is set to + // self::MODE_INTERNAL. if CRYPT_RSA_MODE is set to self::MODE_OPENSSL then CRYPT_RSA_SMALLEST_PRIME + // is ignored (ie. multi-prime RSA support is more intended as a way to speed up RSA key + // generation when there's a chance neither gmp nor OpenSSL are installed) + if (!defined('CRYPT_RSA_SMALLEST_PRIME')) { + define('CRYPT_RSA_SMALLEST_PRIME', 4096); + } + + // OpenSSL uses 65537 as the exponent and requires RSA keys be 384 bits minimum + if ($bits < 384 || CRYPT_RSA_EXPONENT !== 65537) { + throw new LogicException('Create key conditions are incorrect.'); + } + if (!($rsa = openssl_pkey_new(['private_key_bits' => $bits]))) { + throw new LogicException('Openssl pkey new error.'); + } + openssl_pkey_export($rsa, $privatekey, null); + if (!($publickey = openssl_pkey_get_details($rsa))) { + throw new LogicException('Openssl pkey get details error.'); + } + $publickey = $publickey['key']; + + $parsedKey = $this->parseKey($privatekey, self::PRIVATE_FORMAT_PKCS1); + if (!is_array($parsedKey)) { + throw new LogicException('Parse key error.'); + } + $privatekey = $this->convertPrivateKey( + $parsedKey['modulus'], + $parsedKey['publicExponent'], + $parsedKey['privateExponent'], + $parsedKey['primes'], + $parsedKey['exponents'], + $parsedKey['coefficients'] + ); + + $parsedKey = $this->parseKey($publickey, self::PUBLIC_FORMAT_PKCS1); + if (!is_array($parsedKey)) { + throw new LogicException('Parse key error.'); + } + $publickey = $this->convertPublicKey( + $parsedKey['modulus'], + $parsedKey['publicExponent'] + ); + + // clear the buffer of error strings stemming from a minimalistic openssl.cnf + while (openssl_error_string() !== false) { + } + + return [ + 'privatekey' => $privatekey, + 'publickey' => $publickey, + 'partialkey' => false, + ]; + } + + /** + * Break a public or private key down into its constituant components + * + * @param string|array $key + * @param int $type + * + * @return array|bool + */ + private function parseKey($key, $type) + { + if ($type != self::PUBLIC_FORMAT_RAW && !is_string($key)) { + return false; + } + + switch ($type) { + case self::PUBLIC_FORMAT_RAW: + if (!is_array($key)) { + return false; + } + $components = []; + switch (true) { + case isset($key['e']): + $components['publicExponent'] = $key['e']->copy(); + break; + case isset($key['exponent']): + $components['publicExponent'] = $key['exponent']->copy(); + break; + case isset($key['publicExponent']): + $components['publicExponent'] = $key['publicExponent']->copy(); + break; + case isset($key[0]): + $components['publicExponent'] = $key[0]->copy(); + } + switch (true) { + case isset($key['n']): + $components['modulus'] = $key['n']->copy(); + break; + case isset($key['modulo']): + $components['modulus'] = $key['modulo']->copy(); + break; + case isset($key['modulus']): + $components['modulus'] = $key['modulus']->copy(); + break; + case isset($key[1]): + $components['modulus'] = $key[1]->copy(); + } + return isset($components['modulus']) && isset($components['publicExponent']) ? $components : false; + case self::PRIVATE_FORMAT_PKCS1: + case self::PUBLIC_FORMAT_PKCS1: + /* Although PKCS#1 proposes a format that public and private keys can use, encrypting + them is "outside the scope" of PKCS#1. PKCS#1 then refers you to PKCS#12 and PKCS#15 + if you're wanting to protect private keys, however, that's not what OpenSSL* does. + OpenSSL protects private keys by adding two new "fields" to the key - DEK-Info and + Proc-Type. These fields are discussed here: + + http://tools.ietf.org/html/rfc1421#section-4.6.1.1 + http://tools.ietf.org/html/rfc1421#section-4.6.1.3 + + DES-EDE3-CBC as an algorithm, however, is not discussed anywhere, near as I can tell. + DES-CBC and DES-EDE are discussed in RFC1423, however, DES-EDE3-CBC isn't, nor is its + key derivation function. As is, the definitive authority on this encoding scheme isn't + the IETF but rather OpenSSL's own implementation. ie. the implementation *is* the + standard and any bugs that may exist in that implementation are part of the standard, as well. + + * OpenSSL is the de facto standard. It's utilized by OpenSSH and other projects */ + if (!is_string($key)) { + throw new LogicException('Key must be a string.'); + } + if (preg_match('#DEK-Info: (.+),(.+)#', $key, $matches)) { + $iv = pack('H*', trim($matches[2])); + + $symkey = pack('H*', md5($this->password . substr($iv, 0, 8))); // symkey is short for symmetric key + $symkey .= pack('H*', md5($symkey . $this->password . substr($iv, 0, 8))); + + // remove the Proc-Type / DEK-Info sections as they're no longer needed + $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $key); + $ciphertext = $this->extractBER($key); + switch ($matches[1]) { + case 'AES-256-CBC': + $crypto = new AES(); + break; + case 'DES-EDE3-CBC': + $symkey = substr($symkey, 0, 24); + $crypto = new TripleDES(); + break; + default: + throw new LogicException('Wrong crypto.'); + } + $crypto->setKey($symkey); + $crypto->setIV($iv); + $decoded = $crypto->decrypt($ciphertext); + } else { + $decoded = $this->extractBER($key); + } + + if ($decoded !== false) { + $key = $decoded; + } + + $components = []; + + if (ord($this->stringShift($key)) != self::ASN1_SEQUENCE) { + return false; + } + if ($this->decodeLength($key) != strlen($key)) { + return false; + } + + $tag = ord($this->stringShift($key)); + /* intended for keys for which OpenSSL's asn1parse returns the following: + + 0:d=0 hl=4 l= 631 cons: SEQUENCE + 4:d=1 hl=2 l= 1 prim: INTEGER :00 + 7:d=1 hl=2 l= 13 cons: SEQUENCE + 9:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption + 20:d=2 hl=2 l= 0 prim: NULL + 22:d=1 hl=4 l= 609 prim: OCTET STRING + + ie. PKCS8 keys*/ + + if ($tag == self::ASN1_INTEGER && substr($key, 0, 3) == "\x01\x00\x30") { + $this->stringShift($key, 3); + $tag = self::ASN1_SEQUENCE; + } + + if ($tag == self::ASN1_SEQUENCE) { + $temp = $this->stringShift($key, $this->decodeLength($key)); + if (ord($this->stringShift($temp)) != self::ASN1_OBJECT) { + return false; + } + $length = $this->decodeLength($temp); + switch ($this->stringShift($temp, $length)) { + case "\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01": // rsaEncryption + break; + default: + throw new LogicException('Wrong _string_shift'); + } + /* intended for keys for which OpenSSL's asn1parse returns the following: + + 0:d=0 hl=4 l= 290 cons: SEQUENCE + 4:d=1 hl=2 l= 13 cons: SEQUENCE + 6:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption + 17:d=2 hl=2 l= 0 prim: NULL + 19:d=1 hl=4 l= 271 prim: BIT STRING */ + $tag = ord($this->stringShift($key)); // skip over the BIT STRING / OCTET STRING tag + $this->decodeLength($key); // skip over the BIT STRING / OCTET STRING length + // "The initial octet shall encode, as an unsigned binary integer wtih bit 1 as the least + // significant bit, the number of unused bits in the final subsequent octet. The number + // shall be in the range zero to seven." + // -- http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf (section 8.6.2.2) + if ($tag == self::ASN1_BITSTRING) { + $this->stringShift($key); + } + if (ord($this->stringShift($key)) != self::ASN1_SEQUENCE) { + return false; + } + if ($this->decodeLength($key) != strlen($key)) { + return false; + } + $tag = ord($this->stringShift($key)); + } + if ($tag != self::ASN1_INTEGER) { + return false; + } + + $length = $this->decodeLength($key); + $temp = $this->stringShift($key, $length); + if (strlen($temp) != 1 || ord($temp) > 2) { + $components['modulus'] = new BigInteger($temp, 256); + $this->stringShift($key); // skip over self::ASN1_INTEGER + $length = $this->decodeLength($key); + $components[$type == self::PUBLIC_FORMAT_PKCS1 ? 'publicExponent' : 'privateExponent'] = + new BigInteger($this->stringShift($key, $length), 256); + + return $components; + } + if (ord($this->stringShift($key)) != self::ASN1_INTEGER) { + return false; + } + $length = $this->decodeLength($key); + $components['modulus'] = new BigInteger($this->stringShift($key, $length), 256); + $this->stringShift($key); + $length = $this->decodeLength($key); + $components['publicExponent'] = new BigInteger($this->stringShift($key, $length), 256); + $this->stringShift($key); + $length = $this->decodeLength($key); + $components['privateExponent'] = new BigInteger($this->stringShift($key, $length), 256); + $this->stringShift($key); + $length = $this->decodeLength($key); + $components['primes'] = [1 => new BigInteger($this->stringShift($key, $length), 256)]; + $this->stringShift($key); + $length = $this->decodeLength($key); + $components['primes'][] = new BigInteger($this->stringShift($key, $length), 256); + $this->stringShift($key); + $length = $this->decodeLength($key); + $components['exponents'] = [1 => new BigInteger($this->stringShift($key, $length), 256)]; + $this->stringShift($key); + $length = $this->decodeLength($key); + $components['exponents'][] = new BigInteger($this->stringShift($key, $length), 256); + $this->stringShift($key); + $length = $this->decodeLength($key); + $components['coefficients'] = [2 => new BigInteger($this->stringShift($key, $length), 256)]; + + if (!empty($key)) { + return false; + } + + return $components; + + default: + throw new LogicException('Wrong type'); + } + } + + /** + * String Shift + * + * Inspired by array_shift + * + * @param string $string + * @param int $index + * + * @return string + */ + private function stringShift(&$string, $index = 1) + { + $substr = substr($string, 0, $index); + $string = substr($string, $index); + return $substr; + } + + /** + * DER-decode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} + * for more information. + * + * @param string $string + * + * @return int + */ + private function decodeLength(&$string) + { + $length = ord($this->stringShift($string)); + if ($length & 0x80) { // definite length, long form + $length &= 0x7F; + $temp = $this->stringShift($string, $length); + [, $length] = unpack('N', substr(str_pad($temp, 4, chr(0), STR_PAD_LEFT), -4)); + } + return $length; + } + + /** + * DER-encode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} + * for more information. + * + * @param int $length + * + * @return string + */ + private function encodeLength($length) + { + if ($length <= 0x7F) { + return chr($length); + } + + $temp = ltrim(pack('N', $length), chr(0)); + return pack('Ca*', 0x80 | strlen($temp), $temp); + } + + /** + * Extract raw BER from Base64 encoding + * + * @param string $str + * + * @return string + */ + private function extractBER($str) + { + /* X.509 certs are assumed to be base64 encoded but sometimes they'll have additional things in them + * above and beyond the ceritificate. + * ie. some may have the following preceding the -----BEGIN CERTIFICATE----- line: + * + * Bag Attributes + * localKeyID: 01 00 00 00 + * subject=/O=organization/OU=org unit/CN=common name + * issuer=/O=organization/CN=common name + */ + $temp = preg_replace('#.*?^-+[^-]+-+[\r\n ]*$#ms', '', $str, 1); + // remove the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- stuff + $temp = preg_replace('#-+[^-]+-+#', '', $temp); + // remove new lines + $temp = str_replace(["\r", "\n", ' '], '', $temp); + $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? base64_decode($temp) : false; + return $temp != false ? $temp : $str; + } + + /** + * Convert a private key to the appropriate format. + * + * @param BigIntegerInterface $n + * @param BigIntegerInterface $e + * @param BigIntegerInterface $d + * @param BigIntegerInterface[] $primes + * @param BigIntegerInterface[] $exponents + * @param BigIntegerInterface[] $coefficients + * + * @return string + */ + private function convertPrivateKey($n, $e, $d, $primes, $exponents, $coefficients) + { + $signed = true; + $num_primes = count($primes); + $raw = [ + 'version' => $num_primes == 2 ? chr(0) : chr(1), // two-prime vs. multi + 'modulus' => $n->toBytes($signed), + 'publicExponent' => $e->toBytes($signed), + 'privateExponent' => $d->toBytes($signed), + 'prime1' => $primes[1]->toBytes($signed), + 'prime2' => $primes[2]->toBytes($signed), + 'exponent1' => $exponents[1]->toBytes($signed), + 'exponent2' => $exponents[2]->toBytes($signed), + 'coefficient' => $coefficients[2]->toBytes($signed) + ]; + + // if the format in question does not support multi-prime rsa and multi-prime rsa was used, + // call _convertPublicKey() instead. + switch ($this->privateKeyFormat) { + default: // eg. self::PRIVATE_FORMAT_PKCS1 + $components = []; + foreach ($raw as $name => $value) { + $components[$name] = pack( + 'Ca*a*', + self::ASN1_INTEGER, + $this->encodeLength(strlen($value)), + $value + ); + } + + $RSAPrivateKey = implode('', $components); + + if ($num_primes > 2) { + throw new LogicException('Should not be more than 2 primes.'); + } + + $RSAPrivateKey = pack( + 'Ca*a*', + self::ASN1_SEQUENCE, + $this->encodeLength(strlen($RSAPrivateKey)), + $RSAPrivateKey + ); + + if (!empty($this->password) || is_string($this->password)) { + $method = 'DES-EDE3-CBC'; + if (!($ivLen = openssl_cipher_iv_length($method))) { + throw new LogicException('Can no determinate cipher length.'); + } + if (!($iv = openssl_random_pseudo_bytes($ivLen))) { + throw new LogicException('Can no generate random bytes.'); + } + + $symkey = pack('H*', md5($this->password . $iv)); // symkey is short for symmetric key + $symkey .= substr(pack('H*', md5($symkey . $this->password . $iv)), 0, 8); + + $crypt = new TripleDES(); + $crypt->setKey($symkey); + $crypt->setIV($iv); + + $RSAPrivateKeyEncrypted = $crypt->encrypt($RSAPrivateKey); + + $iv = strtoupper(bin2hex($iv)); + $method = strtoupper($method); + $RSAPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\r\n" . + "Proc-Type: 4,ENCRYPTED\r\n" . + "DEK-Info: $method,$iv\r\n" . + "\r\n" . + chunk_split(base64_encode($RSAPrivateKeyEncrypted), 64) . + '-----END RSA PRIVATE KEY-----'; + } else { + $RSAPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\r\n" . + chunk_split(base64_encode($RSAPrivateKey), 64) . + '-----END RSA PRIVATE KEY-----'; + } + + return $RSAPrivateKey; + } + } + + /** + * Convert a public key to the appropriate format + * + * @param BigIntegerInterface $n + * @param BigIntegerInterface $e + * + * @return string + */ + private function convertPublicKey($n, $e) + { + $signed = true; + + $modulus = $n->toBytes($signed); + $publicExponent = $e->toBytes($signed); + + switch ($this->publicKeyFormat) { + default: // eg. self::PUBLIC_FORMAT_PKCS1_RAW or self::PUBLIC_FORMAT_PKCS1 + // from : + // RSAPublicKey ::= SEQUENCE { + // modulus INTEGER, -- n + // publicExponent INTEGER -- e + // } + $components = [ + 'modulus' => pack('Ca*a*', self::ASN1_INTEGER, $this->encodeLength(strlen($modulus)), $modulus), + 'publicExponent' => pack( + 'Ca*a*', + self::ASN1_INTEGER, + $this->encodeLength(strlen($publicExponent)), + $publicExponent + ) + ]; + + $RSAPublicKey = pack( + 'Ca*a*a*', + self::ASN1_SEQUENCE, + $this->encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), + $components['modulus'], + $components['publicExponent'] + ); + + if ($this->publicKeyFormat == self::PUBLIC_FORMAT_PKCS1_RAW) { + $RSAPublicKey = "-----BEGIN RSA PUBLIC KEY-----\r\n" . + chunk_split(base64_encode($RSAPublicKey), 64) . + '-----END RSA PUBLIC KEY-----'; + } else { + // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. + $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $RSAPublicKey = chr(0) . $RSAPublicKey; + $RSAPublicKey = chr(3) . $this->encodeLength(strlen($RSAPublicKey)) . $RSAPublicKey; + + $RSAPublicKey = pack( + 'Ca*a*', + self::ASN1_SEQUENCE, + $this->encodeLength(strlen($rsaOID . $RSAPublicKey)), + $rsaOID . $RSAPublicKey + ); + + $RSAPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . + chunk_split(base64_encode($RSAPublicKey), 64) . + '-----END PUBLIC KEY-----'; + } + + return $RSAPublicKey; + } + } + + public function setPublicKey($key = false) + { + // if a public key has already been loaded return false + if (!empty($this->publicExponent)) { + return false; + } + + if ($key === false && !empty($this->modulus)) { + $this->publicExponent = $this->exponent; + return true; + } + + if (!is_string($key)) { + throw new LogicException('Key must be a string.'); + } + $components = $this->parseKey($key, self::PUBLIC_FORMAT_PKCS1); + + if ($components === false) { + return false; + } + + if (!is_array($components)) { + throw new LogicException('Components must be an array.'); + } + + if (empty($this->modulus) || !$this->modulus->equals($components['modulus'])) { + $this->modulus = $components['modulus']; + $this->exponent = $this->publicExponent = $components['publicExponent']; + return true; + } + + $this->publicExponent = $components['publicExponent']; + + return true; + } + + public function loadKey($key, $type = false) + { + if ($type === false) { + $types = [ + self::PUBLIC_FORMAT_RAW, + self::PRIVATE_FORMAT_PKCS1, + ]; + foreach ($types as $type) { + $components = $this->parseKey($key, $type); + if ($components !== false) { + break; + } + } + } else { + $components = $this->parseKey($key, $type); + } + + if ($components === false) { + $this->modulus = null; + $this->k = null; + $this->exponent = null; + $this->primes = null; + $this->exponents = null; + $this->coefficients = null; + $this->publicExponent = null; + + return false; + } + + if (!is_array($components)) { + throw new LogicException('Components must be an array.'); + } + + $this->modulus = $components['modulus']; + $this->k = strlen($this->modulus->toBytes()); + $this->exponent = isset($components['privateExponent']) ? + $components['privateExponent'] : $components['publicExponent']; + if (isset($components['primes'])) { + $this->primes = $components['primes']; + $this->exponents = $components['exponents']; + $this->coefficients = $components['coefficients']; + $this->publicExponent = $components['publicExponent']; + } else { + $this->primes = []; + $this->exponents = []; + $this->coefficients = []; + $this->publicExponent = false; + } + + switch ($type) { + case self::PUBLIC_FORMAT_RAW: + $this->setPublicKey(); + break; + case self::PRIVATE_FORMAT_PKCS1: + if (!is_string($key)) { + throw new LogicException('Key must be a string.'); + } + switch (true) { + case strpos($key, '-BEGIN PUBLIC KEY-') !== false: + case strpos($key, '-BEGIN RSA PUBLIC KEY-') !== false: + $this->setPublicKey(); + } + break; + default: + throw new LogicException('Wrong type.'); + } + + return true; + } + + public function decrypt($ciphertext) + { + if ($this->k <= 0) { + throw new LogicException('K can not be less than 0.'); + } + + if (!($ciphertext = str_split($ciphertext, $this->k))) { + throw new LogicException('Ciphertext was not split.'); + } + $ciphertext[count($ciphertext) - 1] = str_pad( + $ciphertext[count($ciphertext) - 1], + $this->k, + chr(0), + STR_PAD_LEFT + ); + + $plaintext = ''; + + foreach ($ciphertext as $c) { + $temp = $this->rsaesPkcs1V15Decrypt($c); + + $plaintext .= $temp; + } + + return $plaintext; + } + + public function encrypt($plaintext) + { + $length = $this->k - 11; + if ($length <= 0) { + throw new LogicException('Length must be more 0.'); + } + + if (!($plaintext = str_split($plaintext, $length))) { + throw new LogicException('Plaintext was not split.'); + } + $ciphertext = ''; + foreach ($plaintext as $m) { + $ciphertext .= $this->rsaesPkcs1V15Encrypt($m); + } + return $ciphertext; + } + + public function getPublicKey($type = RSA::PUBLIC_FORMAT_PKCS8) + { + if (empty($this->modulus) || empty($this->publicExponent)) { + return null; + } + + $oldFormat = $this->publicKeyFormat; + $this->publicKeyFormat = $type; + $temp = $this->convertPublicKey($this->modulus, $this->publicExponent); + $this->publicKeyFormat = $oldFormat; + return $temp; + } + + public function getPrivateKey($type = RSA::PUBLIC_FORMAT_PKCS1) + { + if (empty($this->primes)) { + return false; + } + + $oldFormat = $this->privateKeyFormat; + $this->privateKeyFormat = $type; + $temp = $this->convertPrivateKey( + $this->modulus, + $this->publicExponent, + $this->exponent, + $this->primes, + $this->exponents, + $this->coefficients + ); + $this->privateKeyFormat = $oldFormat; + return $temp; + } + + public function sign($message) + { + if (empty($this->modulus) || empty($this->exponent)) { + return null; + } + + return $this->rsassaPkcs1V15Sign($message); + } + + /** + * RSASSA-PKCS1-V1_5-SIGN + * + * See {@link http://tools.ietf.org/html/rfc3447#section-8.2.1 RFC3447#section-8.2.1}. + * + * @access private + * + * @param string $m + * + * @return string + */ + private function rsassaPkcs1V15Sign(string $m) + { + // EMSA-PKCS1-v1_5 encoding + + $em = $this->emsaPkcs1V15Encode($m, $this->k); + + // RSA signature + + $m = $this->os2ip($em); + $s = $this->rsasp1($m); + $s = $this->i2osp($s, $this->k); + + // Output the signature S + + return $s; + } + + /** + * EMSA-PKCS1-V1_5-ENCODE + * + * See {@link http://tools.ietf.org/html/rfc3447#section-9.2 RFC3447#section-9.2}. + * + * @param string $m + * @param int $emLen + * + * @return string + */ + private function emsaPkcs1V15Encode(string $m, int $emLen) + { + $h = $this->hash->hash($m); + + // see http://tools.ietf.org/html/rfc3447#page-43 + switch ($this->hashName) { + case 'sha256': + $t = pack('H*', '3031300d060960864801650304020105000420'); + break; + default: + throw new LogicException('Hash algorithm not supported.'); + } + $t .= $h; + $tLen = strlen($t); + + if ($emLen < $tLen + 11) { + throw new LogicException('Intended encoded message length too short'); + } + + $ps = str_repeat(chr(0xFF), $emLen - $tLen - 3); + + $em = "\0\1$ps\0$t"; + + return $em; + } + + /** + * Octet-String-to-Integer primitive + * + * See {@link http://tools.ietf.org/html/rfc3447#section-4.2 RFC3447#section-4.2}. + * + * @param int|string $x + * + * @return BigIntegerInterface + */ + private function os2ip($x) + { + return new BigInteger($x, 256); + } + + /** + * RSASP1 + * + * See {@link http://tools.ietf.org/html/rfc3447#section-5.2.1 RFC3447#section-5.2.1}. + * + * @param BigIntegerInterface $m + * + * @return BigIntegerInterface + */ + private function rsasp1($m) + { + if ($m->compare($this->zero) < 0 || $m->compare($this->modulus) > 0) { + throw new LogicException('Message representative out of range'); + } + return $this->exponentiate($m); + } + + /** + * Exponentiate with or without Chinese Remainder Theorem + * + * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.1 RFC3447#section-5.1.2}. + * + * @param BigIntegerInterface $x + * + * @return BigIntegerInterface + */ + private function exponentiate($x) + { + switch (true) { + case empty($this->primes): + case $this->primes[1]->equals($this->zero): + case empty($this->coefficients): + case $this->coefficients[2]->equals($this->zero): + case empty($this->exponents): + case $this->exponents[1]->equals($this->zero): + return $x->modPow($this->exponent, $this->modulus); + } + + $num_primes = count($this->primes); + + $smallest = $this->primes[1]; + for ($i = 2; $i <= $num_primes; $i++) { + if ($smallest->compare($this->primes[$i]) > 0) { + $smallest = $this->primes[$i]; + } + } + + $one = new BigInteger(1); + + $r = $one->random($one, $smallest->subtract($one)); + + $m_i = [ + 1 => $this->blind($x, $r, 1), + 2 => $this->blind($x, $r, 2) + ]; + $h = $m_i[1]->subtract($m_i[2]); + $h = $h->multiply($this->coefficients[2]); + [, $h] = $h->divide($this->primes[1]); + $m = $m_i[2]->add($h->multiply($this->primes[2])); + + $r = $this->primes[1]; + for ($i = 3; $i <= $num_primes; $i++) { + $m_i = $this->blind($x, $r, $i); + + $r = $r->multiply($this->primes[$i - 1]); + + $h = $m_i->subtract($m); + $h = $h->multiply($this->coefficients[$i]); + [, $h] = $h->divide($this->primes[$i]); + + $m = $m->add($r->multiply($h)); + } + + return $m; + } + + /** + * Performs RSA Blinding + * + * Protects against timing attacks by employing RSA Blinding. + * Returns $x->modPow($this->exponents[$i], $this->primes[$i]) + * + * @param BigIntegerInterface $x + * @param BigIntegerInterface $r + * @param int $i + * + * @return BigIntegerInterface + */ + private function blind($x, $r, $i) + { + $x = $x->multiply($r->modPow($this->publicExponent, $this->primes[$i])); + $x = $x->modPow($this->exponents[$i], $this->primes[$i]); + + $r = $r->modInverse($this->primes[$i]); + $x = $x->multiply($r); + [, $x] = $x->divide($this->primes[$i]); + + return $x; + } + + /** + * Integer-to-Octet-String primitive + * + * See {@link http://tools.ietf.org/html/rfc3447#section-4.1 RFC3447#section-4.1}. + * + * @param BigIntegerInterface $x + * @param int $xLen + * + * @return string + */ + private function i2osp($x, $xLen) + { + $x = $x->toBytes(); + if (strlen($x) > $xLen) { + throw new LogicException('Integer too large'); + } + return str_pad($x, $xLen, chr(0), STR_PAD_LEFT); + } + + /** + * RSAES-PKCS1-V1_5-ENCRYPT + * + * See {@link http://tools.ietf.org/html/rfc3447#section-7.2.1 RFC3447#section-7.2.1}. + * + * @param string $m + * + * @return string + */ + private function rsaesPkcs1V15Encrypt(string $m) + { + $mLen = strlen($m); + + // Length checking + + if ($mLen > $this->k - 11) { + throw new LogicException('Message too long'); + } + + // EME-PKCS1-v1_5 encoding + + $psLen = $this->k - $mLen - 3; + $ps = ''; + while (strlen($ps) != $psLen) { + if (!($temp = openssl_random_pseudo_bytes($psLen - strlen($ps)))) { + throw new LogicException('Can no generate random bytes.'); + } + $temp = str_replace("\x00", '', $temp); + $ps .= $temp; + } + $type = 2; + // see the comments of _rsaes_pkcs1_v1_5_decrypt() to understand why this is being done + if (defined('CRYPT_RSA_PKCS15_COMPAT') && + (!isset($this->publicExponent) || $this->exponent !== $this->publicExponent)) { + $type = 1; + // "The padding string PS shall consist of k-3-||D|| octets. ... + // for block type 01, they shall have value FF" + $ps = str_repeat("\xFF", $psLen); + } + $em = chr(0) . chr($type) . $ps . chr(0) . $m; + + // RSA encryption + $m = $this->os2ip($em); + $c = $this->rsaep($m); + $c = $this->i2osp($c, $this->k); + + // Output the ciphertext C + + return $c; + } + + /** + * RSAEP + * + * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.1 RFC3447#section-5.1.1}. + * + * @param BigIntegerInterface $m + * + * @return BigIntegerInterface + */ + private function rsaep($m) + { + if ($m->compare($this->zero) < 0 || $m->compare($this->modulus) > 0) { + throw new LogicException('Message representative out of range'); + } + return $this->exponentiate($m); + } + + /** + * RSAES-PKCS1-V1_5-DECRYPT + * + * See {@link http://tools.ietf.org/html/rfc3447#section-7.2.2 RFC3447#section-7.2.2}. + * + * For compatibility purposes, this function departs slightly from the description given in RFC3447. + * The reason being that RFC2313#section-8.1 (PKCS#1 v1.5) states that ciphertext's encrypted by the + * private key should have the second byte set to either 0 or 1 and that ciphertext's encrypted by the + * public key should have the second byte set to 2. In RFC3447 (PKCS#1 v2.1), the second byte is supposed + * to be 2 regardless of which key is used. For compatibility purposes, we'll just check to make sure the + * second byte is 2 or less. If it is, we'll accept the decrypted string as valid. + * + * As a consequence of this, a private key encrypted ciphertext produced with RSA may not decrypt + * with a strictly PKCS#1 v1.5 compliant RSA implementation. Public key encrypted ciphertext's should but + * not private key encrypted ciphertext's. + * + * @param string $c + * + * @return string + */ + private function rsaesPkcs1V15Decrypt($c) + { + // Length checking + + if (strlen($c) != $this->k) { // or if k < 11 + throw new LogicException('Decryption error'); + } + + // RSA decryption + + $c = $this->os2ip($c); + $m = $this->rsadp($c); + + $em = $this->i2osp($m, $this->k); + + // EME-PKCS1-v1_5 decoding + + if (ord($em[0]) != 0 || ord($em[1]) > 2) { + throw new LogicException('Decryption error'); + } + + $ps = substr($em, 2, strpos($em, chr(0), 2) - 2); + $m = substr($em, strlen($ps) + 3); + + if (strlen($ps) < 8) { + throw new LogicException('Decryption error'); + } + + // Output M + + return $m; + } + + /** + * RSADP + * + * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.2 RFC3447#section-5.1.2}. + * + * @param BigIntegerInterface $c + * + * @return BigIntegerInterface + */ + private function rsadp($c) + { + if ($c->compare($this->zero) < 0 || $c->compare($this->modulus) > 0) { + throw new LogicException('Ciphertext representative out of range'); + } + return $this->exponentiate($c); } } diff --git a/src/Models/Crypt/TripleDES.php b/src/Models/Crypt/TripleDES.php new file mode 100644 index 0000000..44b17bb --- /dev/null +++ b/src/Models/Crypt/TripleDES.php @@ -0,0 +1,68 @@ +key = $key; + } + + public function setIV($iv) + { + $this->iv = $iv; + } + + public function decrypt($ciphertext) + { + if (!($decrypted = openssl_decrypt( + $ciphertext, + $this->method, + $this->key, + $options = OPENSSL_RAW_DATA, + $this->iv + ))) { + throw new LogicException('Can not decrypt.'); + } + return $decrypted; + } + + public function encrypt($plaintext) + { + if (!($encrypted = openssl_encrypt( + $plaintext, + $this->method, + $this->key, + $options = OPENSSL_RAW_DATA, + $this->iv + ))) { + throw new LogicException('Can not encrypt.'); + } + return $encrypted; + } +} diff --git a/src/Models/Crypt/X509.php b/src/Models/Crypt/X509.php index 4931f8a..4e0801a 100644 --- a/src/Models/Crypt/X509.php +++ b/src/Models/Crypt/X509.php @@ -2,21 +2,1886 @@ namespace AndrewSvirin\Ebics\Models\Crypt; +use AndrewSvirin\Ebics\Contracts\Crypt\ASN1Interface; +use AndrewSvirin\Ebics\Contracts\Crypt\RSAInterface; use AndrewSvirin\Ebics\Contracts\Crypt\X509Interface; +use DateTime; +use DateTimeZone; +use LogicException; /** - * Crypt RSA model. + * Pure-PHP X.509 Parser * - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @author Andrew Svirin + * Encode and decode X.509 certificates. * - * @property \phpseclib\Math\BigInteger $exponent - * @property \phpseclib\Math\BigInteger $modulus + * The extensions are from {@link http://tools.ietf.org/html/rfc5280 RFC5280} and + * {@link http://web.archive.org/web/19961027104704/http://www3.netscape.com/eng/security/cert-exts.html + * Netscape Certificate Extensions}. + * + * Note that loading an X.509 certificate and resaving it may invalidate the signature. + * The reason being that the signature is based on a portion of the certificate that + * contains optional parameters with default values. ie. if the parameter isn't there + * the default value is used. Problem is, if the parameter is there and it just so happens + * to have the default value there are two ways that that parameter can be encoded. It can + * be encoded explicitly or left out all together. This would effect the signature value + * and thus may invalidate the the certificate all together unless the certificate is re-signed. */ -class X509 extends \phpseclib\File\X509 implements X509Interface +class X509 implements X509Interface { - public function saveX509CurrentCert(): string + + /**#@+*/ + /** + * Save as DER + */ + const FORMAT_DER = 1; + /** + * Auto-detect the format + * + * Used only by the load*() functions + */ + const FORMAT_AUTO_DETECT = 3; + /**#@-*/ + + /** + * Attribute value disposition. + * If disposition is >= 0, this is the index of the target value. + */ + const ATTR_ALL = -1; // All attribute values (array). + const ATTR_APPEND = -2; // Add a value. + const ATTR_REPLACE = -3; // Clear first, then add a value. + + /**#@+*/ + /** + * Return internal array representation + */ + const DN_ARRAY = 0; + /** + * Return string + */ + const DN_STRING = 1; + /** + * Return canonical ASN.1 RDNs string + */ + const DN_CANON = 4; + /**#@-*/ + + /**#@+ + * ASN.1 syntax for various extensions + */ + /** + * @var array + */ + protected $DirectoryString; + + /** + * @var array + */ + protected $PKCS9String; + + /** + * @var array + */ + protected $AttributeValue; + + /** + * @var array + */ + protected $Extensions; + + /** + * @var array + */ + protected $KeyUsage; + + /** + * @var array + */ + protected $ExtKeyUsageSyntax; + + /** + * @var array + */ + protected $BasicConstraints; + + /** + * @var array + */ + protected $KeyIdentifier; + + /** + * @var array + */ + protected $AuthorityKeyIdentifier; + + /** + * @var array + */ + protected $CertificatePolicies; + + /** + * @var array + */ + protected $SubjectAltName; + + /** + * @var array + */ + protected $Name; + + /** + * @var array + */ + protected $RelativeDistinguishedName; + + /** + * @var array + */ + protected $InvalidityDate; + + /** + * ASN.1 syntax for X.509 certificates + * + * @var array + */ + protected $Certificate; + + /** + * Public key + * + * @var RSAInterface|null + */ + protected $publicKey; + + /** + * Private key + * + * @var RSAInterface|null + */ + protected $privateKey; + + /** + * The currently loaded certificate + * + * @var array|null + */ + protected $currentCert; + + /** + * Certificate Start Date + * + * @var string + */ + protected $startDate; + + /** + * Certificate End Date + * + * @var string + */ + protected $endDate; + + /** + * Serial Number + * + * @var string + */ + protected $serialNumber; + + /** + * @var array + */ + protected $domains; + + /** + * The signature subject + * + * There's no guarantee X509 is going to re-encode an X.509 cert in the same way it was originally + * encoded so we take save the portion of the original cert that the signature would have made for. + * + * @var string + */ + protected $signatureSubject; + + /** + * Distinguished Name + * + * @var array|null + */ + protected $dn; + + /** + * Object identifiers for X.509 certificates + * + * @var array + * @link http://en.wikipedia.org/wiki/Object_identifier + */ + protected $oids; + + /** + * Key Identifier + * + * See {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.1 RFC5280#section-4.2.1.1} and + * {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.2 RFC5280#section-4.2.1.2}. + * + * @var string + */ + protected $currentKeyIdentifier; + + /** + * Default Constructor. + */ + public function __construct() + { + // Explicitly Tagged Module, 1988 Syntax + // http://tools.ietf.org/html/rfc5280#appendix-A.1 + + $this->DirectoryString = [ + 'type' => ASN1::TYPE_CHOICE, + 'children' => [ + 'teletexString' => ['type' => ASN1::TYPE_TELETEX_STRING], + 'printableString' => ['type' => ASN1::TYPE_PRINTABLE_STRING], + 'universalString' => ['type' => ASN1::TYPE_UNIVERSAL_STRING], + 'utf8String' => ['type' => ASN1::TYPE_UTF8_STRING], + 'bmpString' => ['type' => ASN1::TYPE_BMP_STRING] + ] + ]; + + $this->PKCS9String = [ + 'type' => ASN1::TYPE_CHOICE, + 'children' => [ + 'ia5String' => ['type' => ASN1::TYPE_IA5_STRING], + 'directoryString' => $this->DirectoryString + ] + ]; + + $this->AttributeValue = ['type' => ASN1::TYPE_ANY]; + + $AttributeType = ['type' => ASN1::TYPE_OBJECT_IDENTIFIER]; + + $AttributeTypeAndValue = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'type' => $AttributeType, + 'value' => $this->AttributeValue + ] + ]; + + /* + In practice, RDNs containing multiple name-value pairs (called "multivalued RDNs") are rare, + but they can be useful at times when either there is no unique attribute in the entry or you + want to ensure that the entry's DN contains some useful identifying information. + + - https://www.opends.org/wiki/page/DefinitionRelativeDistinguishedName + */ + $this->RelativeDistinguishedName = [ + 'type' => ASN1::TYPE_SET, + 'min' => 1, + 'max' => -1, + 'children' => $AttributeTypeAndValue + ]; + + // http://tools.ietf.org/html/rfc5280#section-4.1.2.4 + $RDNSequence = [ + 'type' => ASN1::TYPE_SEQUENCE, + // RDNSequence does not define a min or a max, which means it doesn't have one + 'min' => 0, + 'max' => -1, + 'children' => $this->RelativeDistinguishedName + ]; + + $this->Name = [ + 'type' => ASN1::TYPE_CHOICE, + 'children' => [ + 'rdnSequence' => $RDNSequence + ] + ]; + + // http://tools.ietf.org/html/rfc5280#section-4.1.1.2 + $AlgorithmIdentifier = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'algorithm' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], + 'parameters' => [ + 'type' => ASN1::TYPE_ANY, + 'optional' => true + ] + ] + ]; + + /* + A certificate using system MUST reject the certificate if it encounters + a critical extension it does not recognize; however, a non-critical + extension may be ignored if it is not recognized. + + http://tools.ietf.org/html/rfc5280#section-4.2 + */ + $Extension = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'extnId' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], + 'critical' => [ + 'type' => ASN1::TYPE_BOOLEAN, + 'optional' => true, + 'default' => false + ], + 'extnValue' => ['type' => ASN1::TYPE_OCTET_STRING] + ] + ]; + + $this->Extensions = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + // technically, it's MAX, but we'll assume anything < 0 is MAX + 'max' => -1, + // if 'children' isn't an array then 'min' and 'max' must be defined + 'children' => $Extension + ]; + + $SubjectPublicKeyInfo = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'algorithm' => $AlgorithmIdentifier, + 'subjectPublicKey' => ['type' => ASN1::TYPE_BIT_STRING] + ] + ]; + + $UniqueIdentifier = ['type' => ASN1::TYPE_BIT_STRING]; + + $Time = [ + 'type' => ASN1::TYPE_CHOICE, + 'children' => [ + 'utcTime' => ['type' => ASN1::TYPE_UTC_TIME], + 'generalTime' => ['type' => ASN1::TYPE_GENERALIZED_TIME] + ] + ]; + + // http://tools.ietf.org/html/rfc5280#section-4.1.2.5 + $Validity = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'notBefore' => $Time, + 'notAfter' => $Time + ] + ]; + + $CertificateSerialNumber = ['type' => ASN1::TYPE_INTEGER]; + + $Version = [ + 'type' => ASN1::TYPE_INTEGER, + 'mapping' => ['v1', 'v2', 'v3'] + ]; + + // assert($TBSCertificate['children']['signature'] == $Certificate['children']['signatureAlgorithm']) + $TBSCertificate = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + // technically, default implies optional, but we'll define it as being optional, none-the-less, just to + // reenforce that fact + 'version' => [ + 'constant' => 0, + 'optional' => true, + 'explicit' => true, + 'default' => 'v1' + ] + $Version, + 'serialNumber' => $CertificateSerialNumber, + 'signature' => $AlgorithmIdentifier, + 'issuer' => $this->Name, + 'validity' => $Validity, + 'subject' => $this->Name, + 'subjectPublicKeyInfo' => $SubjectPublicKeyInfo, + // implicit means that the T in the TLV structure is to be rewritten, regardless of the type + 'issuerUniqueID' => [ + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ] + $UniqueIdentifier, + 'subjectUniqueID' => [ + 'constant' => 2, + 'optional' => true, + 'implicit' => true + ] + $UniqueIdentifier, + // doesn't use the EXPLICIT keyword but if + // it's not IMPLICIT, it's EXPLICIT + 'extensions' => [ + 'constant' => 3, + 'optional' => true, + 'explicit' => true + ] + $this->Extensions + ] + ]; + + $this->Certificate = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'tbsCertificate' => $TBSCertificate, + 'signatureAlgorithm' => $AlgorithmIdentifier, + 'signature' => ['type' => ASN1::TYPE_BIT_STRING] + ] + ]; + + $this->KeyUsage = [ + 'type' => ASN1::TYPE_BIT_STRING, + 'mapping' => [ + 'digitalSignature', + 'nonRepudiation', + 'keyEncipherment', + 'dataEncipherment', + 'keyAgreement', + 'keyCertSign', + 'cRLSign', + 'encipherOnly', + 'decipherOnly' + ] + ]; + + $this->BasicConstraints = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'cA' => [ + 'type' => ASN1::TYPE_BOOLEAN, + 'optional' => true, + 'default' => false + ], + 'pathLenConstraint' => [ + 'type' => ASN1::TYPE_INTEGER, + 'optional' => true + ] + ] + ]; + + $this->KeyIdentifier = ['type' => ASN1::TYPE_OCTET_STRING]; + + $OrganizationalUnitNames = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => 4, // ub-organizational-units + 'children' => ['type' => ASN1::TYPE_PRINTABLE_STRING] + ]; + + $PersonalName = [ + 'type' => ASN1::TYPE_SET, + 'children' => [ + 'surname' => [ + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ], + 'given-name' => [ + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ], + 'initials' => [ + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 2, + 'optional' => true, + 'implicit' => true + ], + 'generation-qualifier' => [ + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 3, + 'optional' => true, + 'implicit' => true + ] + ] + ]; + + $NumericUserIdentifier = ['type' => ASN1::TYPE_NUMERIC_STRING]; + + $OrganizationName = ['type' => ASN1::TYPE_PRINTABLE_STRING]; + + $PrivateDomainName = [ + 'type' => ASN1::TYPE_CHOICE, + 'children' => [ + 'numeric' => ['type' => ASN1::TYPE_NUMERIC_STRING], + 'printable' => ['type' => ASN1::TYPE_PRINTABLE_STRING] + ] + ]; + + $TerminalIdentifier = ['type' => ASN1::TYPE_PRINTABLE_STRING]; + + $NetworkAddress = ['type' => ASN1::TYPE_NUMERIC_STRING]; + + $AdministrationDomainName = [ + 'type' => ASN1::TYPE_CHOICE, + // if class isn't present it's assumed to be ASN1::CLASS_UNIVERSAL or + // (if constant is present) ASN1::CLASS_CONTEXT_SPECIFIC + 'class' => ASN1::CLASS_APPLICATION, + 'cast' => 2, + 'children' => [ + 'numeric' => ['type' => ASN1::TYPE_NUMERIC_STRING], + 'printable' => ['type' => ASN1::TYPE_PRINTABLE_STRING] + ] + ]; + + $CountryName = [ + 'type' => ASN1::TYPE_CHOICE, + // if class isn't present it's assumed to be ASN1::CLASS_UNIVERSAL or + // (if constant is present) ASN1::CLASS_CONTEXT_SPECIFIC + 'class' => ASN1::CLASS_APPLICATION, + 'cast' => 1, + 'children' => [ + 'x121-dcc-code' => ['type' => ASN1::TYPE_NUMERIC_STRING], + 'iso-3166-alpha2-code' => ['type' => ASN1::TYPE_PRINTABLE_STRING] + ] + ]; + + $AnotherName = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'type-id' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], + 'value' => [ + 'type' => ASN1::TYPE_ANY, + 'constant' => 0, + 'optional' => true, + 'explicit' => true + ] + ] + ]; + + $ExtensionAttribute = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'extension-attribute-type' => [ + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ], + 'extension-attribute-value' => [ + 'type' => ASN1::TYPE_ANY, + 'constant' => 1, + 'optional' => true, + 'explicit' => true + ] + ] + ]; + + $ExtensionAttributes = [ + 'type' => ASN1::TYPE_SET, + 'min' => 1, + 'max' => 256, // ub-extension-attributes + 'children' => $ExtensionAttribute + ]; + + $BuiltInDomainDefinedAttribute = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'type' => ['type' => ASN1::TYPE_PRINTABLE_STRING], + 'value' => ['type' => ASN1::TYPE_PRINTABLE_STRING] + ] + ]; + + $BuiltInDomainDefinedAttributes = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => 4, // ub-domain-defined-attributes + 'children' => $BuiltInDomainDefinedAttribute + ]; + + $BuiltInStandardAttributes = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'country-name' => ['optional' => true] + $CountryName, + 'administration-domain-name' => ['optional' => true] + $AdministrationDomainName, + 'network-address' => [ + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ] + $NetworkAddress, + 'terminal-identifier' => [ + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ] + $TerminalIdentifier, + 'private-domain-name' => [ + 'constant' => 2, + 'optional' => true, + 'explicit' => true + ] + $PrivateDomainName, + 'organization-name' => [ + 'constant' => 3, + 'optional' => true, + 'implicit' => true + ] + $OrganizationName, + 'numeric-user-identifier' => [ + 'constant' => 4, + 'optional' => true, + 'implicit' => true + ] + $NumericUserIdentifier, + 'personal-name' => [ + 'constant' => 5, + 'optional' => true, + 'implicit' => true + ] + $PersonalName, + 'organizational-unit-names' => [ + 'constant' => 6, + 'optional' => true, + 'implicit' => true + ] + $OrganizationalUnitNames + ] + ]; + + $ORAddress = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'built-in-standard-attributes' => $BuiltInStandardAttributes, + 'built-in-domain-defined-attributes' => ['optional' => true] + $BuiltInDomainDefinedAttributes, + 'extension-attributes' => ['optional' => true] + $ExtensionAttributes + ] + ]; + + $EDIPartyName = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'nameAssigner' => [ + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ] + $this->DirectoryString, + // partyName is technically required but ASN1 doesn't currently support non-optional constants and + // setting it to optional gets the job done in any event. + 'partyName' => [ + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ] + $this->DirectoryString + ] + ]; + + $GeneralName = [ + 'type' => ASN1::TYPE_CHOICE, + 'children' => [ + 'otherName' => [ + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ] + $AnotherName, + 'rfc822Name' => [ + 'type' => ASN1::TYPE_IA5_STRING, + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ], + 'dNSName' => [ + 'type' => ASN1::TYPE_IA5_STRING, + 'constant' => 2, + 'optional' => true, + 'implicit' => true + ], + 'x400Address' => [ + 'constant' => 3, + 'optional' => true, + 'implicit' => true + ] + $ORAddress, + 'directoryName' => [ + 'constant' => 4, + 'optional' => true, + 'explicit' => true + ] + $this->Name, + 'ediPartyName' => [ + 'constant' => 5, + 'optional' => true, + 'implicit' => true + ] + $EDIPartyName, + 'uniformResourceIdentifier' => [ + 'type' => ASN1::TYPE_IA5_STRING, + 'constant' => 6, + 'optional' => true, + 'implicit' => true + ], + 'iPAddress' => [ + 'type' => ASN1::TYPE_OCTET_STRING, + 'constant' => 7, + 'optional' => true, + 'implicit' => true + ], + 'registeredID' => [ + 'type' => ASN1::TYPE_OBJECT_IDENTIFIER, + 'constant' => 8, + 'optional' => true, + 'implicit' => true + ] + ] + ]; + + $GeneralNames = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $GeneralName + ]; + + $this->AuthorityKeyIdentifier = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'keyIdentifier' => [ + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ] + $this->KeyIdentifier, + 'authorityCertIssuer' => [ + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ] + $GeneralNames, + 'authorityCertSerialNumber' => [ + 'constant' => 2, + 'optional' => true, + 'implicit' => true + ] + $CertificateSerialNumber + ] + ]; + + $PolicyQualifierId = ['type' => ASN1::TYPE_OBJECT_IDENTIFIER]; + + $PolicyQualifierInfo = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'policyQualifierId' => $PolicyQualifierId, + 'qualifier' => ['type' => ASN1::TYPE_ANY] + ] + ]; + + $CertPolicyId = ['type' => ASN1::TYPE_OBJECT_IDENTIFIER]; + + $PolicyInformation = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'policyIdentifier' => $CertPolicyId, + 'policyQualifiers' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 0, + 'max' => -1, + 'optional' => true, + 'children' => $PolicyQualifierInfo + ] + ] + ]; + + $this->CertificatePolicies = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $PolicyInformation + ]; + + $KeyPurposeId = ['type' => ASN1::TYPE_OBJECT_IDENTIFIER]; + + $this->ExtKeyUsageSyntax = [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $KeyPurposeId + ]; + + $this->SubjectAltName = $GeneralNames; + + $this->InvalidityDate = ['type' => ASN1::TYPE_GENERALIZED_TIME]; + + // OIDs from RFC5280 and those RFCs mentioned in RFC5280#section-4.1.1.2 + $this->oids = [ + '2.5.4' => 'id-at', + '2.5.4.41' => 'id-at-name', + '2.5.4.4' => 'id-at-surname', + '2.5.4.42' => 'id-at-givenName', + '2.5.4.43' => 'id-at-initials', + '2.5.4.44' => 'id-at-generationQualifier', + '2.5.4.3' => 'id-at-commonName', + '2.5.4.7' => 'id-at-localityName', + '2.5.4.8' => 'id-at-stateOrProvinceName', + '2.5.4.10' => 'id-at-organizationName', + '2.5.4.11' => 'id-at-organizationalUnitName', + '2.5.4.12' => 'id-at-title', + '2.5.4.13' => 'id-at-description', + '2.5.4.46' => 'id-at-dnQualifier', + '2.5.4.6' => 'id-at-countryName', + '2.5.4.5' => 'id-at-serialNumber', + '2.5.4.65' => 'id-at-pseudonym', + '2.5.4.17' => 'id-at-postalCode', + '2.5.4.9' => 'id-at-streetAddress', + '2.5.4.45' => 'id-at-uniqueIdentifier', + '2.5.4.72' => 'id-at-role', + '2.5.4.16' => 'id-at-postalAddress', + + '0.9.2342.19200300.100.1.25' => 'id-domainComponent', + '2.5.29' => 'id-ce', + '2.5.29.35' => 'id-ce-authorityKeyIdentifier', + '2.5.29.14' => 'id-ce-subjectKeyIdentifier', + '2.5.29.15' => 'id-ce-keyUsage', + '2.5.29.16' => 'id-ce-privateKeyUsagePeriod', + '2.5.29.32' => 'id-ce-certificatePolicies', + '2.5.29.32.0' => 'anyPolicy', + + '2.5.29.33' => 'id-ce-policyMappings', + '2.5.29.17' => 'id-ce-subjectAltName', + '2.5.29.18' => 'id-ce-issuerAltName', + '2.5.29.9' => 'id-ce-subjectDirectoryAttributes', + '2.5.29.19' => 'id-ce-basicConstraints', + '2.5.29.30' => 'id-ce-nameConstraints', + '2.5.29.36' => 'id-ce-policyConstraints', + '2.5.29.31' => 'id-ce-cRLDistributionPoints', + '2.5.29.37' => 'id-ce-extKeyUsage', + '2.5.29.37.0' => 'anyExtendedKeyUsage', + '1.3.6.1.5.5.7.3.1' => 'id-kp-serverAuth', + '1.3.6.1.5.5.7.3.2' => 'id-kp-clientAuth', + '1.3.6.1.5.5.7.3.3' => 'id-kp-codeSigning', + '1.3.6.1.5.5.7.3.4' => 'id-kp-emailProtection', + '1.3.6.1.5.5.7.3.8' => 'id-kp-timeStamping', + '1.3.6.1.5.5.7.1.1' => 'id-pe-authorityInfoAccess', + '1.3.6.1.5.5.7.1.11' => 'id-pe-subjectInfoAccess', + + '1.2.840.113549.1.1.1' => 'rsaEncryption', + '1.2.840.113549.1.1.11' => 'sha256WithRSAEncryption', + ]; + } + + public function saveX509CurrentCert() { return $this->saveX509($this->currentCert); } + + public function setStartDate($date) + { + $date = new DateTime($date, new DateTimeZone(@date_default_timezone_get())); + + $this->startDate = $date->format('D, d M Y H:i:s O'); + } + + public function setEndDate($date) + { + $date = new DateTime($date, new DateTimeZone(@date_default_timezone_get())); + + $this->endDate = $date->format('D, d M Y H:i:s O'); + } + + public function setSerialNumber($serial, $base = -256) + { + $this->serialNumber = new BigInteger($serial, $base); + } + + public function sign( + $issuer, + $subject, + $signatureAlgorithm = 'sha1WithRSAEncryption' + ) { + if (empty($issuer->getPrivateKey()) || empty($issuer->getDN())) { + return false; + } + + $subjectPublicKey = $subject->formatSubjectPublicKey(); + + $currentCert = isset($this->currentCert) ? $this->currentCert : null; + $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; + + if (isset($subject->currentCert) && + is_array($subject->currentCert) && + isset($subject->currentCert['tbsCertificate'])) { + $this->currentCert = $subject->currentCert; + $this->currentCert['tbsCertificate']['signature']['algorithm'] = $signatureAlgorithm; + $this->currentCert['signatureAlgorithm']['algorithm'] = $signatureAlgorithm; + + if (!empty($this->startDate)) { + $this->currentCert['tbsCertificate']['validity']['notBefore'] = $this->timeField($this->startDate); + } + if (!empty($this->endDate)) { + $this->currentCert['tbsCertificate']['validity']['notAfter'] = $this->timeField($this->endDate); + } + if (!empty($this->serialNumber)) { + $this->currentCert['tbsCertificate']['serialNumber'] = $this->serialNumber; + } + if (!empty($subject->dn)) { + $this->currentCert['tbsCertificate']['subject'] = $subject->getDN(); + } + if (!empty($subject->publicKey)) { + $this->currentCert['tbsCertificate']['subjectPublicKeyInfo'] = $subjectPublicKey; + } + $this->removeExtension('id-ce-authorityKeyIdentifier'); + if (isset($subject->domains)) { + $this->removeExtension('id-ce-subjectAltName'); + } + } elseif (isset($subject->currentCert) && + is_array($subject->currentCert) && + isset($subject->currentCert['tbsCertList'])) { + return false; + } else { + if (!isset($subject->publicKey)) { + return false; + } + + $startDate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); + $startDate = !empty($this->startDate) ? $this->startDate : $startDate->format('D, d M Y H:i:s O'); + + $endDate = new DateTime('+1 year', new DateTimeZone(@date_default_timezone_get())); + $endDate = !empty($this->endDate) ? $this->endDate : $endDate->format('D, d M Y H:i:s O'); + + /* "The serial number MUST be a positive integer" + "Conforming CAs MUST NOT use serialNumber values longer than 20 octets." + -- https://tools.ietf.org/html/rfc5280#section-4.1.2.2 + + for the integer to be positive the leading bit needs to be 0 hence the + application of a bitmap + */ + if (empty($this->serialNumber)) { + throw new LogicException('Serial number must be defined.'); + } + $serialNumber = $this->serialNumber; + + $this->currentCert = [ + 'tbsCertificate' => + [ + 'version' => 'v3', + 'serialNumber' => $serialNumber, // $this->setSerialNumber() + 'signature' => ['algorithm' => $signatureAlgorithm], + 'issuer' => false, // this is going to be overwritten later + 'validity' => [ + 'notBefore' => $this->timeField($startDate), // $this->setStartDate() + 'notAfter' => $this->timeField($endDate) // $this->setEndDate() + ], + 'subject' => $subject->getDN(), + 'subjectPublicKeyInfo' => $subjectPublicKey + ], + 'signatureAlgorithm' => ['algorithm' => $signatureAlgorithm], + 'signature' => false // this is going to be overwritten later + ]; + + // Copy extensions from CSR. + $csrexts = $subject->getAttribute('pkcs-9-at-extensionRequest', 0); + + if (!empty($csrexts)) { + $this->currentCert['tbsCertificate']['extensions'] = $csrexts; + } + } + + $this->currentCert['tbsCertificate']['issuer'] = $issuer->getDN(); + + if (isset($issuer->currentKeyIdentifier)) { + $this->setExtension('id-ce-authorityKeyIdentifier', [ + 'keyIdentifier' => $issuer->currentKeyIdentifier + ]); + } + + if (isset($subject->currentKeyIdentifier)) { + $this->setExtension('id-ce-subjectKeyIdentifier', $subject->currentKeyIdentifier); + } + + $altName = []; + + if (isset($subject->domains) && count($subject->domains)) { + $altName = array_map([X509::class, 'dnsName'], $subject->domains); + } + + if (isset($subject->ipAddresses) && count($subject->ipAddresses)) { + throw new LogicException('Subject IP address is not supported.'); + } + + if (!empty($altName)) { + $this->setExtension('id-ce-subjectAltName', $altName); + } + + // resync $this->signatureSubject + $tbsCertificate = $this->currentCert['tbsCertificate']; + $this->loadX509($this->saveX509($this->currentCert)); + + $result = $this->signByKey($issuer->getPrivateKey(), $signatureAlgorithm); + $result['tbsCertificate'] = $tbsCertificate; + + $this->currentCert = $currentCert; + $this->signatureSubject = $signatureSubject; + + return $result; + } + + public function loadX509($cert) + { + $asn1 = new ASN1(); + + if ($cert !== false) { + $newcert = $this->extractBER($cert); + $cert = $newcert; + } + + if ($cert === false) { + $this->currentCert = null; + return false; + } + + $asn1->loadOIDs($this->oids); + $decoded = $asn1->decodeBER($cert); + + if (!empty($decoded)) { + $x509 = $asn1->asn1map($decoded[0], $this->Certificate); + } + if (!isset($x509) || $x509 === false) { + $this->currentCert = null; + return false; + } + + $this->signatureSubject = substr( + $cert, + $decoded[0]['content'][0]['start'], + $decoded[0]['content'][0]['length'] + ); + + if (is_array($x509) && $this->isSubArrayValid($x509, 'tbsCertificate/extensions')) { + $this->mapInExtensions($x509, 'tbsCertificate/extensions', $asn1); + } + + $key = &$x509['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']; + $key = $this->reformatKey($x509['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'], $key); + + $this->currentCert = $x509; + $this->dn = $x509['tbsCertificate']['subject']; + + $currentKeyIdentifier = $this->getExtension('id-ce-subjectKeyIdentifier'); + $this->currentKeyIdentifier = is_string($currentKeyIdentifier) ? $currentKeyIdentifier : null; + + return $x509; + } + + /** + * Remove an Extension + * + * @param string $id + * @param string|null $path optional + * + * @return bool + */ + private function removeExtension(string $id, string $path = null) + { + $extensions = &$this->extensions($this->currentCert, $path); + + if (!is_array($extensions)) { + return false; + } + + $result = false; + foreach ($extensions as $key => $value) { + if ($value['extnId'] == $id) { + unset($extensions[$key]); + $result = true; + } + } + + $extensions = array_values($extensions); + // fix for https://bugs.php.net/75433 affecting PHP 7.2 + if (!isset($extensions[0])) { + $extensions = array_splice($extensions, 0, 0); + } + return $result; + } + + public function saveX509($cert) + { + if (!is_array($cert) || !isset($cert['tbsCertificate'])) { + return false; + } + + switch (true) { + // "case !$a: case !$b: break; default: whatever();" is the same thing as "if ($a && $b) whatever()" + case !($algorithm = $this->subArray($cert, 'tbsCertificate/subjectPublicKeyInfo/algorithm/algorithm')): + case is_object($cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']): + break; + default: + switch ($algorithm) { + case 'rsaEncryption': + $cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'] + = base64_encode("\0" . base64_decode(preg_replace( + '#-.+-|[\r\n]#', + '', + $cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'] + ))); + /* "[For RSA keys] the parameters field MUST have ASN.1 type NULL for this + algorithm identifier." + -- https://tools.ietf.org/html/rfc3279#section-2.3.1 + + given that and the fact that RSA keys appear ot be the only key type for + which the parameters field can be blank, it seems like perhaps the ASN.1 + description ought not say the parameters field is OPTIONAL, but whatever. + */ + $cert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['parameters'] = null; + // https://tools.ietf.org/html/rfc3279#section-2.2.1 + $cert['signatureAlgorithm']['parameters'] = null; + $cert['tbsCertificate']['signature']['parameters'] = null; + } + } + + $asn1 = new ASN1(); + $asn1->loadOIDs($this->oids); + + $filters = []; + $type_utf8_string = ['type' => ASN1::TYPE_UTF8_STRING]; + $filters['tbsCertificate']['signature']['parameters'] = $type_utf8_string; + $filters['tbsCertificate']['signature']['issuer']['rdnSequence']['value'] = $type_utf8_string; + $filters['tbsCertificate']['issuer']['rdnSequence']['value'] = $type_utf8_string; + $filters['tbsCertificate']['subject']['rdnSequence']['value'] = $type_utf8_string; + $filters['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['parameters'] = $type_utf8_string; + $filters['signatureAlgorithm']['parameters'] = $type_utf8_string; + $filters['authorityCertIssuer']['directoryName']['rdnSequence']['value'] = $type_utf8_string; + //$filters['policyQualifiers']['qualifier'] = $type_utf8_string; + $filters['distributionPoint']['fullName']['directoryName']['rdnSequence']['value'] = $type_utf8_string; + $filters['directoryName']['rdnSequence']['value'] = $type_utf8_string; + + /* in the case of policyQualifiers/qualifier, the type has to be ASN1::TYPE_IA5_STRING. + ASN1::TYPE_PRINTABLE_STRING will cause OpenSSL's X.509 parser to spit out random + characters. + */ + $filters['policyQualifiers']['qualifier'] = ['type' => ASN1::TYPE_IA5_STRING]; + + $asn1->loadFilters($filters); + + $this->mapOutExtensions($cert, 'tbsCertificate/extensions', $asn1); + + $cert = $asn1->encodeDER($cert, $this->Certificate); + + return "-----BEGIN CERTIFICATE-----\r\n" . chunk_split(base64_encode($cert), 64) . + '-----END CERTIFICATE-----'; + } + + public function setPublicKey($key) + { + $key->setPublicKey(); + $this->publicKey = $key; + } + + public function getPublicKey(): ?RSAInterface + { + return $this->publicKey; + } + + public function setPrivateKey($key) + { + $this->privateKey = $key; + } + + public function getPrivateKey(): ?RSAInterface + { + return $this->privateKey; + } + + public function setDN($dn, $type = 'utf8String') + { + $this->dn = null; + + if (is_array($dn)) { + if (isset($dn['rdnSequence'])) { + $this->dn = $dn; // No merge here. + return true; + } + + // handles stuff generated by openssl_x509_parse() + foreach ($dn as $prop => $value) { + if (!$this->setDNProp($prop, $value, $type)) { + return false; + } + } + return true; + } + + // handles everything else + $results = preg_split('#((?:^|, *|/)(?:C=|O=|OU=|CN=|L=|ST=|SN=|postalCode=|streetAddress=|' . + 'emailAddress=|serialNumber=|organizationalUnitName=|title=|description=|role=|' . + 'x500UniqueIdentifier=|postalAddress=))#', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); + + if (!is_array($results)) { + throw new LogicException('Split result must be an array.'); + } + + for ($i = 1; $i < count($results); $i += 2) { + $prop = trim($results[$i], ', =/'); + $value = $results[$i + 1]; + if (!$this->setDNProp($prop, $value, $type)) { + return false; + } + } + + return true; + } + + public function getDN() + { + return is_array($this->currentCert) && isset($this->currentCert['tbsCertList']) ? + $this->currentCert['tbsCertList']['issuer'] : $this->dn; + } + + public function setDomain() + { + $this->domains = func_get_args(); + $this->removeDNProp('id-at-commonName'); + $this->setDNProp('id-at-commonName', $this->domains[0]); + } + + public function setKeyIdentifier($value) + { + if (empty($value)) { + unset($this->currentKeyIdentifier); + } else { + $this->currentKeyIdentifier = base64_encode($value); + } + } + + public function computeKeyIdentifier($key = null) + { + if (is_null($key)) { + $key = $this; + } + + switch (true) { + case $key instanceof RSAInterface: + $key = $key->getPublicKey(RSA::PUBLIC_FORMAT_PKCS1); + break; + default: + throw new LogicException('Key type incorrect.'); + } + + // If in PEM format, convert to binary. + $key = $this->extractBER($key); + + // Now we have the key string: compute its sha-1 sum. + $hash = new Hash('sha1'); + $hash = $hash->hash($key); + + return $hash; + } + + public function formatSubjectPublicKey(): ?array + { + if ($this->publicKey instanceof RSAInterface) { + // the following two return statements do the same thing. i dunno.. i just prefer the later for some reason. + // the former is a good example of how to do fuzzing on the public key + return [ + 'algorithm' => ['algorithm' => 'rsaEncryption'], + 'subjectPublicKey' => $this->publicKey->getPublicKey(RSA::PUBLIC_FORMAT_PKCS1) + ]; + } + + return null; + } + + /** + * X.509 certificate signing helper function. + * + * @param RSAInterface $key + * @param string $signatureAlgorithm + * + * @return array + */ + private function signByKey(RSAInterface $key, string $signatureAlgorithm) + { + switch ($signatureAlgorithm) { + case 'sha256WithRSAEncryption': + $key->setHash(preg_replace('#WithRSAEncryption$#', '', $signatureAlgorithm)); + + $this->currentCert['signature'] = base64_encode("\0" . $key->sign($this->signatureSubject)); + return $this->currentCert; + default: + throw new LogicException('signatureAlgorithm not defined,'); + } + } + + /** + * Helper function to build a time field according to RFC 3280 section + * - 4.1.2.5 Validity + * - 5.1.2.4 This Update + * - 5.1.2.5 Next Update + * - 5.1.2.6 Revoked Certificates + * by choosing utcTime iff year of date given is before 2050 and generalTime else. + * + * @param string $date in format date('D, d M Y H:i:s O') + * + * @return array + */ + private function timeField(string $date) + { + $dateObj = new DateTime($date, new DateTimeZone('GMT')); + $year = $dateObj->format('Y'); // the same way ASN1.php parses this + if ($year < 2050) { + return ['utcTime' => $date]; + } else { + return ['generalTime' => $date]; + } + } + + public function getAttribute($id, $disposition = self::ATTR_ALL, $csr = null) + { + if (empty($csr)) { + $csr = $this->currentCert; + } + + $attributes = $this->subArray($csr, 'certificationRequestInfo/attributes'); + + if (!is_array($attributes)) { + return false; + } + + foreach ($attributes as $key => $attribute) { + if ($attribute['type'] == $id) { + $n = count($attribute['value']); + switch (true) { + case $disposition == self::ATTR_APPEND: + case $disposition == self::ATTR_REPLACE: + return false; + case $disposition == self::ATTR_ALL: + return $attribute['value']; + case $disposition >= $n: + $disposition -= $n; + break; + default: + return $attribute['value'][$disposition]; + } + } + } + + return false; + } + + /** + * Get a reference to a subarray + * + * @param array $root + * @param string $path absolute path with / as component separator + * @param bool $create optional + * + * @return array|false + */ + private function &subArray(&$root, string $path, bool $create = false) + { + $false = false; + + if (!is_array($root)) { + return $false; + } + + foreach (explode('/', $path) as $i) { + if (!is_array($root)) { + return $false; + } + + if (!isset($root[$i])) { + if (!$create) { + return $false; + } + + $root[$i] = []; + } + + $root = &$root[$i]; + } + + return $root; + } + + /** + * Extract raw BER from Base64 encoding + * + * @param string $str + * + * @return string + */ + private function extractBER(string $str) + { + /* X.509 certs are assumed to be base64 encoded but sometimes they'll have additional things in them + * above and beyond the ceritificate. + * ie. some may have the following preceding the -----BEGIN CERTIFICATE----- line: + * + * Bag Attributes + * localKeyID: 01 00 00 00 + * subject=/O=organization/OU=org unit/CN=common name + * issuer=/O=organization/CN=common name + */ + $temp = strlen($str) <= ini_get('pcre.backtrack_limit') ? + preg_replace('#.*?^-+[^-]+-+[\r\n ]*$#ms', '', $str, 1) : + $str; + // remove new lines + $temp = str_replace(["\r", "\n", ' '], '', $temp); + // remove the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- stuff + $temp = preg_replace('#^-+[^-]+-+|-+[^-]+-+$#', '', $temp); + $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? base64_decode($temp) : false; + return $temp != false ? $temp : $str; + } + + /** + * Check for validity of subarray + * + * This is intended for use in conjunction with _subArrayUnchecked(), + * implementing the checks included in _subArray() but without copying + * a potentially large array by passing its reference by-value to is_array(). + * + * @param array $root + * @param string $path + * + * @return boolean + */ + private function isSubArrayValid($root, string $path) + { + if (!is_array($root)) { + return false; + } + + foreach (explode('/', $path) as $i) { + if (!is_array($root)) { + return false; + } + + if (!isset($root[$i])) { + return true; + } + + $root = $root[$i]; + } + + return true; + } + + /** + * Map extension values from octet string to extension-specific internal + * format. + * + * @param array $root (by reference) + * @param string $path + * @param ASN1Interface $asn1 + * + * @return void + */ + private function mapInExtensions(&$root, string $path, ASN1Interface $asn1) + { + $extensions = &$this->subArrayUnchecked($root, $path); + + if ($extensions) { + for ($i = 0; $i < count($extensions); $i++) { + $id = $extensions[$i]['extnId']; + $value = &$extensions[$i]['extnValue']; + $value = base64_decode($value); + $decoded = $asn1->decodeBER($value); + /* [extnValue] contains the DER encoding of an ASN.1 value + corresponding to the extension type identified by extnID */ + $map = $this->getMapping($id); + if (!is_bool($map)) { + $decoder = $id == 'id-ce-nameConstraints' ? + [$this, '_decodeNameConstraintIP'] : + [$this, '_decodeIP']; + $mapped = $asn1->asn1map($decoded[0], $map, ['iPAddress' => $decoder]); + $value = $mapped === false ? $decoded[0] : $mapped; + } else { + $value = base64_encode($value); + } + } + } + } + + /** + * Reformat public keys + * + * Reformats a public key to a format supported by lib (if applicable) + * + * @param string $algorithm + * @param string $key + * + * @return string + */ + private function reformatKey(string $algorithm, string $key) + { + switch ($algorithm) { + case 'rsaEncryption': + return + "-----BEGIN RSA PUBLIC KEY-----\r\n" . + // subjectPublicKey is stored as a bit string in X.509 certs. the first byte of + // a bit string represents how many bits in the last byte should be ignored. the + // following only supports non-zero stuff but as none of the X.509 certs Firefox + // uses as a cert authority actually use a non-zero bit I think it's safe to assume + // that none do. + chunk_split(base64_encode(substr(base64_decode($key), 1)), 64) . + '-----END RSA PUBLIC KEY-----'; + default: + return $key; + } + } + + public function setExtension($id, $value, $critical = false, $replace = true, string $path = null) + { + $extensions = &$this->extensions($this->currentCert, $path, true); + + if (!is_array($extensions)) { + return false; + } + + $newext = ['extnId' => $id, 'critical' => $critical, 'extnValue' => $value]; + + foreach ($extensions as $key => $value) { + if ($value['extnId'] == $id) { + if (!$replace) { + return false; + } + + $extensions[$key] = $newext; + return true; + } + } + + $extensions[] = $newext; + return true; + } + + /** + * Get a reference to an extension subarray + * + * @param array|null $root + * @param string|null $path optional absolute path with / as component separator + * @param bool $create optional + * + * @return array|false + */ + private function &extensions(&$root, string $path = null, bool $create = false) + { + if (!isset($root)) { + $root = $this->currentCert; + } + + switch (true) { + case !empty($path): + case !is_array($root): + break; + case isset($root['tbsCertificate']): + $path = 'tbsCertificate/extensions'; + break; + } + + $extensions = &$this->subArray($root, $path, $create); + + if (!is_array($extensions)) { + $false = false; + return $false; + } + + return $extensions; + } + + /** + * Map extension values from extension-specific internal format to + * octet string. + * + * @param array $root (by reference) + * @param string $path + * @param ASN1Interface $asn1 + * + * @return void + */ + private function mapOutExtensions(&$root, string $path, ASN1Interface $asn1) + { + $extensions = &$this->subArray($root, $path); + + if (is_array($extensions)) { + $size = count($extensions); + for ($i = 0; $i < $size; $i++) { + $id = $extensions[$i]['extnId']; + $value = &$extensions[$i]['extnValue']; + + switch ($id) { + case 'id-ce-certificatePolicies': + throw new LogicException('id-ce-certificatePolicies not handled.'); + case 'id-ce-authorityKeyIdentifier': // use 00 as the serial number instead of an empty string + if (isset($value['authorityCertSerialNumber'])) { + if ($value['authorityCertSerialNumber']->toBytes() == '') { + throw new LogicException('authorityCertSerialNumber can not be empty.'); + } + } + } + + /* [extnValue] contains the DER encoding of an ASN.1 value + corresponding to the extension type identified by extnID */ + $map = $this->getMapping($id); + if (false === $map) { + throw new LogicException($id . ' is not a currently supported extension'); + } else { + $temp = $asn1->encodeDER($value, $map, ['iPAddress' => [$this, '_encodeIP']]); + $value = base64_encode($temp); + } + } + } + } + + /** + * Associate an extension ID to an extension mapping + * + * @param string $extnId + * + * @return array|false + */ + private function getMapping(string $extnId) + { + switch ($extnId) { + case 'id-ce-keyUsage': + return $this->KeyUsage; + case 'id-ce-basicConstraints': + return $this->BasicConstraints; + case 'id-ce-subjectKeyIdentifier': + return $this->KeyIdentifier; + case 'id-ce-authorityKeyIdentifier': + return $this->AuthorityKeyIdentifier; + case 'id-ce-certificatePolicies': + return $this->CertificatePolicies; + case 'id-ce-extKeyUsage': + return $this->ExtKeyUsageSyntax; + case 'id-ce-subjectAltName': + return $this->SubjectAltName; + case 'id-ce-invalidityDate': + return $this->InvalidityDate; + } + + return false; + } + + /** + * Get a reference to a subarray + * + * This variant of _subArray() does no is_array() checking, + * so $root should be checked with _isSubArrayValid() first. + * + * This is here for performance reasons: + * Passing a reference (i.e. $root) by-value (i.e. to is_array()) + * creates a copy. If $root is an especially large array, this is expensive. + * + * @param array $root + * @param string $path absolute path with / as component separator + * @param bool $create optional + * + * @return array|false + */ + private function &subArrayUnchecked(&$root, string $path, bool $create = false) + { + $false = false; + + foreach (explode('/', $path) as $i) { + if (!isset($root[$i])) { + if (!$create) { + return $false; + } + + $root[$i] = []; + } + + $root = &$root[$i]; + } + + return $root; + } + + /** + * Set a Distinguished Name property + * + * @param string $propName + * @param mixed $propValue + * @param string $type optional + * + * @return bool + */ + private function setDNProp(string $propName, $propValue, string $type = 'utf8String') + { + if (empty($this->dn)) { + $this->dn = ['rdnSequence' => []]; + } + + if (($propName = $this->translateDNProp($propName)) === false) { + return false; + } + + foreach ((array)$propValue as $v) { + if (!is_array($v)) { + $v = [$type => $v]; + } + $this->dn['rdnSequence'][] = [ + [ + 'type' => $propName, + 'value' => $v + ] + ]; + } + + return true; + } + + /** + * "Normalizes" a Distinguished Name property + * + * @param string $propName + * + * @return mixed + */ + private function translateDNProp(string $propName) + { + switch (strtolower($propName)) { + case 'id-at-countryname': + case 'countryname': + case 'c': + return 'id-at-countryName'; + case 'id-at-organizationname': + case 'organizationname': + case 'o': + return 'id-at-organizationName'; + case 'id-at-commonname': + case 'commonname': + case 'cn': + return 'id-at-commonName'; + case 'id-at-stateorprovincename': + case 'stateorprovincename': + case 'state': + case 'province': + case 'provincename': + case 'st': + return 'id-at-stateOrProvinceName'; + case 'id-at-localityname': + case 'localityname': + case 'l': + return 'id-at-localityName'; + default: + return false; + } + } + + /** + * Get a certificate, CSR or CRL Extension + * + * Returns the extension if it exists and false if not + * + * @param string $id + * @param array|null $cert optional + * + * @return mixed + */ + private function getExtension(string $id, array $cert = null) + { + $extensions = $this->extensions($cert); + + if (!is_array($extensions)) { + return false; + } + + foreach ($extensions as $key => $value) { + if ($value['extnId'] == $id) { + return $value['extnValue']; + } + } + + return false; + } + + /** + * Remove Distinguished Name properties + * + * @param string $propName + * + * @return void + */ + private function removeDNProp(string $propName) + { + if (empty($this->dn)) { + return; + } + + if (($propName = $this->translateDNProp($propName)) === false) { + return; + } + + $dn = &$this->dn['rdnSequence']; + $size = count($dn); + for ($i = 0; $i < $size; $i++) { + if ($dn[$i][0]['type'] == $propName) { + unset($dn[$i]); + } + } + + $dn = array_values($dn); + // fix for https://bugs.php.net/75433 affecting PHP 7.2 + if (!isset($dn[0])) { + $dn = array_splice($dn, 0, 0); + } + } + + public function getIssuerDNProp($propName, $withType = false) + { + switch (true) { + case !isset($this->currentCert) || !is_array($this->currentCert): + break; + case isset($this->currentCert['tbsCertificate']): + return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['issuer'], $withType); + case isset($this->currentCert['tbsCertList']): + return $this->getDNProp($propName, $this->currentCert['tbsCertList']['issuer'], $withType); + } + + return false; + } + + /** + * Get Distinguished Name properties + * + * @param string $propName + * @param array|null $dn optional + * @param bool $withType optional + * + * @return mixed + */ + private function getDNProp(string $propName, array $dn = null, bool $withType = false) + { + if (!isset($dn)) { + $dn = $this->dn; + } + + if (empty($dn)) { + return false; + } + + if (($propName = $this->translateDNProp($propName)) === false) { + return false; + } + + $asn1 = new ASN1(); + $asn1->loadOIDs($this->oids); + $filters = []; + $filters['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; + $asn1->loadFilters($filters); + + $dn = $dn['rdnSequence']; + $result = []; + for ($i = 0; $i < count($dn); $i++) { + if ($dn[$i][0]['type'] == $propName) { + $v = $dn[$i][0]['value']; + if (!$withType) { + if (is_array($v)) { + foreach ($v as $type => $s) { + $type = array_search($type, $asn1->getANYmap(), true); + if ($type !== false && isset($asn1->getStringTypeSize()[$type])) { + $s = $asn1->convert($s, (int)$type); + if ($s !== false) { + $v = $s; + break; + } + } + } + if (is_array($v)) { + $v = array_pop($v); // Always strip data type. + } + } else { + throw new LogicException('Value is not an array.'); + } + } + $result[] = $v; + } + } + + return $result; + } + + /** + * Helper function to build domain array + * + * @param string $domain + * + * @return array + */ + private function dnsName(string $domain) + { + return ['dNSName' => $domain]; + } } diff --git a/src/Models/CustomerCreditTransfer.php b/src/Models/CustomerCreditTransfer.php new file mode 100644 index 0000000..9de08fe --- /dev/null +++ b/src/Models/CustomerCreditTransfer.php @@ -0,0 +1,13 @@ +type = $type; $this->version = $version; $this->exponent = $exponent; $this->modulus = $modulus; $this->keyHash = $keyHash; + $this->modulusSize = $modulusSize; } /** @@ -111,6 +118,14 @@ public function getKeyHash(): string return $this->keyHash; } + /** + * @return int + */ + public function getModulusSize(): int + { + return $this->modulusSize; + } + /** * @param string|null $certificateContent */ diff --git a/src/Models/Transaction.php b/src/Models/Transaction.php index 31e3a96..f4c3103 100644 --- a/src/Models/Transaction.php +++ b/src/Models/Transaction.php @@ -33,11 +33,11 @@ class Transaction implements TransactionInterface private $numSegments; /** - * Uses for encrypted OrderData. + * Uses for decoded OrderData Items. * - * @var OrderData + * @var array */ - private $orderData; + private $orderDataItems; /** * Uses for encrypted OrderData. @@ -131,11 +131,24 @@ public function setPlainOrderData(string $plainOrderData): void */ public function getOrderData(): OrderData { - return $this->orderData; + return $this->orderDataItems[0]; } public function setOrderData(OrderData $orderData): void { - $this->orderData = $orderData; + $this->orderDataItems[0] = $orderData; + } + + /** + * @return OrderData[] + */ + public function getOrderDataItems(): array + { + return $this->orderDataItems; + } + + public function setOrderDataItems(array $orderDataItems): void + { + $this->orderDataItems = $orderDataItems; } } diff --git a/src/Models/X509/AbstractX509Generator.php b/src/Models/X509/AbstractX509Generator.php index d1353bb..96079da 100644 --- a/src/Models/X509/AbstractX509Generator.php +++ b/src/Models/X509/AbstractX509Generator.php @@ -7,6 +7,7 @@ use AndrewSvirin\Ebics\Contracts\X509GeneratorInterface; use AndrewSvirin\Ebics\Exceptions\X509\X509GeneratorException; use AndrewSvirin\Ebics\Factories\Crypt\X509Factory; +use AndrewSvirin\Ebics\Services\RandomService; use AndrewSvirin\Ebics\Services\X509\X509ExtensionOptionsNormalizer; use DateTime; use DateTimeInterface; @@ -45,11 +46,17 @@ abstract class AbstractX509Generator implements X509GeneratorInterface */ private $certificateOptions; + /** + * @var RandomService + */ + private $randomService; + public function __construct() { $this->x509Factory = new X509Factory(); $this->x509StartDate = (new DateTime())->modify('-1 day'); $this->x509EndDate = (new DateTime())->modify('+1 year'); + $this->randomService = new RandomService(); $this->serialNumber = $this->generateSerialNumber(); } @@ -192,7 +199,10 @@ private function generateX509( $x509->setSerialNumber($this->serialNumber); // Sign subject to allow add extensions. - $signedSubject = $x509->saveX509($x509->sign($issuer, $subject, $signatureAlgorithm)); + if (!($x509Signed = $x509->sign($issuer, $subject, $signatureAlgorithm))) { + throw new RuntimeException('X509 was not signed.'); + } + $signedSubject = $x509->saveX509($x509Signed); $x509->loadX509($signedSubject); foreach ($options['extensions'] as $id => $extension) { @@ -280,13 +290,7 @@ protected function generateIssuer( */ protected function generateSerialNumber(): string { - // prevent the first number from being 0 - $result = rand(1, 9); - for ($i = 0; $i < 19; ++$i) { - $result .= rand(0, 9); - } - - return (string)$result; + return (string)$this->randomService->digits(20); } /** diff --git a/src/Services/BankLetter/Formatter/HtmlBankLetterFormatter.php b/src/Services/BankLetter/Formatter/HtmlBankLetterFormatter.php index e33f3be..8845fb3 100644 --- a/src/Services/BankLetter/Formatter/HtmlBankLetterFormatter.php +++ b/src/Services/BankLetter/Formatter/HtmlBankLetterFormatter.php @@ -139,9 +139,9 @@ private function formatSection(Bank $bank, User $user, SignatureBankLetter $sign

{$signatureName}

{$certificateSection}

- {$translations['hash']} + {$translations['hash']} (SHA-256)
- {$this->formatBytes($signatureBankLetter->getKeyHash())} + {$this->formatBytes($signatureBankLetter->getKeyHash())} EOF; return $result; @@ -172,11 +172,11 @@ private function formatSectionFromModulusExponent(SignatureBankLetter $certifica $result = <<{$translations['exponent']}
- {$this->formatBytes($certificateBankLetter->getExponent())} + {$this->formatBytes($certificateBankLetter->getExponent())}
- {$translations['modulus']} + {$translations['modulus']} ({$certificateBankLetter->getModulusSize()} bits)
- {$this->formatBytes($certificateBankLetter->getModulus())} + {$this->formatBytes($certificateBankLetter->getModulus())} EOF; return $result; diff --git a/src/Services/BankLetter/HashGenerator/PublicKeyHashGenerator.php b/src/Services/BankLetter/HashGenerator/PublicKeyHashGenerator.php index 477f7d2..c726192 100644 --- a/src/Services/BankLetter/HashGenerator/PublicKeyHashGenerator.php +++ b/src/Services/BankLetter/HashGenerator/PublicKeyHashGenerator.php @@ -34,11 +34,33 @@ public function generate(SignatureInterface $signature): string { $publicKeyDetails = $this->cryptService->getPublicKeyDetails($signature->getPublicKey()); - $key = $this->cryptService->calculateKey( - $publicKeyDetails['e'], - $publicKeyDetails['m'] - ); + $e = $this->formatBytesToHex($publicKeyDetails['e']); + $m = $this->formatBytesToHex($publicKeyDetails['m']); - return $this->cryptService->calculateKeyHash($key); + $key = $this->cryptService->calculateKey($e, $m); + + $hash = $this->cryptService->calculateKeyHash($key); + + return $hash; + } + + /** + * Convert bytes to hex. + * + * @param string $bytes Bytes + * + * @return string + */ + private function formatBytesToHex(string $bytes): string + { + $out = ''; + + // Go over pairs of bytes. + foreach ($this->cryptService->binToArray($bytes) as $byte) { + // Convert to lover case hexadecimal number. + $out .= sprintf("%02x", $byte); + } + + return trim($out); } } diff --git a/src/Services/BankLetterService.php b/src/Services/BankLetterService.php index f58e587..e8284e0 100644 --- a/src/Services/BankLetterService.php +++ b/src/Services/BankLetterService.php @@ -62,12 +62,14 @@ public function formatSignatureForBankLetter( $keyHash = $hashGenerator->generate($signature); $keyHashFormatted = $this->formatKeyHashForBankLetter($keyHash); + $modulusSize = strlen($publicKeyDetails['m']) * 8; // 8 bits in byte. $signatureBankLetter = $this->signatureBankLetterFactory->create( $signature->getType(), $version, $exponentFormatted, $modulusFormatted, - $keyHashFormatted + $keyHashFormatted, + $modulusSize ); if (($content = $signature->getCertificateContent())) { @@ -108,7 +110,7 @@ private function formatBytesForBank(string $bytes): string // Go over pairs of bytes. foreach ($this->cryptService->binToArray($bytes) as $byte) { - // Convert to lover case hexadecimal number. + // Convert to lover case hexadecimal number and add a space. $result .= sprintf('%02x ', $byte); } diff --git a/src/Services/CryptService.php b/src/Services/CryptService.php index 8b0214f..2e9a34e 100644 --- a/src/Services/CryptService.php +++ b/src/Services/CryptService.php @@ -7,18 +7,12 @@ use AndrewSvirin\Ebics\Factories\Crypt\AESFactory; use AndrewSvirin\Ebics\Factories\Crypt\RSAFactory; use AndrewSvirin\Ebics\Factories\OrderDataFactory; -use AndrewSvirin\Ebics\Models\Crypt\AES; use AndrewSvirin\Ebics\Models\Crypt\RSA; use AndrewSvirin\Ebics\Models\KeyRing; use AndrewSvirin\Ebics\Models\OrderData; use AndrewSvirin\Ebics\Models\OrderDataEncrypted; use RuntimeException; -use function call_user_func_array; -use function strlen; - -use const OPENSSL_ZERO_PADDING; - /** * EBICS crypt/decrypt encode/decode hash functions. * @@ -50,12 +44,18 @@ class CryptService */ private $orderDataFactory; + /** + * @var ZipReader + */ + private $zipReader; + public function __construct() { $this->rsaFactory = new RSAFactory(); $this->aesFactory = new AESFactory(); $this->randomService = new RandomService(); $this->orderDataFactory = new OrderDataFactory(); + $this->zipReader = new ZipReader(); } /** @@ -82,12 +82,36 @@ public function calculateHash(string $text, string $algorithm = 'sha256'): strin */ public function decryptOrderData(KeyRing $keyRing, OrderDataEncrypted $orderData): OrderData { - $content = $this->decryptOrderDataContent($keyRing, $orderData); - $orderData = $this->orderDataFactory->buildOrderDataFromContent($content); + $orderDataContent = $this->decryptOrderDataContent($keyRing, $orderData); + $orderData = $this->orderDataFactory->buildOrderDataFromContent($orderDataContent); return $orderData; } + /** + * Decrypt encrypted OrderData items. + * Unzip order data items. + * + * @param KeyRing $keyRing + * @param OrderDataEncrypted $orderData + * + * @return OrderData[] + * @throws EbicsException + */ + public function decryptOrderDataItems(KeyRing $keyRing, OrderDataEncrypted $orderData): array + { + $orderDataContent = $this->decryptOrderDataContent($keyRing, $orderData); + + $orderDataXmlItems = $this->zipReader->extractFilesFromString($orderDataContent); + + $orderDataItems = []; + foreach ($orderDataXmlItems as $orderDataXmlItem) { + $orderDataItems[] = $this->orderDataFactory->buildOrderDataFromContent($orderDataXmlItem); + } + + return $orderDataItems; + } + /** * Decrypt encrypted OrderData. * @@ -97,8 +121,10 @@ public function decryptOrderData(KeyRing $keyRing, OrderDataEncrypted $orderData * @return string * @throws EbicsException */ - public function decryptOrderDataContent(KeyRing $keyRing, OrderDataEncrypted $orderData): string - { + public function decryptOrderDataContent( + KeyRing $keyRing, + OrderDataEncrypted $orderData + ): string { if (!($signatureE = $keyRing->getUserSignatureE())) { throw new RuntimeException('Signature E is not set.'); } @@ -106,10 +132,9 @@ public function decryptOrderDataContent(KeyRing $keyRing, OrderDataEncrypted $or $rsa = $this->rsaFactory->create(); $rsa->setPassword($keyRing->getPassword()); $rsa->loadKey($signatureE->getPrivateKey()); - $rsa->setEncryptionMode(RSA::ENCRYPTION_PKCS1); $transactionKeyDecrypted = $rsa->decrypt($orderData->getTransactionKey()); // aes-128-cbc encrypting format. - $aes = $this->aesFactory->create(AES::MODE_CBC); + $aes = $this->aesFactory->create(); $aes->setKeyLength(128); $aes->setKey($transactionKeyDecrypted); // Force openssl_options. @@ -118,7 +143,7 @@ public function decryptOrderDataContent(KeyRing $keyRing, OrderDataEncrypted $or // Try to uncompress from gz order data. if (!($orderData = gzuncompress($decrypted))) { - throw new EbicsException('Order Data were uncompressed wrongly.'); + throw new RuntimeException('Order Data were uncompressed wrongly.'); } return $orderData; } @@ -133,8 +158,10 @@ public function decryptOrderDataContent(KeyRing $keyRing, OrderDataEncrypted $or * * @throws EbicsException */ - public function cryptSignatureValue(KeyRing $keyRing, string $hash): string - { + public function cryptSignatureValue( + KeyRing $keyRing, + string $hash + ): string { $digestToSignBin = $this->filter($hash); if (!($signatureX = $keyRing->getUserSignatureX()) || !($privateKey = $signatureX->getPrivateKey())) { @@ -151,7 +178,6 @@ public function cryptSignatureValue(KeyRing $keyRing, string $hash): string if (!defined('CRYPT_RSA_PKCS15_COMPAT')) { define('CRYPT_RSA_PKCS15_COMPAT', true); } - $rsa->setEncryptionMode(RSA::ENCRYPTION_PKCS1); $encrypted = $rsa->encrypt($digestToSignBin); if (empty($encrypted)) { throw new EbicsException('Incorrect authorization.'); @@ -173,13 +199,15 @@ public function cryptSignatureValue(KeyRing $keyRing, string $hash): string * ] * @throws EbicsException */ - public function generateKeys(KeyRing $keyRing, string $algorithm = 'sha256', int $length = 2048): array - { + public function generateKeys( + KeyRing $keyRing, + string $algorithm = 'sha256', + int $length = 2048 + ): array { $rsa = $this->rsaFactory->create(); $rsa->setPublicKeyFormat(RSA::PRIVATE_FORMAT_PKCS1); $rsa->setPrivateKeyFormat(RSA::PUBLIC_FORMAT_PKCS1); $rsa->setHash($algorithm); - $rsa->setMGFHash($algorithm); $rsa->setPassword($keyRing->getPassword()); return $rsa->createKey($length); @@ -192,8 +220,9 @@ public function generateKeys(KeyRing $keyRing, string $algorithm = 'sha256', int * * @return string */ - private function filter(string $hash): string - { + private function filter( + string $hash + ): string { $RSA_SHA256prefix = [ 0x30, 0x31, @@ -233,8 +262,13 @@ private function filter(string $hash): string * @param int $d * @param int $length */ - private function systemArrayCopy(array $a, int $c, array &$b, int $d, int $length): void - { + private function systemArrayCopy( + array $a, + int $c, + array &$b, + int $d, + int $length + ): void { for ($i = 0; $i < $length; ++$i) { $b[$i + $d] = $a[$i + $c]; } @@ -247,8 +281,9 @@ private function systemArrayCopy(array $a, int $c, array &$b, int $d, int $lengt * * @return string (bytes) */ - private function arrayToBin(array $bytes): string - { + private function arrayToBin( + array $bytes + ): string { return call_user_func_array('pack', array_merge(['c*'], $bytes)); } @@ -259,8 +294,9 @@ private function arrayToBin(array $bytes): string * * @return array */ - public function binToArray(string $bytes): array - { + public function binToArray( + string $bytes + ): array { $result = unpack('C*', $bytes); if (false === $result) { throw new RuntimeException('Can not convert bytes to array.'); @@ -279,16 +315,23 @@ public function binToArray(string $bytes): array * * @param SignatureInterface $signature * @param string $algorithm + * @param bool $rawOutput * * @return string */ - public function calculateDigest(SignatureInterface $signature, $algorithm = 'sha256'): string - { + public function calculateDigest( + SignatureInterface $signature, + $algorithm = 'sha256', + $rawOutput = false + ): string { $rsa = $this->rsaFactory->create(); $rsa->loadKey($signature->getPublicKey()); + + $exponent = $rsa->getExponent()->toHex(true); $modulus = $rsa->getModulus()->toHex(true); - // If key was formed incorrect with Modulus and Exponent mismatch, then change the place of key parts. + // If key was formed with switched Modulus and Exponent, then change the place of key parts. + // It can happens for Bank. if (strlen($exponent) > strlen($modulus)) { $buffer = $exponent; $exponent = $modulus; @@ -296,7 +339,7 @@ public function calculateDigest(SignatureInterface $signature, $algorithm = 'sha } $key = $this->calculateKey($exponent, $modulus); - return $this->calculateKeyHash($key, $algorithm, true); + return $this->calculateKeyHash($key, $algorithm, $rawOutput); } /** @@ -307,8 +350,10 @@ public function calculateDigest(SignatureInterface $signature, $algorithm = 'sha * * @return string */ - public function calculateKey(string $exponent, string $modulus): string - { + public function calculateKey( + string $exponent, + string $modulus + ): string { // Remove leading 0. $exponent = ltrim($exponent, '0'); $modulus = ltrim($modulus, '0'); @@ -325,8 +370,11 @@ public function calculateKey(string $exponent, string $modulus): string * * @return string */ - public function calculateKeyHash(string $key, string $algorithm = 'sha256', bool $rawOutput = false): string - { + public function calculateKeyHash( + string $key, + string $algorithm = 'sha256', + bool $rawOutput = false + ): string { return hash($algorithm, $key, $rawOutput); } @@ -337,10 +385,9 @@ public function calculateKeyHash(string $key, string $algorithm = 'sha256', bool */ public function generateNonce(): string { - $bytes = $this->randomService->string(16); - $nonce = bin2hex($bytes); + $nonce = $this->randomService->hex(32); - return strtoupper($nonce); + return $nonce; } /** @@ -353,8 +400,9 @@ public function generateNonce(): string * 'm' => '', * ] */ - public function getPublicKeyDetails(string $publicKey): array - { + public function getPublicKeyDetails( + string $publicKey + ): array { $rsa = $this->rsaFactory->create(); $rsa->setPublicKey($publicKey); diff --git a/src/Services/HttpClient.php b/src/Services/HttpClient.php index 296d3e6..5e6f2b5 100644 --- a/src/Services/HttpClient.php +++ b/src/Services/HttpClient.php @@ -12,8 +12,6 @@ * * @license http://www.opensource.org/licenses/mit-license.html MIT License * @author Andrew Svirin - * - * @internal */ class HttpClient implements HttpClientInterface { diff --git a/src/Services/KeyRingManager.php b/src/Services/KeyRingManager.php index c0e0b4b..117a1f4 100644 --- a/src/Services/KeyRingManager.php +++ b/src/Services/KeyRingManager.php @@ -16,8 +16,6 @@ * * @license http://www.opensource.org/licenses/mit-license.html MIT License * @author Andrew Svirin - * - * @internal */ class KeyRingManager implements KeyRingManagerInterface { diff --git a/src/Services/RandomService.php b/src/Services/RandomService.php index 95db0ab..a86bce4 100644 --- a/src/Services/RandomService.php +++ b/src/Services/RandomService.php @@ -2,8 +2,6 @@ namespace AndrewSvirin\Ebics\Services; -use phpseclib\Crypt\Random; - /** * Random function. * @@ -16,13 +14,56 @@ class RandomService { /** + * Generate random string form HEX characters in upper register. + * * @param int $length * * @return string - * @see Random::string */ - public function string(int $length): string + public function hex(int $length): string { - return Random::string($length); + $characters = '0123456789ABCDEF'; + $randomHex = $this->random($characters, $length); + + return $randomHex; + } + + /** + * Generate random digits. + * + * @param int $length + * + * @return string + */ + public function digits(int $length): string + { + $characters = '0123456789'; + $randomDigits = $this->random($characters, $length); + + return $randomDigits; + } + + /** + * Generate random characters where first character not 0. + * @param string $characters + * @param int $length + * + * @return string + */ + private function random(string $characters, int $length) + { + $charactersLength = strlen($characters); + + $random = ''; + + // Avoid set 0 as first character. + $random .= $characters[rand(1, $charactersLength - 1)]; + + // Generate other characters randomly. + for ($i = 1; $i < $length; $i++) { + $random .= $characters[rand(0, $charactersLength - 1)]; + } + + return $random; } } diff --git a/src/Services/X509/X509ExtensionOptionsNormalizer.php b/src/Services/X509/X509ExtensionOptionsNormalizer.php index 325c2e7..f8d6b4f 100644 --- a/src/Services/X509/X509ExtensionOptionsNormalizer.php +++ b/src/Services/X509/X509ExtensionOptionsNormalizer.php @@ -2,8 +2,6 @@ namespace AndrewSvirin\Ebics\Services\X509; -use phpseclib\File\X509; - /** * X509 extensions options normalizer. * @@ -27,7 +25,7 @@ class X509ExtensionOptionsNormalizer * 'replace' => '', * ] * - * @see X509::setExtension() + * @see \AndrewSvirin\Ebics\Models\Crypt\X509::setExtension() */ public static function normalize($options): array { diff --git a/src/Services/ZipReader.php b/src/Services/ZipReader.php new file mode 100644 index 0000000..14747a4 --- /dev/null +++ b/src/Services/ZipReader.php @@ -0,0 +1,62 @@ +createTmpFile(); + file_put_contents($tempFile, $zippedContent); + + $zip = new ZipArchive(); + if (true !== $zip->open($tempFile)) { + throw new RuntimeException('Zip archive wa not opened.'); + } + + // Read zipped order data items. + $fileContentItems = []; + for ($i = 0; $i < $zip->numFiles; $i++) { + $fileContentItems[] = $zip->getFromIndex($i); + } + + // Close zip. + $zip->close(); + // Remove temporary file. + unlink($tempFile); + + return $fileContentItems; + } +} diff --git a/tests/AbstractEbicsTestCase.php b/tests/AbstractEbicsTestCase.php index d66c357..c3f2a0d 100644 --- a/tests/AbstractEbicsTestCase.php +++ b/tests/AbstractEbicsTestCase.php @@ -12,6 +12,7 @@ use AndrewSvirin\Ebics\Models\User; use AndrewSvirin\Ebics\Services\KeyRingManager; use PHPUnit\Framework\TestCase; +use RuntimeException; /** * Class TestCase extends basic TestCase for add extra setups. @@ -26,8 +27,11 @@ abstract class AbstractEbicsTestCase extends TestCase protected $fixtures = __DIR__ . '/_fixtures'; - protected function setupClient(int $credentialsId, X509GeneratorInterface $x509Generator = null, $fake = false): EbicsClientInterface - { + protected function setupClient( + int $credentialsId, + X509GeneratorInterface $x509Generator = null, + $fake = false + ): EbicsClientInterface { $credentials = $this->credentialsDataProvider($credentialsId); $bank = new Bank($credentials['hostId'], $credentials['hostURL']); @@ -118,6 +122,11 @@ protected function assertExceptionCode(string $code = null) public function credentialsDataProvider(int $credentialsId): array { $path = sprintf('%s/credentials/credentials_%d.json', $this->data, $credentialsId); + + if (!file_exists($path)) { + throw new RuntimeException('Credentials missing'); + } + $credentialsEnc = json_decode(file_get_contents($path), true); return [ diff --git a/tests/EbicsClientTest.php b/tests/EbicsClientTest.php index 5f3d104..4e145ea 100644 --- a/tests/EbicsClientTest.php +++ b/tests/EbicsClientTest.php @@ -28,6 +28,8 @@ class EbicsClientTest extends AbstractEbicsTestCase * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testHEV(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -47,6 +49,8 @@ public function testHEV(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testINI(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -77,6 +81,8 @@ public function testINI(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testHIA(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -109,6 +115,8 @@ public function testHIA(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testHPB(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -133,6 +141,8 @@ public function testHPB(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testHKD(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -161,6 +171,8 @@ public function testHKD(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testHTD(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -188,6 +200,8 @@ public function testHTD(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testHPD(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -216,6 +230,8 @@ public function testHPD(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testHAA(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -245,6 +261,8 @@ public function testHAA(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testVMK(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -270,6 +288,8 @@ public function testVMK(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testSTA(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -295,6 +315,8 @@ public function testSTA(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testFDL(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { @@ -348,13 +370,19 @@ public function testFDL(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testZ53(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { $client = $this->setupClient($credentialsId, $x509Generator, $codes['Z53']['fake']); $this->assertExceptionCode($codes['Z53']['code']); - $z53 = $client->Z53(); + $z53 = $client->Z53( + new DateTime(), + (new DateTime())->modify('-30 day'), + (new DateTime())->modify('-1 day') + ); $responseHandler = new ResponseHandler(); @@ -373,13 +401,73 @@ public function testZ53(int $credentialsId, array $codes, X509GeneratorInterface * @param int $credentialsId * @param array $codes * @param X509GeneratorInterface|null $x509Generator + * + * @covers */ public function testC53(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) { $client = $this->setupClient($credentialsId, $x509Generator, $codes['C53']['fake']); $this->assertExceptionCode($codes['C53']['code']); - $c53 = $client->C53(); + $c53 = $client->C53( + new DateTime(), + (new DateTime())->modify('-30 day'), + (new DateTime())->modify('-1 day') + ); + + $responseHandler = new ResponseHandler(); + + $c53Receipt = $client->transferReceipt($c53); + $code = $responseHandler->retrieveH004ReturnCode($c53Receipt); + $reportText = $responseHandler->retrieveH004ReportText($c53Receipt); + + $this->assertResponseDone($code, $reportText); + } + + /** + * @dataProvider serversDataProvider + * + * @group CCT + * + * @param int $credentialsId + * @param array $codes + * @param X509GeneratorInterface|null $x509Generator + * + * @covers + */ + public function testCCT(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) + { + $client = $this->setupClient($credentialsId, $x509Generator, $codes['CCT']['fake']); + + $this->assertExceptionCode($codes['CCT']['code']); + $c53 = $client->CCT(new DateTime()); + + $responseHandler = new ResponseHandler(); + + $c53Receipt = $client->transferReceipt($c53); + $code = $responseHandler->retrieveH004ReturnCode($c53Receipt); + $reportText = $responseHandler->retrieveH004ReportText($c53Receipt); + + $this->assertResponseDone($code, $reportText); + } + + /** + * @dataProvider serversDataProvider + * + * @group CDD + * + * @param int $credentialsId + * @param array $codes + * @param X509GeneratorInterface|null $x509Generator + * + * @covers + */ + public function testCDD(int $credentialsId, array $codes, X509GeneratorInterface $x509Generator = null) + { + $client = $this->setupClient($credentialsId, $x509Generator, $codes['CDD']['fake']); + + $this->assertExceptionCode($codes['CDD']['code']); + $c53 = $client->CDD(new DateTime()); $responseHandler = new ResponseHandler(); @@ -432,6 +520,8 @@ public function serversDataProvider() 'camt.xxx.cfonb120.stm' => ['code' => '091010', 'fake' => false], 'camt.xxx.cfonb240.act' => ['code' => '091010', 'fake' => false], ], + 'CCT' => ['code' => '061002', 'fake' => false], + 'CDD' => ['code' => '061002', 'fake' => false], ], new WeBankX509Generator(), ], @@ -454,6 +544,8 @@ public function serversDataProvider() 'camt.xxx.cfonb120.stm' => ['code' => '091112', 'fake' => false], 'camt.xxx.cfonb240.act' => ['code' => '091112', 'fake' => false], ], + 'CCT' => ['code' => '091005', 'fake' => false], + 'CDD' => ['code' => '091005', 'fake' => false], ], ], ]; diff --git a/tests/Factories/X509/X509GeneratorTest.php b/tests/Factories/X509/X509GeneratorTest.php index 837019d..a52d469 100644 --- a/tests/Factories/X509/X509GeneratorTest.php +++ b/tests/Factories/X509/X509GeneratorTest.php @@ -35,7 +35,7 @@ public function testGenerateWeBankCertificateContent() $signature = $signatureFactory->createSignatureAFromKeys([ 'publickey' => $publicKey, 'privatekey' => $privateKey, - ], $generator); + ], 'test123', $generator); $this->assertEquals($signature->getPrivateKey(), $privateKey); $this->assertEquals($signature->getPublicKey(), $publicKey); @@ -63,7 +63,7 @@ public function testGenerateSilarhiCertificateContent() $certificate = $certificateFactory->createSignatureAFromKeys([ 'publickey' => $publicKey, 'privatekey' => $privateKey, - ], $generator); + ], 'test123', $generator); $this->assertEquals($certificate->getPrivateKey(), $privateKey); $this->assertEquals($certificate->getPublicKey(), $publicKey); @@ -110,7 +110,7 @@ private function assertCertificateEquals(string $generatedContent, string $fileC */ private function getCertificateContent(string $name) { - return file_get_contents($this->data.'/certificates/'.$name); + return file_get_contents($this->data . '/certificates/' . $name); } /** @@ -118,7 +118,7 @@ private function getCertificateContent(string $name) */ private function getPrivateKey() { - return file_get_contents($this->data.'/private_key.rsa'); + return file_get_contents($this->data . '/private_key.rsa'); } /** @@ -126,6 +126,6 @@ private function getPrivateKey() */ private function getPublicKey() { - return file_get_contents($this->data.'/public_key.rsa'); + return file_get_contents($this->data . '/public_key.rsa'); } } diff --git a/tests/FakerHttpClient.php b/tests/FakerHttpClient.php index 6bfb202..545d490 100644 --- a/tests/FakerHttpClient.php +++ b/tests/FakerHttpClient.php @@ -62,6 +62,12 @@ private function fixtureOrderType(string $orderType, array $options = null): Res case 'FDL': $fileName = sprintf('fdl.%s.xml', $options['file_format']); break; + case 'C53': + $fileName = sprintf('c53.xml', $options['file_format']); + break; + case 'STA': + $fileName = sprintf('sta.xml', $options['file_format']); + break; default: throw new LogicException(sprintf('Faked order type `%s` not supported.', $orderType)); } diff --git a/tests/Services/BankLetter/HashGeneratorTest.php b/tests/Services/BankLetter/HashGeneratorTest.php new file mode 100644 index 0000000..b8af260 --- /dev/null +++ b/tests/Services/BankLetter/HashGeneratorTest.php @@ -0,0 +1,90 @@ +getPrivateKey(); + $publicKey = $this->getPublicKey(); + + //Certificate generated with https://certificatetools.com/ the 22/03/2020 (1 year validity) + $certificateGenerator = new WeBankX509Generator(); + $certificateGenerator->setX509StartDate(new DateTime('2020-03-22')); + $certificateGenerator->setX509EndDate(new DateTime('2021-03-22')); + $certificateGenerator->setSerialNumber('37376365613564393736653364353135633333333932376336366134393663336133663135323432'); + + $certificateFactory = new SignatureFactory(); + + $signature = $certificateFactory->createSignatureAFromKeys([ + 'publickey' => $publicKey, + 'privatekey' => $privateKey, + ], 'test123', $certificateGenerator); + + $hash = $hashGenerator->generate($signature); + + $this->assertEquals('fc3f5d1340438d9603697be274c6f807e4faa5b6a566cf56b4651bde9159ae80', $hash); + } + + /** + * @group hash-generator-public-key + * @covers + */ + public function testGeneratePublicKeyHash() + { + $hashGenerator = new PublicKeyHashGenerator(); + + $privateKey = $this->getPrivateKey(); + $publicKey = $this->getPublicKey(); + + $certificateFactory = new SignatureFactory(); + + $signature = $certificateFactory->createSignatureAFromKeys([ + 'publickey' => $publicKey, + 'privatekey' => $privateKey, + ], 'test123'); + + $hash = $hashGenerator->generate($signature); + + $this->assertEquals('e1955c3873327e1791aca42e350cea48196f7934648d48b60228eaf5d10ee0c4', $hash); + } + + /** + * @return string + */ + private function getPrivateKey() + { + return file_get_contents($this->data . '/private_key.rsa'); + } + + /** + * @return string + */ + private function getPublicKey() + { + return file_get_contents($this->data . '/public_key.rsa'); + } +} diff --git a/tests/Services/CryptServiceTest.php b/tests/Services/CryptServiceTest.php new file mode 100644 index 0000000..df4b380 --- /dev/null +++ b/tests/Services/CryptServiceTest.php @@ -0,0 +1,34 @@ +setupClient($credentialsId); + $cryptService = new CryptService(); + + $keys = $cryptService->generateKeys($client->getKeyRing()); + + $this->assertArrayHasKey('privatekey', $keys); + $this->assertArrayHasKey('publickey', $keys); + $this->assertArrayHasKey('partialkey', $keys); + } +} diff --git a/tests/_data/workspace/keyring_3.json b/tests/_data/workspace/keyring_3.json index 04ee5ae..9b44096 100644 --- a/tests/_data/workspace/keyring_3.json +++ b/tests/_data/workspace/keyring_3.json @@ -2,18 +2,18 @@ "USER": { "A": { "CERTIFICATE": null, - "PUBLIC_KEY": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBODZRSjk1bE80b1JMbExKZG9DV3QNCm4wSy8vMHlsQm5zQnBCbjJ4NU9RWUdMaHkzbWg1Q2d3NE5UTDQ4S0xXOHBDV2JCdGVxRmNLYVpYZy9IMkVRNkENCllBTXhKWDF2REpnTFZqcW9MUC9hcEFudXdWTDM1N3dhNG54aU54WDJleVBiZzFocll2M3Fla2ovOGk5UU9xd1gNCnF1SW1tNWNQNGRhRVpJdGNwMElMRHk3QkhkOG80Uzd2OXJJVFNtdlhucjFEaWIwSVRxRlRTMUNpQmhoL091eTkNCklJUmZEWGoyQVZOWWR5MGNvZ0FuekVObTNTSmpDU1lSa1VaVjdsWlIxaUNxMUp1eWlpS1pkcG05MEZrM200UnQNCit3cnFSZW5tY1JqY2NxQVVCbk9JK3FieUdBRVZZeHZFN2JLNm03M1EwZ1dCazJXNTRBTWdhRG9rM3N2OE1mM2ENCm53SURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0=", - "PRIVATE_KEY": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KUHJvYy1UeXBlOiA0LEVOQ1JZUFRFRA0KREVLLUluZm86IERFUy1FREUzLUNCQyw0RDg1Rjg1N0VGQzFBMEY3DQoNCnBwSGVMZmRlU1gwQVJtQ201bEN2QkFkQ0g5ZG1FRzJiQ0VKdEFJTG5aSGFhejN6MjAxeVlGNjNFdXNSZVdkTEMNCjg1U1NwVVpKUEF6SVN0THk4c3RKbW1VcDkxSjQ4OVBYNTVIYndBeHB1ajZUdTc0TUkwTG83TlFGY0tEQnVqZW0NCjFaMzIrKzQ2WW5hM2trQ1BBSHVVUFJrS04zMGpVY28zN2ZZamhReGkzMmVnQ05PU2FjenBCcUNUY3EvUUtJWDQNCjg4RmI0bEplcVJIbzNueDJTT0dpRmZXMDVrTzV0bU1Ocy9CZFQ5dHF2dXJWQWpBSEhuT0xFTDdWaFBUZ1RXRlANCjBWalJpVEthZ1VDb01QeExNWE8zZmVzZGtQMTk0eUpkZm5aOHNqZUlsN3ZvUmhlZ0FLOHZ1enB1ajZHOTFwbkwNCkFEdmtyNEthMFVWTk00amhycEdTcS9CMUFMNXhhcE50VXAydVd2UkYrNzh6S2FkMzJXZFRrZGVNQnlUTXVTVjcNCnFBK0JJOGIvekFmOEo4Mjl0S3NkTHlWRWRxaFhvaGwvSU94Q3IyTnNqM0RuVEJqRkdadHBsME9RL1ZNdmJveDUNCkNzNVFveEJVUzFxWnY4aUsrckNFZ1ZYQ0FSaDFBakpvOWxZaDZaS2Y1MVE3T1JybGd6M1JDcEtydWw4aDkzZE0NCmpUNXFGRzhwMU5IZzRRY1pFL1RscEtBbG8rVVI2NkJsdGs4U3VObTFHRVhiTXU4SFAvRUpaOE04bnp5NVFMV3YNCndVcmVuR01ETmk1dVUydE82UGRGaC9EclM0SVhDK1JEYnQ5b3BoZHNuUGtESE52ZE04V0paa3FGZUpyWlR0R2INCnMvWUhyVy92VjZWSmRTVHNUVEt6SG8vWXZrYnJYRUUwYVV2VDdwVUk0R1hTZWw4cnl6bHBVSGE0a2JjbVVjOFgNCkNESmttOHZocHI2MlRYbCtsc3dNcS9sOElTSStpd01nUnlBUXNjTG1JREl3N3I5S3JBQ0FkbU9iNCtrM0hGKzgNCnVWQk1wNzdDdFJPY2o2dVF3N0svZzgvcnc0dUIyKzdEeVJxTnEvMXF4VDNJZWl1SnVWZXdhLzNSUlBQV0VFVnINClU4SEJQZkFDd2NUelpHUks0clZ4bmxqWXJsNmVURXRoUVlwa245a003MWEyVTZOQnJyaDl0T2dMV3dkZFZIT1oNCnA2Zjl3bjEraE55bk5PVGM3Y1p3L2JPL3NMMFRpV3kwOTZxR1JFL0FlL1RIekJtamJ4TG03eEoyaHlRNzBtS0cNCmp4cjZxbUtidEpmSjBQNjdGNTN5MGFNQWpPbDN1bXlYVjYxWjZQQ1dtaXRXUUdBaldmVmo2cVUrUytOd2lUSkwNCjZmUGRoL1l1aURPR0lRZ05Ka0pZZTdBcHYremFXUDROU3BHeTAvSTlqb1NTN3Z5THMwSkZMQ2djSDFWSW5BZXkNCkRvRThSQkkwS1N3Y3phUEJWY3VBbDBQUjhDUlJVRU53NllNTlNGQjZVL1p1OVRuUFZsM2wrbTBab0EzMEd5b1MNClRrQ25Jb2pLcjM3STRiQmR0bnRXYTd2SkwxWFpaTS9zNCszRmxMcWF3cWN4aFZnUEhJa1grY3ZDQUpscjlteU4NCklWWmNwc3pkSGx5Vi9uODBlTmdDam5vTWE4eGN6cjI4QWlXd2cwUHpjN3hMWStReFNJenVCaEEvbFROOWh0WGkNCktyY2RKa0dKeXhoZElsenlKVUxMa0ZlWFB2UUwxWktzaDVwN0dmNU1HdlFyZUt3TmN4WlRzSWJnTklKY0ZPeE4NCmVMNUMzNFlyazJtbWJsQlFLWHgyWk5KS283ZXdtRG4xKzRxZFZGb2FmR3NhWnRGdzY5MDdpZ0hKR001VERkdTUNClhSaitFQkFnUmI2QWRFRDZiSHp0aWpaNk8zM1h2TWJBN2RIMnJmRW84MVIrMlRQMmJ2bEVrTjNNT3RWbXZXbUoNCnhVc00rR0JqZUJMTW00OUZCMjVnaGMvbjNBY1ZpZFFEMVhtRXNNS0I1VlNkNmRKTWdlb2t3akVPTFNHR3ozMm8NCndEUjB1aWxpaUxMS1llaFg0NHc1MUhNZXBnOW1mWE5YRjQxcnladG9vU1ludHRja2Z0M3FPNXRBQUdZa3pRdVQNCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t" + "PUBLIC_KEY": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelloc2dySnN1MEd4ZjVyNHJwaUQNCmJKNWtGWDhZT0xJNlUrVzRmcHdPbStZclJKUEFwbGlkVUNGSEloNXh4aExjNGdrY0hSUVJMZC9SNElGeWFtMmMNCis2ZHRTTGM4ZEdzMGk5eFMwaTZVTFBld0lyV09laDJ3T215WXpNNXJmcUQvQitVZS9TKzFHYk10amRMWUc3c24NCk04QXE5T08rS1NpaXVxb3F1bFdvdkFEWTUzdnZMRzNwQ21zMVkrblE2VFdYcktYR3JLajZzY1pVTnRreE8yZGkNCmJjZHdJN3EyazFQN3dGV1FvaVRmTzhhREl4cVlHNjdkTUhreFVtRi9BR3NlVnlRa0xjMi9oTC9qa3hSRGtaL3UNCk9YcWVmWTVQQUdGSyt3Sk02MzZnUjVMUGhPeGdUcXEwY2UzUE1ocGdaRGpSS2h3R3lST09RSEhybzllSG5XclUNCnFRSURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0=", + "PRIVATE_KEY": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KUHJvYy1UeXBlOiA0LEVOQ1JZUFRFRA0KREVLLUluZm86IERFUy1FREUzLUNCQywzQ0M4NDNCMzcyRENEQjUwDQoNCnpZRFgzbFFWYXMrbWNjSUxOd2ZuUTJyKzA5QW1qOWFXbStNZ2pSdnUxQWxXT1d1RXdRVlBTMW1mbUNZaHFPcWsNCi9rQnVJRWgweEh1SjlvMnBQUnU2Z1lIcUVRbU1OUjNoMnBBZCtDNTlVRTUxNXJVVTJobVNqdjNqVFFlMFZhSmwNCk03Z0l5QkVuc0paQ3ZIR2NZSjFlMW1iYUZYdlNsRENBUnlwUjhvbEUxMml3ejVlaXUyeWtQdjYrUTRpU1c1RnANCk1hM01tTXJhTk1TbXJiMHJEQUliQjN6Q1pqUndrUWpheUZrcXZjKzVTRmV5SkxpNjRCMTdwQU9vdHJDTFZaa1cNCllMbUxzcjlEbXhxZlIxbUU5akdlNlI0NXA1dDZhZFJNTFJCZ3V3NXM0TXZmbkRLcEFyKzRxL1JINnZHRTlSNnUNClhHMXZaYkhJVjRTYXF0bWZqUkhuK08xYTMySnhIeVpUQmJWSUZJR0wyeGJCeXdPa250ZDFuaDIvNjN0TEpTNUsNClQ4UEpjZ2hvYW5PQW5kYzQ1VHhmbzNTK055clZUSzdrYTBIQ1dWdHdYNUZUVWdvQXptcTVmWHZPZFdoTW5ldGoNCmJtMWw1elphZ0NnNjkzSi95VS9PcE5QbjE1OEErTkpINCtlWG1zdlBDaHE2c2pkWjVhN3EwQU5WMDVENVcxa3kNCmVKWVRjYzBVWEZFc3pLNkJWSnN6aHNiRHpEZGZ0VWNQV1ZVdU5xMGtYQzN6YnJBZkFsRzF6blIxZ0ZuNnk4dGMNCk5vWGdkN3RZU1RKWGRISFlaK0U1Zk1icUwra3ZNUHJpcG1Qd2dnSHpValpMKzBaMG0vMTV5RFR5WlVzdjBSaWUNCjB6V0lpWU5rRUFFVFVEdFNDN2tjT2RCS3RyTDNwTmF5a3JwRWxQWmNMZXRuTDg3WHorVEh2Q0o2Tk5hSDdyUisNCmZsQUhxMDF6Mi9OVTN2TU95dytpMmRENXdyTXozQXJ1aXZiMnVNbXNDWWwxK3RiTUtNWm82ZnFaK3d0dUhhdkwNCk1IV25wdURKTm96UWphdC9oUmxTZmFMWmFMeGpKRVc4V1lNZktoWGM3TlhjZFozcXoxV0Q5Mnd3ZFZoNkZITkcNCkc3dW00U0svVm90dGZIeXFtTjRscXhuTTlSRENxQUdpZ2hrNE9Mckk1SHBFYXE5Wk9HOW84RndrVGQyazIwWDUNCjdsRWtsWkp1K29xaTRYMWZxRmdjTngrZFBzUEVUb3N6eHlDeWhhc2s4MEMrWTgwejAvYmVTcnBmcCs1QmRQNXANCk1ZZnQ4cW9zd2ZZbGI1M2s1QUZpVkttb0Q3T1NUNUJTc2Z5UkN4QUEwK2txU1ptelpnUXgvSDYraW5UMDVJTk4NCjFFVWxHSUxzNDAvYjdzcGZ3RTVsZzBmcnhYbzdtWG5Gb0h3OVBFMms0YStCTlRzaXY2YmVtNHZQWTkvK0dUYTMNCkF3d1ZkN3kyQThZV2JlZ00weExzRmI0dlF5dUZHbVh3N0VMNEgzclpCZUhrdURnWWhNZVlubHFITmxXTWZTUjANCmN0T1huWWcrM3c4QmhPSFo2S0JXVVljM3BzK1JSeHg5VTNtYnd0STM0Q21YT254RXB6bU0vRXdqQU1ERm4zWW0NCjZlcGFTd2tiT29kVFdTODBIdjV3dmphSWZpcFVVa3dUTHZZN1JFM3hVUlJMbS9kenA5YU9NRExRSitESDlQbjANCldmejVvejlOcGF1YStPejV3bVh6Wk9PZjl4NzBxSCtEd3ZrNjVUUGJZeE9aallrQXN4RGNuZ29QaDZlbWE0RTINCmpQK0lFSnE3VTFJOG5VOHZwbTBJU3ZJbHBJRUl1TVhOYkZVMEtsS1hDRTlLMklIOWgyMVRjNGN4M1FPRkpPcDYNCnlBdXZGOVBCVERXTWIzKy9TbnVZajZsTVdFYXZHbmZsWE13Y3lyVUh4dHJtWTl1THFnMGFjaThYckVXdEZxK00NCnBFck5RSHhyb0NLOG1IOVB5NCt0NXlza050c01NNVFYVkpqQm9icmozRU05cm94bm1RZm15YnlNTGQwZW5nSTQNCmpGRi90eFB4ZU5yQUNpMmR4YytDcVVHSFMyK0QydzNEOG84ejBHMVUwMlJ2Ri9uVFhxemltUUZPTmpudXBDZjcNCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t" }, "E": { "CERTIFICATE": null, - "PUBLIC_KEY": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnNoNCtQM3hncERURGJDTGEvOW0NClg1UFBVR3ZkNGdOd0l5R1htZ0RRN2JxU1BJRmJhSVZrSXpiZS9kUk1id2lzRzVUQWk0TmJQa1JWcWpkcWcvTWwNCm9FZ0dMVEpDRjZEa29IdDNPMnJXa2FMV2x0aldxMzVBZ0tHUjJ3YzdrYXhtUGZ2TmR4L0xqTUJGTyt2ZmJRL1ANClRqdHg0S3FLZ0JMTzVpb2JvcjhSSXVaQVlOc2xTbWpYRDRDbnhTV0ZJSHc0Z2w3NVRnSG1wMmZBbDJhTG5mSHoNCnZzMjFyekRTWUVUSlR1VWdZTWRlTGJjUTBzdlVGaHpnazRhWlFTUWUrRFBXUy9ucm4zWEpwWUNvczRDZ21JR2oNCmQ1YmphcFA2dTJnK1QwOUx0d2lrNjF2YzBwZUVTN09GT3lpQ3dIc0p3eGVpNlNuSytEbDA0SUVKSDBjSXVqejMNCmlRSURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0=", - "PRIVATE_KEY": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KUHJvYy1UeXBlOiA0LEVOQ1JZUFRFRA0KREVLLUluZm86IERFUy1FREUzLUNCQyxGMzEzNjIzOTgyRDFGMTEyDQoNCllmeGFtTTE3NXcxSTVDcCtVOW5FcHpVeWY0YjBORVh0dWJWYnFIWGZvdjV3d1R6clV1Qytsbk1ObTVWQ0ROam8NCnFMQkFrTUxyUXUvNzV2MFJOd0IzSTdlWVduYzgrL0h3T0U1bldNOTBnZGxqNU42WFM4ZG9PYUxRK0xrOXB1c2wNCk91K3d1b0ZTSVgxb3NwZ05SWTJ1ckowM3F3MlFubW5uRlgxNEtuNkdWdDVGSHdTbExBUE9zK00rUk9yMXd2ZHENCnlHTDJOTUd1VUZxajZpRUZLa0xZblJSd3FVbjBKWUQxdHFKbVRqZEJ3NHNYT1MvM0JCNXkrRXFVYlRrY0EzTFoNCkxZZjl0eUE1RGJqU3V0SU9KQnBBTmNEQXBSWUt4T3Z3bytKemdEVkxscnh3Vzk5M1VOdjlFNXp5U0dSbnNiTi8NCkt6QlZIclp3OFBSODFQazAwYmtaQXV1MW52anZyZHk2YjFoVXJGam91R0lpN1pJNmorTm9vdEwwOFhsVW9IN0sNCktoNm5kdllQbklad0REWVdZdUYxb1g3aTUrNUdLQ0RyY0ZnYjNSQlZvTnRzb0JzeG8ybktvWFlNWkFZWFpRNVgNCjVwY0ZiWkVnMnV6QlIwWVRYeUhwVGJOWWdjTWhDZENwNURZNWc5NklGM2pyZGxNa0gvcEJLNzlJSEpHYmtBTnMNCnpUZ3NoUE5FWUNJVzlMSzM5QUZJNld1TG1UZ0RPYVZWc3BpYnZuVS83YzVPbzZENEJCYUhSUEhLeGtNdVpFQk8NCkRETzhsSDk2dzZxWDBiZTJyQ3hKeWh6QnYrTXFaNHRTeU1hYTExTkFRMkdxZXNWckFXaGgvcGtGWjVoNDJ5QlMNCmI4a0RCK0tmOGljM2NVZFRtdlkzRFlZbDhlNW5TN3VtblRDUDlValpVWTNtbFpFSERUdzJoWGtaS3dIYUgxb2UNCkVWc3luYm5taU1JdGpQTHlDZVdYTVF3akZZVWNJWDBYVXdsMkhrb25VMXlqVld2WmNHVTN1VnJrYm1MVngwQ24NClVFNEp6THdtTnRQdWJ1MTRsKzh5T0lPTWJLRFNJSmJNNnYyZ3JTTXV6cG0wMWdZcnA5V0kxamRNb2dXMkZYdlcNCkptQmFBcnNLV0duelE0OEFtemlTc2xHT094a2tPWjdKNHdDZXhXYkVvWVRUcUlXN2VibEpVaGVsdGExT2hEVm0NCksrcUczbnVCRWRZUVFVVy9FWjByeHMzWm9jSXpwczhmUlhNaWs2clpjTWlsUlUyODVNZDVMWVRCNUZPSDYzRGwNCnUySHJINnhpZG93UXNaTjNZTmZwRmRYb1ZmNk81TTYrNXRmQU5MaW92TWMvUEp3YWd2MHRBc3NMeEJSN3NMTDkNCi91aktqUUlxb29lUmxpRHNLRFVLR2dRVFBOR0VVTGtBaG4rMDQ5bk5Cc0JjZHRKRXVad3hBaE0rYlAxL1gwMXYNCmJVSDZSRC84VU83cWVUWmJOdSs5TVc0MHlIaFFZYmtqaHFsalZIMGUvNnBHSXc0bnNaVzY3STBreWdiZ2Z6TUcNCnBQRzhIMDFGc25vVEU0TjBySng3anNVM3JnWUJpZFlDRjcwdEkxYVJySDRBUDdlMHFwck9rcE9XcDIvSm9JN0cNCnBqKzlLSUxjWFlXTjZoZEErcVVjMnp3b2xJUjgrVU1ZRXVucDNmYmxlNjdnYkFFcks1QlhnNE42NDFaaGUrQ1QNCjRlSHRtcUoxVUZhWE82Yit1Zys5clNPSEd6VzlZNnVCaFpUVHNqVXdwSmNMQld0TzRIeEw2eng5NFlUZ2FjakMNClhEUlVvc2I2N1hha3FSNVRlWVVhckl6bzBTaGxZOE9PR1RRcHRRTkJWaUJPQmdKVmtYVDRFV0ZNZXZwWG5GSEYNCkl4c3VhdnRnNVREZENkSVovbXNpeTFoMlJQZmExSVdXOXFRNHdTcjZCbDZyVWg5akhxWTA1eHU0cncwcUdxK2QNCmpyTTFSZGlneVRsZ2I3eDVoQzB0bjdkaVYrY2J2c3BsT1N6WkdTaEJib1FGbHl1WURhQ2lvK3AzMU14STB1a1kNClphOGUxWTk1OU9aaGV3cWtFbGRYS1NoalhsVEV4eW45bjlSUHVJOS9peVdiNTJSTTVra3ArZz09DQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ==" + "PUBLIC_KEY": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1BLUmVyUzcxcUpPZEVES1EwR0wNCmd0a3Zid21lazRsYUczMlJ1NTEzWUhNYnUzWjNoa3pFdlRMQk9Zb1M0bTNDK1NmTlNwNzdMa1c5V21OL0Y4UmcNCkNuWFdkbTZ4TW1SSXFHNVJzVVVRRTgzZGc1WFRqb1ZVM01JcGVzeVBkaG13b0tPZklYdUF0K3FuSzExb2NSTGkNClNTcUw2cjNWVHE1L1d3elA3b3JEU3hwR2taQTAxOXlQTGx1cWg0cmRYdVM0M20ycmNBZ0Jkd1I0aWN4citmQjcNCmsvNlhHU1pVQ1JZbVV3aHgydDNvODVpVEw1UkFHZE1uS3JWL21UeHJQWXZyaDNVSllrcFBSbWQvRlVWbWZCRXkNCitVemNHajZEOXIrQzhsTlo5VXlJZ3RERjFzUTd0cS9JWEl3dHIwdnNMaElHZlh1cWhwditDRTdVTzhScGl2VEsNCjV3SURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0=", + "PRIVATE_KEY": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KUHJvYy1UeXBlOiA0LEVOQ1JZUFRFRA0KREVLLUluZm86IERFUy1FREUzLUNCQyxBMzQ4ODA3QzU4QUY4MDJBDQoNClJDWGZJNjlQZDlRNUQ2bU1pcFNnK25ER3BycW9Sd3J0R3c4WHZyTUoxVG5Ic1JlVDBscnRuUW9pbXRtcjc0TGINCnlBWmVGTk55aXpVZ1BObmwyQlZRaXJUZFNKOWtBWC85TXYyd3hMSGE3S3RCdXdkaEdxblQxYmRqU2ltRnJWVEMNCnltc0RMM0tTdllMZjdTU0pIYjNsZmZFM2k2N2o4OTdmSjQyeFZLdTdiUHluTXpZY0RVZ2orNURJdTdjaFlOSi8NCmpQbkwzOXAyNll1bTdFK3lycjdyNXFDcFRZU1cyYkptZXZtZnpsREtwRS83UkJUcVh5MWVXamhOSml3NFNwSXUNCjlKeGdUK1pKU1pON0IrdkhVV3Z4YXJFS2NxRDJMd2dxVFVoYnA3TmlSRHNKQXB3TlpCaGZOeE9zSHZQaUJKQzcNCitLcFY4TE4yaGlqYUhVaVFHcitYL1FoZVFQb0o3Tmo0YlRFUERlZkFvWGRiT0dKZUFCUmRzU05nTzA0NVM3ZDINCkRVeVJzbjdhU2JDM2l1OHhZdGwwMWN3L0pxdm16Nld1MUYxWnA1cDQ2bVRRZ2JnakdmeDBUMDloeE5IR0wxeUINCnh0akFyc3hYdlBsUkRaUnlpNVFmSGJ5RndjTy95RFVaejh2QjFtKzBZejJ0RDBudEpDVjB5NDFub1BXbjV4M2wNCllYSk1pbDZoeWJvbzdkT08xSXMxVU1wdURFNUF4WmQ5ZXplU0VqdHRWT0JLK0NmcGpuampvamVLR2htV2wwK20NCmRHSjhwVk03SUJsMStsQmZuK1FGTmg3ZVNRTDJtUEQ2NWpYWmpydjV3UlJGdWMyQkRpcU5aZEEyVEZ1TEJtZE8NClY0Ynp3T0lvMWJSUWNvaTJsSVM5TmN6S2hHWkY3bnk2U1ZMYVVqajg1OE1PMHFQTGU0R3NHbld1UzRRaVJlbFQNCnd2NE5xWmJkUVltSXlqY1ppZ2YxZ2ZodzBqSkdBaHdhSTEwaUlNT0p2NzR5NWFFbWZIN3kyb3R4OEVIZml4bnkNClZrYTNmazJiN3ArWVRJQ1lxWGRqOGh4aHkzczYwQnR4a1Rkamt4bjhjVXl5d3dCSTBvNXhrUWJncXFvTkUyejYNCmFLVGUwK2hUakUxR0NhR0hlUDRaanFEaWJDSE4wcm1VaURVVmEvRjRrbUZwbmNsaTRsQ2hvdzdEWnd1eXdSL1kNClFFYllPNzd6YjVYN2liWUhTTGpybEpqOXl5ZEhxbGo4YjhZQWswcXVGNGNTNXJaOWF5UVU2RkJQQmdTVzVJaE8NCm1NeU1xUjlqZW56eWovZ0U5SkpsVEtoZGhaV1ZybHUvcVp2S1piUituRWV5dk5IL1dkTWl5NFBRbDE5aCsyWXANClRZN0hMM3hsT0VFSE5RQkVpRkdHMC96NjRHRzlOSXdlUkRiR00xL2U1TmxLR2lRd1Z6VzVzbm5MYmhtay9GRzQNCnRVaVh6ZlUyNGo1bEVWYlN3dkgzS25XNlgyd2xFWGd4MWVQYTNIa0tjbWJITWR6L3NSNFllM0xaZ2t1WmpnbkUNCmRJU1U5SXVRSmswQ3c3U0FxUCtsMGJNaURhWTBJWWxSZC9TdzZ2djFLZC9RODFXUGlFSnhKZVJHNXFYOUptMXcNCkVTejFPM29IT0cxejZxMzlLYnlBY29CSEF6STlBekRnOGdjV0NLV05NSFJEUGJ3V2VkVVB2UkYwKzAvWHhKeEUNCmgwcmEwS3pOMStBOGZNN0RYUVhXTDV1NUp0eWI5cTZzTHlrTlI3VnVwZ0NjTlVHNGtvYm5KdUFkSkdpbWRKQlANCk9zejJ0V2VndVBaTGc4QlpMSEo0SnYreTFlQ1NGay9xTDlBeWlLNmxSYldSTmhBRDVYWExNUDJGRGdRRGkxK0oNCjZDSDZsbWdMVnR6VUlicGg4QUpSOFFEaWtiRkJsNHhsVHhPQktEOW9FcUVaSjhaN1NBUXVjVnV6aE5MczJqN0cNCllSRC9VVTVUbWVFL3Q5L3FwTDNibm1IN2drUUd1ZDAyRU9GanpYaHNGS3JjR2l5V3diQjZ0UksxbUxjb285SFkNClZjM08xMHdTUFNSNS9uaXpzVlJFQ0EvTFFqS1RBTFFhNHFHcXpYNmVFSjJqZzlaMm10UXRLQT09DQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ==" }, "X": { "CERTIFICATE": null, - "PUBLIC_KEY": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXlVQ1h4UWs2SUFuMkRLMW95bTMNClBWWWk3Z0svRG9HN1hTOXo5bmZRTnBLQVRuVjVuZUtYNDBFQmZNbEN5RHlRVjVhVC9rZ1RmcC9EY2NydjhqZUoNCnFDRFJVdVg0aDFPRkxVelE5STV0S2RnbzB4SElyU09BWkJLaDg2dkptZStSd2dEdXF0MDlKSVRocjFjdk5VWXkNCkFzZmJETU83R3NidFliSHpyTTd4RXdLTE90a09OVTVJQjZZOHB4MTEzTndPNERXenN3T1o4Ky9mRXdPM0hmSlkNCko4Z2Rkc05SaHRkWThWN3FXTkw4V05BTEEwTUdMOFR5KzVqaDNXdnA5WllNbEd1aU5ZTGkwbElpM2cwU1cxWVoNClFKUGIvSk95WWZJRGJHQmY3MGQ1UkZYN1V4VDdxKzNVZXVtM1dJaktQeVkzSCtIbk9OOC9oL0ZsRkk3WVBWN2ENClV3SURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0=", - "PRIVATE_KEY": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KUHJvYy1UeXBlOiA0LEVOQ1JZUFRFRA0KREVLLUluZm86IERFUy1FREUzLUNCQyxDNzUzMDQyN0Y1M0VENjE0DQoNCjdrZVprVXRQQXpmOVIwOXlKcW9iQ0pBNXVaTFgxUXhOTktnaGM0c3paSGl5YnZBeGJlbmE3dGdpTUM4Q0NpakoNClNhWDRYeStQVU5IN2w1YUJzSHZIMWovTDVXbzRxWDREd05mdmhTMXhKeUlPWFB1N0NVRnFSTFZISFgwbllLMnUNCjZXU3psR3hhYlg5c3hoOTJvOFQwTlQ5YWM3WkFmUkhwRXNITW1sOWQ5R2tFT3hJbEEwbTY2OCtTa3NIMUNhYlINCnFMRHVPSjV5MWFsMWNnMnppcWY2MU9PUFQ1SjNXVXRqRDhvMWNCU04xTGlNK1BybnB6N0t2T0M4Nzd1NVlBQ0ENCjl2ejExa3E0OW4yOGlRZUxDbEpsZHBUTG1oYkNTVlZsMjh4QUJZSm1vazdUM2d2WGxRL3kvUDN4azdVNkhwbnANClR4MVBxQTh6WUJrQzFyR2M3WGk4OEN3WVlENnhLcjlsUG55WWZYeVZwZmtkQmZBeU92VEFTS0EySytQelI2MHMNCmRWV2ZTZG9wd3NURkQ4T1B3SXI5dlVuZjYxRnpPTEs5SUoxWkFBaXl6YWFGQmloeHlaTUhNNUVKdGt3MUhYVFYNClBMdjJCZVRvOWNkVGQyZ1pWVFM3M3kvQTlwbnBZMEhPWHJKaHlBMTN5VjNOSUxiR3ZxOWlnaFQyWEtXRUpIRnANClZxVENnTzNZcWRNQmdiUXkvQzdPY0NnYStmQ1hMUzZUb0E3U29QWWdrUlBYb2RsKzMwaHpSZlkzRFA5M2Q0UEkNCitsYWVDQmFKRHpPc0hVaFpXSVNLK1E0YmhhdEFGOStyTzZOQTJqRVUwem52elFrS09BVWZQZlJMYTRuMnR3VEYNClFac1ZEM3lIelVzNjdnbG9YOENWd2VObkl3dncvaHJYbVJZc01laTNvSUZQSVcwcXFMOUgrS0RvWnkrc2VKKzINCkVYb1FtbCtHSWlOQzM1aVR3eGtpdktDZ25uRHhwWjJQekJnWEJJUzJwWlFYZTBoRFpqRTdQRk5JaFR0RWhlNmoNCjJoVm16d1ZwYnE1Y3AySDh5aVZNZVZKK2hTbStwNlhZWXI1cGllblVIZlI2UkRNWHQwa1VSSVJ5Y09OTGw3QXYNClB1eVFKVVM1Zk8wUmEya3gvdGU3c1c4Y1VvejR5ZG5kQ2ZqMkZTbEdMTkN5TWtRZU5OU0w5NFA3K2dVRkI0M2oNCnNMd0RTRFh5K2srWkcwa0ZpVGFwMTRsa0NwTGh6WmdRaHk4eVFDeE05RzVpRFZBUzBFS3h6eVRLTElUd05RZmoNCldObnlPUHNtdVArdlBZcVIvMjFKWm9iY0JZTmNJZW9lbWZZbXdRdlVWOEdTQklWZVVCdDBSKzZXckw0Z0RxUmUNCnowQkR5MUlXQnRnK2JjRTZOVWFDVERpVWl2cGlnTWlCVHdXL21aV0FzUkFqMmNFK2FmN2NJNnlxWXVhdXRLQlANCk1pcUkzR0Fqd1pZOGM4Y3ZDL0JzWHlONUpKckNsK05obUpJbnRsS1hDN0xQQmZHZXg3WWVYbWZqY3ozMDFKNjcNCllhUGo1ZTE1U2tvQ0UyRERTeFJxVVNUL2RTaUR4OUdnOHoxa3pnWktOSHBBSytpQ3I1bWZMYy96eXNHQ2hrcEoNCjVwbzFWU1hWWnYxQTB3bEJNOFpFOExKVEcxaUxXVVBvNjRlN2VNalRtUGRENTNiWlptV1M5ZDBERFlRdzRQemQNCkFOeUl2UjczdlRLRTRIR2NFU2pibnVyWlB3bGpveXRCZDFIa2lBZGU4NFlXQUs3S094TjRFSlJFWlZ2QkJqMmENCjFQREYxSkh2dEJLQnN2dE9lbGk5TVh2UE0yb0h6cDA3ZjRva1RzNXV2RFhFZTQ2MExPeGcyclpzRFVCeStEL04NCkMrS3doaWI1TWFWSEhWOGQ5dEh3bjZxbEdDM1BOSG9iRFoyTU9FSkdPVjBkQ0NqckNVMTJBZy95d3ErMjhjZjENCkR4RmJrVEhLYVNiNjA5TjAxV2J4eGhFbXdBcGhmOEhCbFJqeG5sbU84ck4yc2pzYnQwOUpsM1VWcGFSenhxNDQNCkJwc1lBV21RVm9mYWR5MnFxZHkyMXZpYklRWkd6MUVIMW4yNGdBK1pkL1Z2ZEhidzU1YTBHZz09DQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ==" + "PUBLIC_KEY": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjJvSTBxbWJXamtsT1Vydkp1T2YNCm1ZZHF5Uk5ITXhPMHlLK2NwYXhlU2hqaW9HRURRNUFEOG5CNDdNRTJBUlZwajgwZWNKa1FwY3pGYlFQb1RhWUYNCk1LRG9GbThQWlRYakFCbGlzeGxmRkFEZ3lvcVBrNEk3OFhOTXIvWjl3SlRnQmh0N0UzYkdNN1RKR0c1Mk1XRXANCkJrb1RnZ2RyeXlmV2J6R0YyQmpUTG8vVDA3RU8rb1BoTXdIMGlZUFVDbDJnckxuSU4xdmhaa1Q0SnJpTHJHUkINClp2eUYrQVgvZXpab2RPdXpsZmtBM2U5T2FsTDh3TlZlY1UxWHdCUlhXbitJdC9xQUtXbk1scXR3a1FwZFV4c1ANCnpzQ1paclRtaGtNVTNJakpjaGJxTFlmTzZMVmduSWZpWThKb3FDT1dOQTF3NlpWWE9ISjR6NnREdWVyU1NVZ0kNClpRSURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0=", + "PRIVATE_KEY": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ0KUHJvYy1UeXBlOiA0LEVOQ1JZUFRFRA0KREVLLUluZm86IERFUy1FREUzLUNCQyxGQ0NDQTFBNEM0Qjg3MjRGDQoNCnJ3dk1tQ1M2QXArYjZwbnFGOWVpcW14Z0V5QjZuVHZaRWxGSWNKMWtZM3lJTjdYdFl0bGlBQXJhY2hRZ2hoZ3UNCjVUcDBRdGIxQjdnRmkwQjFwSGUxNzF0NTE5bitzNHpSNEt5b3lCUS9iQTlSVzVPSnhSTS9OY2xDeVFqbHJtQ1UNCnhEcDQ3V1ZNNHdveW1qd0dER1NKMXN4b0M0dDZWRHpqd0ZLREFSbFptQ0xnTXVVcThjMGFEWHdVSGtpdnJSZnkNCldGSHUyelZaSFNEL1VMWExld3V1cVZOK1A4ZVoyUEVaQ01EUFM2bWxEZE5ZcWx0dlduN3dPOXdML2tHVE5iekINCnczbndCUVpCOXlNbXlqUmEycjF6djBMcEJNdk5LTi9mTWduOHBYT0ZGc3BROWJGdm9OMHZKcHdpcnFJZDFUZmoNCmI0TzZyTXZadXFoNmdBenEzQS9wZFpZTUlhUHUxUzN3bU5SNXhzeFBiRkRyRTAwbFptU0ptSkRobzhySFVaZ3cNCmN3K1pTNzUrakV1Y2pUL3ZIS1RrcWN3WnRoTXRoVnQrNmhIdkFJcHVnYkxEQm9OSlBpdkxkZUpURzZQSUo0VDkNCk1TcTdMekplL0FnUkJFdHdQamhSMnFta2hWQUcwYXdyRC9hSVZkVnlaZGR1RVAwVFcxNXB1VDd2UzA0VC8zQ2YNCml3UTJUZVQ0emx3VkhPdzhnMUU2QjRuK1FWNUtmYS9abUFVRWdwdkVQajQ3dUxBclNWZFk3eExPL3JQcUtCa3kNClJINy9wOGJjNCtTTTlhWW5tZWJaNWhxekpOVlVHaEhTdmZCNnQyc2tBcXdZK1phUlYxSlo3TU5kaVlTRzV4KysNCjkzdzJYYk41b2NnRnppa0FyTi9JbUtPZm1VTnZ4S2NOaGxKM0J4dmUwZjRwNmJCSFFzbzdCWlV4TWczR2lMQkcNCjdKMzdWZW1yVVlBazJFK3B3TlFzL2hyaGVITnNzbFZPWVQvaVJYL0QvcjBNdU5aQUIvWjRGeFhyM0Vuc2d3SDgNCkdQVFlLejNNZzllVGhPRy9UY0wxdSt2eU1aSkM1L2tWZmVCbjV0dVRMV255bkNrM2NtR3A5WC9mcjRuQS9HZ0kNCkZ1bFZFb3p4aFQ5QXhiU3o2MEUvcG9uTWlSc2Q1Q3lpSW5kWnVqbDJlUm0wcGdjSC82MmJRNTh6QTRYeVlYb2wNCmF5UnVmMXFtSWw5MVdpZnE5Z0IwWkhhMDI2cVEwbHRad2pDaXhMRGNxWHZGYS8rbFB0OUNFZnNmaStEZmRITXcNCndOYXk4UVU1R1Zza0d6dWNCbGRZQWpOMk1sdGhUSVRyT2lMZ3lmTjVJYW95cU1lUTBVKy9SOEp4djAwSmhCdmcNCmQvUCszWUhwWG1yRUdwSWxENHJxb2NEMTZLaUd5bERrMjNBekFBeUpnaUluNGxId2grdVJyczFNdHErSEFaRFINCnpPQXdWR0MxVmxXbXRsL0VzTGRocmxuUE1SaEtnRmIrQmNIUm96aGpaemZhQU9oU1l1V2lkTDVtUnk0QzhFTHkNCnJzVkovOFZBRk1oZlQxS0paRzBjL0hRamVYYVdJSlV0VVNHTG95aWFhNTV1Q2ZTZTRJL3pvVmtROWVvVEh4NmMNCm1ERzZuY0U1bmExV2RJR2wxV0dGSkxqYmk4aXgrMWRyYXhuUDFYdkdFa1plNmpWd2x6cGJMNDZFSkZOYWhNQU0NCjdYNjFpcHNhSDEzQURUbjEwM3ZrUGNZaXR5KzJnb214VzN0R0JPN2FRYnRib0h6eDFCWEZwK3JVRHViSlNHSVUNClpRd2wxSnhjWW01NCtibWRicktPOVZPK2pKZXdYWnhJSytzZnFLbE1za2RZKzdpcndGcEI0NnVWZDFoZEE2dHQNCjRzWW1Wb2pIVkgzQ3dQY2RMWHpzV3Z1NUpLTVQ4Sm1oald4TEVmMTVLbEViNWhOU0I5YjRBVFhaRkppVjdzMWENCjdZZE1KeFkyMk9mNXM3QjBhTmNqV3lwWUNMcWIwMDZjMGJKWVMxUG5zUEVHbzAyQU1pWlA0MU1lWVNFTDJ6RGQNCnBaQnNCVkhUWU1SemtJV2ZkNjFaYkFYR0JKRHprREp0TUhMT1dLSjZteHp4elowcHlQczBCV2puUy9pWEIwc2wNCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t" } }, "BANK": { diff --git a/tests/_fixtures/pain.001.001.03.xml b/tests/_fixtures/pain.001.001.03.xml new file mode 100644 index 0000000..ac857d2 --- /dev/null +++ b/tests/_fixtures/pain.001.001.03.xml @@ -0,0 +1,172 @@ + + + + + message-id-001 + 2010-09-28T14:07:00 + 1 + 0.10 + + Bedrijfsnaam + + + + 123456789123456 + + + + + + + minimaal gevuld + TRF + 1 + 0.10 + 2009-11-01 + + Naam + + + + NL44RABO0123456789 + + + + + RABONL2U + + + + + non ref + + + 10.1 + + SLEV + + + ABNANL2A + + + + Naam creditor + + + + NL90ABNA0111111111 + + + + vrije tekst + + + + + maximaal gevuld + TRF + true + 1 + 0.20 + + NORM + + SEPA + + + IDEAL + + + SECU + + + 2009-11-01 + + Naam + + NL + Debtor straat 1 + 9999 XX Plaats debtor + + + + + NL44RABO0123456789 + + + + + RABONL2U + + + + + + + 12345678 + + klantnummer + + klantnummer uitgifte instantie + + + + + SLEV + + + debtor-to-debtor-bank-01 + End-to-end-id-debtor-to-creditor-01 + + + 20.2 + + + + ABNANL2A + + + + Naam creditor + + NL + Straat creditor 1 + 9999 XX Plaats creditor + + + + + NL90ABNA0111111111 + + + + + + + 1969-07-03 + PLAATS + NL + + + + + + CHAR + + + + + + + SCOR + + CUR + + 1234567 + + + + + + + diff --git a/tests/_fixtures/pain.008.001.02.xml b/tests/_fixtures/pain.008.001.02.xml new file mode 100644 index 0000000..9c0e002 --- /dev/null +++ b/tests/_fixtures/pain.008.001.02.xml @@ -0,0 +1,134 @@ + + + + + Message-ID + 2010-11-21T09:30:47.000Z + 2 + + Initiator Name + + + + Payment-ID + DD + 2 + 0.30 + + + SEPA + + + CORE + + RCUR + + 2010-12-03 + + Creditor Name + + + + DE87200500001234567890 + + + + + BANKDEFFXXX + + + SLEV + + + + + DE00ZZZ00099999999 + + SEPA + + + + + + + + OriginatorID1234 + + 0.10 + + + Mandate-Id + 2010-11-20 + true + + + Original Creditor Name + + + + AA00ZZZOriginalCreditorID + + SEPA + + + + + + + + + + + SPUEDE2UXXX + + + + Debtor Name + + + + DE21500500009876543210 + + + + Ultimate Debtor Name + + + Unstructured Remittance Information + + + + + OriginatorID1235 + + 0.20 + + + Other0Mandate0Id + 2010-11-20 + false + + + + + SPUEDE2UXXX + + + + Other Debtor Name + + + + DE21500500001234567897 + + + + Ultimate Debtor Name + + + Unstructured Remittance Information + + + + + \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php index efef421..6c8c4f5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,10 +1,3 @@