Hi metaclassing,
Tell me if I’m wrong : if there really is a problem with de way I create and submit JWS with the CSR as payload, any CSR would give the same error. But I can issue certificate for each name alone… So I don’t understand. I keep looking for differences between our two requests. Thanks for your help.
Here is my post request body :
{"header":{"alg":"RS256","jwk":{"kty":"RSA","n":"vUTInXcMeB4BNwFsIL6ZyTS3uU5HtQhwq9K3p2BBow4KumNXenavDageh40RuAp15sImDovKSC-dB4PtgCr8xg9389mQ06_MqvtF846JcocJnAbgdU3Fa503kgKkvO7gZEvjkXcQgkZuQ3NhQ2iyfSKeHEVXB5jcCPzVdoTqJrC3xcP3TXIhEIw5EN1JT9gaskm3xVY51s6nsQx-y51yOeFyytWLeefQMzlTLimYlF2xMtv5aoCA5xst5kfXXcwi13fAP2MdUtHQUoO8ogUT2R4c3VVOi6HPCxS4fNICDk3xR_8CUXCxMY7LUUW_Rx0Vf-J5Y32LH5DsxjbN24NmGQ","e":"AQAB"}},"protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoidlVUSW5YY01lQjRCTndGc0lMNlp5VFMzdVU1SHRRaHdxOUszcDJCQm93NEt1bU5YZW5hdkRhZ2VoNDBSdUFwMTVzSW1Eb3ZLU0MtZEI0UHRnQ3I4eGc5Mzg5bVEwNl9NcXZ0Rjg0Nkpjb2NKbkFiZ2RVM0ZhNTAza2dLa3ZPN2daRXZqa1hjUWdrWnVRM05oUTJpeWZTS2VIRVZYQjVqY0NQelZkb1RxSnJDM3hjUDNUWEloRUl3NUVOMUpUOWdhc2ttM3hWWTUxczZuc1F4LXk1MXlPZUZ5eXRXTGVlZlFNemxUTGltWWxGMnhNdHY1YW9DQTV4c3Q1a2ZYWGN3aTEzZkFQMk1kVXRIUVVvTzhvZ1VUMlI0YzNWVk9pNkhQQ3hTNGZOSUNEazN4Ul84Q1VYQ3hNWTdMVVVXX1J4MFZmLUo1WTMyTEg1RHN4amJOMjRObUdRIiwiZSI6IkFRQUIifSwibm9uY2UiOiJJNTR6Z2Q1TWlHUEt6Q1RUVXZpOTdRcFBRSzJoVDFwaHQ5VjJwZkdQckpzIn0","payload":"eyJyZXNvdXJjZSI6Im5ldy1jZXJ0IiwiY3NyIjoiTUlJQzlEQ0NBZDRDQVFBd0tERW1NQ1FHQTFVRUF3d2RjbVZ6WlhKMllYUnBiMjR1ZG1Gc2JHVmxMVzExYm5OMFxyXG5aWEl1WlhVd2dnRWdNQXNHQ1NxR1NJYjNEUUVCQVFPQ0FROEFNSUlCQ2dLQ0FRRUFyMnZmdHpwc2ZsM3J5dDQ0XHJcblphTGFUMDRuZ3VnU2Y3NkY0Z2lkTXNaZmk2MHlHSVJ6dmJuWm82djh0MlJBdG1ES3lsc2F4SXlrSWQ0QS12MXVcclxuSlFIbDBOUm9nY1RIaGFQZkhQbWRpZW1SeVR2cmRnOEdWWDJBS2h1cHJSWEhId3BqOER3Ui1GV0xpcEVmZVNySVxyXG5QVzFteTVvSU9MajhXdFU0V3NQUEpJZXlaTnZsVjBNZkItX1N4SnZScHJpNEoxWllOdTNCSkxKYXFEZ2RqcUV6XHJcblFsbXpHMkJ3TjBwYUxJTUVGNzlyTlc1Nnc1Tzl3dEFyQWFlSEIweVpmSGo5c3BKYjBrWE1JN1lJN2tHNGxJbjFcclxuS2lrbFU5MHVFdDdNUjlEaFhEVmhpU2pBSUNjSndORDJheV9MMmNSd3lneGViUE9nRWFnbkNPMGVRZ3daRWU4SFxyXG45RTNGeXdJREFRQUJvSUdLTUlHSEJna3Foa2lHOXcwQkNRNHhlakI0TUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQXHJcbkJBUURBZ1hnTUY0R0ExVWRFUVJYTUZXQ0hYSmxjMlZ5ZG1GMGFXOXVMblpoYkd4bFpTMXRkVzV6ZEdWeUxtVjFcclxuZ2hsaWIyOXJhVzVuTG5aaGJHeGxaUzF0ZFc1emRHVnlMbVYxZ2hsaWRXTm9kVzVuTG5aaGJHeGxaUzF0ZFc1elxyXG5kR1Z5TG1WMU1Bc0dDU3FHU0liM0RRRUJDd09DQVFFQVRrajNwTlYzY3g5blVzNDl4MUFuN0k2T1BPNHNMbDNBXHJcblBaZjYxS0V2S2gxcThFOEZQZThQdFE4UENDVkFzZER3RGQxRjhlMFp5SUJEODdYRnRBWW5mbzBjX3BBbTJ1emtcclxuTEQxZC1hNk41T3pHWmZSWFA5MzhUdTBwcUIwT2xZOWwzV0x5SzdJMEFObFotNzh3a09YU0tfRmNUakttQmFCbVxyXG5yakJzQTcxU3BnZGxRVHQ3RzVKMmpvOEQ0ejE0cTQzS1JvZ18wQU9lelFac3FqQ3FpMzl5YlJMSW9wWW9NU29PXHJcbkFFbFZ6OGY2VFlPN1pia3NBQnBVLUlRYmFIZ3AtVHMtdEFxOTNaaGNrNEIyLWxKYlY2ZXgzRDJyYVFLT1ZERk1cclxudjlUWUNUU2FCTllfVUMzX0d6c2ZuUEgwMEc1OWl5Ui00WklIOVMyZUd2MVc5VlFORTRqR0d3In0","signature":"lSTPEVkxGs8r36IQ2bvFPgU6vrPqONJzHPeqfg6aK0O0bPJEiulqrw_6zuF7yo1D-CvZya6eZ9hBc7VWr99C4v9a8yy83EU3N8BxAMlHcjpH4jVMgijvrGDR4_-UXtb3IYNNkxaYbCVczsymxfaRlgw5ZN8saLiqL9RK9opcaUPEWJyP3DZi9N9Z7L8ZjjcI28MvA8f3vToes0DM4L5tXi_90okNwLhhDQS4Owy1GONlKQuC1uu5uUw3y8bvC1d6MkRqOvdC-TSrfFBNo4wQXyEsNb2TzZRsusema8AZX_6f8gOfZuo-TzqE-GddocYfg_cGbgrrTZRCZHOIKHKuYA"}
Here is my acme class code :
<?php
namespace admin\certification;
class acme
{
protected $key; // openSSL private key ressource
protected $url; // acme service base URL
protected $urls; // array of services URLs
protected $nonce; // last returned replay nonce
protected $code; // last returned HTTP code
protected $header; // associative array of last returned HTTP header attributes
protected $body; // last returned HTTP body
/**
* @param resource $key an openSSL private key resource
* @param string $url base acme service URL
**/
public function __construct($key,$url)
{
$this->key = $key;
$this->url = rtrim($url,'/');
$this->_get($this->url.'/directory');
$this->urls = $this->body;
}
public static function ACMEBase64url($str)
{
return rtrim(strtr(base64_encode($str),'+/','-_'),'=');
}
public static function PEM($str)
{
return sprintf("-----BEGIN CERTIFICATE-----\n%s-----END CERTIFICATE-----\n",chunk_split(base64_encode($str),64,"\n"));
}
/**
* return a JWS containing $payload data and signed with the private key
* @param string $payload the payload of the JWS
* @return string JWS
**/
protected function _sign($payload)
{
$details = openssl_pkey_get_details($this->key);
$header = array(
'alg' => 'RS256',
'jwk' => array(
'kty' => 'RSA',
'n' => self::ACMEBase64url($details['rsa']['n']),
'e' => self::ACMEBase64url($details['rsa']['e']),
)
);
$protected = $header;
$protected['nonce'] = $this->nonce;
$payload64 = self::ACMEBase64url(str_replace('\\/', '/', json_encode($payload)));
$protected64 = self::ACMEBase64url(json_encode($protected));
openssl_sign($protected64.'.'.$payload64,$signed,$this->key,OPENSSL_ALGO_SHA256);
$signed64 = self::ACMEBase64url($signed);
$data = array(
'header' => $header,
'protected' => $protected64,
'payload' => $payload64,
'signature' => $signed64
);
return json_encode($data);
}
/**
* fill the header property with indexed array
* @param string $header row HTTP header
**/
protected function _parse_header($header)
{
$this->header = array();
$lines = explode("\r\n",trim($header));
foreach ($lines as $line)
{
if (preg_match('|^([a-z-]+)\h*:\h*(.*)$|i',$line,$matches))
{
$key = strtolower($matches[1]);
if (array_key_exists($key,$this->header))
{
if (!is_array($this->header[$key]))
$this->header[$key] = array($this->header[$key]);
$this->header[$key][] = $matches[2];
}
else
$this->header[$key] = $matches[2];
}
elseif ($line === '')
$this->header = array();
}
}
protected function _url($service)
{
if (array_key_exists($service,$this->urls))
return $this->urls[$service];
else
return $this->url.$service;
}
/**
* Issue a HTTP GET request and fill the code, nonce, header and body properties
* @param string $service relative URL or index of urls property
**/
protected function _get($url,$headers = array('Content-Type: application/json','Accept: application/json'))
{
$handle = curl_init($url);
curl_setopt($handle,CURLOPT_HTTPHEADER,$headers);
curl_setopt($handle,CURLOPT_RETURNTRANSFER,true);
curl_setopt($handle,CURLOPT_HEADER,true);
$response = curl_exec($handle);
if(curl_errno($handle))
throw new Exception('Curl: '.curl_error($handle));
$this->code = curl_getinfo($handle, CURLINFO_HTTP_CODE);
$header_size = curl_getinfo($handle,CURLINFO_HEADER_SIZE);
$this->_parse_header(substr($response,0,$header_size));
$this->nonce = (array_key_exists('replay-nonce',$this->header))?$this->header['replay-nonce']:null;
print_r($this->header);
if ($this->header['content-type'] === 'application/json')
{
$this->body = json_decode(substr($response,$header_size),true);
print_r($this->body);
}
else
$this->body = substr($response,$header_size);
curl_close($handle);
}
protected function _post($url,$data,$headers = array('Accept: application/json'))
{
$headers[] = 'Content-Type: application/json';
$handle = curl_init($url);
curl_setopt($handle,CURLOPT_HTTPHEADER,$headers);
curl_setopt($handle,CURLOPT_RETURNTRANSFER,true);
curl_setopt($handle,CURLOPT_HEADER,true);
curl_setopt($handle,CURLOPT_POST,true);
curl_setopt($handle,CURLOPT_POSTFIELDS,$data);
$response = curl_exec($handle);
if(curl_errno($handle))
throw new Exception('Curl: '.curl_error($handle));
$this->code = curl_getinfo($handle, CURLINFO_HTTP_CODE);
$header_size = curl_getinfo($handle,CURLINFO_HEADER_SIZE);
$this->_parse_header(substr($response,0,$header_size));
$this->nonce = (array_key_exists('replay-nonce',$this->header))?$this->header['replay-nonce']:null;
print_r($this->header);
if ($this->header['content-type'] === 'application/json')
{
$this->body = json_decode(substr($response,$header_size),true);
print_r($this->body);
}
else
$this->body = substr($response,$header_size);
curl_close($handle);
}
public function code()
{
return $this->code;
}
public function header($name)
{
return $this->header[strtolower($name)] ?? null;
}
public function body()
{
return $this->body;
}
public function register($infos)
{
$request = array(
'resource' => 'new-reg',
'contact' => $infos);
$this->_post($this->_url('new-reg'),$this->_sign($request));
$url = $this->header['location'];
$request = array(
'resource' => 'reg',
'contact' => $infos
);
$this->_post($url,$this->_sign($request));
if (array_key_exists('link',$this->header))
{
if (is_array($this->header['link']))
{
foreach ($this->header['link'] as $link)
{
if (preg_match('|^<([^>]+)>;rel="([^"]+)"$|',$link,$matches) && ($matches[2] === 'terms-of-service'))
{
$request['agreement'] = $matches[1];
break;
}
}
}
elseif (preg_match('|^<([^>]+)>;rel="([^"]+)"$|',$this->header['link'],$matches) && ($matches[2] === 'terms-of-service'))
$request['agreement'] = $matches[1];
$this->_post($url,$this->_sign($request));
}
}
public function authorize($domain)
{
$request = array(
'resource' => 'new-authz',
'identifier' => array(
'type' => 'dns',
'value' => $domain
)
);
$this->_post($this->_url('new-authz'),$this->_sign($request));
// return DateTime::createFromFormat('Y-m-d\TH:i:s\.') validité de l'autorisation
}
public function getChallengesList()
{
if (($this->code === 201) && array_key_exists('challenges',$this->body))
return $this->body['challenges'];
return array();
}
public function getChallenge($type)
{
if (($this->code !== 201) || !array_key_exists('challenges',$this->body))
throw new Exception('no challenge have been given');
foreach ($this->body['challenges'] as $challenge)
{
if ($challenge['type'] === $type)
break;
}
if ($challenge['type'] !== $type)
throw new Exception('no challenge of that type');
$details = openssl_pkey_get_details($this->key);
$challenge['keyAuthorization'] = $challenge['token'].'.'.self::ACMEBase64url(hash('sha256',sprintf('{"e":"%s","kty":"RSA","n":"%s"}',self::ACMEBase64url($details['rsa']['e']),self::ACMEBase64url($details['rsa']['n'])),true));
return $challenge;
}
public function faceChallenge($challenge)
{
$request = array(
'resource' => 'challenge',
'type' => $challenge['type'],
'keyAuthorization' => $challenge['keyAuthorization']
);
$this->_post($challenge['uri'],$this->_sign($request));
}
public function challengeStatus($uri)
{
$this->_get($uri);
return $this->body;
}
/**
* issue a new cert from a PEM format Certificate Signing Request
**/
public function issue($csr)
{
if (preg_match('|^[^\r\n]+[\r\n]+([a-z0-9/+]+(?:[\r\n]+[a-z0-9/+]+)*)|mi',$csr,$matches))
{
$request = array(
'resource' => 'new-cert',
'csr' => strtr($matches[1],'+/','-_')
);
$this->_post($this->_url('new-cert'),$this->_sign($request),array('Accept: application/pkix-cert'));
}
else
throw new Exception('error converting CSR PEM encoding to special base64url ACME encoding.');
}
public function getCertificate($uri)
{
$this->_get($uri,array('Accept: application/pkix-cert'));
}
public function revoke($crt)
{
if (preg_match('|^[^\r\n]+[\r\n]+([a-z0-9/+]+(?:[\r\n]+[a-z0-9/+]+)*)|mi',$crt,$matches))
{
$request = ['resource' => 'revoke-cert','certificate' => strtr($matches[1],'+/','-_')];
$this->_post($this->_url('revoke-cert'),$this->_sign($request));
}
else
throw new Exception('error converting certificate PEM encoding to special base64url ACME encoding.');
}
}