hiimtmac / pass-kit

Apple Wallet Pass helpers
4 stars 0 forks source link

Running into issue with getting a signature using PassGen. #15

Closed mohalibou closed 2 months ago

mohalibou commented 2 months ago

I have been trying to use func signatureData(manifest: Data, cert: Data, key: Data) throws -> Data to get a signature for my pass. However, when I attempt to use it, I get this error:

Failed to create pass: ASN1Error.invalidPEMDocument: could not find PEM start marker SwiftASN1/PEMDocument.swift:135

I realize that this error comes from the swift-asn1 package, although I figured it was needed to be mentioned here.

For reference, this is the a snippet of the code I am using:

var generator = try PassGenerator()

guard let icon1x = Bundle.main.url(forResource: "icon", withExtension: "png")?
    .dataRepresentation else { throw PassGenerationError.missingIcon }
guard let icon2x = Bundle.main.url(forResource: "icon@2x", withExtension: "png")?
    .dataRepresentation else { throw PassGenerationError.missingIcon }
guard let icon3x = Bundle.main.url(forResource: "icon@3x", withExtension: "png")?
    .dataRepresentation else { throw PassGenerationError.missingIcon }

try generator.add(pass: pass)
try generator.add(image: icon1x, as: .icon(.x1))
try generator.add(image: icon2x, as: .icon(.x2))
try generator.add(image: icon3x, as: .icon(.x3))

let manifest = try generator.manifestData()
try generator.add(manifest: manifest)

guard let certificate = Bundle.main.url(forResource: "Certificate", withExtension: "pem")?
    .dataRepresentation else { throw PassGenerationError.missingCertificate }
guard let key = Bundle.main.url(forResource: "Key", withExtension: "pem")?
    .dataRepresentation else { throw PassGenerationError.missingKey }

let signature = try generator.signatureData(manifest: manifest, cert: certificate, key: key) // Where I get the error.
try generator.add(signature: signature)

let archive = try generator.archiveData()

I'm guessing this has something to do with the certificates I'm using? They should be fine though, so I'm confused where the problem may be arising.

hiimtmac commented 2 months ago

Hi there! Yeah my guess would be that given the error is from swift-asn1 it probably is a bad formatted cert/key.

What does your certificate and key look like? They should be formatted like this example:

cert.pemkey.pem
``` -----BEGIN CERTIFICATE----- blahblahblahblahblahblahblahblah blahblahblahblahblahblahblahblah ... blahblahblah -----END CERTIFICATE----- ``` ``` -----BEGIN PRIVATE KEY----- blahblahblahblahblahblahblahblah blahblahblahblahblahblahblahblah ... blahblahblah -----END PRIVATE KEY----- ```

One thing to note is that the cert/key does not (right now) support being exported with a password so that could also be the problem (but given the error you are getting, less likely the culprit). Does this help at all?

mohalibou commented 2 months ago

This is how it appears to me:

Certificate.pem:

Bag Attributes
    friendlyName: Pass Type ID: pass.com.blahblah.blahblah
    localKeyID: 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 
subject=UID=pass.com.blahblah.blahblah, CN=Pass Type ID: pass.com.blahblah.blahblah OU=A1B2C3D4E5, O=My Name, C=US
issuer=CN=Apple Worldwide Developer Relations Certification Authority, OU=G4, O=Apple Inc., C=US
-----BEGIN CERTIFICATE-----
blahblahblahblahblahblahblahblah
blahblahblahblahblahblahblahblah
...
blahblahblah
-----END CERTIFICATE-----

Key.pem:

Bag Attributes
    friendlyName: My Name
    localKeyID: 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
blahblahblahblahblahblahblahblah
blahblahblahblahblahblahblahblah
...
blahblahblah
-----END PRIVATE KEY-----

I did change my code a bit. The error that gets printed to me is a bit different now, as it doesn't reference swift-asn1, but still shows me invalidPEMDocument.

I obtained the data from the below code:

var generator = try PassGenerator()

guard let certificateURL = Bundle.main.url(forResource: "Certificate", withExtension: "pem") else { /* ... */ }
guard let keyURL = Bundle.main.url(forResource: "Key", withExtension: "pem") else { /* ... */ }

let certificateData = try Data(contentsOf: certificateURL)
let keyData = try Data(contentsOf: keyURL)

if let certificateString = String(data: certificateData, encoding: .utf8) { print(certificateString) }
if let keyString = String(data: keyData, encoding: .utf8) { print(keyString) }

I know the certificate and key work, as I've tried signing a pass manually, and it works without issue:

https://github.com/user-attachments/assets/5de11780-9d7f-46b2-aa47-0cf104282b5a

hiimtmac commented 2 months ago

Could you try the following to see if we can figure out why its not happy?

import Foundation
import X509
import _CryptoExtras

do {
  let certUrl = Bundle.main.url(forResource: "Certificate", withExtension: "pem")!
  let certData = try Data(contentsOf: certUrl)
  let certString = String(decoding: certData, as: UTF8.self)
  let cert = try Certificate(pemEncoded: certString)
} catch {
  print("Cert error: \(error)") // is this the error, if so what?
}

do {
  let keyUrl = Bundle.main.url(forResource: "Key", withExtension: "pem")!
  let keyData = try Data(contentsOf: keyUrl)
  let keyString = String(decoding: keyData, as: UTF8.self)
  let key = try _RSA.Signing.PrivateKey(pemRepresentation: keyString)
} catch {
  print("Key error: \(error)") // is this the error, if so what?
}
mohalibou commented 2 months ago

This is what I get:

Key error: invalidPEMDocument

So it looks like this is an issue with the key. I'm not sure if this will be helpful, but these are the steps I went through to get my key:

  1. I took my .cer file and converted it to a .p12 file with Keychain Access.
  2. I used the .p12 file to create the Certificate.pem and Key.pem files using the below terminal commands:
    • openssl pkcs12 -in Certificates.p12 -out Certificate.pem -clcerts -nokeys -legacy
    • openssl pkcs12 -in Certificates.p12 -out Key.pem -nocerts -nodes -legacy
hiimtmac commented 2 months ago

So it looks like this is an issue with the key

Ok glad we are narrowing it down a bit!

  • openssl pkcs12 -in Certificates.p12 -out Key.pem -nocerts -nodes -legacy

Could you try without the -nodes argument for generating the key out of the p12? Just looking at the notes I made for myself for when I need to regenerate both, seems I do not have the -nodes option in there. The other option to try would be to explicitly generate with an empty password also -passout pass:.

Please let me know if either of these help! Otherwise we can keep digging.

mohalibou commented 2 months ago

Here are the commands I tried used:

openssl pkcs12 -in Certificates.p12 -out Key.pem -nocerts -legacy -passout pass:

openssl pkcs12 -in Certificates.p12 -out Key.pem -nocerts -legacy

I added the file to my project, and I still run into the same error:

Key error: invalidPEMDocument

BTW, I don't know if this is helpful, but the -nodes option is so that I don't have to enter a PEM pass phrase when creating the file. The private key won't be encrypted, and instead be stored in plain text.

hiimtmac commented 2 months ago

Ok last thought before I try doing a deep dive is to just remove all the content before the -----BEGIN PRIVATE KEY----- in the Key.pem file.

Ie remove this:

Bag Attributes
    friendlyName: My Name
    localKeyID: 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 
Key Attributes: <No Attributes>

If that doesn't work ill start trying to repro with a new cert on my end.

mohalibou commented 2 months ago

Removing this fixed the issue.

Unrelated but, I am still a bit new to using passes. After getting the zip data using generator.archiveData(), how would I go about saving it as a .pkpass file and distributing it? And giving the user the option to add the pass to their wallet?

hiimtmac commented 2 months ago

A pkpass is just a zip archive with a different extension (as long as the contents follow the spec format). So all you need to do is take the data returned from archiveData() and save it to a file with the .pkpass extensions:

let data = try generator.archiveData()
let url = URL(fileURLWithPath: "/users/.../mypass.pkpass")
try data.write(to: url) // now you have the pass on your filesystem at that url

Or you can take that data and add it to the user wallet in an iOS app:

import PassKit

let data = try generator.archiveData()
let pass = PKPass(data: data)

let vc = PKAddPassesViewController(pass: pass)!
present(vc, animated: true) // this will show the ui to add the pass to wallet from your app

Or you can just send it as an email attachment or via messages and users can add to wallet from there. Your server could return it from a route (or it could be a link on a webpage that triggers the download), just make sure you set the content type header Content-Type: application/vnd.apple.pkpass and then the system should know what to do with it.

Hope this helps!

mohalibou commented 2 months ago

Thank you for all your help. Wasn't expecting the replies to be so quick lol.

The guidance was much appreciated. šŸ™‚šŸ‘