Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(passkit): Signature creation #87

Merged
merged 14 commits into from
Sep 18, 2024
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('[email protected]'),
image3: findUint8ListForFile('[email protected]'),
image1: findBytesForFile('$name.png'),
image2: findBytesForFile('[email protected]'),
image3: findBytesForFile('[email protected]'),
);
}

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
Loading