From 83660fa190d892e42ead197384a3dd0023ca7f3a Mon Sep 17 00:00:00 2001 From: Maximilian Kresse <545671+MaximilianKresse@users.noreply.github.com> Date: Mon, 2 May 2022 17:24:04 +0200 Subject: [PATCH 1/7] Added async-demo.php Added Client::getOauth2Info() --- .idea/misc.xml | 5 + README.md | 89 ++++------- examples/.gitignore | 3 +- examples/async-demo.php | 278 +++++++++++++++++++++++++++++++++ examples/demo.php | 8 +- examples/generate-token.php | 39 ++--- examples/ltv-demo.php | 10 +- examples/settings.php.dist | 2 +- src/Client.php | 51 ++++-- src/Module.php | 300 ++++++++++++++++++++---------------- 10 files changed, 558 insertions(+), 227 deletions(-) create mode 100644 examples/async-demo.php diff --git a/.idea/misc.xml b/.idea/misc.xml index d918df0..622b97b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -9,4 +9,9 @@ + + + \ No newline at end of file diff --git a/README.md b/README.md index 5d55ddf..7cf960e 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,13 @@ Electronic Seals to digital sign PDF documents in pure PHP. The API documentation can be found on the Cloud Signature Consortium website: https://cloudsignatureconsortium.org/resources/download-api-specifications/ -At writing time the module is tested with the eSigner CSC API from SSL.com. +At the time of writing the module is tested with the eSigner CSC API from SSL.com and the Entrust CSC API. It currently does not support all features or variances that may appear in other API implementations. -You can follow this integration guide to get a better understanding of how to setup a test environment and how the -signature workflow works: -https://www.ssl.com/guide/integration-guide-testing-remote-signing-with-esigner-csc-api/ -We implemented the same workflow in this module but instead of using postman you can use the module directly and -sign your PDF documents locally. +For usage at ssl.com you can follow this integration guide to get a better understanding of how to setup a test +environment and how the signature workflow works: +https://www.ssl.com/guide/integration-guide-testing-remote-signing-with-esigner-csc-api/ +(instead of using postman you can use this module directly and sign your PDF documents locally). ## Known not implemented features @@ -95,70 +94,34 @@ This class is a kind of proxy class to the CSC API. Its constructor requires the If you need to call an endpoint which is not covered by a proxy method, you can use the `call(string $path, ?string $accessToken = null, array $inputData = [])` method. -### The `Module` class +### How do I get an access token? -This is the main signature module which can be used with the [SetaPDF-Signer](https://www.setasign.com/signer) -component. It's constructor requires the following arguments: +An access token is returned by an authorization to the API service. -- `$accessToken` The access token -- `$client` A `Client` instance - see above +This was tested only by an OAuth2 authorization yet. You can to use an OAuth2 implementation such as +[league/oauth2-client](https://github.com/thephpleague/oauth2-client). +Sample code for this can be found in "[examples/generate-token.php](examples/generate-token.php)". -### How do I get an access token? +### Authorization modes -An access token is returned by an authorization to the API service. +Accessing a credential for remote signing requires an authorization from the user who owns it to control the signing +key associated to it. -This was tested only by an OAuth2 authorization yet. You can to use an OAuth2 implementation such as +The CSC API supports multiple authorization modes. The authorization mode also defines whether the signing process must +be asynchronous or not. To get this information you can call `Client::credentialsInfo()` and in the key "authMode" you'll +find one of the following authorization modes: + +- implicit: the authorization process is managed by the remote service autonomously. Authentication factors are managed by the remote signing service provider by interacting directly with the user, and not by the signature application. +- explicit: the authorization process is managed by the signature application, which collects authentication factors like PIN or One-Time Passwords (OTP). +- oauth2code: the authorization process is managed by the remote service using an OAuth 2.0 mechanism based on authorization code. + +For both "implicit" and "explicit" you can use the synchronous process (see [examples/demo.php](examples/demo.php) and [examples/ltv-demo.php](examples/ltv-demo.php)). + +For "oauth2code" you must use the asynchronous process (see [examples/demo-async.php](examples/async-demo.php)). This +will require an oauth2 implementation such as [league/oauth2-client](https://github.com/thephpleague/oauth2-client). -Sample code for this can be found in "[examples/generate-token.php](examples/generate-token.php)". -### Demo - -A simple complete signature process would look like this: - -```php -$accessToken = '...COMES E.G. FROM THE OAUTH2 AUTHORIZATION...'; -$otp = '123456'; // one-time-password - -$httpClient = new GuzzleHttp\Client(); -// if you are using php 7.0 or 7.1 -//$httpClient = new Mjelamanov\GuzzlePsr18\Client($httpClient); -$requestFactory = new Http\Factory\Guzzle\RequestFactory(); -$streamFactory = new Http\Factory\Guzzle\StreamFactory(); - -$client = new Client($apiUri, $httpClient, $requestFactory, $streamFactory); - -$credentialIds = ($client->credentialsList($accessToken)['credentialIds']); -// we just use the first credential on the list -$credentialId = $credentialIds[0]; -// fetch all informations regarding your credential id like the certificates -$credentialInfo = $client->credentialsInfo($accessToken, $credentialId, 'chain', true, true); -// get the certificate chain -$certificates = $credentialInfo['cert']['certificates']; -// the first certificate is always the signing certificate -$certificate = array_shift($certificates); -$algorithm = $credentialInfo['key']['algo'][0]; - -$module = new setasign\SetaPDF\Signer\Module\CSC\Module( - $accessToken, - $client -); -$module->setSignatureAlgorithmOid($algorithm); -$module->setCertificate($certificate); -$module->setExtraCertificates($certificates); -$module->setOtp($otp); - -// the file to sign -$fileToSign = __DIR__ . '/assets/Laboratory-Report.pdf'; - -// create a writer instance -$writer = new SetaPDF_Core_Writer_File('signed.pdf'); -// create the document instance -$document = SetaPDF_Core_Document::loadByFilename($fileToSign, $writer); - -// create the signer instance -$signer = new SetaPDF_Signer($document); -$signer->sign($module); -``` +More about the authorization modes can be found in "8.2 Credential authorization" of the CSC API. ## License diff --git a/examples/.gitignore b/examples/.gitignore index a4fef74..f384534 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -1,2 +1,3 @@ /settings.php -/signed.pdf \ No newline at end of file +/signed.pdf +/private \ No newline at end of file diff --git a/examples/async-demo.php b/examples/async-demo.php new file mode 100644 index 0000000..250eb06 --- /dev/null +++ b/examples/async-demo.php @@ -0,0 +1,278 @@ +If you want to restart the signature process click here: Restart'; + return; + } + $authorizationUrl = $_SESSION[__FILE__]['oauth2AuthorizationUrl']; + + // Redirect the user to the authorization URL. + header('Location: ' . $authorizationUrl); + die(); +} + +// to create or update your access token you have to call generate-token.php first +if (!isset($_SESSION['accessToken']['access_token'])) { + echo 'Missing access token! Login here'; + die(); +} +// check if the access token is still valid +if (!isset($_SESSION['accessToken']['expires']) || $_SESSION['accessToken']['expires'] < time()) { + echo 'Access token is expired! Renew here'; + die(); +} +$accessToken = $_SESSION['accessToken']['access_token']; + +$httpClient = new GuzzleHttp\Client(); +$httpClient = new Mjelamanov\GuzzlePsr18\Client($httpClient); +$requestFactory = new Http\Factory\Guzzle\RequestFactory(); +$streamFactory = new Http\Factory\Guzzle\StreamFactory(); +$client = new Client($apiUri, $httpClient, $requestFactory, $streamFactory); + +$oauth2Urls = $client->getOauth2Info(); +$provider = new GenericProvider([ + 'clientId' => $settings['clientId'], + 'clientSecret' => $settings['clientSecret'], + 'redirectUri' => rtrim($settings['demoUrl'], '/') . '/async-demo.php', + 'urlAuthorize' => $oauth2Urls['urlAuthorize'], + 'urlAccessToken' => $oauth2Urls['urlAccessToken'], + 'urlResourceOwnerDetails' => $oauth2Urls['urlResourceOwnerDetails'], +]); + +//var_dump($client->info()); +$credentialIds = ($client->credentialsList($accessToken))['credentialIDs']; +//var_dump($credentialIds); +// we just use the first credential on the list +$credentialId = $credentialIds[0]; + +// fetch all information regarding your credential id like the certificates +$credentialInfo = $client->credentialsInfo($accessToken, $credentialId, 'chain', true, true); +// var_dump($credentialInfo);die(); +if ($credentialInfo['authMode'] !== 'oauth2code') { + echo 'The selected credentialId does not support oauth2code authentification.' + . ' A asynchronous sign request is not possible - take a look at the other demos instead.'; + die(); +} + +$certificates = $credentialInfo['cert']['certificates']; +$certificates = array_map(function (string $certificate) { + return new SetaPDF_Signer_X509_Certificate($certificate); +}, $certificates); +// to cache the certificate files +//foreach ($certificates as $k => $certificate) { +// file_put_contents('cert-' . $k . '.pem', $certificate->get()); +//} + +// the first certificate is always the signing certificate +$certificate = array_shift($certificates); + +if (!isset($_GET['code'])) { + $signatureAlgorithmOid = $credentialInfo['key']['algo'][0]; + + // create a writer instance + $writer = new SetaPDF_Core_Writer_File($resultPath); + // create the document instance + $document = SetaPDF_Core_Document::loadByFilename($fileToSign, $writer); + + // create the signer instance + $signer = new SetaPDF_Signer($document); + + $module = new SetaPDF_Signer_Signature_Module_Pades(); + $module->setCertificate($certificate); + $module->setExtraCertificates($certificates); + + ['hashAlgorithm' => $hashAlgorithm, 'signAlgorithm' => $signAlgorithm] = Module::findHashAndSignAlgorithm($signatureAlgorithmOid); + $module->setDigest($hashAlgorithm); + + // create a collector instance + $collector = new SetaPDF_Signer_ValidationRelatedInfo_Collector(new SetaPDF_Signer_X509_Collection($certificates)); + // collect revocation information for this certificate + $vriData = $collector->getByCertificate($certificate); + + foreach ($vriData->getOcspResponses() as $ocspResponse) { + $module->addOcspResponse($ocspResponse); + } + foreach ($vriData->getCrls() as $crl) { + $module->addCrl($crl); + } + + $signer->setSignatureContentLength(20000); + $tmpDocument = $signer->preSign( + new SetaPDF_Core_Writer_File(SetaPDF_Core_Writer_TempFile::createTempPath()), + $module + ); + if ($signAlgorithm === Digest::RSA_PSS_ALGORITHM) { + $signatureAlgorithmParameters = Module::fixPssPadding($this->padesModule); + } + $hashData = base64_encode(hash($hashAlgorithm, $module->getDataToSign($tmpDocument->getHashFile()), true)); + + $authorizationUrl = $provider->getAuthorizationUrl([ + 'scope' => 'credential', + 'credentialID' => $credentialId, + 'hash' => $hashData + ]); + + $_SESSION[__FILE__] = [ + 'tmpDocument' => $tmpDocument, + 'hashData' => $hashData, + 'module' => $module, + 'signAlgorithm' => $signAlgorithm, + 'signAlgorithmOid' => $signatureAlgorithmOid, + 'vriData' => $vriData, + 'oauth2state' => $provider->getState(), + 'oauth2AuthorizationUrl' => $authorizationUrl + ]; + + echo '

' + . '
Sign
'; +} else { + if (!isset($_SESSION[__FILE__]['hashData'])) { + echo 'No session data found.
If you want to restart the signature process click here: Restart'; + return; + } + + // Check given state against previously stored one to mitigate CSRF attacks and replay attacks + if ($_GET['state'] !== $_SESSION[__FILE__]['oauth2state']) { + echo 'Invalid state
If you want to restart the signature process click here: Restart'; + return; + } + + $hashData = $_SESSION[__FILE__]['hashData']; + + /** + * @var SetaPDF_Signer_Signature_Module_Pades $module + */ + $module = $_SESSION[__FILE__]['module']; + + /** + * @var SetaPDF_Signer_TmpDocument $tmpDocument + */ + $tmpDocument = $_SESSION[__FILE__]['tmpDocument']; + + $sad = $provider->getAccessToken('authorization_code', [ + 'code' => $_GET['code'] + ]); + + $result = $client->signaturesSignHash( + $accessToken, + $credentialId, + $sad->getToken(), + [$hashData], + $_SESSION[__FILE__]['signAlgorithmOid'], + Digest::$oids[$module->getDigest()], + isset($signatureAlgorithmParameters) ? (string)$signatureAlgorithmParameters : null + ); +// var_dump($result); + $signatureValue = (string) \base64_decode($result['signatures'][0]); + if ($_SESSION[__FILE__]['signAlgorithm'] === Digest::ECDSA_ALGORITHM) { + $signatureValue = Module::fixEccSignatures($signatureValue); + } + + $module->setSignatureValue($signatureValue); + + // get the CMS structur from the signature module + $cms = (string) $module->getCms(); + + $reader = new SetaPDF_Core_Reader_File($fileToSign); + $tmpWriter = new SetaPDF_Core_Writer_TempFile(); + + $document = SetaPDF_Core_Document::load($reader, $tmpWriter); + $signer = new SetaPDF_Signer($document); + + $field = $signer->getSignatureField(); + $fieldName = $field->getQualifiedName(); + $signer->setSignatureFieldName($fieldName); + + $signer->saveSignature($tmpDocument, $cms); + $document->finish(); + + $writer = new SetaPDF_Core_Writer_String(); + $document = \SetaPDF_Core_Document::loadByFilename($tmpWriter->getPath(), $writer); + + // create a VRI collector instance + $collector = new SetaPDF_Signer_ValidationRelatedInfo_Collector(new SetaPDF_Signer_X509_Collection($certificates)); + $vriData = $collector->getByFieldName( + $document, + $fieldName, + SetaPDF_Signer_ValidationRelatedInfo_Collector::SOURCE_OCSP_OR_CRL, + null, + null, + $_SESSION[__FILE__]['vriData'] // pass the previously gathered VRI data + ); + // and add it to the document. + $dss = new SetaPDF_Signer_DocumentSecurityStore($document); + $dss->addValidationRelatedInfoByFieldName( + $fieldName, + $vriData->getCrls(), + $vriData->getOcspResponses(), + $vriData->getCertificates() + ); + + // save and finish the final document + $document->save()->finish(); + + $_SESSION[__FILE__] = [ + 'pdf' => [ + 'name' => 'signed.pdf', + 'data' => $writer->getBuffer() + ] + ]; + + echo 'The file was successfully signed. You can download the result here.
' + . ' If you want to restart the signature process click here: Restart'; +} \ No newline at end of file diff --git a/examples/demo.php b/examples/demo.php index 09f17fa..dad294b 100644 --- a/examples/demo.php +++ b/examples/demo.php @@ -47,10 +47,16 @@ var_dump($credentialIds); // we just use the first credential on the list $credentialId = $credentialIds[0]; -// fetch all informations regarding your credential id like the certificates +// fetch all information regarding your credential id like the certificates $credentialInfo = $client->credentialsInfo($accessToken, $credentialId, 'chain', true, true); var_dump($credentialInfo); echo ''; +if ($credentialInfo['authMode'] === 'oauth2code') { + echo 'The selected credentialId does only support oauth2code authentification.' + . ' A synchronous sign request is not possible - take a look at the async-demo instead.'; + die(); +} + $certificates = $credentialInfo['cert']['certificates']; // INFO: YOU SHOULD CACHE THE DATA IN $credentialInfo FOR LESS API REQUESTS diff --git a/examples/generate-token.php b/examples/generate-token.php index 8a7e2c2..81dc093 100644 --- a/examples/generate-token.php +++ b/examples/generate-token.php @@ -23,36 +23,35 @@ $streamFactory = new Http\Factory\Guzzle\StreamFactory(); $client = new Client($apiUri, $httpClient, $requestFactory, $streamFactory); -$info = $client->info(); - -if (!in_array('oauth2code', $info['authType'])) { - throw new Exception('OAuth2 isn\'t supported by your CSC API.'); -} - -session_start(); +$oauth2Urls = $client->getOauth2Info(); $provider = new GenericProvider([ 'clientId' => $settings['clientId'], 'clientSecret' => $settings['clientSecret'], - 'redirectUri' => $settings['oauth2redirectUrl'], - 'urlAuthorize' => $info['oauth2'] . '/oauth2/authorize', - 'urlAccessToken' => $info['oauth2'] . '/oauth2/token', - 'urlResourceOwnerDetails' => $info['oauth2'] . '/oauth2/resource', + 'redirectUri' => rtrim($settings['demoUrl'], '/') . '/generate-token.php', + 'urlAuthorize' => $oauth2Urls['urlAuthorize'], + 'urlAccessToken' => $oauth2Urls['urlAccessToken'], + 'urlResourceOwnerDetails' => $oauth2Urls['urlResourceOwnerDetails'], ]); +session_start(); + if (isset($_GET['reset'])) { $_SESSION = []; +} elseif (isset($_SESSION['accessToken'])) { + $accessToken = new AccessToken($_SESSION['accessToken']); } -if (isset($_SESSION['accessToken'])) { - $accessToken = new AccessToken($_SESSION['accessToken']); - if ($accessToken->hasExpired()) { - $accessToken = $provider->getAccessToken('refresh_token', [ - 'refresh_token' => $accessToken->getRefreshToken() - ]); +/** @noinspection PhpStatementHasEmptyBodyInspection */ +if (isset($accessToken) && !$accessToken->hasExpired()) { + // do nothing - the access token is still valid +} elseif (isset($accessToken) && $accessToken->getRefreshToken() !== null) { + // access token has expired, but we have refresh token + $accessToken = $provider->getAccessToken('refresh_token', [ + 'refresh_token' => $accessToken->getRefreshToken() + ]); - $_SESSION['accessToken'] = $accessToken->jsonSerialize(); - } + $_SESSION['accessToken'] = $accessToken->jsonSerialize(); } else { // If we don't have an authorization code then get one if (!isset($_GET['code'])) { @@ -100,3 +99,5 @@ echo 'Go to demo.php
'; echo 'Go to ltv-demo.php
'; +echo 'Go to async-demo.php
'; + diff --git a/examples/ltv-demo.php b/examples/ltv-demo.php index 031b8c7..5af12b6 100644 --- a/examples/ltv-demo.php +++ b/examples/ltv-demo.php @@ -50,13 +50,19 @@ var_dump($credentialIds); // we just use the first credential on the list $credentialId = $credentialIds[0]; -// fetch all informations regarding your credential id like the certificates +// fetch all information regarding your credential id like the certificates $credentialInfo = $client->credentialsInfo($accessToken, $credentialId, 'chain', true, true); var_dump($credentialInfo); echo ''; // INFO: YOU SHOULD CACHE THE DATA IN $credentialInfo FOR LESS API REQUESTS +if ($credentialInfo['authMode'] === 'oauth2code') { + echo 'The selected credentialId does only support oauth2code authentification.' + . ' A synchronous sign request is not possible - take a look at the async-demo instead.'; + die(); +} + $certificates = $credentialInfo['cert']['certificates']; $certificates = array_map(function (string $certificate) { return new SetaPDF_Signer_X509_Certificate($certificate); @@ -70,7 +76,7 @@ $module->setSignatureAlgorithmOid($algorithm); $module->setCertificate($certificate); -// now add these information to the CMS container +// now add this information to the CMS container $module->setExtraCertificates($certificates); // create a collection of trusted certificats: diff --git a/examples/settings.php.dist b/examples/settings.php.dist index 157c129..8b46281 100644 --- a/examples/settings.php.dist +++ b/examples/settings.php.dist @@ -3,7 +3,7 @@ return [ 'clientId' => 'your-client-id', 'clientSecret' => 'your-client-secret', - 'oauth2redirectUrl' => 'https://your-domain/generate-token.php', + 'demoUrl' => 'https://your-domain/', 'oauth2scope' => 'service', 'apiUri' => 'https://cs-try.ssl.com/csc/v0' ]; diff --git a/src/Client.php b/src/Client.php index 3573de3..41e21c7 100644 --- a/src/Client.php +++ b/src/Client.php @@ -4,11 +4,12 @@ namespace setasign\SetaPDF\Signer\Module\CSC; +use BadMethodCallException; +use Exception; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; -use SetaPDF_Signer_Signature_Module_Pades; /** * CSC API Client @@ -33,14 +34,14 @@ class Client protected $streamFactory; /** - * @var SetaPDF_Signer_Signature_Module_Pades Internal pades module. + * @var string */ - protected $padesModule; + protected $apiUri; /** - * @var string + * @var array|null */ - protected $apiUri; + protected $info; /** * Client constructor. @@ -62,6 +63,33 @@ public function __construct( $this->streamFactory = $streamFactory; } + /** + * Helper method to fetch the oauth2 url endpoints. + * + * @return array + * @throws ClientExceptionInterface + */ + public function getOauth2Info(): array + { + $info = $this->info(); + if (!\array_key_exists('oauth2', $info) || $info['oauth2'] === '') { + throw new BadMethodCallException('OAuth2 isn\'t supported by your CSC API.'); + } + + // this should contain the base URI of the OAuth 2.0 authorization server endpoint + $baseUrl = $info['oauth2']; + // some endpoints seem to ignore the official documentation for oauth2 value, so we try to fix this here + if (strpos($baseUrl, '/oauth2/authorize') !== false) { + $baseUrl = substr($info['oauth2'], 0, -strlen('/oauth2/authorize')); + } + + return [ + 'urlAuthorize' => $baseUrl . '/oauth2/authorize', + 'urlAccessToken' => $baseUrl . '/oauth2/token', + 'urlResourceOwnerDetails' => $baseUrl . '/oauth2/resource' + ]; + } + /** * Helper method to handle errors in json_decode * @@ -126,6 +154,8 @@ public function call(string $path, ?string $accessToken = null, array $inputData * Returns information about the remote service and the list of the API methods it supports. * This method SHALL be implemented by any remote service conforming to this specification. * + * Note: the result of this method is memoized. + * * @param string|null $lang * @return array * @throws ClientExceptionInterface @@ -133,11 +163,14 @@ public function call(string $path, ?string $accessToken = null, array $inputData */ public function info(?string $lang = null): array { - $inputData = []; - if ($lang !== null) { - $inputData['lang'] = $lang; + if ($this->info === null || !\array_key_exists($lang, $this->info)) { + $inputData = []; + if ($lang !== null) { + $inputData['lang'] = $lang; + } + $this->info[$lang ?? 'none'] = $this->call('/info', null, $inputData); } - return $this->call('/info', null, $inputData); + return $this->info[$lang ?? 'none']; } /** diff --git a/src/Module.php b/src/Module.php index 776387e..0d92f88 100644 --- a/src/Module.php +++ b/src/Module.php @@ -32,6 +32,159 @@ class Module implements SetaPDF_Signer_Signature_DictionaryInterface, SetaPDF_Signer_Signature_DocumentInterface { + public static function findHashAndSignAlgorithm(string $signatureAlgorithmOid): array + { + $found = false; + $hashAlgorithm = $signAlgorithm = null; + foreach (Digest::$encryptionOids as $signAlgorithm => $hashAlgorithms) { + $hashAlgorithm = \array_search($signatureAlgorithmOid, $hashAlgorithms, true); + if ($hashAlgorithm === false) { + continue; + } + $found = true; + break; + } + + if (!$found) { + throw new InvalidArgumentException(\sprintf('Unknown signature algorithm OID "%s"', $signatureAlgorithmOid)); + } + return ['hashAlgorithm' => $hashAlgorithm, 'signAlgorithm' => $signAlgorithm]; + } + + /** + * Update CMS SignatureAlgorithmIdentifier according to Probabilistic Signature Scheme (RSASSA-PSS) + * + * @param SetaPDF_Signer_Signature_Module_Pades $padesModule + * @return Asn1Element + * @throws \SetaPDF_Exception_NotImplemented + */ + public static function fixPssPadding( + SetaPDF_Signer_Signature_Module_Pades $padesModule + ): Asn1Element { + throw new \SetaPDF_Exception_NotImplemented( + 'Signatures with PSS padding were not tested yet. Please contact support@setasign.com with details of your CSC API.' + ); + +// $padesDigest = $padesModule->getDigest(); +// +// // let's use a salt length of the same size as the hash function output +// $saltLength = 256 / 8; +// if ($padesDigest === \SetaPDF_Signer_Digest::SHA_384) { +// $saltLength = 384 / 8; +// } elseif ($padesDigest === \SetaPDF_Signer_Digest::SHA_512) { +// $saltLength = 512 / 8; +// } +// +// $cms = $padesModule->getCms(); +// +// $signatureAlgorithmIdentifier = Asn1Element::findByPath('1/0/4/0/4', $cms); +// $signatureAlgorithmIdentifier->getChild(0)->setValue( +// Asn1Oid::encode("1.2.840.113549.1.1.10") +// ); +// $signatureAlgorithmIdentifier->removeChild($signatureAlgorithmIdentifier->getChild(1)); +// $signatureAlgorithmParameters = new Asn1Element( +// Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, +// '', +// [ +// new Asn1Element( +// Asn1Element::TAG_CLASS_CONTEXT_SPECIFIC | Asn1Element::IS_CONSTRUCTED, +// '', +// [ +// new Asn1Element( +// Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, +// '', +// [ +// new Asn1Element( +// Asn1Element::OBJECT_IDENTIFIER, +// Asn1Oid::encode(Digest::getOid($padesDigest)) +// ), +// new Asn1Element(Asn1Element::NULL) +// ] +// ) +// ] +// ), +// new Asn1Element( +// Asn1Element::TAG_CLASS_CONTEXT_SPECIFIC | Asn1Element::IS_CONSTRUCTED | "\x01", +// '', +// [ +// new Asn1Element( +// Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, +// '', +// [ +// new Asn1Element( +// Asn1Element::OBJECT_IDENTIFIER, +// Asn1Oid::encode('1.2.840.113549.1.1.8') +// ), +// new Asn1Element( +// Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, +// '', +// [ +// new Asn1Element( +// Asn1Element::OBJECT_IDENTIFIER, +// Asn1Oid::encode(Digest::getOid( +// $padesDigest +// )) +// ), +// new Asn1Element(Asn1Element::NULL) +// ] +// ) +// ] +// ) +// ] +// ), +// new Asn1Element( +// Asn1Element::TAG_CLASS_CONTEXT_SPECIFIC | Asn1Element::IS_CONSTRUCTED | "\x02", '', +// [ +// new Asn1Element(Asn1Element::INTEGER, \chr($saltLength)) +// ] +// ) +// ] +// ); +// $signatureAlgorithmIdentifier->addChild($signatureAlgorithmParameters); +// +// return $signatureAlgorithmParameters; + } + + public static function fixEccSignatures(string $signatureValue): string + { + throw new \SetaPDF_Exception_NotImplemented( + 'EC signatures were not tested yet. Please contact support@setasign.com with details of your CSC API.' + ); + // Let's ensure that the ECDSA-Sig-Value is DER encoded. + // Some other services (e.g. KMS systems) return the signature value as raw concatenated "r+s" value. + // Maybe this also happens by a CSC API? The signature encoding is sadly not defined. +// try { +// Asn1Element::parse($signatureValue); +// +// } catch (\SetaPDF_Signer_Asn1_Exception $e) { +// /* According to RFC5753 2.1.1: +// * - signature MUST contain the DER encoding (as an octet string) of a value of the ASN.1 type +// * ECDSA-Sig-Value (see Section 7.2). +// */ +// $len = strlen($signatureValue); +// +// $s = \substr($signatureValue, 0, $len / 2); +// if (\ord($s[0]) & 0x80) { // ensure positive integers +// $s = "\0" . $s; +// } +// $r = \substr($signatureValue, $len / 2); +// if (\ord($r[0]) & 0x80) { // ensure positive integers +// $r = "\0" . $r; +// } +// +// $signatureValue = new Asn1Element( +// Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, +// '', +// [ +// new Asn1Element(Asn1Element::INTEGER, $s), +// new Asn1Element(Asn1Element::INTEGER, $r), +// ] +// ); +// } +// +// return $signatureValue; + } + /** * @var Client */ @@ -116,21 +269,7 @@ public function setCertificate($certificate) */ public function setSignatureAlgorithmOid(string $signatureAlgorithmOid) { - $found = false; - $hashAlgorithm = $signAlgorithm = null; - foreach (Digest::$encryptionOids as $signAlgorithm => $hashAlgorithms) { - $hashAlgorithm = \array_search($signatureAlgorithmOid, $hashAlgorithms, true); - if ($hashAlgorithm === false) { - continue; - } - $found = true; - break; - } - - if (!$found) { - throw new InvalidArgumentException(\sprintf('Unknown signature algorithm OID "%s"', $signatureAlgorithmOid)); - } - + ['hashAlgorithm' => $hashAlgorithm, 'signAlgorithm' => $signAlgorithm] = self::findHashAndSignAlgorithm($signatureAlgorithmOid); $this->padesModule->setDigest($hashAlgorithm); $this->signAlgorithm = $signAlgorithm; $this->signatureAlgorithmOid = $signatureAlgorithmOid; @@ -144,6 +283,11 @@ public function getSignatureAlgorithmOid(): ?string return $this->signatureAlgorithmOid; } + public function getPadesDigest(): string + { + return $this->padesModule->getDigest(); + } + /** * Add additional certificates which are placed into the CMS structure. * @@ -177,6 +321,11 @@ public function addCrl($crl) $this->padesModule->addCrl($crl); } + public function setSignatureValue(string $signatureValue) + { + $this->padesModule->setSignatureValue($signatureValue); + } + /** * @inheritDoc */ @@ -221,87 +370,10 @@ public function createSignature(SetaPDF_Core_Reader_FilePath $tmpPath) // get the hash data from the module $padesDigest = $this->padesModule->getDigest(); + $signatureAlgorithmParameters = null; - // update CMS SignatureAlgorithmIdentifier according to Probabilistic Signature Scheme (RSASSA-PSS) if ($this->signAlgorithm === Digest::RSA_PSS_ALGORITHM) { - // let's use a salt length of the same size as the hash function output - $saltLength = 256 / 8; - if ($padesDigest === \SetaPDF_Signer_Digest::SHA_384) { - $saltLength = 384 / 8; - } elseif ($padesDigest === \SetaPDF_Signer_Digest::SHA_512) { - $saltLength = 512 / 8; - } - - $cms = $this->padesModule->getCms(); - - $signatureAlgorithmIdentifier = Asn1Element::findByPath('1/0/4/0/4', $cms); - $signatureAlgorithmIdentifier->getChild(0)->setValue( - Asn1Oid::encode("1.2.840.113549.1.1.10") - ); - $signatureAlgorithmIdentifier->removeChild($signatureAlgorithmIdentifier->getChild(1)); - $signatureAlgorithmParameters = new Asn1Element( - Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, - '', - [ - new Asn1Element( - Asn1Element::TAG_CLASS_CONTEXT_SPECIFIC | Asn1Element::IS_CONSTRUCTED, - '', - [ - new Asn1Element( - Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, - '', - [ - new Asn1Element( - Asn1Element::OBJECT_IDENTIFIER, - Asn1Oid::encode(Digest::getOid($this->padesModule->getDigest())) - ), - new Asn1Element(Asn1Element::NULL) - ] - ) - ] - ), - new Asn1Element( - Asn1Element::TAG_CLASS_CONTEXT_SPECIFIC | Asn1Element::IS_CONSTRUCTED | "\x01", - '', - [ - new Asn1Element( - Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, - '', - [ - new Asn1Element( - Asn1Element::OBJECT_IDENTIFIER, - Asn1Oid::encode('1.2.840.113549.1.1.8') - ), - new Asn1Element( - Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, - '', - [ - new Asn1Element( - Asn1Element::OBJECT_IDENTIFIER, - Asn1Oid::encode(Digest::getOid( - $this->padesModule->getDigest() - )) - ), - new Asn1Element(Asn1Element::NULL) - ] - ) - ] - ) - ] - ), - new Asn1Element( - Asn1Element::TAG_CLASS_CONTEXT_SPECIFIC | Asn1Element::IS_CONSTRUCTED | "\x02", '', - [ - new Asn1Element(Asn1Element::INTEGER, \chr($saltLength)) - ] - ) - ] - ); - $signatureAlgorithmIdentifier->addChild($signatureAlgorithmParameters); - - throw new \SetaPDF_Exception_NotImplemented( - 'Signatures with PSS padding were not tested yet. Please contact support@setasign.com with details of your CSC API.' - ); + $signatureAlgorithmParameters = self::fixPssPadding($this->padesModule); } $hashData = \base64_encode(hash($padesDigest, $this->padesModule->getDataToSign($tmpPath), true)); @@ -323,48 +395,14 @@ public function createSignature(SetaPDF_Core_Reader_FilePath $tmpPath) Digest::$oids[$padesDigest], isset($signatureAlgorithmParameters) ? (string)$signatureAlgorithmParameters : null ); - $signatureValue = \base64_decode($result['signatures'][0]); + $signatureValue = (string) \base64_decode($result['signatures'][0]); if ($this->signAlgorithm === Digest::ECDSA_ALGORITHM) { - // Let's ensure that the ECDSA-Sig-Value is DER encoded. - // Some other services (e.g. KMS systems) return the signature value as raw concatenated "r+s" value. - // Maybe this also happens by a CSC API? The signature encoding is sadly not defined. - try { - Asn1Element::parse($signatureValue); - - } catch (\SetaPDF_Signer_Asn1_Exception $e) { - /* According to RFC5753 2.1.1: - * - signature MUST contain the DER encoding (as an octet string) of a value of the ASN.1 type - * ECDSA-Sig-Value (see Section 7.2). - */ - $len = strlen($signatureValue); - - $s = \substr($signatureValue, 0, $len / 2); - if (\ord($s[0]) & 0x80) { // ensure positive integers - $s = "\0" . $s; - } - $r = \substr($signatureValue, $len / 2); - if (\ord($r[0]) & 0x80) { // ensure positive integers - $r = "\0" . $r; - } - - $signatureValue = new Asn1Element( - Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, - '', - [ - new Asn1Element(Asn1Element::INTEGER, $s), - new Asn1Element(Asn1Element::INTEGER, $r), - ] - ); - } - - throw new \SetaPDF_Exception_NotImplemented( - 'EC signatures were not tested yet. Please contact support@setasign.com with details of your CSC API.' - ); + $signatureValue = self::fixEccSignatures($signatureValue); } // pass it to the module - $this->padesModule->setSignatureValue((string) $signatureValue); + $this->padesModule->setSignatureValue($signatureValue); return (string) $this->padesModule->getCms(); } From 6a0798b5b090741cb5ac59a4e22dc9c4406573f6 Mon Sep 17 00:00:00 2001 From: Maximilian Kresse <545671+MaximilianKresse@users.noreply.github.com> Date: Tue, 3 May 2022 09:10:08 +0200 Subject: [PATCH 2/7] Reordered async-demo --- examples/async-demo.php | 394 +++++++++++++++++++++------------------- 1 file changed, 203 insertions(+), 191 deletions(-) diff --git a/examples/async-demo.php b/examples/async-demo.php index 250eb06..fe6d956 100644 --- a/examples/async-demo.php +++ b/examples/async-demo.php @@ -29,42 +29,6 @@ $fileToSign = __DIR__ . '/Laboratory-Report.pdf'; $resultPath = 'signed.pdf'; -$action = $_GET['action'] ?? 'unknown'; -switch ($action) { - case 'preview': - header('Content-Type: application/pdf'); - header('Content-Disposition: inline; filename="' . basename($fileToSign, '.pdf') . '.pdf"'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Pragma: public'); - $data = file_get_contents($fileToSign); - header('Content-Length: ' . strlen($data)); - echo $data; - flush(); - die(); - case 'download': - $doc = $_SESSION[__FILE__]['pdf']; - - header('Content-Type: application/pdf'); - header('Content-Disposition: attachment; filename="' . $doc['name']); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Pragma: public'); - header('Content-Length: ' . strlen($doc['data'])); - echo $doc['data']; - flush(); - die(); - case 'sign': - if (!isset($_SESSION[__FILE__]['oauth2AuthorizationUrl'])) { - echo 'No session data found.
If you want to restart the signature process click here: Restart'; - return; - } - $authorizationUrl = $_SESSION[__FILE__]['oauth2AuthorizationUrl']; - - // Redirect the user to the authorization URL. - header('Location: ' . $authorizationUrl); - die(); -} // to create or update your access token you have to call generate-token.php first if (!isset($_SESSION['accessToken']['access_token'])) { @@ -88,7 +52,7 @@ $provider = new GenericProvider([ 'clientId' => $settings['clientId'], 'clientSecret' => $settings['clientSecret'], - 'redirectUri' => rtrim($settings['demoUrl'], '/') . '/async-demo.php', + 'redirectUri' => rtrim($settings['demoUrl'], '/') . '/async-demo.php?action=sign', 'urlAuthorize' => $oauth2Urls['urlAuthorize'], 'urlAccessToken' => $oauth2Urls['urlAccessToken'], 'urlResourceOwnerDetails' => $oauth2Urls['urlResourceOwnerDetails'], @@ -121,158 +85,206 @@ // the first certificate is always the signing certificate $certificate = array_shift($certificates); -if (!isset($_GET['code'])) { - $signatureAlgorithmOid = $credentialInfo['key']['algo'][0]; - - // create a writer instance - $writer = new SetaPDF_Core_Writer_File($resultPath); - // create the document instance - $document = SetaPDF_Core_Document::loadByFilename($fileToSign, $writer); - - // create the signer instance - $signer = new SetaPDF_Signer($document); - - $module = new SetaPDF_Signer_Signature_Module_Pades(); - $module->setCertificate($certificate); - $module->setExtraCertificates($certificates); - - ['hashAlgorithm' => $hashAlgorithm, 'signAlgorithm' => $signAlgorithm] = Module::findHashAndSignAlgorithm($signatureAlgorithmOid); - $module->setDigest($hashAlgorithm); - - // create a collector instance - $collector = new SetaPDF_Signer_ValidationRelatedInfo_Collector(new SetaPDF_Signer_X509_Collection($certificates)); - // collect revocation information for this certificate - $vriData = $collector->getByCertificate($certificate); - - foreach ($vriData->getOcspResponses() as $ocspResponse) { - $module->addOcspResponse($ocspResponse); - } - foreach ($vriData->getCrls() as $crl) { - $module->addCrl($crl); - } - - $signer->setSignatureContentLength(20000); - $tmpDocument = $signer->preSign( - new SetaPDF_Core_Writer_File(SetaPDF_Core_Writer_TempFile::createTempPath()), - $module - ); - if ($signAlgorithm === Digest::RSA_PSS_ALGORITHM) { - $signatureAlgorithmParameters = Module::fixPssPadding($this->padesModule); - } - $hashData = base64_encode(hash($hashAlgorithm, $module->getDataToSign($tmpDocument->getHashFile()), true)); - - $authorizationUrl = $provider->getAuthorizationUrl([ - 'scope' => 'credential', - 'credentialID' => $credentialId, - 'hash' => $hashData - ]); - - $_SESSION[__FILE__] = [ - 'tmpDocument' => $tmpDocument, - 'hashData' => $hashData, - 'module' => $module, - 'signAlgorithm' => $signAlgorithm, - 'signAlgorithmOid' => $signatureAlgorithmOid, - 'vriData' => $vriData, - 'oauth2state' => $provider->getState(), - 'oauth2AuthorizationUrl' => $authorizationUrl - ]; - - echo '

' - . '
Sign
'; -} else { - if (!isset($_SESSION[__FILE__]['hashData'])) { - echo 'No session data found.
If you want to restart the signature process click here: Restart'; - return; - } - - // Check given state against previously stored one to mitigate CSRF attacks and replay attacks - if ($_GET['state'] !== $_SESSION[__FILE__]['oauth2state']) { - echo 'Invalid state
If you want to restart the signature process click here: Restart'; - return; - } - - $hashData = $_SESSION[__FILE__]['hashData']; - - /** - * @var SetaPDF_Signer_Signature_Module_Pades $module - */ - $module = $_SESSION[__FILE__]['module']; - - /** - * @var SetaPDF_Signer_TmpDocument $tmpDocument - */ - $tmpDocument = $_SESSION[__FILE__]['tmpDocument']; - - $sad = $provider->getAccessToken('authorization_code', [ - 'code' => $_GET['code'] - ]); - - $result = $client->signaturesSignHash( - $accessToken, - $credentialId, - $sad->getToken(), - [$hashData], - $_SESSION[__FILE__]['signAlgorithmOid'], - Digest::$oids[$module->getDigest()], - isset($signatureAlgorithmParameters) ? (string)$signatureAlgorithmParameters : null - ); + +$action = $_GET['action'] ?? 'preSign'; +// if the oauth request was unsuccessful, return to preSign +if ($action === 'sign' && $_GET['error']) { + $action = 'preSign'; +} + +switch ($action) { + case 'preSign': + $signatureAlgorithmOid = $credentialInfo['key']['algo'][0]; + + // create a writer instance + $writer = new SetaPDF_Core_Writer_File($resultPath); + // create the document instance + $document = SetaPDF_Core_Document::loadByFilename($fileToSign, $writer); + + // create the signer instance + $signer = new SetaPDF_Signer($document); + + $module = new SetaPDF_Signer_Signature_Module_Pades(); + $module->setCertificate($certificate); + $module->setExtraCertificates($certificates); + + ['hashAlgorithm' => $hashAlgorithm, 'signAlgorithm' => $signAlgorithm] = Module::findHashAndSignAlgorithm($signatureAlgorithmOid); + $module->setDigest($hashAlgorithm); + + // create a collector instance + $collector = new SetaPDF_Signer_ValidationRelatedInfo_Collector(new SetaPDF_Signer_X509_Collection($certificates)); + // collect revocation information for this certificate + $vriData = $collector->getByCertificate($certificate); + + foreach ($vriData->getOcspResponses() as $ocspResponse) { + $module->addOcspResponse($ocspResponse); + } + foreach ($vriData->getCrls() as $crl) { + $module->addCrl($crl); + } + + $signer->setSignatureContentLength(20000); + $tmpDocument = $signer->preSign( + new SetaPDF_Core_Writer_File(SetaPDF_Core_Writer_TempFile::createTempPath()), + $module + ); + if ($signAlgorithm === Digest::RSA_PSS_ALGORITHM) { + $signatureAlgorithmParameters = Module::fixPssPadding($this->padesModule); + } + $hashData = base64_encode(hash($hashAlgorithm, $module->getDataToSign($tmpDocument->getHashFile()), true)); + + $authorizationUrl = $provider->getAuthorizationUrl([ + 'scope' => 'credential', + 'credentialID' => $credentialId, + 'hash' => $hashData + ]); + + $_SESSION[__FILE__] = [ + 'tmpDocument' => $tmpDocument, + 'hashData' => $hashData, + 'module' => $module, + 'signAlgorithm' => $signAlgorithm, + 'signAlgorithmOid' => $signatureAlgorithmOid, + 'vriData' => $vriData, + 'oauth2state' => $provider->getState(), + 'oauth2AuthorizationUrl' => $authorizationUrl + ]; + + echo '

' + . '
Sign
'; + break; + + case 'preview': + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; filename="' . basename($fileToSign, '.pdf') . '.pdf"'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Pragma: public'); + $data = file_get_contents($fileToSign); + header('Content-Length: ' . strlen($data)); + echo $data; + flush(); + break; + + case 'oauthAuthorize': + if (!isset($_SESSION[__FILE__]['oauth2AuthorizationUrl'])) { + echo 'No session data found.
If you want to restart the signature process click here: Restart'; + return; + } + $authorizationUrl = $_SESSION[__FILE__]['oauth2AuthorizationUrl']; + + // Redirect the user to the authorization URL. + header('Location: ' . $authorizationUrl); + break; + + case 'sign': + if (!isset($_SESSION[__FILE__]['hashData'])) { + echo 'No session data found.
If you want to restart the signature process click here: Restart'; + return; + } + + // Check given state against previously stored one to mitigate CSRF attacks and replay attacks + if ($_GET['state'] !== $_SESSION[__FILE__]['oauth2state']) { + echo 'Invalid state
If you want to restart the signature process click here: Restart'; + return; + } + + $hashData = $_SESSION[__FILE__]['hashData']; + + /** + * @var SetaPDF_Signer_Signature_Module_Pades $module + */ + $module = $_SESSION[__FILE__]['module']; + + /** + * @var SetaPDF_Signer_TmpDocument $tmpDocument + */ + $tmpDocument = $_SESSION[__FILE__]['tmpDocument']; + + $sad = $provider->getAccessToken('authorization_code', [ + 'code' => $_GET['code'] + ]); + + $result = $client->signaturesSignHash( + $accessToken, + $credentialId, + $sad->getToken(), + [$hashData], + $_SESSION[__FILE__]['signAlgorithmOid'], + Digest::$oids[$module->getDigest()], + isset($signatureAlgorithmParameters) ? (string)$signatureAlgorithmParameters : null + ); // var_dump($result); - $signatureValue = (string) \base64_decode($result['signatures'][0]); - if ($_SESSION[__FILE__]['signAlgorithm'] === Digest::ECDSA_ALGORITHM) { - $signatureValue = Module::fixEccSignatures($signatureValue); - } - - $module->setSignatureValue($signatureValue); - - // get the CMS structur from the signature module - $cms = (string) $module->getCms(); - - $reader = new SetaPDF_Core_Reader_File($fileToSign); - $tmpWriter = new SetaPDF_Core_Writer_TempFile(); - - $document = SetaPDF_Core_Document::load($reader, $tmpWriter); - $signer = new SetaPDF_Signer($document); - - $field = $signer->getSignatureField(); - $fieldName = $field->getQualifiedName(); - $signer->setSignatureFieldName($fieldName); - - $signer->saveSignature($tmpDocument, $cms); - $document->finish(); - - $writer = new SetaPDF_Core_Writer_String(); - $document = \SetaPDF_Core_Document::loadByFilename($tmpWriter->getPath(), $writer); - - // create a VRI collector instance - $collector = new SetaPDF_Signer_ValidationRelatedInfo_Collector(new SetaPDF_Signer_X509_Collection($certificates)); - $vriData = $collector->getByFieldName( - $document, - $fieldName, - SetaPDF_Signer_ValidationRelatedInfo_Collector::SOURCE_OCSP_OR_CRL, - null, - null, - $_SESSION[__FILE__]['vriData'] // pass the previously gathered VRI data - ); - // and add it to the document. - $dss = new SetaPDF_Signer_DocumentSecurityStore($document); - $dss->addValidationRelatedInfoByFieldName( - $fieldName, - $vriData->getCrls(), - $vriData->getOcspResponses(), - $vriData->getCertificates() - ); - - // save and finish the final document - $document->save()->finish(); - - $_SESSION[__FILE__] = [ - 'pdf' => [ - 'name' => 'signed.pdf', - 'data' => $writer->getBuffer() - ] - ]; - - echo 'The file was successfully signed. You can download the result here.
' - . ' If you want to restart the signature process click here: Restart'; -} \ No newline at end of file + $signatureValue = (string) \base64_decode($result['signatures'][0]); + if ($_SESSION[__FILE__]['signAlgorithm'] === Digest::ECDSA_ALGORITHM) { + $signatureValue = Module::fixEccSignatures($signatureValue); + } + + $module->setSignatureValue($signatureValue); + + // get the CMS structur from the signature module + $cms = (string) $module->getCms(); + + $reader = new SetaPDF_Core_Reader_File($fileToSign); + $tmpWriter = new SetaPDF_Core_Writer_TempFile(); + + $document = SetaPDF_Core_Document::load($reader, $tmpWriter); + $signer = new SetaPDF_Signer($document); + + $field = $signer->getSignatureField(); + $fieldName = $field->getQualifiedName(); + $signer->setSignatureFieldName($fieldName); + + $signer->saveSignature($tmpDocument, $cms); + $document->finish(); + + $writer = new SetaPDF_Core_Writer_String(); + $document = \SetaPDF_Core_Document::loadByFilename($tmpWriter->getPath(), $writer); + + // create a VRI collector instance + $collector = new SetaPDF_Signer_ValidationRelatedInfo_Collector(new SetaPDF_Signer_X509_Collection($certificates)); + $vriData = $collector->getByFieldName( + $document, + $fieldName, + SetaPDF_Signer_ValidationRelatedInfo_Collector::SOURCE_OCSP_OR_CRL, + null, + null, + $_SESSION[__FILE__]['vriData'] // pass the previously gathered VRI data + ); + // and add it to the document. + $dss = new SetaPDF_Signer_DocumentSecurityStore($document); + $dss->addValidationRelatedInfoByFieldName( + $fieldName, + $vriData->getCrls(), + $vriData->getOcspResponses(), + $vriData->getCertificates() + ); + + // save and finish the final document + $document->save()->finish(); + + $_SESSION[__FILE__] = [ + 'pdf' => [ + 'name' => 'signed.pdf', + 'data' => $writer->getBuffer() + ] + ]; + + echo 'The file was successfully signed. You can download the result here.
' + . ' If you want to restart the signature process click here: Restart'; + break; + + case 'download': + $doc = $_SESSION[__FILE__]['pdf']; + + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; filename="' . $doc['name']); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Pragma: public'); + header('Content-Length: ' . strlen($doc['data'])); + echo $doc['data']; + flush(); + break; +} + From f0d6b97266e8b7d1fb554cb49615264fa3b59356 Mon Sep 17 00:00:00 2001 From: Maximilian Kresse <545671+MaximilianKresse@users.noreply.github.com> Date: Tue, 3 May 2022 09:24:26 +0200 Subject: [PATCH 3/7] Update async-demo.php --- examples/async-demo.php | 105 ++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/examples/async-demo.php b/examples/async-demo.php index fe6d956..13db3bb 100644 --- a/examples/async-demo.php +++ b/examples/async-demo.php @@ -58,42 +58,56 @@ 'urlResourceOwnerDetails' => $oauth2Urls['urlResourceOwnerDetails'], ]); -//var_dump($client->info()); -$credentialIds = ($client->credentialsList($accessToken))['credentialIDs']; -//var_dump($credentialIds); -// we just use the first credential on the list -$credentialId = $credentialIds[0]; - -// fetch all information regarding your credential id like the certificates -$credentialInfo = $client->credentialsInfo($accessToken, $credentialId, 'chain', true, true); -// var_dump($credentialInfo);die(); -if ($credentialInfo['authMode'] !== 'oauth2code') { - echo 'The selected credentialId does not support oauth2code authentification.' - . ' A asynchronous sign request is not possible - take a look at the other demos instead.'; - die(); +$action = $_GET['action'] ?? 'preview'; +// if the oauth request was unsuccessful, return to preSign +if ($action === 'sign' && isset($_GET['error'])) { + $action = 'preview'; } -$certificates = $credentialInfo['cert']['certificates']; -$certificates = array_map(function (string $certificate) { - return new SetaPDF_Signer_X509_Certificate($certificate); -}, $certificates); -// to cache the certificate files -//foreach ($certificates as $k => $certificate) { -// file_put_contents('cert-' . $k . '.pem', $certificate->get()); -//} +switch ($action) { + case 'preview': + echo '

' + . '
Sign
'; + break; -// the first certificate is always the signing certificate -$certificate = array_shift($certificates); + case 'previewDocument': + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; filename="' . basename($fileToSign, '.pdf') . '.pdf"'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Pragma: public'); + $data = file_get_contents($fileToSign); + header('Content-Length: ' . strlen($data)); + echo $data; + flush(); + break; + case 'preSign': + $credentialIds = ($client->credentialsList($accessToken))['credentialIDs']; + // we just use the first credential on the list + $credentialId = $credentialIds[0]; + + // fetch all information regarding your credential id like the certificates + $credentialInfo = $client->credentialsInfo($accessToken, $credentialId, 'chain', true, true); + // var_dump($credentialInfo);die(); + if ($credentialInfo['authMode'] !== 'oauth2code') { + echo 'The selected credentialId does not support oauth2code authentification.' + . ' A asynchronous sign request is not possible - take a look at the other demos instead.'; + die(); + } -$action = $_GET['action'] ?? 'preSign'; -// if the oauth request was unsuccessful, return to preSign -if ($action === 'sign' && $_GET['error']) { - $action = 'preSign'; -} + $certificates = $credentialInfo['cert']['certificates']; + $certificates = array_map(function (string $certificate) { + return new SetaPDF_Signer_X509_Certificate($certificate); + }, $certificates); + // to cache the certificate files + //foreach ($certificates as $k => $certificate) { + // file_put_contents('cert-' . $k . '.pem', $certificate->get()); + //} + + // the first certificate is always the signing certificate + $certificate = array_shift($certificates); -switch ($action) { - case 'preSign': $signatureAlgorithmOid = $credentialInfo['key']['algo'][0]; // create a writer instance @@ -142,38 +156,15 @@ $_SESSION[__FILE__] = [ 'tmpDocument' => $tmpDocument, 'hashData' => $hashData, + 'credentialID' => $credentialId, 'module' => $module, 'signAlgorithm' => $signAlgorithm, 'signAlgorithmOid' => $signatureAlgorithmOid, + 'certificates' => $certificates, 'vriData' => $vriData, 'oauth2state' => $provider->getState(), - 'oauth2AuthorizationUrl' => $authorizationUrl ]; - echo '

' - . '
Sign
'; - break; - - case 'preview': - header('Content-Type: application/pdf'); - header('Content-Disposition: inline; filename="' . basename($fileToSign, '.pdf') . '.pdf"'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Pragma: public'); - $data = file_get_contents($fileToSign); - header('Content-Length: ' . strlen($data)); - echo $data; - flush(); - break; - - case 'oauthAuthorize': - if (!isset($_SESSION[__FILE__]['oauth2AuthorizationUrl'])) { - echo 'No session data found.
If you want to restart the signature process click here: Restart'; - return; - } - $authorizationUrl = $_SESSION[__FILE__]['oauth2AuthorizationUrl']; - - // Redirect the user to the authorization URL. header('Location: ' . $authorizationUrl); break; @@ -207,7 +198,7 @@ $result = $client->signaturesSignHash( $accessToken, - $credentialId, + $_SESSION[__FILE__]['credentialID'], $sad->getToken(), [$hashData], $_SESSION[__FILE__]['signAlgorithmOid'], @@ -242,7 +233,7 @@ $document = \SetaPDF_Core_Document::loadByFilename($tmpWriter->getPath(), $writer); // create a VRI collector instance - $collector = new SetaPDF_Signer_ValidationRelatedInfo_Collector(new SetaPDF_Signer_X509_Collection($certificates)); + $collector = new SetaPDF_Signer_ValidationRelatedInfo_Collector(new SetaPDF_Signer_X509_Collection($_SESSION[__FILE__]['certificates'])); $vriData = $collector->getByFieldName( $document, $fieldName, From 82995d65dd3444f8aba31680d575d8c28d7c7bca Mon Sep 17 00:00:00 2001 From: Maximilian Kresse <545671+MaximilianKresse@users.noreply.github.com> Date: Tue, 3 May 2022 09:48:59 +0200 Subject: [PATCH 4/7] Update settings.php.dist --- examples/settings.php.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/settings.php.dist b/examples/settings.php.dist index 8b46281..27f6148 100644 --- a/examples/settings.php.dist +++ b/examples/settings.php.dist @@ -3,7 +3,7 @@ return [ 'clientId' => 'your-client-id', 'clientSecret' => 'your-client-secret', - 'demoUrl' => 'https://your-domain/', + 'demoUrl' => 'https://your-domain/path-to-this-directory/', 'oauth2scope' => 'service', 'apiUri' => 'https://cs-try.ssl.com/csc/v0' ]; From eb56c99fcc17f9c07dbcd397cb50623ae244476d Mon Sep 17 00:00:00 2001 From: Maximilian Kresse <545671+MaximilianKresse@users.noreply.github.com> Date: Tue, 3 May 2022 09:55:58 +0200 Subject: [PATCH 5/7] Removed new methods --- src/Module.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Module.php b/src/Module.php index 0d92f88..3eed799 100644 --- a/src/Module.php +++ b/src/Module.php @@ -283,11 +283,6 @@ public function getSignatureAlgorithmOid(): ?string return $this->signatureAlgorithmOid; } - public function getPadesDigest(): string - { - return $this->padesModule->getDigest(); - } - /** * Add additional certificates which are placed into the CMS structure. * @@ -321,11 +316,6 @@ public function addCrl($crl) $this->padesModule->addCrl($crl); } - public function setSignatureValue(string $signatureValue) - { - $this->padesModule->setSignatureValue($signatureValue); - } - /** * @inheritDoc */ From cfbdcfea164b281dd30ab67cb4ddcc577d90f950 Mon Sep 17 00:00:00 2001 From: Maximilian Kresse <545671+MaximilianKresse@users.noreply.github.com> Date: Tue, 3 May 2022 10:02:47 +0200 Subject: [PATCH 6/7] Renamed fixPssPadding to updateCmsForPssPadding Small typo fixes in readme --- README.md | 4 ++-- examples/async-demo.php | 2 +- src/Module.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7cf960e..4489ea3 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ Electronic Seals to digital sign PDF documents in pure PHP. The API documentation can be found on the Cloud Signature Consortium website: https://cloudsignatureconsortium.org/resources/download-api-specifications/ -At the time of writing the module is tested with the eSigner CSC API from SSL.com and the Entrust CSC API. +At the time of writing the module is tested with the eSigner CSC API from SSL.com and the Remote Signing Service CSC API from Entrust. It currently does not support all features or variances that may appear in other API implementations. -For usage at ssl.com you can follow this integration guide to get a better understanding of how to setup a test +For usage with ssl.com you can follow this integration guide to get a better understanding of how to setup a test environment and how the signature workflow works: https://www.ssl.com/guide/integration-guide-testing-remote-signing-with-esigner-csc-api/ (instead of using postman you can use this module directly and sign your PDF documents locally). diff --git a/examples/async-demo.php b/examples/async-demo.php index 13db3bb..49a5de5 100644 --- a/examples/async-demo.php +++ b/examples/async-demo.php @@ -143,7 +143,7 @@ $module ); if ($signAlgorithm === Digest::RSA_PSS_ALGORITHM) { - $signatureAlgorithmParameters = Module::fixPssPadding($this->padesModule); + $signatureAlgorithmParameters = Module::updateCmsForPssPadding($this->padesModule); } $hashData = base64_encode(hash($hashAlgorithm, $module->getDataToSign($tmpDocument->getHashFile()), true)); diff --git a/src/Module.php b/src/Module.php index 3eed799..9d881d7 100644 --- a/src/Module.php +++ b/src/Module.php @@ -58,7 +58,7 @@ public static function findHashAndSignAlgorithm(string $signatureAlgorithmOid): * @return Asn1Element * @throws \SetaPDF_Exception_NotImplemented */ - public static function fixPssPadding( + public static function updateCmsForPssPadding( SetaPDF_Signer_Signature_Module_Pades $padesModule ): Asn1Element { throw new \SetaPDF_Exception_NotImplemented( @@ -363,7 +363,7 @@ public function createSignature(SetaPDF_Core_Reader_FilePath $tmpPath) $signatureAlgorithmParameters = null; if ($this->signAlgorithm === Digest::RSA_PSS_ALGORITHM) { - $signatureAlgorithmParameters = self::fixPssPadding($this->padesModule); + $signatureAlgorithmParameters = self::updateCmsForPssPadding($this->padesModule); } $hashData = \base64_encode(hash($padesDigest, $this->padesModule->getDataToSign($tmpPath), true)); From 1d4dce76f72d7c21c9adca3aabaebe2d075006e9 Mon Sep 17 00:00:00 2001 From: Maximilian Kresse <545671+MaximilianKresse@users.noreply.github.com> Date: Tue, 3 May 2022 10:04:09 +0200 Subject: [PATCH 7/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4489ea3..4294a43 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ https://cloudsignatureconsortium.org/resources/download-api-specifications/ At the time of writing the module is tested with the eSigner CSC API from SSL.com and the Remote Signing Service CSC API from Entrust. It currently does not support all features or variances that may appear in other API implementations. -For usage with ssl.com you can follow this integration guide to get a better understanding of how to setup a test +For usage with SSL.com you can follow this integration guide to get a better understanding of how to setup a test environment and how the signature workflow works: https://www.ssl.com/guide/integration-guide-testing-remote-signing-with-esigner-csc-api/ (instead of using postman you can use this module directly and sign your PDF documents locally).