├── .devcontainer
└── devcontainer.json
├── LICENSE
├── README.md
├── composer.json
└── src
├── Certificate
└── AppleWWDRCA.pem
├── FinanceOrder.php
├── PKPass.php
└── PKPassException.php
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "image": "mcr.microsoft.com/devcontainers/universal:2",
3 | "features": {
4 | "ghcr.io/shyim/devcontainers-features/php:0": {}
5 | }
6 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Includable
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 | # PHP library to create passes for iOS Wallet
2 |
3 | [](https://packagist.org/packages/pkpass/pkpass)
4 | [](https://packagist.org/packages/pkpass/pkpass)
5 | [](LICENSE)
6 |
7 | This class provides the functionality to create passes for Wallet in Apple's iOS. It creates,
8 | signs and packages the pass as a `.pkpass` file according to Apple's documentation.
9 |
10 | ## Requirements
11 |
12 | - PHP 7.0 or higher (may also work with older versions)
13 | - PHP [ZIP extension](http://php.net/manual/en/book.zip.php) (often installed by default)
14 | - Access to filesystem to write temporary cache files
15 |
16 | ## Installation
17 |
18 | Simply run the following command in your project's root directory to install via [Composer](https://getcomposer.org/):
19 |
20 | ```
21 | composer require pkpass/pkpass
22 | ```
23 |
24 | Or add to your composer.json: `"pkpass/pkpass": "^2.0.0"`
25 |
26 | ## Usage
27 |
28 | Please take a look at the [examples/example.php](examples/example.php) file for example usage. For more info on the JSON for the pass and how to
29 | style it, take a look at the [docs at developers.apple.com](https://developer.apple.com/library/ios/documentation/UserExperience/Reference/PassKit_Bundle/Chapters/Introduction.html).
30 |
31 | ### Included demos
32 |
33 | - 📦 [Simple example](examples/example.php)
34 | - ✈️ [Flight ticket example](examples/full_sample/)
35 | - ☕️ [Starbucks card example](examples/starbucks_sample/)
36 |
37 | ### Functions to add files
38 |
39 | - `addFile` : add a file without locale like `icon.png`
40 | - `addRemoteFile` : add a file from a url without locale like `https://xyz.io/icon.png`
41 | - `addLocaleFile` : add a localized file like `strip.png`
42 | - `addLocaleRemoteFile` : add a localized file from a url like `https://xyz.io/strip.png`
43 |
44 | ## Requesting the Pass Certificate
45 |
46 | 1. Go to the [iOS Provisioning portal](https://developer.apple.com/account/ios/identifier/passTypeId).
47 | 2. Create a new Pass Type ID, and write down the Pass ID you choose, you'll need it later.
48 | 3. Click the edit button under your newly created Pass Type ID and generate a certificate according to the instructions
49 | shown on the page. Make sure _not_ to choose a name for the Certificate but keep it empty instead.
50 | 4. Download the .cer file and drag it into Keychain Access.
51 | 5. Choose to filter by **Certificates** in the top filter bar.
52 | 6. Find the certificate you just imported and click the triangle on the left to reveal the private key.
53 | 7. Select both the certificate and the private key it, then right-click the certificate in Keychain Access and
54 | choose `Export 2 items…`.
55 | 8. Choose a password and export the file to a folder.
56 |
57 | 
58 |
59 | ### Getting the example.php sample to work
60 |
61 | 1. Request the Pass certificate (`.p12`) as described above and upload it to your server.
62 | 2. Set the correct path and password on [line 22](examples/example.php#L22).
63 | 3. Change the `passTypeIdentifier` and `teamIndentifier` to the correct values on lines [29](examples/example.php#L29) and [31](examples/example.php#L31) (`teamIndentifier` can be found on the [Developer Portal](https://developer.apple.com/account/#/membership)).
64 |
65 | After completing these steps, you should be ready to go. Upload all the files to your server and navigate to the address
66 | of the examples/example.php file on your iPhone.
67 |
68 | ## Debugging
69 |
70 | ### Using the Console app
71 |
72 | If you aren't able to open your pass on an iPhone, plug the iPhone into a Mac and open the 'Console' application. On the left, you can select your iPhone. You will then be able to inspect any errors that occur while adding the pass:
73 |
74 | 
75 |
76 | - `Trust evaluate failure: [leaf TemporalValidity]`: If you see this error, your pass was signed with an outdated certificate.
77 | - `Trust evaluate failure: [leaf LeafMarkerOid]`: You did not leave the name of the certificate empty while creating it in the developer portal.
78 |
79 | ### OpenSSL errors
80 |
81 | When you get the error 'Could not read certificate file', this might be related to using an OpenSSL version that has deprecated some older hashes - [more info here](https://schof.link/2Et6z3m).
82 |
83 | There may be no need to configure OpenSSL to use legacy algorithms. It's easier and more portable just to convert the encrypted certificates file. The steps below use a .p12 file but it should work to swap these commands for a .pfx file.
84 |
85 | Instructions:
86 |
87 | 1. `openssl pkcs12 -legacy -in key.p12 -nodes -out key_decrypted.tmp` (replace key.p12 with your .p12 file name).
88 | 2. `openssl pkcs12 -in key_decrypted.tmp -export -out key_new.p12 -certpbe AES-256-CBC -keypbe AES-256-CBC -iter 2048` (use the newly generated key_new.p12 file in your pass generation below)
89 |
90 | The `key_new.p12` file should now be compatible with OpenSSL v3+.
91 |
92 | ## Changelog
93 |
94 | **Version 2.3.2 - September 2024**
95 |
96 | - Fix order mime type, add better error reporting.
97 |
98 | **Version 2.3.1 - March 2024**
99 |
100 | - Chore: add gitattributes.
101 |
102 | **Version 2.3.0 - February 2024**
103 |
104 | - Add support for Wallet Orders.
105 |
106 | **Version 2.2.0 - December 2023**
107 |
108 | - Update default WWDR certificate to G4.
109 |
110 | **Version 2.1.0 - April 2023**
111 |
112 | - Add alternative method for extracting P12 contents to circumvent issues in recent versions of OpenSSL.
113 |
114 | **Version 2.0.2 - October 2022**
115 |
116 | - Switch to `ZipArchive::OVERWRITE` method of opening ZIP due to PHP 8 deprecation ([#120](https://github.com/includable/php-pkpass/pull/120)).
117 |
118 | **Version 2.0.1 - October 2022**
119 |
120 | - Update WWDR certificate to v6 ([#118](https://github.com/includable/php-pkpass/issues/118)).
121 |
122 | **Version 2.0.0 - September 2022**
123 |
124 | - Changed signature of constructor to take out third `$json` parameter.
125 | - Remove deprecated `setJSON()` method.
126 | - Removed `checkError()` and `getError()` methods in favor of exceptions.
127 |
128 | ## Support & documentation
129 |
130 | Please read the instructions above and consult the [Wallet Documentation](https://developer.apple.com/wallet/) before
131 | submitting tickets or requesting support. It might also be worth
132 | to [check Stackoverflow](http://stackoverflow.com/search?q=%22PHP-PKPass%22), which contains quite a few questions about
133 | this library.
134 |
135 |
136 |
137 | ---
138 |
139 |
148 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pkpass/pkpass",
3 | "license": "MIT",
4 | "description": "PHP PKPass class for iOS Wallet",
5 | "keywords": [
6 | "PHP",
7 | "apple",
8 | "ios",
9 | "iphone",
10 | "ipad",
11 | "passbook",
12 | "wallet"
13 | ],
14 | "homepage": "https://github.com/includable/php-pkpass",
15 | "type": "library",
16 | "authors": [
17 | {
18 | "name": "Thomas Schoffelen",
19 | "homepage": "https://schof.co/",
20 | "email": "thomas@includable.com"
21 | }
22 | ],
23 | "autoload": {
24 | "psr-4": {
25 | "PKPass\\": "src"
26 | }
27 | },
28 | "require": {
29 | "php": ">=7.0",
30 | "ext-zip": "*",
31 | "ext-json": "*",
32 | "ext-openssl": "*"
33 | },
34 | "require-dev": {
35 | "phpunit/phpunit": "^9.6"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Certificate/AppleWWDRCA.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEVTCCAz2gAwIBAgIUE9x3lVJx5T3GMujM/+Uh88zFztIwDQYJKoZIhvcNAQEL
3 | BQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsT
4 | HUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBS
5 | b290IENBMB4XDTIwMTIxNjE5MzYwNFoXDTMwMTIxMDAwMDAwMFowdTFEMEIGA1UE
6 | Aww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNh
7 | dGlvbiBBdXRob3JpdHkxCzAJBgNVBAsMAkc0MRMwEQYDVQQKDApBcHBsZSBJbmMu
8 | MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAf
9 | eKp6JzKwRl/nF3bYoJ0OKY6tPTKlxGs3yeRBkWq3eXFdDDQEYHX3rkOPR8SGHgjo
10 | v9Y5Ui8eZ/xx8YJtPH4GUnadLLzVQ+mxtLxAOnhRXVGhJeG+bJGdayFZGEHVD41t
11 | QSo5SiHgkJ9OE0/QjJoyuNdqkh4laqQyziIZhQVg3AJK8lrrd3kCfcCXVGySjnYB
12 | 5kaP5eYq+6KwrRitbTOFOCOL6oqW7Z+uZk+jDEAnbZXQYojZQykn/e2kv1MukBVl
13 | PNkuYmQzHWxq3Y4hqqRfFcYw7V/mjDaSlLfcOQIA+2SM1AyB8j/VNJeHdSbCb64D
14 | YyEMe9QbsWLFApy9/a8CAwEAAaOB7zCB7DASBgNVHRMBAf8ECDAGAQH/AgEAMB8G
15 | A1UdIwQYMBaAFCvQaUeUdgn+9GuNLkCm90dNfwheMEQGCCsGAQUFBwEBBDgwNjA0
16 | BggrBgEFBQcwAYYoaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwMy1hcHBsZXJv
17 | b3RjYTAuBgNVHR8EJzAlMCOgIaAfhh1odHRwOi8vY3JsLmFwcGxlLmNvbS9yb290
18 | LmNybDAdBgNVHQ4EFgQUW9n6HeeaGgujmXYiUIY+kchbd6gwDgYDVR0PAQH/BAQD
19 | AgEGMBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBCwUAA4IBAQA/Vj2e5bbD
20 | eeZFIGi9v3OLLBKeAuOugCKMBB7DUshwgKj7zqew1UJEggOCTwb8O0kU+9h0UoWv
21 | p50h5wESA5/NQFjQAde/MoMrU1goPO6cn1R2PWQnxn6NHThNLa6B5rmluJyJlPef
22 | x4elUWY0GzlxOSTjh2fvpbFoe4zuPfeutnvi0v/fYcZqdUmVIkSoBPyUuAsuORFJ
23 | EtHlgepZAE9bPFo22noicwkJac3AfOriJP6YRLj477JxPxpd1F1+M02cHSS+APCQ
24 | A1iZQT0xWmJArzmoUUOSqwSonMJNsUvSq3xKX+udO7xPiEAGE/+QF4oIRynoYpgp
25 | pU8RBWk6z/Kf
26 | -----END CERTIFICATE-----
27 |
--------------------------------------------------------------------------------
/src/FinanceOrder.php:
--------------------------------------------------------------------------------
1 | json);
40 |
41 | // Creates SHA hashes for string files in each project.
42 | foreach ($this->locales as $language => $strings) {
43 | $sha[$language . '.lproj/' . self::FILE_TYPE . '.strings'] = hash(self::HASH_ALGO, $strings);
44 | }
45 |
46 | foreach ($this->files as $name => $path) {
47 | $sha[$name] = hash(self::HASH_ALGO, file_get_contents($path));
48 | }
49 |
50 | foreach ($this->remote_file_urls as $name => $url) {
51 | $sha[$name] = hash(self::HASH_ALGO, file_get_contents($url));
52 | }
53 |
54 | foreach ($this->files_content as $name => $content) {
55 | $sha[$name] = hash(self::HASH_ALGO, $content);
56 | }
57 |
58 | return json_encode((object)$sha);
59 | }
60 |
61 | /**
62 | * Creates .pkpass zip archive.
63 | *
64 | * @param string $manifest
65 | * @param string $signature
66 | * @return string
67 | * @throws PKPassException
68 | */
69 | protected function createZip($manifest, $signature)
70 | {
71 | // Package file in Zip (as .order)
72 | $zip = new ZipArchive();
73 | $filename = tempnam($this->tempPath, self::FILE_TYPE);
74 | if (!$zip->open($filename, ZipArchive::OVERWRITE)) {
75 | throw new PKPassException('Could not open ' . basename($filename) . ' with ZipArchive extension.');
76 | }
77 | $zip->addFromString('signature', $signature);
78 | $zip->addFromString('manifest.json', $manifest);
79 | $zip->addFromString(self::PAYLOAD_FILE, $this->json);
80 |
81 | // Add translation dictionary
82 | foreach ($this->locales as $language => $strings) {
83 | if (!$zip->addEmptyDir($language . '.lproj')) {
84 | throw new PKPassException('Could not create ' . $language . '.lproj folder in zip archive.');
85 | }
86 | $zip->addFromString($language . '.lproj/' . self::FILE_TYPE . '.strings', $strings);
87 | }
88 |
89 | foreach ($this->files as $name => $path) {
90 | $zip->addFile($path, $name);
91 | }
92 |
93 | foreach ($this->remote_file_urls as $name => $url) {
94 | $download_file = file_get_contents($url);
95 | $zip->addFromString($name, $download_file);
96 | }
97 |
98 | foreach ($this->files_content as $name => $content) {
99 | $zip->addFromString($name, $content);
100 | }
101 |
102 | $zip->close();
103 |
104 | if (!file_exists($filename) || filesize($filename) < 1) {
105 | @unlink($filename);
106 | throw new PKPassException('Error while creating order.order. Check your ZIP extension.');
107 | }
108 |
109 | $content = file_get_contents($filename);
110 | unlink($filename);
111 |
112 | return $content;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/PKPass.php:
--------------------------------------------------------------------------------
1 | tempPath = sys_get_temp_dir();
98 | $this->wwdrCertPath = __DIR__ . '/Certificate/AppleWWDRCA.pem';
99 |
100 | if ($certificatePath) {
101 | $this->setCertificatePath($certificatePath);
102 | }
103 | if ($certificatePassword) {
104 | $this->setCertificatePassword($certificatePassword);
105 | }
106 | }
107 |
108 | /**
109 | * Sets the path to a certificate
110 | * Parameter: string, path to certificate
111 | * Return: boolean, always true.
112 | *
113 | * @param string $path
114 | *
115 | * @return bool
116 | */
117 | public function setCertificatePath($path)
118 | {
119 | $this->certPath = $path;
120 |
121 | return true;
122 | }
123 |
124 | /**
125 | * Sets the certificate's password
126 | * Parameter: string, password to the certificate
127 | * Return: boolean, always true.
128 | *
129 | * @param string $password
130 | *
131 | * @return bool
132 | */
133 | public function setCertificatePassword($password)
134 | {
135 | $this->certPass = $password;
136 |
137 | return true;
138 | }
139 |
140 | /**
141 | * Sets the path to the WWDR Intermediate certificate
142 | * Parameter: string, path to certificate
143 | * Return: boolean, always true.
144 | *
145 | * @param string $path
146 | *
147 | * @return bool
148 | */
149 | public function setWwdrCertificatePath($path)
150 | {
151 | $this->wwdrCertPath = $path;
152 |
153 | return true;
154 | }
155 |
156 | /**
157 | * Set the path to the temporary directory.
158 | *
159 | * @param string $path Path to temporary directory
160 | */
161 | public function setTempPath($path)
162 | {
163 | $this->tempPath = $path;
164 | }
165 |
166 | /**
167 | * Set pass data.
168 | *
169 | * @param string|array|object $data
170 | * @throws PKPassException
171 | */
172 | public function setData($data)
173 | {
174 | // Array is passed as input
175 | if (is_array($data) || is_object($data)) {
176 | $this->json = json_encode($data);
177 | return;
178 | }
179 |
180 | // JSON string is passed as input
181 | if (json_decode($data) !== false) {
182 | $this->json = $data;
183 | return;
184 | }
185 |
186 | throw new PKPassException('Invalid data passed to setData: this is not a JSON string.');
187 | }
188 |
189 | /**
190 | * Add dictionary of strings for translation.
191 | *
192 | * @param string $language language project need to be added
193 | * @param array $strings a key value pair of translation strings (default is equal to [])
194 | * @throws PKPassException
195 | */
196 | public function addLocaleStrings($language, $strings = [])
197 | {
198 | if (!is_array($strings) || empty($strings)) {
199 | throw new PKPassException('Translation strings empty or not an array.');
200 | }
201 |
202 | $dictionary = "";
203 | foreach ($strings as $key => $value) {
204 | $dictionary .= '"' . $this->escapeLocaleString($key) . '" = "' . $this->escapeLocaleString($value) . '";' . PHP_EOL;
205 | }
206 | $this->locales[$language] = $dictionary;
207 | }
208 |
209 | /**
210 | * Add a localized file to the file array.
211 | *
212 | * @param string $language language for which file to be added
213 | * @param string $path Path to file
214 | * @param string $name Filename to use in pass archive (default is equal to $path)
215 | * @throws PKPassException
216 | */
217 | public function addLocaleFile($language, $path, $name = null)
218 | {
219 | if (!file_exists($path)) {
220 | throw new PKPassException(sprintf('File %s does not exist.', $path));
221 | }
222 |
223 | $name = $name ?: basename($path);
224 | $this->files[$language . '.lproj/' . $name] = $path;
225 | }
226 |
227 | /**
228 | * Add a file to the file array.
229 | *
230 | * @param string $path Path to file
231 | * @param string $name Filename to use in pass archive (default is equal to $path)
232 | * @throws PKPassException
233 | */
234 | public function addFile($path, $name = null)
235 | {
236 | if (!file_exists($path)) {
237 | throw new PKPassException(sprintf('File %s does not exist.', $path));
238 | }
239 |
240 | $name = $name ?: basename($path);
241 | $this->files[$name] = $path;
242 |
243 | return false;
244 | }
245 |
246 | /**
247 | * Add a file from a url to the remote file urls array.
248 | *
249 | * @param string $url URL to file
250 | * @param string $name Filename to use in pass archive (default is equal to $url)
251 | */
252 | public function addRemoteFile($url, $name = null)
253 | {
254 | $name = $name ?: basename($url);
255 | $this->remote_file_urls[$name] = $url;
256 | }
257 |
258 | /**
259 | * Add a locale file from a url to the remote file urls array.
260 | *
261 | * @param string $language language for which file to be added
262 | * @param string $content Content of file
263 | * @param string $name Filename to use in pass archive (default is equal to $url)
264 | */
265 | public function addLocaleRemoteFile($language, $url, $name = null)
266 | {
267 | $name = $name ?: basename($url);
268 | $this->remote_file_urls[$language . '.lproj/' . $name] = $url;
269 | }
270 |
271 | /**
272 | * Add a file from a string to the string files array.
273 | *
274 | * @param string $content Content of file
275 | * @param string $name Filename to use in pass archive (default is equal to $url)
276 | */
277 | public function addFileContent($content, $name)
278 | {
279 | $this->files_content[$name] = $content;
280 | }
281 |
282 | /**
283 | * Add a locale file from a string to the string files array.
284 | *
285 | * @param string $language language for which file to be added
286 | * @param string $content Content of file
287 | * @param string $name Filename to use in pass archive (default is equal to $url)
288 | */
289 | public function addLocaleFileContent($language, $content, $name)
290 | {
291 | $this->files_content[$language . '.lproj/' . $name] = $content;
292 | }
293 |
294 | /**
295 | * Create the actual .pkpass file.
296 | *
297 | * @param bool $output Whether to output it directly or return the pass contents as a string.
298 | *
299 | * @return string
300 | * @throws PKPassException
301 | */
302 | public function create($output = false)
303 | {
304 | // Prepare payload
305 | $manifest = $this->createManifest();
306 | $signature = $this->createSignature($manifest);
307 |
308 | // Build ZIP file
309 | $zip = $this->createZip($manifest, $signature);
310 |
311 | // Return pass
312 | if (!$output) {
313 | return $zip;
314 | }
315 |
316 | // Output pass
317 | header('Content-Description: File Transfer');
318 | header('Content-Type: ' . static::MIME_TYPE);
319 | header('Content-Disposition: attachment; filename="' . $this->getName() . '"');
320 | header('Content-Transfer-Encoding: binary');
321 | header('Connection: Keep-Alive');
322 | header('Expires: 0');
323 | header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
324 | header('Last-Modified: ' . gmdate('D, d M Y H:i:s T'));
325 | header('Pragma: public');
326 | echo $zip;
327 |
328 | return '';
329 | }
330 |
331 | /**
332 | * Get filename.
333 | *
334 | * @return string
335 | */
336 | public function getName()
337 | {
338 | $name = $this->name ?: static::FILE_TYPE;
339 | if (!strstr($name, '.')) {
340 | $name .= '.' . static::FILE_EXT;
341 | }
342 |
343 | return $name;
344 | }
345 |
346 | /**
347 | * Set filename.
348 | *
349 | * @param string $name
350 | */
351 | public function setName($name)
352 | {
353 | $this->name = $name;
354 | }
355 |
356 | /**
357 | * Sub-function of create()
358 | * This function creates the hashes for the files and adds them into a json string.
359 | *
360 | * @throws PKPassException
361 | */
362 | protected function createManifest()
363 | {
364 | // Creates SHA hashes for all files in package
365 | $sha = [];
366 | $sha['pass.json'] = sha1($this->json);
367 |
368 | // Creates SHA hashes for string files in each project.
369 | foreach ($this->locales as $language => $strings) {
370 | $sha[$language . '.lproj/pass.strings'] = sha1($strings);
371 | }
372 |
373 | $has_icon = false;
374 | foreach ($this->files as $name => $path) {
375 | if (strtolower($name) == 'icon.png') {
376 | $has_icon = true;
377 | }
378 | $sha[$name] = sha1(file_get_contents($path));
379 | }
380 |
381 | foreach ($this->remote_file_urls as $name => $url) {
382 | if (strtolower($name) == 'icon.png') {
383 | $has_icon = true;
384 | }
385 | $sha[$name] = sha1(file_get_contents($url));
386 | }
387 |
388 | foreach ($this->files_content as $name => $content) {
389 | if (strtolower($name) == 'icon.png') {
390 | $has_icon = true;
391 | }
392 | $sha[$name] = sha1($content);
393 | }
394 |
395 | if (!$has_icon) {
396 | throw new PKPassException('Missing required icon.png file.');
397 | }
398 |
399 | return json_encode((object)$sha);
400 | }
401 |
402 | /**
403 | * Converts PKCS7 PEM to PKCS7 DER
404 | * Parameter: string, holding PKCS7 PEM, binary, detached
405 | * Return: string, PKCS7 DER.
406 | *
407 | * @param string $signature
408 | *
409 | * @return string
410 | */
411 | protected function convertPEMtoDER($signature)
412 | {
413 | $begin = 'filename="smime.p7s"';
414 | $end = '------';
415 | $signature = substr($signature, strpos($signature, $begin) + strlen($begin));
416 |
417 | $signature = substr($signature, 0, strpos($signature, $end));
418 | $signature = trim($signature);
419 |
420 | return base64_decode($signature);
421 | }
422 |
423 | /**
424 | * Read a PKCS12 certificate string and turn it into an array.
425 | *
426 | * @return array
427 | * @throws PKPassException
428 | */
429 | protected function readP12()
430 | {
431 | // Use the built-in reader first
432 | if (!$pkcs12 = file_get_contents($this->certPath)) {
433 | throw new PKPassException('Could not read the certificate.');
434 | }
435 | $certs = [];
436 | if (openssl_pkcs12_read($pkcs12, $certs, $this->certPass)) {
437 | return $certs;
438 | }
439 |
440 | // That failed, let's check why
441 | $error = '';
442 | while ($text = openssl_error_string()) {
443 | $error .= $text;
444 | }
445 |
446 | // General error
447 | if (!strstr($error, 'digital envelope routines::unsupported')) {
448 | throw new PKPassException(
449 | 'Invalid certificate file. Make sure you have a ' .
450 | 'P12 certificate that also contains a private key, and you ' .
451 | 'have specified the correct password!' . PHP_EOL . PHP_EOL .
452 | 'OpenSSL error: ' . $error
453 | );
454 | }
455 |
456 | // Try an alternative route using shell_exec
457 | try {
458 | $value = @shell_exec(
459 | "openssl pkcs12 -in " . escapeshellarg($this->certPath) .
460 | " -passin " . escapeshellarg("pass:" . $this->certPass) .
461 | " -passout " . escapeshellarg("pass:" . $this->certPass) .
462 | " -legacy"
463 | );
464 | if ($value) {
465 | $cert = substr($value, strpos($value, '-----BEGIN CERTIFICATE-----'));
466 | $cert = substr($cert, 0, strpos($cert, '-----END CERTIFICATE-----') + 25);
467 | $key = substr($value, strpos($value, '-----BEGIN ENCRYPTED PRIVATE KEY-----'));
468 | $key = substr($key, 0, strpos($key, '-----END ENCRYPTED PRIVATE KEY-----') + 35);
469 | if (strlen($cert) > 0 && strlen($key) > 0) {
470 | $certs['cert'] = $cert;
471 | $certs['pkey'] = $key;
472 | return $certs;
473 | }
474 | }
475 | } catch (\Throwable $e) {
476 | // no need to do anything
477 | }
478 |
479 | throw new PKPassException(
480 | 'Could not read certificate file. This might be related ' .
481 | 'to using an OpenSSL version that has deprecated some older ' .
482 | 'hashes. More info here: https://schof.link/2Et6z3m ' . PHP_EOL . PHP_EOL .
483 | 'OpenSSL error: ' . $error
484 | );
485 | }
486 |
487 | /**
488 | * Creates a signature and saves it.
489 | *
490 | * @param string $manifest
491 | * @throws PKPassException
492 | */
493 | protected function createSignature($manifest)
494 | {
495 | $manifest_path = tempnam($this->tempPath, 'pkpass');
496 | $signature_path = tempnam($this->tempPath, 'pkpass');
497 | file_put_contents($manifest_path, $manifest);
498 |
499 | $certs = $this->readP12();
500 | $certdata = openssl_x509_read($certs['cert']);
501 | $privkey = openssl_pkey_get_private($certs['pkey'], $this->certPass);
502 |
503 | $openssl_args = [
504 | $manifest_path,
505 | $signature_path,
506 | $certdata,
507 | $privkey,
508 | [],
509 | PKCS7_BINARY | PKCS7_DETACHED
510 | ];
511 |
512 | if (!empty($this->wwdrCertPath)) {
513 | if (!file_exists($this->wwdrCertPath)) {
514 | throw new PKPassException('WWDR Intermediate Certificate does not exist.');
515 | }
516 |
517 | $openssl_args[] = $this->wwdrCertPath;
518 | }
519 |
520 | call_user_func_array('openssl_pkcs7_sign', $openssl_args);
521 |
522 | $signature = file_get_contents($signature_path);
523 | unlink($manifest_path);
524 | unlink($signature_path);
525 |
526 | return $this->convertPEMtoDER($signature);
527 | }
528 |
529 | /**
530 | * Creates .pkpass zip archive.
531 | *
532 | * @param string $manifest
533 | * @param string $signature
534 | * @return string
535 | * @throws PKPassException
536 | */
537 | protected function createZip($manifest, $signature)
538 | {
539 | // Package file in Zip (as .pkpass)
540 | $zip = new ZipArchive();
541 | $filename = tempnam($this->tempPath, 'pkpass');
542 | if (!$zip->open($filename, ZipArchive::OVERWRITE)) {
543 | throw new PKPassException('Could not open ' . basename($filename) . ' with ZipArchive extension.');
544 | }
545 | $zip->addFromString('signature', $signature);
546 | $zip->addFromString('manifest.json', $manifest);
547 | $zip->addFromString('pass.json', $this->json);
548 |
549 | // Add translation dictionary
550 | foreach ($this->locales as $language => $strings) {
551 | if (!$zip->addEmptyDir($language . '.lproj')) {
552 | throw new PKPassException('Could not create ' . $language . '.lproj folder in zip archive.');
553 | }
554 | $zip->addFromString($language . '.lproj/pass.strings', $strings);
555 | }
556 |
557 | foreach ($this->files as $name => $path) {
558 | $zip->addFile($path, $name);
559 | }
560 |
561 | foreach ($this->remote_file_urls as $name => $url) {
562 | $download_file = file_get_contents($url);
563 | $zip->addFromString($name, $download_file);
564 | }
565 |
566 | foreach ($this->files_content as $name => $content) {
567 | $zip->addFromString($name, $content);
568 | }
569 |
570 | $zip->close();
571 |
572 | if (!file_exists($filename) || filesize($filename) < 1) {
573 | @unlink($filename);
574 | throw new PKPassException('Error while creating pass.pkpass. Check your ZIP extension.');
575 | }
576 |
577 | $content = file_get_contents($filename);
578 | unlink($filename);
579 |
580 | return $content;
581 | }
582 |
583 | protected static $escapeChars = [
584 | "\n" => "\\n",
585 | "\r" => "\\r",
586 | "\"" => "\\\"",
587 | "\\" => "\\\\"
588 | ];
589 |
590 | /**
591 | * Escapes strings for use in locale files
592 | * @param string $string
593 | * @return string
594 | */
595 | protected function escapeLocaleString($string)
596 | {
597 | return strtr($string, self::$escapeChars);
598 | }
599 | }
600 |
--------------------------------------------------------------------------------
/src/PKPassException.php:
--------------------------------------------------------------------------------
1 |