Skip to content

Commit

Permalink
feat(passkit): Signature creation (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
ueman authored Sep 18, 2024
1 parent 44cfd51 commit 45c955d
Show file tree
Hide file tree
Showing 17 changed files with 304 additions and 106 deletions.
2 changes: 1 addition & 1 deletion app/lib/import_pass/import_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class PkPassImportSource {

final String? contentResolverPath;
final String? filePath;
final List<int>? bytes;
final Uint8List? bytes;

Future<PkPass> getPass() async {
if (contentResolverPath != null) {
Expand Down
20 changes: 18 additions & 2 deletions app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.5.0"
asn1lib:
dependency: transitive
description:
name: asn1lib
sha256: "2ca377ad4d677bbadca278e0ba4da4e057b80a10b927bfc8f7d8bda8fe2ceb75"
url: "https://pub.dev"
source: hosted
version: "1.5.4"
async:
dependency: transitive
description:
Expand Down Expand Up @@ -262,6 +270,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0+11"
encrypt:
dependency: transitive
description:
name: encrypt
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
fake_async:
dependency: transitive
description:
Expand Down Expand Up @@ -712,14 +728,14 @@ packages:
path: "../passkit"
relative: true
source: path
version: "0.0.5"
version: "0.0.6"
passkit_ui:
dependency: "direct main"
description:
path: "../passkit_ui"
relative: true
source: path
version: "0.0.4"
version: "0.0.5"
path:
dependency: "direct main"
description:
Expand Down
8 changes: 4 additions & 4 deletions apple_passkit/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.4"
version: "14.2.5"
webdriver:
dependency: transitive
description:
Expand All @@ -271,5 +271,5 @@ packages:
source: hosted
version: "3.0.3"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
6 changes: 6 additions & 0 deletions passkit/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.0.7

- The library is now able to create properly signed `pkpass` files that work with Apple Wallet.
Follow the guide [here](https://github.com/ueman/passkit/blob/master/passkit/SIGNING.md) to learn more.
- Pretty much every use of `List<int>` has been changed to `Uint8List`. This is potentially breaking.

## 0.0.6

- Add ability to create PkPass signature via OpenSSL or other command line tools
Expand Down
16 changes: 7 additions & 9 deletions passkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,18 @@ void main() {

> [!WARNING]
> This is experimental.
> The resulting file not yet get accepted by Apple Wallet due to missing support for writing the pass signature.
>
> If you know how to create the PkPass signature it in pure Dart code, please add an example
> [here](https://github.com/ueman/passkit/issues/74) or create
> a PR for [this](https://github.com/ueman/passkit/issues/74) issue.
> Follow the guide [here](https://github.com/ueman/passkit/blob/master/passkit/SIGNING.md) to learn more.
```dart
import 'package:passkit/passkit.dart';
void main() {
final pkPass = PkPass(...);
final pkPassFile = pass.write();
await File('path/to/pass.pkpass').writeAsBytes(pkPassFile);
final pass = PkPass(...);
final binaryData pass.write(
certificatePem: File('pass_certificate.pem').readAsStringSync(),
privateKeyPem: File('private_key.pem').readAsStringSync(),
);
File('pass.pkpass').writeAsBytesSync(binaryData);
}
```

Expand Down Expand Up @@ -136,7 +135,6 @@ Please feel encouraged to create PRs for the following features.
- PassKit Web Service: This functionality is existing, but might not work. Please file an issue or create a PR with a fix for bugs you encounter.
- Push Notification update registration is only working on iOS due to this whole specification being an Apple thingy.
- Localization: Existing, but still inconvenient to use. There might be issues due to localizations being UTF-16 formatted, but the library currently uses UTF-8 to read localizations.
- [Passkit creation](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html#//apple_ref/doc/uid/TP40012195-CH4-SW54) is partially supported. See note further above.

## Bugs and parsing issues

Expand Down
42 changes: 42 additions & 0 deletions passkit/SIGNING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Signing a PkPass file

> [!WARNING]
> THIS SHOULD NOT BE USED WITHIN AN APP.
> IT SHOULD ONLY BE USED SERVER SIDE, OTHERWISE YOU RISK EXPOSING YOUR CERTIFICATE AND PRIVATE KEY.
This guide assumes you're working on a macOS system.

## Step 1: Get a Pass ID and Pass Certificate from the Apple Developer Portal

Follow for example this guide: https://www.kodeco.com/2855-beginning-passbook-in-ios-6-part-1-2?page=3#toc-anchor-007
At the end you should have a certificate and private key in <kbd>Keychain Access<kbd>.

## Step 2: Create `.pem` files

This is the important step!

Export your certificate and private key from <kbd>Keychain Access<kbd> as `Certificate.p12`. It will ask you to set a password.

You should replace `<your-password-here>` with your password in the following commands.
(The `--legacy` part at the end of the following command may or may not be needed depending on your openssl version)

Create the certificate `.pem` file.
```shell
openssl pkcs12 -in Certificates.p12 -clcerts -nokeys -out pass_certificate.pem -passin pass:<your-password-here> --legacy
```

Create the private key `.pem` file. Unfortunately, it's not yet possible to use a password protected private key file.
```shell
openssl pkcs12 -in Certificates.p12 -out private_key.pem -nocerts -nodes -passin pass:<your-password-here> --legacy
```

Then you can the generated file to sign PkPass files:

```dart
final pass = PkPass(...);
final binaryData pass.write(
certificatePem: File('pass_certificate.pem').readAsStringSync(),
privateKeyPem: File('private_key.pem').readAsStringSync(),
);
File('pass.pkpass').writeAsBytesSync(binaryData); // The file ending is important
```
6 changes: 3 additions & 3 deletions passkit/lib/src/apple_wwdr_certificate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ X509 get wwdrG4 =>
/// More info at:
/// https://developer.apple.com/help/account/reference/wwdr-intermediate-certificates/
/// https://www.apple.com/certificateauthority/
// ignore: constant_identifier_names
const worldwide_Developer_Relations_G4 = [
// ignore: constant_identifier_names, non_constant_identifier_names
final worldwide_Developer_Relations_G4 = Uint8List.fromList([
48,
130,
4,
Expand Down Expand Up @@ -1127,4 +1127,4 @@ const worldwide_Developer_Relations_G4 = [
207,
242,
159,
];
]);
21 changes: 8 additions & 13 deletions passkit/lib/src/archive_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:typed_data';

import 'package:archive/archive.dart';
import 'package:crypto/crypto.dart';
import 'package:passkit/src/archive_file_extension.dart';
import 'package:passkit/src/pkpass/exceptions.dart';
import 'package:passkit/src/pkpass/pk_pass_image.dart';
import 'package:passkit/src/strings_parser/naive_strings_file_parser.dart';
Expand All @@ -15,13 +16,8 @@ import 'package:passkit/src/strings_parser/naive_strings_file_parser.dart';
final _utf8JsonDecoder = const Utf8Decoder().fuse(const JsonDecoder());

extension ArchiveX on Archive {
List<int>? findBytesForFile(String fileName) =>
findFile(fileName)?.content as List<int>?;

Uint8List? findUint8ListForFile(String fileName) {
final data = findBytesForFile(fileName);
return data == null ? null : Uint8List.fromList(data);
}
Uint8List? findBytesForFile(String fileName) =>
findFile(fileName)?.binaryContent;

Map<String, dynamic>? findFileAndReadAsJson(String fileName) {
final bytes = findBytesForFile(fileName);
Expand All @@ -33,9 +29,9 @@ extension ArchiveX on Archive {

PkImage? loadImage(String name) {
return PkImage.fromImages(
image1: findUint8ListForFile('$name.png'),
image2: findUint8ListForFile('$name@2.png'),
image3: findUint8ListForFile('$name@3.png'),
image1: findBytesForFile('$name.png'),
image2: findBytesForFile('$name@2.png'),
image3: findBytesForFile('$name@3.png'),
);
}

Expand All @@ -52,8 +48,7 @@ extension ArchiveX on Archive {
for (final languageFile in translationFiles) {
final language = languageFile.name.split('.').first;

languageData[language] =
parseStringsFile(languageFile.content as List<int>);
languageData[language] = parseStringsFile(languageFile.binaryContent);
}
return languageData;
}
Expand Down Expand Up @@ -88,7 +83,7 @@ extension ArchiveX on Archive {

for (final file in filesWithoutSignatureAndManifest) {
final checksumInManifest = manifest[file.name] as String?;
final digest = sha1.convert(file.content as List<int>);
final digest = sha1.convert(file.binaryContent);
if (checksumInManifest != digest.toString()) {
throw ChecksumMismatchException(file.name);
}
Expand Down
7 changes: 7 additions & 0 deletions passkit/lib/src/archive_file_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'dart:typed_data';

import 'package:archive/archive.dart';

extension ArchiveFileX on ArchiveFile {
Uint8List get binaryContent => Uint8List.fromList(content as List<int>);
}
11 changes: 5 additions & 6 deletions passkit/lib/src/order/pk_order.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:typed_data';

import 'package:archive/archive.dart';
import 'package:passkit/src/archive_extensions.dart';
import 'package:passkit/src/archive_file_extension.dart';
import 'package:passkit/src/pkpass/exceptions.dart';
import 'package:passkit/src/signature_verification.dart';

Expand All @@ -20,7 +21,7 @@ class PkOrder {
/// verification and validation.
// TODO(ueman): Provide an async method for this.
static PkOrder fromBytes(
final List<int> bytes, {
final Uint8List bytes, {
bool skipChecksumVerification = false,
bool skipSignatureVerification = false,
}) {
Expand All @@ -38,10 +39,8 @@ class PkOrder {

if (skipSignatureVerification) {
final manifestContent =
archive.findFile('manifest.json')!.content as List<int>;
final signatureContent = Uint8List.fromList(
archive.findFile('signature')!.content as List<int>,
);
archive.findFile('manifest.json')!.binaryContent;
final signatureContent = archive.findFile('signature')!.binaryContent;

verifySignature(
signatureBytes: signatureContent,
Expand Down Expand Up @@ -77,7 +76,7 @@ class PkOrder {
final Map<String, Map<String, dynamic>>? languageData;

/// The bytes of this PkPass
final List<int> sourceData;
final Uint8List sourceData;

/// Indicates whether a webservices is available.
bool get isWebServiceAvailable => order.webServiceURL != null;
Expand Down
74 changes: 74 additions & 0 deletions passkit/lib/src/pkpass/pass_data.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:csslib/parser.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:passkit/src/pkpass/barcode.dart';
Expand Down Expand Up @@ -266,4 +267,77 @@ class PassData {
/// a pass and suggest related actions.
@JsonKey(name: 'semantics')
final Semantics? semantics;

PassData copyWith({
String? description,
int? formatVersion,
String? organizationName,
String? passTypeIdentifier,
String? serialNumber,
String? teamIdentifier,
String? appLaunchURL,
List<int>? associatedStoreIdentifiers,
Map<String, dynamic>? userInfo,
DateTime? expirationDate,
bool? voided,
List<Beacon>? beacons,
List<Location>? locations,
num? maxDistance,
DateTime? relevantDate,
PassStructure? boardingPass,
PassStructure? coupon,
PassStructure? eventTicket,
PassStructure? generic,
PassStructure? storeCard,
Barcode? barcode,
List<Barcode>? barcodes,
Color? backgroundColor,
Color? foregroundColor,
String? groupingIdentifier,
Color? labelColor,
String? logoText,
bool? suppressStripShine,
bool? sharingProhibited,
String? authenticationToken,
Uri? webServiceURL,
Nfc? nfc,
Semantics? semantics,
}) {
return PassData(
description: description ?? this.description,
formatVersion: formatVersion ?? this.formatVersion,
organizationName: organizationName ?? this.organizationName,
passTypeIdentifier: passTypeIdentifier ?? this.passTypeIdentifier,
serialNumber: serialNumber ?? this.serialNumber,
teamIdentifier: teamIdentifier ?? this.teamIdentifier,
appLaunchURL: appLaunchURL ?? this.appLaunchURL,
associatedStoreIdentifiers:
associatedStoreIdentifiers ?? this.associatedStoreIdentifiers,
userInfo: userInfo ?? this.userInfo,
expirationDate: expirationDate ?? this.expirationDate,
voided: voided ?? this.voided,
beacons: beacons ?? this.beacons,
locations: locations ?? this.locations,
maxDistance: maxDistance ?? this.maxDistance,
relevantDate: relevantDate ?? this.relevantDate,
boardingPass: boardingPass ?? this.boardingPass,
coupon: coupon ?? this.coupon,
eventTicket: eventTicket ?? this.eventTicket,
generic: generic ?? this.generic,
storeCard: storeCard ?? this.storeCard,
barcode: barcode ?? this.barcode,
barcodes: barcodes ?? this.barcodes,
backgroundColor: backgroundColor ?? this.backgroundColor,
foregroundColor: foregroundColor ?? this.foregroundColor,
groupingIdentifier: groupingIdentifier ?? this.groupingIdentifier,
labelColor: labelColor ?? this.labelColor,
logoText: logoText ?? this.logoText,
suppressStripShine: suppressStripShine ?? this.suppressStripShine,
sharingProhibited: sharingProhibited ?? this.sharingProhibited,
authenticationToken: authenticationToken ?? this.authenticationToken,
webServiceURL: webServiceURL ?? this.webServiceURL,
nfc: nfc ?? this.nfc,
semantics: semantics ?? this.semantics,
);
}
}
Loading

0 comments on commit 45c955d

Please sign in to comment.