Skip to content

Commit

Permalink
Move to ZIPFoundation and swift-certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
fpseverino committed Aug 5, 2024
1 parent 2ab527a commit 06fb9ae
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 64 deletions.
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ let package = Package(
.package(url: "https://github.com/vapor/vapor.git", from: "4.102.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.11.0"),
.package(url: "https://github.com/vapor/apns.git", from: "4.1.0"),
.package(url: "https://github.com/fpseverino/Zip.git", branch: "update"),
.package(url: "https://github.com/weichsel/ZIPFoundation.git", branch: "development"),
.package(url: "https://github.com/apple/swift-certificates.git", branch: "main"),
// used in tests
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.7.4"),
],
Expand All @@ -25,7 +26,8 @@ let package = Package(
.product(name: "Fluent", package: "fluent"),
.product(name: "Vapor", package: "vapor"),
.product(name: "VaporAPNS", package: "apns"),
.product(name: "Zip", package: "zip"),
.product(name: "ZIPFoundation", package: "zipfoundation"),
.product(name: "X509", package: "swift-certificates"),
],
swiftSettings: swiftSettings
),
Expand Down
2 changes: 1 addition & 1 deletion Sources/Orders/Orders.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ final class OrderDelegate: OrdersDelegate {
}
```

> Important: You **must** explicitly declare ``OrdersDelegate/pemPrivateKeyPassword`` as a `String?` or Swift will ignore it as it'll think it's a `String` instead.
> Important: If you have an encrypted PEM private key, you **must** explicitly declare ``OrdersDelegate/pemPrivateKeyPassword`` as a `String?` or Swift will ignore it as it'll think it's a `String` instead.
### Initialize the Service

Expand Down
98 changes: 69 additions & 29 deletions Sources/Orders/OrdersServiceCustom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import APNSCore
import Fluent
import NIOSSL
import PassKit
import Zip
import ZIPFoundation
@_spi(CMS) import X509

/// Class to handle ``OrdersService``.
///
Expand Down Expand Up @@ -350,7 +351,7 @@ extension OrdersServiceCustom {

// MARK: - order file generation
extension OrdersServiceCustom {
private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws {
private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws -> Data {
var manifest: [String: String] = [:]

let paths = try FileManager.default.subpathsOfDirectory(atPath: root.unixPath())
Expand All @@ -362,36 +363,68 @@ extension OrdersServiceCustom {
manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined()
}

try encoder.encode(manifest)
.write(to: root.appendingPathComponent("manifest.json"))
let data = try encoder.encode(manifest)
try data.write(to: root.appendingPathComponent("manifest.json"))
return data
}

private func generateSignatureFile(in root: URL) throws {
private func generateSignatureFile(for manifest: Data, in root: URL) throws {
// If the caller's delegate generated a file we don't have to do it.
if delegate.generateSignatureFile(in: root) { return }

let sslBinary = delegate.sslBinary
guard FileManager.default.fileExists(atPath: sslBinary.unixPath()) else {
throw OrdersError.opensslBinaryMissing
}
// Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that.
if let password = delegate.pemPrivateKeyPassword {
let sslBinary = delegate.sslBinary
guard FileManager.default.fileExists(atPath: sslBinary.unixPath()) else {
throw OrdersError.opensslBinaryMissing
}

let proc = Process()
proc.currentDirectoryURL = delegate.sslSigningFilesDirectory
proc.executableURL = sslBinary
proc.arguments = [
"smime", "-binary", "-sign",
"-certfile", delegate.wwdrCertificate,
"-signer", delegate.pemCertificate,
"-inkey", delegate.pemPrivateKey,
"-in", root.appendingPathComponent("manifest.json").unixPath(),
"-out", root.appendingPathComponent("signature").unixPath(),
"-outform", "DER"
]
if let pwd = delegate.pemPrivateKeyPassword {
proc.arguments!.append(contentsOf: ["-passin", "pass:\(pwd)"])
let proc = Process()
proc.currentDirectoryURL = delegate.sslSigningFilesDirectory
proc.executableURL = sslBinary
proc.arguments = [
"smime", "-binary", "-sign",
"-certfile", delegate.wwdrCertificate,
"-signer", delegate.pemCertificate,
"-inkey", delegate.pemPrivateKey,
"-in", root.appendingPathComponent("manifest.json").unixPath(),
"-out", root.appendingPathComponent("signature").unixPath(),
"-outform", "DER",
"-passin", "pass:\(password)"
]

try proc.run()
proc.waitUntilExit()
return
}
try proc.run()
proc.waitUntilExit()

let signature = try CMS.sign(
manifest,
signatureAlgorithm: .sha256WithRSAEncryption,
additionalIntermediateCertificates: [
Certificate(
pemEncoded: String(
contentsOf: delegate.sslSigningFilesDirectory
.appending(path: delegate.wwdrCertificate)
)
)
],
certificate: Certificate(
pemEncoded: String(
contentsOf: delegate.sslSigningFilesDirectory
.appending(path: delegate.pemCertificate)
)
),
privateKey: .init(
pemEncoded: String(
contentsOf: delegate.sslSigningFilesDirectory
.appending(path: delegate.pemPrivateKey)
)
),
signingTime: Date()
)

try Data(signature).write(to: root.appendingPathComponent("signature"))
}

/// Generates the order content bundle for a given order.
Expand All @@ -415,9 +448,16 @@ extension OrdersServiceCustom {
let encoder = JSONEncoder()
try await self.delegate.encode(order: order, db: db, encoder: encoder)
.write(to: root.appendingPathComponent("order.json"))

try Self.generateManifestFile(using: encoder, in: root)
try self.generateSignatureFile(in: root)
return try Data(contentsOf: Zip.quickZipFiles([root], fileName: "\(UUID().uuidString).order"))

try self.generateSignatureFile(
for: Self.generateManifestFile(using: encoder, in: root),
in: root
)

let zipFile = tmp.appendingPathComponent("\(UUID().uuidString).order")
try FileManager.default.zipItem(at: root, to: zipFile, shouldKeepParent: false)
defer { _ = try? FileManager.default.removeItem(at: zipFile) }

return try Data(contentsOf: zipFile)
}
}
2 changes: 1 addition & 1 deletion Sources/Passes/Passes.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ final class PassDelegate: PassesDelegate {
}
```

> Important: You **must** explicitly declare ``PassesDelegate/pemPrivateKeyPassword`` as a `String?` or Swift will ignore it as it'll think it's a `String` instead.
> Important: If you have an encrypted PEM private key, you **must** explicitly declare ``PassesDelegate/pemPrivateKeyPassword`` as a `String?` or Swift will ignore it as it'll think it's a `String` instead.
### Initialize the Service

Expand Down
106 changes: 75 additions & 31 deletions Sources/Passes/PassesServiceCustom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import APNSCore
import Fluent
import NIOSSL
import PassKit
import Zip
import ZIPFoundation
@_spi(CMS) import X509

/// Class to handle ``PassesService``.
///
Expand Down Expand Up @@ -421,7 +422,7 @@ extension PassesServiceCustom {

// MARK: - pkpass file generation
extension PassesServiceCustom {
private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws {
private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws -> Data {
var manifest: [String: String] = [:]

let paths = try FileManager.default.subpathsOfDirectory(atPath: root.unixPath())
Expand All @@ -433,36 +434,68 @@ extension PassesServiceCustom {
manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined()
}

try encoder.encode(manifest)
.write(to: root.appendingPathComponent("manifest.json"))
let data = try encoder.encode(manifest)
try data.write(to: root.appendingPathComponent("manifest.json"))
return data
}

private func generateSignatureFile(in root: URL) throws {
private func generateSignatureFile(for manifest: Data, in root: URL) throws {
// If the caller's delegate generated a file we don't have to do it.
if delegate.generateSignatureFile(in: root) { return }

let sslBinary = delegate.sslBinary
guard FileManager.default.fileExists(atPath: sslBinary.unixPath()) else {
throw PassesError.opensslBinaryMissing
}
// Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that.
if let password = delegate.pemPrivateKeyPassword {
let sslBinary = delegate.sslBinary
guard FileManager.default.fileExists(atPath: sslBinary.unixPath()) else {
throw PassesError.opensslBinaryMissing
}

let proc = Process()
proc.currentDirectoryURL = delegate.sslSigningFilesDirectory
proc.executableURL = sslBinary
proc.arguments = [
"smime", "-binary", "-sign",
"-certfile", delegate.wwdrCertificate,
"-signer", delegate.pemCertificate,
"-inkey", delegate.pemPrivateKey,
"-in", root.appendingPathComponent("manifest.json").unixPath(),
"-out", root.appendingPathComponent("signature").unixPath(),
"-outform", "DER"
]
if let pwd = delegate.pemPrivateKeyPassword {
proc.arguments!.append(contentsOf: ["-passin", "pass:\(pwd)"])
}
try proc.run()
proc.waitUntilExit()
let proc = Process()
proc.currentDirectoryURL = delegate.sslSigningFilesDirectory
proc.executableURL = sslBinary
proc.arguments = [
"smime", "-binary", "-sign",
"-certfile", delegate.wwdrCertificate,
"-signer", delegate.pemCertificate,
"-inkey", delegate.pemPrivateKey,
"-in", root.appendingPathComponent("manifest.json").unixPath(),
"-out", root.appendingPathComponent("signature").unixPath(),
"-outform", "DER",
"-passin", "pass:\(password)"
]

try proc.run()
proc.waitUntilExit()
return
}

let signature = try CMS.sign(
manifest,
signatureAlgorithm: .sha256WithRSAEncryption,
additionalIntermediateCertificates: [
Certificate(
pemEncoded: String(
contentsOf: delegate.sslSigningFilesDirectory
.appending(path: delegate.wwdrCertificate)
)
)
],
certificate: Certificate(
pemEncoded: String(
contentsOf: delegate.sslSigningFilesDirectory
.appending(path: delegate.pemCertificate)
)
),
privateKey: .init(
pemEncoded: String(
contentsOf: delegate.sslSigningFilesDirectory
.appending(path: delegate.pemPrivateKey)
)
),
signingTime: Date()
)

try Data(signature).write(to: root.appendingPathComponent("signature"))
}

/// Generates the pass content bundle for a given pass.
Expand Down Expand Up @@ -492,9 +525,16 @@ extension PassesServiceCustom {
try encodedPersonalization.write(to: root.appendingPathComponent("personalization.json"))
}

try Self.generateManifestFile(using: encoder, in: root)
try self.generateSignatureFile(in: root)
return try Data(contentsOf: Zip.quickZipFiles([root], fileName: "\(UUID().uuidString).pkpass"))
try self.generateSignatureFile(
for: Self.generateManifestFile(using: encoder, in: root),
in: root
)

let zipFile = tmp.appendingPathComponent("\(UUID().uuidString).pkpass")
try FileManager.default.zipItem(at: root, to: zipFile, shouldKeepParent: false)
defer { _ = try? FileManager.default.removeItem(at: zipFile) }

return try Data(contentsOf: zipFile)
}

/// Generates a bundle of passes to enable your user to download multiple passes at once.
Expand All @@ -521,7 +561,11 @@ extension PassesServiceCustom {
try await self.generatePassContent(for: pass, on: db)
.write(to: root.appendingPathComponent("pass\(i).pkpass"))
}

return try Data(contentsOf: Zip.quickZipFiles([root], fileName: "\(UUID().uuidString).pkpasses"))

let zipFile = tmp.appendingPathComponent("\(UUID().uuidString).pkpasses")
try FileManager.default.zipItem(at: root, to: zipFile, shouldKeepParent: false)
defer { _ = try? FileManager.default.removeItem(at: zipFile) }

return try Data(contentsOf: zipFile)
}
}

0 comments on commit 06fb9ae

Please sign in to comment.