├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── main_action.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml ├── src ├── Commands │ ├── SignPdfCommand.php │ └── ValidatePdfSignatureCommand.php ├── Entities │ ├── BaseEntity.php │ ├── CertificateProcessed.php │ ├── EncryptedCertificate.php │ └── ValidatedSignedPDF.php ├── Exceptions │ ├── CertificateOutputNotFoundException.php │ ├── FileNotFoundException.php │ ├── HasNoSignatureOrInvalidPkcs7Exception.php │ ├── InvalidCertificateContentException.php │ ├── InvalidImageDriverException.php │ ├── InvalidPFXException.php │ ├── InvalidPdfFileException.php │ ├── InvalidPdfSignModeTypeException.php │ ├── InvalidX509PrivateKeyException.php │ └── ProcessRunTimeException.php ├── Helpers │ └── helpers.php ├── LaravelA1PdfSignServiceProvider.php ├── Resources │ ├── font │ │ └── Roboto-Medium.ttf │ └── img │ │ ├── sign-seal.pdn │ │ └── sign-seal.png ├── Sign │ ├── ManageCert.php │ ├── SealImage.php │ ├── SignaturePdf.php │ └── ValidatePdfSignature.php └── Temp │ └── .gitkeep └── tests ├── CommandsTest.php ├── HelpersTest.php ├── ManageCertTest.php ├── Resources ├── .gitignore ├── CertInfoExamples │ ├── with-comma.json │ ├── with-comma.txt │ ├── without-comma.json │ └── without-comma.txt └── test.pdf ├── SealImageTest.php ├── Sign └── ValidatePdfSignature │ └── ProcessDataInfoFunctionTest.php └── TestCase.php /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: lsnepomuceno 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Versions involved:** 27 | - Laravel: [e.g. 9.21] 28 | - Laravel A1 Pdf Sign [e.g. 0.0.18] 29 | - Composer [e.g. 2.1] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: documentation, enhancement 6 | assignees: lsnepomuceno 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | **Versions involved:** 23 | - Laravel: [e.g. 9.21] 24 | - Laravel A1 Pdf Sign [e.g. 0.0.18] 25 | - Composer [e.g. 2.1] 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/main_action.yml: -------------------------------------------------------------------------------- 1 | name: A1 Pdf Sign Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ main, dev, v1.x-dev ] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | # Laravel 9 supports PHP 8.1–8.2 19 | - php: 8.1 20 | laravel: 9.* 21 | - php: 8.2 22 | laravel: 9.* 23 | 24 | # Laravel 10 supports PHP 8.1–8.3 25 | - php: 8.1 26 | laravel: 10.* 27 | - php: 8.2 28 | laravel: 10.* 29 | - php: 8.3 30 | laravel: 10.* 31 | 32 | # Laravel 11 supports PHP 8.2–8.4 33 | - php: 8.2 34 | laravel: 11.* 35 | - php: 8.3 36 | laravel: 11.* 37 | - php: 8.4 38 | laravel: 11.* 39 | 40 | # Laravel 12 supports PHP 8.2–8.4 41 | - php: 8.2 42 | laravel: 12.* 43 | - php: 8.3 44 | laravel: 12.* 45 | - php: 8.4 46 | laravel: 12.* 47 | 48 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} 49 | 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v4 53 | 54 | - name: Setup PHP 55 | uses: shivammathur/setup-php@v2 56 | with: 57 | php-version: ${{ matrix.php }} 58 | extensions: mbstring, dom, fileinfo, openssl, json, imagick, swoole, sqlite3 59 | coverage: none 60 | 61 | - name: Validate PHP and Composer 62 | run: | 63 | php -v 64 | composer -V 65 | composer validate 66 | 67 | - name: Configure Laravel Version 68 | run: | 69 | composer require "illuminate/support:${{ matrix.laravel }}" \ 70 | "illuminate/encryption:${{ matrix.laravel }}" \ 71 | "illuminate/http:${{ matrix.laravel }}" \ 72 | --no-interaction --no-update 73 | 74 | - name: Install dependencies 75 | run: | 76 | composer update --prefer-dist --no-interaction --no-progress 77 | 78 | - name: Run tests 79 | run: composer test 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.project 3 | /.buildpath 4 | /composer.phar 5 | /vendor 6 | /nbproject 7 | .vagrant 8 | Vagrantfile 9 | .idea 10 | .php_cs.cache 11 | composer.lock 12 | .phpunit.result.cache 13 | .phpunit.cache/ 14 | *.bak 15 | demo.php 16 | *Zone.Identifier 17 | Temp 18 | *.pdf 19 | *.pfx 20 | node_modules 21 | dist 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | e-mail. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/lsnepomuceno/laravel-a1-pdf-sign). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lucas Nepomuceno 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Signature image 3 |

4 | 5 |

Sign PDF files with valid x509 certificate

6 | 7 |

8 | 9 | Latest Stable Version 10 | 11 | 12 | Total Downloads 13 | 14 | 15 | Latest Unstable Version 16 | 17 | 18 | License 19 | 20 | 21 | Tests 22 | 23 |

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
Reference
Laravel versionPHP versionPackage versionDocs
^8 ~8.54^7.4^0 ~0.0.11Official Doc
^8.56+^0.0.12
^9^8.1 || ^8.2^1Official Doc
^10^8.1 || ^8.2 || ^8.3
^11 || ^12^8.2 || ^8.3 || ^8.4
67 | 68 | 69 |

Project supported by JetBrains

70 |

Special thanks to the team at JetBrains for supporting Open Source projects with licenses to use.

71 |

72 | 73 | JetBrains Logo 74 | 75 |

76 | 77 |

Do you want to support this project?

78 |

79 | 80 | Buy Me A Coffee 81 | 82 |

83 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lsnepomuceno/laravel-a1-pdf-sign", 3 | "description": "Sign PDF files with valid x509 certificates", 4 | "license": "MIT", 5 | "type": "library", 6 | "homepage": "https://github.com/lsnepomuceno/laravel-a1-pdf-sign", 7 | "support": { 8 | "issues": "https://github.com/lsnepomuceno/laravel-a1-pdf-sign/issues", 9 | "source": "https://github.com/lsnepomuceno/laravel-a1-pdf-sign" 10 | }, 11 | "keywords": [ 12 | "a1", 13 | "sign pdf", 14 | "sign", 15 | "x509", 16 | "laravel", 17 | "certificate", 18 | "icp brasil" 19 | ], 20 | "authors": [ 21 | { 22 | "name": "Lucas Nepomuceno", 23 | "email": "lsn.nepomuceno@gmail.com", 24 | "homepage": "https://github.com/lsnepomuceno" 25 | } 26 | ], 27 | "require": { 28 | "php": ">=8.1 <8.5", 29 | "ext-gd": "*", 30 | "ext-json": "*", 31 | "ext-fileinfo": "*", 32 | "ext-mbstring": "*", 33 | "ext-openssl": "*", 34 | "illuminate/support": "^9 || ^10 || ^11 || ^12", 35 | "illuminate/encryption": "^9 || ^10 || ^11 || ^12", 36 | "illuminate/http": "^9 || ^10 || ^11 || ^12", 37 | "tecnickcom/tc-lib-pdf": "^8", 38 | "tecnickcom/tcpdf": "^6", 39 | "setasign/fpdi": "^2.6", 40 | "symfony/process": "^6 || ^7", 41 | "intervention/image": "^3.11" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "LSNepomuceno\\LaravelA1PdfSign\\": "./src" 46 | }, 47 | "files": [ 48 | "src/Helpers/helpers.php" 49 | ] 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "LSNepomuceno\\LaravelA1PdfSign\\Tests\\": "./tests" 54 | } 55 | }, 56 | "scripts": { 57 | "test": "vendor/bin/testbench package:test" 58 | }, 59 | "extra": { 60 | "laravel": { 61 | "providers": [ 62 | "LSNepomuceno\\LaravelA1PdfSign\\LaravelA1PdfSignServiceProvider" 63 | ] 64 | } 65 | }, 66 | "suggest": { 67 | "ext-gd": "To use GD library based image processing.", 68 | "ext-imagick": "To use Imagick based image processing.", 69 | "lsnepomuceno/laravel-brazilian-ceps": "A package for querying zip codes for Brazilian addresses." 70 | }, 71 | "require-dev": { 72 | "orchestra/testbench": "^7 || ^8 || ^9 || ^10", 73 | "nunomaduro/collision": "^6 || ^7 || ^8" 74 | }, 75 | "config": { 76 | "allow-plugins": { 77 | "pestphp/pest-plugin": true 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | ./tests/ 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Commands/SignPdfCommand.php: -------------------------------------------------------------------------------- 1 | line('Your PDF file is being signed!', 'info'); 23 | 24 | try { 25 | $pdfPath = $this->argument(key: 'pdfPath'); 26 | $pfxPath = $this->argument(key: 'pfxPath'); 27 | $password = $this->argument(key: 'password'); 28 | $fileName = $this->defineFileName($this->argument(key: 'fileName')); 29 | 30 | $signedFileResource = signPdfFromFile($pfxPath, $password, $pdfPath); 31 | 32 | File::put($fileName, $signedFileResource); 33 | 34 | $this->line("Your file has been signed and is available at: \"{$fileName}\"", 'info'); 35 | 36 | return self::SUCCESS; 37 | } catch (\Throwable $th) { 38 | $this->line("Could not sign your file, error occurred: {$th->getMessage()}", 'error'); 39 | return self::FAILURE; 40 | } 41 | } 42 | 43 | private function defineFileName(?string $fileName): string 44 | { 45 | if ($fileName && !Str::endsWith(strtolower($fileName), '.pdf')) { 46 | return "{$fileName}.pdf"; 47 | } 48 | 49 | if (!$fileName) { 50 | $fileName = a1TempDir(tempFile: true, fileExt: '.pdf'); 51 | } 52 | 53 | return $fileName; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Commands/ValidatePdfSignatureCommand.php: -------------------------------------------------------------------------------- 1 | line('Your PDF document is being validated.', 'info'); 18 | try { 19 | $pdfPath = $this->argument(key: 'pdfPath'); 20 | 21 | $validated = validatePdfSignature($pdfPath); 22 | $validationText = $validated->isValidated ? 'VALID' : 'INVALID'; 23 | 24 | $this->line("Your PDF document is {$validationText}", 'info'); 25 | return $validated->isValidated ? self::SUCCESS : self::INVALID; 26 | } catch (\Throwable $th) { 27 | $this->line("Unable to validate your file signature, an error occurred: {$th->getMessage()}", 'error'); 28 | return self::FAILURE; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Entities/BaseEntity.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/FileNotFoundException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Exceptions/HasNoSignatureOrInvalidPkcs7Exception.php: -------------------------------------------------------------------------------- 1 | currentFile = $currentFile; 15 | $message = 'The file is unsigned or the signature is not compatible with the PKCS7 type.'; 16 | parent::__construct($message, $code, $previous); 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidCertificateContentException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidImageDriverException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidPFXException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidPdfFileException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidPdfSignModeTypeException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidX509PrivateKeyException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exceptions/ProcessRunTimeException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Helpers/helpers.php: -------------------------------------------------------------------------------- 1 | fromPfx($pfxPath, $password, $usePathEnv), 27 | $mode 28 | ))->signature(); 29 | } 30 | } 31 | 32 | if (!function_exists('signPdfFromUpload')) { 33 | /** 34 | * @throws Throwable 35 | */ 36 | function signPdfFromUpload( 37 | UploadedFile $uploadedPfx, 38 | string $password, 39 | string $pdfPath, 40 | string $mode = SignaturePdf::MODE_RESOURCE, 41 | bool $usePathEnv = false 42 | ): BinaryFileResponse|string 43 | { 44 | return (new SignaturePdf( 45 | $pdfPath, 46 | (new ManageCert)->fromUpload($uploadedPfx, $password, $usePathEnv), 47 | $mode 48 | ))->signature(); 49 | } 50 | } 51 | 52 | if (!function_exists('encryptCertData')) { 53 | /** 54 | * @throws Throwable 55 | */ 56 | function encryptCertData( 57 | UploadedFile|string $uploadedOrPfxPath, 58 | string $password, 59 | bool $usePathEnv = false 60 | ): EncryptedCertificate 61 | { 62 | $cert = new ManageCert; 63 | 64 | if ($uploadedOrPfxPath instanceof UploadedFile) { 65 | $cert->fromUpload($uploadedOrPfxPath, $password, $usePathEnv); 66 | } else { 67 | $cert->fromPfx($uploadedOrPfxPath, $password, $usePathEnv); 68 | } 69 | 70 | return new EncryptedCertificate( 71 | certificate: $cert->getEncrypter()->encryptString($cert->getCert()->original), 72 | password: $cert->getEncrypter()->encryptString($password), 73 | hash: $cert->getHashKey() // IMPORTANT, USE ON DECRYPT HELPER 74 | ); 75 | } 76 | } 77 | 78 | if (!function_exists('decryptCertData')) { 79 | /** 80 | * @throws Throwable 81 | */ 82 | function decryptCertData( 83 | string $hashKey, 84 | string $encryptCert, 85 | string $password, 86 | bool $isBase64 = false, 87 | bool $usePathEnv = false, 88 | ): ManageCert 89 | { 90 | $cert = (new ManageCert)->setHashKey($hashKey); 91 | $uuid = Str::orderedUuid(); 92 | $pfxName = "{$cert->getTempDir()}{$uuid}.pfx"; 93 | 94 | $decryptedData = $cert->getEncrypter()->decryptString($encryptCert); 95 | File::put($pfxName, $isBase64 ? base64_decode($decryptedData) : $decryptedData); 96 | 97 | return $cert->fromPfx( 98 | $pfxName, 99 | $cert->getEncrypter()->decryptString($password), 100 | $usePathEnv 101 | ); 102 | } 103 | } 104 | 105 | if (!function_exists('a1TempDir')) { 106 | function a1TempDir(bool $tempFile = false, string $fileExt = '.pfx'): string 107 | { 108 | $tempDir = dirname(__DIR__) . '/Temp/'; 109 | 110 | if (!is_writable($tempDir)) { 111 | $tempDir = sys_get_temp_dir() . '/'; 112 | } 113 | 114 | if ($tempFile) { 115 | $tempDir .= Str::orderedUuid() . $fileExt; 116 | } 117 | 118 | return $tempDir; 119 | } 120 | } 121 | 122 | if (!function_exists('validatePdfSignature')) { 123 | /** 124 | * @throws Throwable 125 | */ 126 | function validatePdfSignature(string $pdfPath): ValidatedSignedPDF 127 | { 128 | return ValidatePdfSignature::from($pdfPath); 129 | } 130 | } 131 | 132 | if (!function_exists('runCliCommandProcesses')) { 133 | /** 134 | * @throws ProcessRunTimeException 135 | */ 136 | function runCliCommandProcesses(string $command, bool $usePathEnv = false): void 137 | { 138 | $env = null; 139 | 140 | if ($usePathEnv) { 141 | $env = [ 142 | 'PATH' => getenv('PATH') 143 | ]; 144 | } 145 | 146 | $process = Process::fromShellCommandline( 147 | command: $command, 148 | env: $env 149 | ); 150 | $process->run(); 151 | while ($process->isRunning()) continue; 152 | 153 | if (!$process->isSuccessful()) { 154 | throw new ProcessRunTimeException($process->getErrorOutput()); 155 | } 156 | 157 | $process->stop(1); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/LaravelA1PdfSignServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 18 | $this->commands([ 19 | SignPdfCommand::class, 20 | ValidatePdfSignatureCommand::class 21 | ]); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Resources/font/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsnepomuceno/laravel-a1-pdf-sign/bdc396dac6f45966a4532b70ae15b7a62643be6f/src/Resources/font/Roboto-Medium.ttf -------------------------------------------------------------------------------- /src/Resources/img/sign-seal.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsnepomuceno/laravel-a1-pdf-sign/bdc396dac6f45966a4532b70ae15b7a62643be6f/src/Resources/img/sign-seal.pdn -------------------------------------------------------------------------------- /src/Resources/img/sign-seal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsnepomuceno/laravel-a1-pdf-sign/bdc396dac6f45966a4532b70ae15b7a62643be6f/src/Resources/img/sign-seal.png -------------------------------------------------------------------------------- /src/Sign/ManageCert.php: -------------------------------------------------------------------------------- 1 | tempDir = a1TempDir(); 40 | $this->generateHashKey()->setEncrypter(); 41 | 42 | if (!File::exists($this->tempDir)) { 43 | File::makeDirectory($this->tempDir); 44 | } 45 | } 46 | 47 | public function setPreservePfx(bool $preservePfx = true): self 48 | { 49 | $this->preservePfx = $preservePfx; 50 | return $this; 51 | } 52 | 53 | public function setIsLegacy(bool $isLegacy = true): self 54 | { 55 | $this->isLegacy = $isLegacy; 56 | return $this; 57 | } 58 | 59 | /** 60 | * @throws CertificateOutputNotFoundException 61 | * @throws FileNotFoundException 62 | * @throws InvalidCertificateContentException 63 | * @throws InvalidPFXException 64 | * @throws Invalidx509PrivateKeyException 65 | * @throws ProcessRunTimeException 66 | */ 67 | public function fromPfx(string $pfxPath, string $password, bool $usePathEnv = false): self 68 | { 69 | if (!Str::of($pfxPath)->lower()->endsWith('.pfx')) { 70 | throw new InvalidPFXException($pfxPath); 71 | } 72 | 73 | if (!File::exists($pfxPath)) { 74 | throw new FileNotFoundException($pfxPath); 75 | } 76 | 77 | $this->password = $password; 78 | $output = a1TempDir(true, '.crt'); 79 | $shellPfxPath = escapeshellarg($pfxPath); 80 | $shellOutput = escapeshellarg($output); 81 | $shellArgPassword = escapeshellarg($password); 82 | $legacyFlag = $this->isLegacy ? self::LEGACY_FLAG : ''; 83 | $openSslCommand = "openssl pkcs12 -in {$shellPfxPath} -out {$shellOutput} -nodes -password pass:{$shellArgPassword} {$legacyFlag}"; 84 | 85 | runCliCommandProcesses($openSslCommand, $usePathEnv); 86 | 87 | if (!File::exists($output)) { 88 | throw new CertificateOutputNotFoundException; 89 | } 90 | 91 | $content = File::get($output); 92 | 93 | $filesToBeDelete = [$output]; 94 | 95 | !$this->preservePfx && ($filesToBeDelete[] = $pfxPath); 96 | 97 | File::delete($filesToBeDelete); 98 | 99 | return $this->setCertContent($content); 100 | } 101 | 102 | /** 103 | * @throws CertificateOutputNotFoundException 104 | * @throws FileNotFoundException 105 | * @throws InvalidCertificateContentException 106 | * @throws InvalidPFXException 107 | * @throws Invalidx509PrivateKeyException 108 | * @throws ProcessRunTimeException 109 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 110 | */ 111 | public function fromUpload(UploadedFile $uploadedPfx, string $password, bool $usePathEnv = false): self 112 | { 113 | $pfxTemp = a1TempDir(true); 114 | 115 | if (File::exists($pfxTemp)) { 116 | $pfxTemp = microtime() . $pfxTemp; 117 | } 118 | 119 | File::put($pfxTemp, $uploadedPfx->get()); 120 | 121 | $this->fromPfx($pfxTemp, $password, $usePathEnv); 122 | 123 | File::delete($pfxTemp); 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * @throws InvalidCertificateContentException 130 | * @throws Invalidx509PrivateKeyException 131 | */ 132 | public function setCertContent(string $certContent): self 133 | { 134 | $this->originalCertContent = $certContent; 135 | $this->certContent = openssl_x509_read(certificate: $certContent); 136 | $this->parsedData = openssl_x509_parse(certificate: $this->certContent, short_names: false); 137 | $this->validate(); 138 | return $this; 139 | } 140 | 141 | /** 142 | * @throws InvalidCertificateContentException 143 | * @throws Invalidx509PrivateKeyException 144 | */ 145 | public function validate(): void 146 | { 147 | if (!$this->certContent) { 148 | $this->invalidate(); 149 | throw new InvalidCertificateContentException; 150 | } 151 | 152 | if (!openssl_x509_check_private_key(certificate: $this->certContent, private_key: $this->originalCertContent)) { 153 | $this->invalidate(); 154 | throw new Invalidx509PrivateKeyException; 155 | } 156 | } 157 | 158 | private function invalidate(): void 159 | { 160 | $this->originalCertContent = ''; 161 | $this->certContent = false; 162 | $this->parsedData = []; 163 | $this->password = ''; 164 | } 165 | 166 | public function getCert(): CertificateProcessed 167 | { 168 | return new CertificateProcessed( 169 | original: $this->originalCertContent, 170 | openssl: $this->certContent, 171 | data: $this->parsedData, 172 | password: $this->password 173 | ); 174 | } 175 | 176 | public function getTempDir(): string 177 | { 178 | return $this->tempDir; 179 | } 180 | 181 | public function generateHashKey(): self 182 | { 183 | $this->hashKey = Encrypter::generateKey(self::CIPHER); 184 | $this->setEncrypter(); 185 | return $this; 186 | } 187 | 188 | public function setHashKey(string $hashKey): self 189 | { 190 | $this->hashKey = $hashKey; 191 | $this->setEncrypter(); 192 | return $this; 193 | } 194 | 195 | private function setEncrypter(): void 196 | { 197 | $this->encrypter = new Encrypter($this->hashKey, self::CIPHER); 198 | } 199 | 200 | public function getHashKey(): string 201 | { 202 | return $this->encrypter->getKey(); 203 | } 204 | 205 | public function getEncrypter(): Encrypter 206 | { 207 | return $this->encrypter; 208 | } 209 | 210 | /** 211 | * @throws EncryptException 212 | */ 213 | public function encryptBase64BlobString(string $blobString): string 214 | { 215 | return $this->encrypter->encryptString(base64_encode($blobString)); 216 | } 217 | 218 | /** 219 | * @throws DecryptException 220 | */ 221 | public function decryptBase64BlobString(string $encryptedBlobString): string 222 | { 223 | $string = $this->encrypter->decryptString($encryptedBlobString); 224 | return base64_decode($string); 225 | } 226 | 227 | /** 228 | * @throws CertificateOutputNotFoundException 229 | * @throws FileNotFoundException 230 | * @throws InvalidCertificateContentException 231 | * @throws InvalidPFXException 232 | * @throws Invalidx509PrivateKeyException 233 | * @throws ProcessRunTimeException 234 | */ 235 | public function makeDebugCertificate(bool $returnPathAndPass = false, bool $wrongPass = false): array|static 236 | { 237 | $pass = "example's password with special chars: $ & * ? \" '"; 238 | $shellArgPassword = escapeshellarg($pass); 239 | $name = $this->tempDir . Str::orderedUuid(); 240 | 241 | $genCommands = [ 242 | "openssl req -x509 -newkey rsa:4096 -sha256 -keyout {$name}.key -out {$name}.crt -subj \"/CN=Test Certificate /OU=LucasNepomuceno\" -days 600 -passout pass:{$shellArgPassword}", 243 | "openssl pkcs12 -export -name test.com -out {$name}.pfx -inkey {$name}.key -in {$name}.crt -passin pass:{$shellArgPassword} -passout pass:{$shellArgPassword}" 244 | ]; 245 | 246 | foreach ($genCommands as $command) { 247 | runCliCommandProcesses($command); 248 | } 249 | 250 | File::delete(["{$name}.key", "{$name}.crt"]); 251 | 252 | if ($returnPathAndPass) { 253 | return ["{$name}.pfx", $pass]; 254 | } 255 | 256 | return $this->fromPfx("{$name}.pfx", $wrongPass ? 'wrongPass' : $pass); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/Sign/SealImage.php: -------------------------------------------------------------------------------- 1 | setImageDriver($imageDriver); 38 | } 39 | 40 | public static function fromCert( 41 | ManageCert $cert, 42 | string $fontSize = self::FONT_SIZE_LARGE, 43 | bool $showDueDate = false, 44 | string $dueDateFormat = 'd/m/Y H:i:s' 45 | ): string 46 | { 47 | $subject = new Fluent($cert->getCert()->data['subject']); 48 | $firstLine = $subject->commonName ?? $subject->organizationName; 49 | $issuer = new Fluent($cert->getCert()->data['issuer']); 50 | $secondLine = $issuer->organizationalUnitName ?? $issuer->commonName ?? $issuer->organizationName; 51 | 52 | $certDueDate = $showDueDate 53 | ? now() 54 | ->createFromTimestamp( 55 | $cert->getCert()->data['validTo_time_t'] 56 | )->format($dueDateFormat) 57 | : null; 58 | 59 | $callback = function ($font) use ($fontSize) { 60 | $font->file(dirname(__DIR__) . '/Resources/font/Roboto-Medium.ttf'); 61 | 62 | $size = match ($fontSize) { 63 | self::FONT_SIZE_SMALL => 15, 64 | self::FONT_SIZE_MEDIUM => 20, 65 | default => 28 66 | }; 67 | 68 | $font->size($size); 69 | $font->color('#16A085'); 70 | }; 71 | 72 | $selfObj = new static; 73 | 74 | return $selfObj 75 | ->setImagePath() 76 | ->addTextField( 77 | text: $selfObj->breakText($firstLine ?? $secondLine ?? '', $fontSize), 78 | textX: 160, 79 | textY: 80, 80 | callback: $callback 81 | ) 82 | ->addTextField( 83 | text: $selfObj->breakText($firstLine ? $secondLine : '', $fontSize), 84 | textX: 160, 85 | textY: 150, 86 | callback: $callback 87 | ) 88 | ->addTextField( 89 | text: $certDueDate ?? '', 90 | textX: 160, 91 | textY: 250, 92 | callback: $callback) 93 | ->generateImage(); 94 | } 95 | 96 | private function breakText(string $text, string $fontSize = self::FONT_SIZE_LARGE): string 97 | { 98 | $cropSize = match ($fontSize) { 99 | self::FONT_SIZE_SMALL => 60, 100 | self::FONT_SIZE_MEDIUM => 48, 101 | default => 35 102 | }; 103 | 104 | $this->previousTextBreakLine = strlen($text) >= $cropSize; 105 | 106 | if ($this->previousTextBreakLine) { 107 | $textSplit = str_split(string: $text, length: ($cropSize - 3)); 108 | $textSplit = array_map(callback: 'trim', array: $textSplit); 109 | $text = join(separator: PHP_EOL, array: $textSplit); 110 | } 111 | 112 | return $text; 113 | } 114 | 115 | /** 116 | * @throws InvalidImageDriverException 117 | */ 118 | public function setImageDriver(AbstractDriver $imageDriver): self 119 | { 120 | if (!in_array($imageDriver::class, [GDDriver::class, ImagickDriver::class])) { 121 | throw new InvalidImageDriverException($imageDriver::class); 122 | } 123 | 124 | $this->imageDriver = $imageDriver; 125 | 126 | return $this; 127 | } 128 | 129 | public function setImagePath(string $imagePathOrContent = null): self 130 | { 131 | $this->imagePathOrContent = $imagePathOrContent ?? dirname(__DIR__) . '/Resources/img/sign-seal.png'; 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * @link http://image.intervention.io/api/text 138 | */ 139 | public function addTextField( 140 | string $text, 141 | float $textX, 142 | float $textY, 143 | Closure $callback = null 144 | ): self 145 | { 146 | $newText = [ 147 | 'text' => $text, 148 | 'x' => $textX, 149 | 'y' => $textY, 150 | 'callback' => $callback ?? fn() => null 151 | ]; 152 | 153 | $this->textFieldsDefinitions[] = $newText; 154 | 155 | return $this; 156 | } 157 | 158 | /** 159 | * @throw \Intervention\Image\ImageManager\Exception\NotReadableException 160 | */ 161 | public function generateImage(string $returnType = self::RETURN_IMAGE_CONTENT): string 162 | { 163 | $image = new IMG(driver: $this->imageDriver); 164 | $image = $image->read($this->imagePathOrContent); 165 | 166 | foreach ($this->textFieldsDefinitions as $text) { 167 | ['text' => $text, 'x' => $x, 'y' => $y, 'callback' => $callback] = $text; 168 | $image->text($text, $x, $y, $callback); 169 | } 170 | 171 | if ($returnType === self::RETURN_IMAGE_CONTENT) { 172 | return $image->encode(encoder: new JpegEncoder)->toString(); 173 | } 174 | 175 | return $image->encode(encoder: new JpegEncoder)->toDataUri(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Sign/SignaturePdf.php: -------------------------------------------------------------------------------- 1 | cert = $cert; 60 | 61 | // Throws exception on invalidate certificate 62 | try { 63 | $this->cert->validate(); 64 | } catch (Throwable $th) { 65 | throw new $th; 66 | } 67 | 68 | $this->setFileName($fileName) 69 | ->setHasSignedSuffix($hasSignedSuffix) 70 | ->setSealImgOnEveryPages(false); 71 | 72 | $this->mode = $mode; 73 | $this->pdfPath = $pdfPath; 74 | $this->setPdf(); 75 | } 76 | 77 | public function setInfo( 78 | ?string $name = null, 79 | ?string $location = null, 80 | ?string $reason = null, 81 | ?string $contactInfo = null 82 | ): SignaturePdf 83 | { 84 | $info = []; 85 | $name && ($info['Name'] = $name); 86 | $location && ($info['Location'] = $location); 87 | $reason && ($info['Reason'] = $reason); 88 | $contactInfo && ($info['ContactInfo'] = $contactInfo); 89 | $this->info = $info; 90 | return $this; 91 | } 92 | 93 | public function getPdfInstance(): Fpdi 94 | { 95 | return $this->pdf; 96 | } 97 | 98 | public function setPdf( 99 | string $orientation = 'P', 100 | string $unit = 'mm', 101 | string $pageFormat = 'A4', 102 | bool $unicode = true, 103 | string $encoding = 'UTF-8' 104 | ): SignaturePdf 105 | { 106 | $this->pdf = new Fpdi($orientation, $unit, $pageFormat, $unicode, $encoding); 107 | return $this; 108 | } 109 | 110 | public function setImage( 111 | string $imagePath, 112 | float $pageX = 155, 113 | float $pageY = 250, 114 | float $imageW = 50, 115 | float $imageH = 0, 116 | int $page = -1 117 | ): SignaturePdf 118 | { 119 | $this->image = compact('imagePath', 'pageX', 'pageY', 'imageW', 'imageH', 'page'); 120 | return $this; 121 | } 122 | 123 | public function setSealImgOnEveryPages(bool $hasSealImgOnEveryPages = true): SignaturePdf 124 | { 125 | $this->hasSealImgOnEveryPages = $hasSealImgOnEveryPages; 126 | return $this; 127 | } 128 | 129 | public function setFileName(string $fileName): SignaturePdf 130 | { 131 | $ext = explode('.', $fileName); 132 | $ext = end($ext); 133 | $this->fileName = str_replace(".{$ext}", '', $fileName); 134 | return $this; 135 | } 136 | 137 | public function setHasSignedSuffix(bool $hasSignedSuffix): SignaturePdf 138 | { 139 | $this->hasSignedSuffix = $hasSignedSuffix; 140 | return $this; 141 | } 142 | 143 | private function implementSignatureImage(?int $currentPage = null): void 144 | { 145 | if ($this->image) { 146 | /** 147 | * @var string $imagePath 148 | * @var float|null $pageX 149 | * @var float|null $pageY 150 | * @var float $imageW 151 | * @var float $imageH 152 | * @var int $page 153 | * @see setImage() 154 | */ 155 | extract($this->image); 156 | $this->pdf->Image($imagePath, $pageX, $pageY, $imageW, $imageH, 'PNG'); 157 | $this->pdf->setSignatureAppearance($pageX, $pageY, $imageW, $imageH, $currentPage ?? $page); 158 | } 159 | } 160 | 161 | /** 162 | * @throws CrossReferenceException 163 | * @throws FilterException 164 | * @throws PdfParserException 165 | * @throws PdfTypeException 166 | * @throws PdfReaderException 167 | */ 168 | public function signature(): string|BinaryFileResponse 169 | { 170 | $pageCount = $this->pdf->setSourceFile($this->pdfPath); 171 | 172 | for ($i = 1; $i <= $pageCount; $i++) { 173 | $pageIndex = $this->pdf->importPage($i); 174 | $this->pdf->SetPrintHeader(false); 175 | $this->pdf->SetPrintFooter(false); 176 | 177 | $templateSize = $this->pdf->getTemplateSize($pageIndex); 178 | ['width' => $width, 'height' => $height] = $templateSize; 179 | 180 | $this->pdf->AddPage($width > $height ? 'L' : 'P', [$width, $height]); 181 | $this->pdf->useTemplate($pageIndex); 182 | 183 | $insertImageOnLastPage = !empty($this->image['page']) && $this->image['page'] === -1 && $i === $pageCount; 184 | if ($this->hasSealImgOnEveryPages || 185 | $i === ($this->image['page'] ?? 0) || 186 | $insertImageOnLastPage 187 | ) { 188 | $this->implementSignatureImage($i); 189 | } 190 | } 191 | 192 | $certificate = $this->cert->getCert()->original; 193 | $password = $this->cert->getCert()->password; 194 | 195 | $this->pdf->setSignature( 196 | $certificate, 197 | $certificate, 198 | $password, 199 | '', 200 | 3, 201 | $this->info, 202 | 'A' // Authorize certificate 203 | ); 204 | 205 | if (empty($this->fileName)) $this->fileName = Str::orderedUuid(); 206 | if ($this->hasSignedSuffix) $this->fileName .= '_signed'; 207 | 208 | $this->fileName .= '.pdf'; 209 | 210 | $output = "{$this->cert->getTempDir()}{$this->fileName}"; 211 | 212 | // Required to receive data from the server, such as timestamp and allocation hash. 213 | if (!File::exists($output)) File::put($output, $this->pdf->output($this->fileName, 'S')); 214 | 215 | switch ($this->mode) { 216 | case self::MODE_RESOURCE: 217 | $content = File::get($output); 218 | File::delete([$output]); 219 | return $content; 220 | break; 221 | 222 | case self::MODE_DOWNLOAD: 223 | default: 224 | return response()->download($output)->deleteFileAfterSend(); 225 | break; 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/Sign/ValidatePdfSignature.php: -------------------------------------------------------------------------------- 1 | setPdfPath($pdfPath) 24 | ->extractSignatureData() 25 | ->convertSignatureDataToPlainText() 26 | ->convertPlainTextToObject(); 27 | } 28 | 29 | /** 30 | * @throws FileNotFoundException 31 | * @throws InvalidPdfFileException 32 | */ 33 | private function setPdfPath(string $pdfPath): self 34 | { 35 | if (!Str::of($pdfPath)->lower()->endsWith('.pdf')) { 36 | throw new InvalidPdfFileException($pdfPath); 37 | } 38 | 39 | if (!File::exists($pdfPath)) { 40 | throw new FileNotFoundException($pdfPath); 41 | } 42 | 43 | $this->pdfPath = $pdfPath; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * @throws HasNoSignatureOrInvalidPkcs7Exception 50 | */ 51 | private function extractSignatureData(): self 52 | { 53 | $content = File::get($this->pdfPath); 54 | $regexp = '#ByteRange\[\s*(\d+) (\d+) (\d+)#'; // subexpressions are used to extract b and c 55 | $result = []; 56 | preg_match_all($regexp, $content, $result); 57 | 58 | // $result[2][0] and $result[3][0] are b and c 59 | if (!isset($result[2][0]) && !isset($result[3][0])) { 60 | throw new HasNoSignatureOrInvalidPkcs7Exception($this->pdfPath); 61 | } 62 | 63 | $start = $result[2][0]; 64 | $end = $result[3][0]; 65 | 66 | if ($stream = fopen($this->pdfPath, 'rb')) { 67 | $signature = stream_get_contents($stream, $end - $start - 2, $start + 1); // because we need to exclude < and > from start and end 68 | fclose($stream); 69 | $this->pkcs7Path = a1TempDir(tempFile: true, fileExt: '.pkcs7'); 70 | File::put($this->pkcs7Path, hex2bin($signature)); 71 | } 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * @throws FileNotFoundException 78 | * @throws HasNoSignatureOrInvalidPkcs7Exception 79 | * @throws ProcessRunTimeException 80 | */ 81 | private function convertSignatureDataToPlainText(): self 82 | { 83 | if (!$this->pkcs7Path) { 84 | throw new HasNoSignatureOrInvalidPkcs7Exception($this->pdfPath); 85 | } 86 | 87 | $output = a1TempDir(tempFile: true, fileExt: '.txt'); 88 | $openSslCommand = "openssl pkcs7 -in {$this->pkcs7Path} -inform DER -print_certs > {$output}"; 89 | 90 | runCliCommandProcesses($openSslCommand); 91 | 92 | if (!File::exists($output)) { 93 | throw new FileNotFoundException($output); 94 | } 95 | 96 | $this->plainTextContent = File::get($output); 97 | 98 | File::delete([$output, $this->pkcs7Path]); 99 | 100 | return $this; 101 | } 102 | 103 | private function convertPlainTextToObject(): ValidatedSignedPDF 104 | { 105 | $finalContent = []; 106 | $delimiter = '|CROP|'; 107 | $content = $this->plainTextContent; 108 | $content = preg_replace('/(-----BEGIN .+?-----(?s).+?-----END .+?-----)/mi', $delimiter, $content); 109 | $content = preg_replace('/(\s\s+|\\n|\\r)/', ' ', $content); 110 | $content = array_filter(explode($delimiter, $content), 'trim'); 111 | $content = (array)array_map(fn($data) => $this->processDataToInfo($data), $content)[0]; 112 | 113 | foreach ($content as $value) { 114 | $val = $value[key($value)]; 115 | $key = &$finalContent[key($value)]; 116 | 117 | !in_array($val, ($key ?? [])) && ($key[] = $val); 118 | } 119 | 120 | $finalContent['validated'] = !!count(array_intersect_key(array_flip(['OU', 'CN']), $finalContent)); 121 | return new ValidatedSignedPDF($finalContent['validated'], Arr::except($finalContent, 'validated')); 122 | } 123 | 124 | private function processDataToInfo(string $data): array 125 | { 126 | /** it allows to split by "," except when "," inside of quoutes */ 127 | $data = preg_split('/\s*,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)/', trim($data)); 128 | 129 | $finalData = []; 130 | 131 | foreach ($data as $info) { 132 | $infoTemp = explode(' = ', trim($info)); 133 | if (isset($infoTemp[0]) && $infoTemp[1]) { 134 | $finalData[] = [$infoTemp[0] => $infoTemp[1]]; 135 | } 136 | } 137 | return $finalData; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Temp/.gitkeep: -------------------------------------------------------------------------------- 1 | ~ -------------------------------------------------------------------------------- /tests/CommandsTest.php: -------------------------------------------------------------------------------- 1 | makeDebugCertificate(true); 28 | $pdfPath = __DIR__ . '/Resources/test.pdf'; 29 | $fileName = a1TempDir(true, '.pdf'); 30 | $parameters = [ 31 | 'pdfPath' => $pdfPath, 32 | 'pfxPath' => $pfxPath, 33 | 'password' => $pass, 34 | 'fileName' => $fileName 35 | ]; 36 | 37 | $this->artisan('pdf:sign', $parameters) 38 | ->assertSuccessful() 39 | ->expectsOutput('Your PDF file is being signed!') 40 | ->expectsOutput("Your file has been signed and is available at: \"{$fileName}\""); 41 | } 42 | 43 | public function testWhenTheSignatureCommandDoesNotFinishSuccessfully() 44 | { 45 | $parameters = [ 46 | 'pdfPath' => a1TempDir(true, '.pdf'), 47 | 'pfxPath' => a1TempDir(true, '.pfx'), 48 | 'password' => Str::random(32), 49 | 'fileName' => a1TempDir(true, '.pdf') 50 | ]; 51 | 52 | $this->artisan('pdf:sign', $parameters) 53 | // ->assertFailed() 54 | ->expectsOutput('Your PDF file is being signed!') 55 | ->expectsOutputToContain('Could not sign your file, error occurred:'); 56 | 57 | } 58 | 59 | /** 60 | * @throws FileNotFoundException 61 | * @throws ProcessRunTimeException 62 | * @throws Invalidx509PrivateKeyException 63 | * @throws Throwable 64 | * @throws InvalidCertificateContentException 65 | * @throws CertificateOutputNotFoundException 66 | * @throws InvalidPFXException 67 | */ 68 | public function testWhenASignedPdfIsSuccessfullyValidated() 69 | { 70 | $cert = new ManageCert; 71 | list($pfxPath, $pass) = $cert->makeDebugCertificate(true); 72 | 73 | $signed = signPdfFromFile($pfxPath, $pass, __DIR__ . '/Resources/test.pdf'); 74 | $pdfPath = a1TempDir(true, '.pdf'); 75 | 76 | File::put($pdfPath, $signed); 77 | $fileExists = File::exists($pdfPath); 78 | 79 | $this->assertTrue($fileExists); 80 | 81 | $parameters = [ 82 | 'pdfPath' => $pdfPath 83 | ]; 84 | 85 | $this->artisan('pdf:validate-signature', $parameters) 86 | ->assertSuccessful() 87 | ->expectsOutput('Your PDF document is being validated.') 88 | ->expectsOutput('Your PDF document is VALID'); 89 | } 90 | 91 | public function testWhenAnUnsignedDocumentThrowsAnErrorWhenRunningAValidationCommand() 92 | { 93 | $pdfPath = __DIR__ . '/Resources/test.pdf'; 94 | $parameters = [ 95 | 'pdfPath' => $pdfPath 96 | ]; 97 | 98 | $this->artisan('pdf:validate-signature', $parameters) 99 | ->assertFailed() 100 | ->expectsOutput('Your PDF document is being validated.') 101 | ->expectsOutputToContain('Unable to validate your file signature, an error occurred:'); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/HelpersTest.php: -------------------------------------------------------------------------------- 1 | makeDebugCertificate(true); 32 | 33 | $signed = signPdfFromFile($pfxPath, $pass, __DIR__ . '/Resources/test.pdf'); 34 | $pdfPath = a1TempDir(true, '.pdf'); 35 | 36 | File::put($pdfPath, $signed); 37 | $fileExists = File::exists($pdfPath); 38 | 39 | $this->assertTrue($fileExists); 40 | File::delete([$pfxPath, $pdfPath]); 41 | } 42 | 43 | /** 44 | * @throws FileNotFoundException 45 | * @throws ProcessRunTimeException 46 | * @throws Invalidx509PrivateKeyException 47 | * @throws Throwable 48 | * @throws InvalidCertificateContentException 49 | * @throws InvalidPFXException 50 | * @throws CertificateOutputNotFoundException 51 | */ 52 | public function testWhenAFileIsSignedByTheSignPdfFromFileHelperUsingPathEnv() 53 | { 54 | $cert = new ManageCert; 55 | list($pfxPath, $pass) = $cert->makeDebugCertificate(true); 56 | 57 | $signed = signPdfFromFile( 58 | pfxPath: $pfxPath, 59 | password: $pass, 60 | pdfPath: __DIR__ . '/Resources/test.pdf', 61 | usePathEnv: true 62 | ); 63 | $pdfPath = a1TempDir(true, '.pdf'); 64 | 65 | File::put($pdfPath, $signed); 66 | $fileExists = File::exists($pdfPath); 67 | 68 | $this->assertTrue($fileExists); 69 | File::delete([$pfxPath, $pdfPath]); 70 | } 71 | 72 | /** 73 | * @throws FileNotFoundException 74 | * @throws ProcessRunTimeException 75 | * @throws Invalidx509PrivateKeyException 76 | * @throws Throwable 77 | * @throws InvalidCertificateContentException 78 | * @throws CertificateOutputNotFoundException 79 | * @throws InvalidPFXException 80 | */ 81 | public function testWhenAFileIsSignedByTheSignPdfFromUploadHelper() 82 | { 83 | $cert = new ManageCert; 84 | list($pfxPath, $pass) = $cert->makeDebugCertificate(true); 85 | 86 | $uploadedFile = new UploadedFile($pfxPath, 'testCertificate.pfx', null, null, true); 87 | $signed = signPdfFromUpload($uploadedFile, $pass, __DIR__ . '/Resources/test.pdf'); 88 | $pdfPath = a1TempDir(true, '.pdf'); 89 | 90 | File::put($pdfPath, $signed); 91 | $fileExists = File::exists($pdfPath); 92 | 93 | $this->assertTrue($fileExists); 94 | File::delete([$pfxPath, $pdfPath]); 95 | } 96 | 97 | /** 98 | * @throws FileNotFoundException 99 | * @throws ProcessRunTimeException 100 | * @throws Invalidx509PrivateKeyException 101 | * @throws Throwable 102 | * @throws InvalidCertificateContentException 103 | * @throws CertificateOutputNotFoundException 104 | * @throws InvalidPFXException 105 | */ 106 | public function testWhenAFileIsSignedByTheSignPdfFromUploadHelperUsingPathEnv() 107 | { 108 | $cert = new ManageCert; 109 | list($pfxPath, $pass) = $cert->makeDebugCertificate(true); 110 | 111 | $uploadedFile = new UploadedFile($pfxPath, 'testCertificate.pfx', null, null, true); 112 | $signed = signPdfFromUpload( 113 | uploadedPfx: $uploadedFile, 114 | password: $pass, 115 | pdfPath: __DIR__ . '/Resources/test.pdf', 116 | usePathEnv: true 117 | ); 118 | $pdfPath = a1TempDir(true, '.pdf'); 119 | 120 | File::put($pdfPath, $signed); 121 | $fileExists = File::exists($pdfPath); 122 | 123 | $this->assertTrue($fileExists); 124 | } 125 | 126 | /** 127 | * @throws FileNotFoundException 128 | * @throws ProcessRunTimeException 129 | * @throws Invalidx509PrivateKeyException 130 | * @throws Throwable 131 | * @throws InvalidCertificateContentException 132 | * @throws CertificateOutputNotFoundException 133 | * @throws InvalidPFXException 134 | */ 135 | public function testWhenCertificateDataIsEncrypted() 136 | { 137 | $cert = new ManageCert; 138 | list($pfxPath, $pass) = $cert->makeDebugCertificate(true); 139 | 140 | $encryptedData = encryptCertData($pfxPath, $pass); 141 | 142 | foreach (['certificate', 'password', 'hash'] as $key) { 143 | $this->assertArrayHasKey($key, $encryptedData->toArray()); 144 | } 145 | } 146 | 147 | public function testWhenTheA1TempDirHelperCreatesTheFilesCorrectly() 148 | { 149 | $this->assertTrue( 150 | File::isDirectory(a1TempDir()) 151 | ); 152 | 153 | $this->assertTrue( 154 | Str::endsWith(a1TempDir(true), '.pfx') 155 | ); 156 | 157 | $this->assertTrue( 158 | Str::endsWith( 159 | a1TempDir(true, '.pdf'), 160 | '.pdf' 161 | ) 162 | ); 163 | } 164 | 165 | /** 166 | * @throws FileNotFoundException 167 | * @throws ProcessRunTimeException 168 | * @throws Invalidx509PrivateKeyException 169 | * @throws InvalidCertificateContentException 170 | * @throws Throwable 171 | * @throws CertificateOutputNotFoundException 172 | * @throws InvalidPFXException 173 | */ 174 | public function testWhenASignedPdfFileIsCorrectlyValidatedByTheValidatePdfSignatureHelper() 175 | { 176 | $cert = new ManageCert; 177 | list($pfxPath, $pass) = $cert->makeDebugCertificate(true); 178 | 179 | $signed = signPdfFromFile($pfxPath, $pass, __DIR__ . '/Resources/test.pdf'); 180 | $pdfPath = a1TempDir(true, '.pdf'); 181 | 182 | File::put($pdfPath, $signed); 183 | $fileExists = File::exists($pdfPath); 184 | 185 | $this->assertTrue($fileExists); 186 | 187 | $validation = validatePdfSignature($pdfPath); 188 | $this->assertTrue($validation->isValidated); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /tests/ManageCertTest.php: -------------------------------------------------------------------------------- 1 | makeDebugCertificate(); 32 | 33 | $this->assertInstanceOf(CertificateProcessed::class, $cert->getCert()); 34 | 35 | foreach (['original', 'openssl', 'data', 'password'] as $key) { 36 | $this->assertArrayHasKey($key, $cert->getCert()->toArray()); 37 | } 38 | 39 | $this->assertStringContainsStringIgnoringCase('BEGIN CERTIFICATE', $cert->getCert()->original); 40 | 41 | is_object($cert->getCert()->openssl) 42 | ? $this->assertInstanceOf(OpenSSLCertificate::class, $cert->getCert()->openssl) 43 | : $this->assertIsResource($cert->getCert()->openssl); 44 | 45 | $this->assertIsArray($cert->getCert()->data); 46 | $this->assertArrayHasKey('validTo_time_t', $cert->getCert()->data); //important field 47 | $this->assertNotNull($cert->getCert()->password); 48 | } 49 | 50 | /** 51 | * @throws InvalidCertificateContentException 52 | * @throws InvalidPFXException 53 | * @throws CertificateOutputNotFoundException 54 | * @throws ProcessRunTimeException 55 | * @throws Invalidx509PrivateKeyException 56 | */ 57 | public function testValidateNotFoundPfxFileException() 58 | { 59 | $this->expectException(FileNotFoundException::class); 60 | 61 | $cert = new ManageCert; 62 | $cert->fromPfx('imaginary/path/to/file.pfx', '12345'); 63 | } 64 | 65 | /** 66 | * @throws FileNotFoundException 67 | * @throws InvalidCertificateContentException 68 | * @throws CertificateOutputNotFoundException 69 | * @throws ProcessRunTimeException 70 | * @throws Invalidx509PrivateKeyException 71 | */ 72 | public function testValidatePfxFileExtensionException() 73 | { 74 | $this->expectException(InvalidPFXException::class); 75 | 76 | $cert = new ManageCert; 77 | $cert->fromPfx('imaginary/path/to/file.pfz', '12345'); 78 | } 79 | 80 | /** 81 | * @throws FileNotFoundException 82 | * @throws InvalidCertificateContentException 83 | * @throws CertificateOutputNotFoundException 84 | * @throws InvalidPFXException 85 | * @throws ProcessRunTimeException 86 | * @throws Invalidx509PrivateKeyException 87 | */ 88 | public function testValidateEncryperInstanceAndResources() 89 | { 90 | $cert = new ManageCert; 91 | $cert->makeDebugCertificate(); 92 | 93 | $this->assertInstanceOf(Encrypter::class, $cert->getEncrypter()); 94 | $this->assertTrue( 95 | $cert->getEncrypter()->supported($cert->getHashKey(), $cert::CIPHER) 96 | ); 97 | } 98 | 99 | /** 100 | * @throws FileNotFoundException 101 | * @throws InvalidCertificateContentException 102 | * @throws InvalidPFXException 103 | * @throws CertificateOutputNotFoundException 104 | * @throws Invalidx509PrivateKeyException 105 | */ 106 | public function testValidateProcessRunTimeException() 107 | { 108 | $this->expectException(ProcessRunTimeException::class); 109 | 110 | $cert = new ManageCert; 111 | $cert->makeDebugCertificate(false, true); 112 | } 113 | 114 | /** 115 | * @throws FileNotFoundException 116 | * @throws ProcessRunTimeException 117 | * @throws Invalidx509PrivateKeyException 118 | * @throws InvalidCertificateContentException 119 | * @throws InvalidPFXException 120 | * @throws CertificateOutputNotFoundException 121 | */ 122 | public function testValidatesIfThePfxFileWillBeDeletedAfterBeingPreserved() 123 | { 124 | $cert = new ManageCert; 125 | list($pfxPath, $pass) = $cert->makeDebugCertificate(true); 126 | 127 | $cert->setPreservePfx()->fromPfx($pfxPath, $pass); 128 | 129 | $this->assertTrue(File::exists($pfxPath)); 130 | $this->assertTrue(File::delete($pfxPath)); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Resources/.gitignore: -------------------------------------------------------------------------------- 1 | !*.pdf -------------------------------------------------------------------------------- /tests/Resources/CertInfoExamples/with-comma.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "subject=CN": "NAME" }, 3 | { "name": "NAME" }, 4 | { "O": "NAME" }, 5 | { "x500UniqueIdentifier": "AAA010101AAA" }, 6 | { "serialNumber": "AAA010101AAAA123" }, 7 | { "OU": "A NAME issuer=CN" }, 8 | { "O": "SERVICIO DE ADMINISTRACION TRIBUTARIA" }, 9 | { "OU": "SAT-IES Authority" }, 10 | { "emailAddress": "contacto.tecnico@example.com" }, 11 | { "street": "\"AV. HIDALGO 77, COL. GUERRERO\"" }, 12 | { "postalCode": "06300" }, 13 | { "C": "MX" }, 14 | { "ST": "CIUDAD DE MEXICO" }, 15 | { "L": "CUAUHTEMOC" }, 16 | { "x500UniqueIdentifier": "XXX010101XXX" }, 17 | { 18 | "unstructuredName": "responsable: ADMINISTRACION CENTRAL DE SERVICIOS TRIBUTARIOS AL CONTRIBUYENTE" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /tests/Resources/CertInfoExamples/with-comma.txt: -------------------------------------------------------------------------------- 1 | subject=CN = NAME, name = NAME , O = NAME, x500UniqueIdentifier = AAA010101AAA, serialNumber = AAA010101AAAA123, OU = A NAME issuer=CN = AUTORIDAD CERTIFICADORA, O = SERVICIO DE ADMINISTRACION TRIBUTARIA, OU = SAT-IES Authority, emailAddress = contacto.tecnico@example.com, street = "AV. HIDALGO 77, COL. GUERRERO", postalCode = 06300, C = MX, ST = CIUDAD DE MEXICO, L = CUAUHTEMOC, x500UniqueIdentifier = XXX010101XXX, unstructuredName = responsable: ADMINISTRACION CENTRAL DE SERVICIOS TRIBUTARIOS AL CONTRIBUYENTE -------------------------------------------------------------------------------- /tests/Resources/CertInfoExamples/without-comma.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "subject=CN": "ESCUELA KEMPER URGATE SA DE CV" }, 3 | { "name": "ESCUELA KEMPER URGATE SA DE CV" }, 4 | { "O": "ESCUELA KEMPER URGATE SA DE CV" }, 5 | { "x500UniqueIdentifier": "EKU9003173C9 / VADA800927DJ3" }, 6 | { "serialNumber": "\" / VADA800927HSRSRL05\"" }, 7 | { "OU": "Sucursal 1 issuer=CN" }, 8 | { "O": "SERVICIO DE ADMINISTRACION TRIBUTARIA" }, 9 | { "OU": "SAT-IES Authority" }, 10 | { "emailAddress": "oscar.martinez@sat.gob.mx" }, 11 | { "street": "3ra cerrada de caliz" }, 12 | { "postalCode": "06370" }, 13 | { "C": "MX" }, 14 | { "ST": "CIUDAD DE MEXICO" }, 15 | { "L": "COYOACAN" }, 16 | { "x500UniqueIdentifier": "2.5.4.45" }, 17 | { "unstructuredName": "responsable: ACDMA-SAT" } 18 | ] 19 | -------------------------------------------------------------------------------- /tests/Resources/CertInfoExamples/without-comma.txt: -------------------------------------------------------------------------------- 1 | subject=CN = ESCUELA KEMPER URGATE SA DE CV, name = ESCUELA KEMPER URGATE SA DE CV, O = ESCUELA KEMPER URGATE SA DE CV, x500UniqueIdentifier = EKU9003173C9 / VADA800927DJ3, serialNumber = " / VADA800927HSRSRL05", OU = Sucursal 1 issuer=CN = AC UAT, O = SERVICIO DE ADMINISTRACION TRIBUTARIA, OU = SAT-IES Authority, emailAddress = oscar.martinez@sat.gob.mx, street = 3ra cerrada de caliz, postalCode = 06370, C = MX, ST = CIUDAD DE MEXICO, L = COYOACAN, x500UniqueIdentifier = 2.5.4.45, unstructuredName = responsable: ACDMA-SAT -------------------------------------------------------------------------------- /tests/Resources/test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsnepomuceno/laravel-a1-pdf-sign/bdc396dac6f45966a4532b70ae15b7a62643be6f/tests/Resources/test.pdf -------------------------------------------------------------------------------- /tests/SealImageTest.php: -------------------------------------------------------------------------------- 1 | makeDebugCertificate(); 35 | 36 | $image = SealImage::fromCert($cert); 37 | 38 | $interventionImg = new IMG(driver: new GDDriver); 39 | $interventionImg = $interventionImg->read($image); 40 | 41 | $this->assertEqualsIgnoringCase('image/png', $interventionImg->toPng()->mediaType()); 42 | $this->assertEquals(590, $interventionImg->width()); 43 | $this->assertEquals(295, $interventionImg->height()); 44 | } 45 | 46 | /** 47 | * @throws CertificateOutputNotFoundException 48 | * @throws FileNotFoundException 49 | * @throws InvalidCertificateContentException 50 | * @throws InvalidPFXException 51 | * @throws Invalidx509PrivateKeyException 52 | * @throws ProcessRunTimeException 53 | * @throws InvalidPdfSignModeTypeException 54 | * @throws Throwable 55 | * @throw \Intervention\Image\ImageManager\Exception\NotReadableException 56 | */ 57 | public function testInsertSealImageOnPdfFile() 58 | { 59 | $cert = new ManageCert; 60 | $cert->makeDebugCertificate(); 61 | 62 | $image = SealImage::fromCert($cert); 63 | $imagePath = a1TempDir(true, '.png'); 64 | File::put($imagePath, $image); 65 | $this->assertTrue(File::exists($imagePath)); 66 | 67 | $pdfPath = a1TempDir(true, '.pdf'); 68 | try { 69 | $pdf = new SignaturePdf(__DIR__ . '/Resources/test.pdf', $cert); 70 | $resource = $pdf->setImage($imagePath)->signature(); 71 | File::put($pdfPath, $resource); 72 | } catch (Throwable $e) { 73 | throw new $e; 74 | } 75 | 76 | $this->assertTrue(File::exists($pdfPath)); 77 | 78 | $validation = validatePdfSignature($pdfPath); 79 | $this->assertTrue($validation->isValidated); 80 | 81 | File::delete([$imagePath, $pdfPath]); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Sign/ValidatePdfSignature/ProcessDataInfoFunctionTest.php: -------------------------------------------------------------------------------- 1 | > $expectedResponse 35 | * 36 | * @return void 37 | */ 38 | public function testProcessDataToInfoFunction(array $expectedResponse, string $content) 39 | { 40 | $method = new ReflectionMethod( 41 | '\LSNepomuceno\LaravelA1PdfSign\Sign\ValidatePdfSignature', 42 | 'processDataToInfo' 43 | ); 44 | $method->setAccessible(true); 45 | 46 | $data = $method->invoke(new ValidatePdfSignature(), $content); 47 | $this->assertSame($expectedResponse, $data); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | getFilename() !== '.gitkeep') { 19 | File::delete($file->getPathname()); 20 | } 21 | } 22 | } 23 | parent::tearDown(); 24 | } 25 | 26 | protected function getPackageProviders($app): array 27 | { 28 | return [ 29 | LaravelA1PdfSignServiceProvider::class, 30 | ]; 31 | } 32 | } 33 | --------------------------------------------------------------------------------