Error 0480006C:PEM routines::no start line When Verifying Apple Signed Payload
I'm currently working on verifying the payload of an Apple App Store Server Notification (JWS) in a Laravel application. The signed payload includes a certificate chain in the x5c header, which I need to validate using OpenSSL. However, I'm getting the following error when trying to verify the signature:
0480006C:PEM routines::no start line
The issue seems to occur when I try to extract the public key from the leaf certificate and then verify the JWS signature.
Here's the flow I'm implementing: Extract the x5c header from the JWS payload to get the certificate chain (leaf, intermediate, root). Convert the base64-encoded certificates from x5c to PEM format using the following helper function:
private function buildPemFromX5c(string $x5c): string
{
return "-----BEGIN CERTIFICATE-----\n" . wordwrap($x5c, 64, "\n", true) . "\n-----END CERTIFICATE-----";
}
Load the public key from the leaf certificate using OpenSSL's openssl_pkey_get_public() function. Verify the JWS signature using openssl_verify():
$isValid = openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256);
Despite these steps, I’m getting the 0480006C:PEM routines::no start line error, which suggests that there's an issue with the format of the certificate or the public key. It seems like the PEM data is not being parsed correctly, or the format of the public key is invalid.
Heres the whole code:
public function signedPayload(Request $request)
{
try {
// Step 1: Get the signed payload from the request
$signedPayload = $request->signedPayload;
// Step 2: Split the signed payload into header, payload, and signature
[$encodedHeader, $encodedPayload, $encodedSignature] = explode('.', $signedPayload);
// Step 3: Decode the header and payload using Base64Url
$decodedHeader = base64_decode($encodedHeader);
$decodedPayload = base64_decode($encodedPayload);
$signature = base64_decode($encodedSignature);
// Step 4: Parse the JWS header
$decodedHeaderData = json_decode($decodedHeader, true);
if (!$decodedHeaderData || !isset($decodedHeaderData['x5c'])) {
throw new Exception('Invalid header: Missing x5c');
}
// Step 5: Get the certificate chain (x5c) from the header
$x5c = $decodedHeaderData['x5c'];
$this->validateCertificateChain($x5c);
// Step 6: Build the PEM format public key from x5c[0] (leaf certificate)
$pem = $this->buildPemFromX5c($x5c[0]);
// Step 7: Extract the public key from the PEM certificate using OpenSSL
$publicKey = openssl_pkey_get_public($pem);
if (!$publicKey) {
throw new Exception('Failed to extract public key from PEM certificate');
}
Log::info('Public Key Loaded: ', ['publicKey' => openssl_pkey_get_details($publicKey)['key']]);
// Step 8: Prepare the data to verify (header + payload)
$dataToVerify = $encodedHeader . '.' . $encodedPayload;
// Step 9: Verify the signature using OpenSSL
$isValid = openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256);
// Step 10: Check if signature is valid
if ($isValid === 1) {
Log::info('Signature is valid');
return response()->json(['status' => 'success', 'message' => 'Payload signature is valid']);
} elseif ($isValid === 0) {
throw new Exception('Signature verification failed');
} else {
throw new Exception('Error during signature verification: ' . openssl_error_string());
}
} catch (Exception $e) {
Log::error('Error validating signed payload: ' . $e->getMessage());
return response()->json(['status' => 'error', 'message' => $e->getMessage()], 400);
}
}
private function buildPemFromX5c(string $x5c): string
{
return "-----BEGIN CERTIFICATE-----\n" . wordwrap($x5c, 64, "\n", true) . "\n-----END CERTIFICATE-----";
}
private function validateCertificateChain(array $x5c): bool
{
$rootCAPath = storage_path('AppleRootCA-G3.pem'); // Path to Apple's root certificate (download it)
$tempDir = sys_get_temp_dir();
$certFiles = [];
foreach ($x5c as $index => $cert) {
$certPath = "{$tempDir}/cert{$index}.pem";
file_put_contents($certPath, $this->buildPemFromX5c($cert));
$certFiles[] = $certPath;
}
// Validate the chain (ensure each cert is signed by the next)
for ($i = 0; $i < count($certFiles) - 1; $i++) {
$currentCert = $certFiles[$i];
$parentCert = $certFiles[$i + 1];
$currentCertDetails = openssl_x509_parse(file_get_contents($currentCert));
$parentCertKey = openssl_pkey_get_public(file_get_contents($parentCert));
if (!openssl_x509_verify(file_get_contents($currentCert), $parentCertKey)) {
throw new Exception("Invalid certificate chain at position {$i}");
}
}
// Verify the root certificate
$lastCert = $certFiles[count($certFiles) - 1];
$isRootValid = openssl_x509_checkpurpose($lastCert, X509_PURPOSE_ANY, [$rootCAPath]);
if (!$isRootValid) {
throw new Exception('Invalid certificate chain: Root CA does not match');
}
Log::info('Root CA matched successfully');
return true;
}
Please or to participate in this conversation.