I'm trying to get a new ACME client working with PHP. I think I'm close but something is not quite right.
This is a request that was generated by the script (my unit test generates new keys on each run):
{"protected":"eyJhbGciOiJFUzI1NiIsImp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwiYWxnIjoiRVMyNTYiLCJ1c2UiOiJzaWciLCJ4IjoiaTVPVWhCUWRsTHhLMHpFM2ttRGVEWGJSdjE0UnFPSU94b1o1ak9RM3FZWT0iLCJ5IjoiN1RPQlNmYjJxQWxyeVVBWVUzUnUwXC82RlZGaHh4NjJ2N3FVV3dCQ1lpVnc9IiwiZCI6Ilo5OExJRUZYcUlJTHd1bno5VlZwV3BPWjhLVEpcL2o0NnFLOHdNV3hLNVRjPSJ9LCJub25jZSI6IjNNM3g5dDhQaGxacGhlUjg5OTU2ZVF4VGdFYWwxNHZLdVNkZ2ljanJQUm41TmxTQ2xpWSIsInVybCI6Imh0dHBzOlwvXC9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmdcL2FjbWVcL25ldy1hY2N0In0","payload":"eyJ0ZXJtc09mU2VydmljZUFncmVlZCI6dHJ1ZSwiY29udGFjdCI6WyJtYWlsdG86Y2VydC1hZG1pbkBleGFtcGxlLm9yZyIsIm1haWx0bzphZG1pbkBleGFtcGxlLm9yZyJdfQ","signature":"MEUCIDCOgXGbLsRRFHrOT-FGfhQHayO5iwywfwZqbCRNL4lfAiEAk4Lb6tFGhe9h6KrMg9PyrrLROqxWuwWKYujeNQbfqaM"}
The response is:
{
"type": "urn:ietf:params:acme:error:malformed",
"detail": "Parse error reading JWS",
"status": 400
}
Here's the relevant PHP code:
public function CreateAccount(){
$this->CreateKeys();
$Content = $this->PrepareAccountRequest();
$this->DoAccountAPICall($Content);
}
private function CreateKeys(){
// Create a new private key
$this->oOpenSSLAsymmetricKey = openssl_pkey_new([
'private_key_type' => OPENSSL_KEYTYPE_EC,
'curve_name' => 'prime256v1'
]);
// Export a human readable key
openssl_pkey_export($this->oOpenSSLAsymmetricKey,$this->sPrivateKey);
// Get key details
$aKeyDetails = openssl_pkey_get_details($this->oOpenSSLAsymmetricKey);
$this->sPublicKey = $aKeyDetails['key'];
$aJSONKeyDetails = [
'kty' => 'EC',
'crv' => 'P-256',
'alg' => 'ES256',
'use' => 'sig',
'x' => base64_encode($aKeyDetails['ec']['x']),
'y' => base64_encode($aKeyDetails['ec']['y']),
'd' => base64_encode($aKeyDetails['ec']['d'])
];
$this->JWK = $aJSONKeyDetails;
}
private function PrepareAccountRequest(){
$Protected = $this->Base64URL(json_encode([
"alg" => "ES256",
"jwk" => $this->JWK,
"nonce" => $this->nonce,
"url" => $this->aEndpoints['newAccount']
]));
$Payload = $this->Base64URL(json_encode([
"termsOfServiceAgreed" => true,
"contact" => [
"mailto:cert-admin@example.org",
"mailto:admin@example.org"
]
]));
$Signature = $this->Base64URL($this->Sign($Protected.'.'.$Payload));
$Content = json_encode([
'protected' => $Protected,
'payload' => $Payload,
'signature' => $Signature
]);
return $Content;
}
private function Sign($Content){
$BinarySignature = ''; // If the call was successful the signature is returned in $BinarySignature.
openssl_sign($Content, $BinarySignature, $this->oOpenSSLAsymmetricKey, OPENSSL_ALGO_SHA256);
// Test the signature using the public key.
// Returns 1 if the signature is correct, 0 if it is incorrect, and -1 or false on error.
if(1 === openssl_verify($Content, $BinarySignature, $this->sPublicKey, OPENSSL_ALGO_SHA256)){
return $BinarySignature;
}else{
return false;
}
}
private function DoAccountAPICall($Content){
$ch = curl_init();
$Headers = [
'Cache-Control: no-cache',
'Content-Type: application/jose+json',
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $Headers);
curl_setopt($ch, CURLOPT_URL, $this->aEndpoints['newAccount']);
curl_setopt($ch, CURLOPT_HEADER, false); // Don't include header in the output.
curl_setopt($ch, CURLOPT_POST, true); // Do a POST Request.
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Output the response from curl_exec
curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, 'HandleHeaderLine')); // Store Header details.
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // Timeout connect at five seconds
curl_setopt($ch, CURLOPT_DNS_CACHE_TIMEOUT, 300); // Cache DNS for 5 minuites.
curl_setopt($ch, CURLOPT_POSTFIELDS, $Content);
$Response = curl_exec($ch);
if(curl_error($ch)) {
echo 'A CURL Error Occurred.'.curl_error($ch);
}
curl_close($ch);
echo '<h1>JSON Response</h1>';
$aJSONResponse = json_decode($Response,true);
var_dump($aJSONResponse);
}
private function Base64URL($Input){
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($Input));
}