如何在ColdFusion中实现Apple Wallet Passkit优惠券签名与打包?
I’ve been struggling to find clear guidance on how to sign and package Apple Wallet Passkit coupons in ColdFusion—most Google searches lead to dead ends or unanswered questions. I do have a fully functional PHP implementation for this workflow, and I’m hoping to adapt this logic to ColdFusion. Reusable Java libraries would be especially helpful here, so if anyone has experience with this, please share your approach!
My Working PHP Implementation Reference
//load .p12 certificate $PKCS12 = file_get_contents($Certificates['Certificate']); $certs = array(); if(openssl_pkcs12_read($PKCS12, $certs, $Certificates['CertPassword']) == true) { $certdata = openssl_x509_read($certs['cert']); $privkey = openssl_pkey_get_private($certs['pkey'], $Certificates['CertPassword']); } //sign file hashes with AppleWWDRCA certificate openssl_pkcs7_sign($ManifestPath, $SignaturePath, $certdata, $privkey, array(), PKCS7_BINARY | PKCS7_DETACHED, $Certificates['AppleWWDRCA']); $ManifestSignature = file_get_contents($SignaturePath); $ManifestSignatureDER = PEM2DER($ManifestSignature); //put files (and strings as files) in a zip archive $ZIP = new ZipArchive(); $ZIP->open($PKPassPath, ZIPARCHIVE::CREATE); $ZIP->addFromString('signature', $ManifestSignatureDER); $ZIP->addFromString('manifest.json', $Manifest); $ZIP->addFromString('pass.json', $JSON); foreach($ImageFiles as $ImagePath) { $ImageName = basename($ImagePath); $ZIP->addFile($ImagePath, $ImageName); } $ZIP->close(); //load pass data und delete temp files (if debug mode is off) $Pass['data'] = file_get_contents($PKPassPath); $Pass['size'] = filesize($PKPassPath); $Pass['name'] = $PassName; function echoPass($Pass) { //send http headers and zip archive content to client header('Pragma: no-cache'); header('Content-type: application/vnd.apple.pkpass'); header('Content-length: '.$Pass['size']); header('Content-Disposition: attachment; filename="'.$Pass['name'].'.pkpass"'); echo $Pass['data']; }
ColdFusion Implementation Approach
Since ColdFusion runs on Java, we can leverage Java’s security and zip libraries to replicate the PHP workflow. Here’s a step-by-step breakdown with code examples:
1. Load PKCS12 Certificate & Apple WWDR CA Certificate
ColdFusion can use java.security.KeyStore to load the PKCS12 file, which contains your private key and developer certificate. You’ll also need the Apple WWDR CA certificate (in DER or PEM format).
<cfscript> // Paths to your certificates var pkcs12Path = "/path/to/your/certificate.p12"; var pkcs12Password = "your-cert-password"; var wwdrCertPath = "/path/to/AppleWWDRCA.cer"; // Load PKCS12 keystore var keyStore = createObject("java", "java.security.KeyStore").getInstance("PKCS12"); var fileInputStream = createObject("java", "java.io.FileInputStream").init(pkcs12Path); keyStore.load(fileInputStream, pkcs12Password.toCharArray()); fileInputStream.close(); // Get private key and developer certificate var alias = keyStore.aliases().nextElement(); // Assuming single entry in PKCS12 var privateKey = keyStore.getKey(alias, pkcs12Password.toCharArray()); var devCertificate = keyStore.getCertificate(alias); // Load Apple WWDR CA certificate var certFactory = createObject("java", "java.security.cert.CertificateFactory").getInstance("X.509"); var wwdrInputStream = createObject("java", "java.io.FileInputStream").init(wwdrCertPath); var wwdrCertificate = certFactory.generateCertificate(wwdrInputStream); wwdrInputStream.close(); </cfscript>
2. Generate Manifest.json
First, create a manifest file that contains SHA1 hashes of all files in the pass (pass.json, images, etc.).
<cfscript> var manifest = {}; var passJsonContent = '{"formatVersion":1, "passTypeIdentifier":"your-pass-type-id", "serialNumber":"unique-serial", ...}'; // Your pass JSON var imagePaths = ["/path/to/icon.png", "/path/to/logo.png"]; // List of pass images // Add hash for pass.json var md5 = createObject("java", "java.security.MessageDigest").getInstance("SHA1"); var passJsonBytes = charsetDecode(passJsonContent, "UTF-8"); md5.update(passJsonBytes); manifest["pass.json"] = lcase(binaryEncode(md5.digest(), "hex")); // Add hashes for images for (var imgPath in imagePaths) { var imgFile = createObject("java", "java.io.File").init(imgPath); var imgInputStream = createObject("java", "java.io.FileInputStream").init(imgFile); var buffer = createObject("java", "byte[]").init(8192); var bytesRead = 0; md5.reset(); while ((bytesRead = imgInputStream.read(buffer)) != -1) { md5.update(buffer, 0, bytesRead); } imgInputStream.close(); manifest[getFileFromPath(imgPath)] = lcase(binaryEncode(md5.digest(), "hex")); } // Convert manifest to JSON string var manifestJson = serializeJson(manifest); </cfscript>
3. Sign the Manifest (PKCS7 Detached Signature)
To replicate openssl_pkcs7_sign, we recommend using the BouncyCastle library (simpler for PKCS7 handling). You’ll need to add the BouncyCastle JCE provider JAR to your ColdFusion WEB-INF/lib directory and restart the server.
<cfscript> // Initialize BouncyCastle provider var security = createObject("java", "java.security.Security"); security.addProvider(createObject("java", "org.bouncycastle.jce.provider.BouncyCastleProvider").init()); // Prepare manifest bytes var manifestBytes = charsetDecode(manifestJson, "UTF-8"); // Create PKCS7 signed data var generator = createObject("java", "org.bouncycastle.cms.CMSSignedDataGenerator").init(); var signerInfoGeneratorBuilder = createObject("java", "org.bouncycastle.cms.SignerInfoGeneratorBuilder").init( createObject("java", "org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder").init().build() ); var signerGenerator = signerInfoGeneratorBuilder.build( createObject("java", "org.bouncycastle.operator.jcajce.JcaContentSignerBuilder").init("SHA1withRSA").build(privateKey), createObject("java", "org.bouncycastle.cert.jcajce.JcaX509CertificateHolder").init(devCertificate) ); generator.addSignerInfoGenerator(signerGenerator); // Add Apple WWDR CA certificate as additional certificate generator.addCertificate(createObject("java", "org.bouncycastle.cert.jcajce.JcaX509CertificateHolder").init(wwdrCertificate)); // Generate detached signature var cmsSignedData = generator.generate( createObject("java", "org.bouncycastle.cms.CMSProcessableByteArray").init(manifestBytes), true // Detached signature ); // Get DER-encoded signature (matches PHP's PEM2DER output) var signatureDER = cmsSignedData.getEncoded(); </cfscript>
4. Package into PKPass Zip File
Use Java’s ZipOutputStream to create the .pkpass archive, adding all required files:
<cfscript> var pkpassPath = "/path/to/output.pkpass"; var fileOutputStream = createObject("java", "java.io.FileOutputStream").init(pkpassPath); var zipOutputStream = createObject("java", "java.util.zip.ZipOutputStream").init(fileOutputStream); // Helper function to add bytes to zip function addToZip(zipStream, entryName, contentBytes) { var entry = createObject("java", "java.util.zip.ZipEntry").init(entryName); zipStream.putNextEntry(entry); zipStream.write(contentBytes); zipStream.closeEntry(); } // Add signature file addToZip(zipOutputStream, "signature", signatureDER); // Add manifest.json addToZip(zipOutputStream, "manifest.json", charsetDecode(manifestJson, "UTF-8")); // Add pass.json addToZip(zipOutputStream, "pass.json", charsetDecode(passJsonContent, "UTF-8")); // Add images for (var imgPath in imagePaths) { var imgFile = createObject("java", "java.io.File").init(imgPath); var imgInputStream = createObject("java.io.FileInputStream").init(imgFile); var imgBytes = createObject("java", "org.apache.commons.io.IOUtils").toByteArray(imgInputStream); // Use Commons IO or read manually imgInputStream.close(); addToZip(zipOutputStream, getFileFromPath(imgPath), imgBytes); } // Close zip stream zipOutputStream.close(); fileOutputStream.close(); </cfscript>
5. Serve the PKPass to the Client
Finally, send the correct headers and output the PKPass file content to the user:
<cfscript> var passFile = createObject("java", "java.io.File").init(pkpassPath); var passSize = passFile.length(); var passName = "MyCoupon"; // Set response headers cfheader(name="Pragma", value="no-cache"); cfheader(name="Content-Type", value="application/vnd.apple.pkpass"); cfheader(name="Content-Length", value=passSize); cfheader(name="Content-Disposition", value='attachment; filename="#passName#.pkpass"'); // Output the file cfcontent(file=pkpassPath, type="application/vnd.apple.pkpass", deletefile=false); </cfscript>
Key Notes
- BouncyCastle Dependency: The PKCS7 signing step is much simpler with BouncyCastle. Download the BouncyCastle JCE provider (bcprov-jdk15on.jar) and place it in your ColdFusion
WEB-INF/libdirectory, then restart the server. - Certificate Formats: Ensure your Apple WWDR CA certificate is in DER format (convert from PEM if needed using
openssl x509 -in AppleWWDRCA.pem -out AppleWWDRCA.cer -outform DER). - Hash Algorithm: Apple requires SHA1 hashes for the manifest.json file (even though SHA1 is deprecated, this is still required for Passkit).
内容的提问来源于stack exchange,提问作者M.A

