I could use some help with implementing the JSON Web Signature (JWS) and JSON Web Key (JWK) portions of the ACME protocol with PHP.

My specific questions/issues are:

  1. The ACME documentation suggests ES256 as the algorithm, but hash_hmac doesn't seem to have that as an option. What function in PHP can use ES256 --OR-- what option will ACME allow that hash_hmac can create?

  2. What values are acceptable for creating a JSON Web Key? Is this a cryptographic function, or a random string of text? What is the lifespan of the JWK value? ie. is it a "password" that needs to be used for years, for the whole life of the ACME account? Or is it disposable for each session?

  3. Does the $key, third argument to hash_hmac, need to correlate with the JWK in some way, or are they separate and arbitrary? What is the lifespan of the hash key? ie. is it a key that needs to be used for years, for the whole life of the ACME account? Or is it disposable for each session?

  4. What function(s) should be used to generate the hash_hmac key value and the JWK values?

  5. Does openssl_sign() with OPENSSL_ALGO_SHA256 implement ES256 (ECDSA)?

Here is the skeleton code that is successfully making requests but getting "malformed JWS" errors:

   public function CreateAccount(){
      
      $Content = $this->PrepareAccountRequest();
      $this->DoAccountAPICall($Content);
      
   }   
   
   
   private function PrepareAccountRequest(){
   
      $Protected = $this->Base64URL(json_encode([
         "alg" => "ES256",
         "jwk" => 'whatisthis',
         "nonce" => $this->nonce,
         "url" => $this->aEndpoints['newAccount']
      ]));
      
      $Payload = $this->Base64URL(json_encode([
         "termsOfServiceAgreed" => true,
         "contact" => [
            "mailto:[email protected]",
            "mailto:[email protected]"
         ]
      ]));
      
      $Signature = hash_hmac('sha256', $Protected . "." . $Payload, 'fsdjjkdlsdjlkasdf', false);
      
      $Content = json_encode([
         'protected' => $Protected,
         'payload' => $Payload,
         'signature' => $Signature
      ]);
      return $Content;
   }
   
   
   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')); 
      
      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 Occured.'.curl_error($ch);
      }
      curl_close($ch);
      
      echo '<h1>Headers</h1>';
      echo PP($this->aLastHeaders);
      echo '<h1>Response</h1>';
      echo $Response;
      echo '<h1>JSON Response</h1>';
      $aJSONResponse = json_decode($Response,true);
      PP($aJSONResponse);
   }
   
   
   private function Base64URL($Input){
      return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($Input));      
   }
   
   private $aLastHeaders = array();
   private function HandleHeaderLine( $curl, $header_line ) {      
      $this->aLastHeaders[] = $header_line;
      return strlen($header_line);
   }

UPDATE:

I think I'm close to making this work, but I don't think the parts are fitting together quite right.

    public function CreateKeys(){
      
      // Create a new private key
      $this->oOpenSSLAsymmetricKey = openssl_pkey_new([
         'private_key_type' => OPENSSL_KEYTYPE_EC,
         'curve_name' => 'prime256v1'
      ]);
      
     
      // Get key details
      $aKeyDetails = openssl_pkey_get_details($this->oOpenSSLAsymmetricKey);
      
      $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 = json_encode($aJSONKeyDetails);
      
      echo $this->JWK;      
   }

This outputs:

{
"kty":"EC",
"crv":"P-256",
"alg":"ES256",
"use":"sig",
"x":"mlX7B4E9POpWAhnyXJxW+xA7CbjJAXp5GVAYAkKZB\/4=",
"y":"5HMng\/ZKHEcIM5\/srINSugx1pmTR5hhidIvJaTno5VU=",
"d":"LoksxPi5A45IyBqECoXW18B1Ld6yvYAFpSooleX4UyU="
} 

Which looks OK, and I think it should go where it says whatisthis in the PrepareAccountRequest() function above.

I've also added a signing function:

    public function Sign($Content){
    
      // Sign the text.      
      $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)){
         //echo 'Signature Confirmed!';
         return $BinarySignature;
      }else{
         //echo 'Signature FAILED!';
         return false;
      }
   }

Which is returning binary. Now the line from before which read:

$Signature = hash_hmac('sha256', $Protected . "." . $Payload, 'fsdjjkdlsdjlkasdf', false);

gets replaced with:

$Signature = $this->Base64URL($this->Sign($Protected . "." . $Payload));
$Encoded = $this->Base64URL($Protected.$Payload.$Signature);

echo $Encoded;

Which produces:

ZXlKaGJHY2lPaUpGVXpJMU5pSXNJbXAzYXlJNkludGNJbXQwZVZ3aU9sd2lSVU5jSWl4Y0ltTnlkbHdpT2x3aVVDMHlOVFpjSWl4Y0ltRnNaMXdpT2x3aVJWTXlOVFpjSWl4Y0luVnpaVndpT2x3aWMybG5YQ0lzWENKNFhDSTZYQ0pTSzFReE9XWTRSR2hPWmtkcVN6WkZOM0l4ZW5WUlVYbGxlVzlUTTJWMGVHWjBWMVJZTkUxbEt5dHpQVndpTEZ3aWVWd2lPbHdpZHpkQ1VWeGNYQzlZYmxReGVFTm1jMmxVT0Z4Y1hDOWxSVGt3TnpONE1qWm1aa2hCTW5WRVFXZFFja2w0YTBKRE1EMWNJaXhjSW1SY0lqcGNJa2hxWW1sWFdUVXpkbTExVDNKdlNXWk5jVmdyTTI1eE4zQTViMWwzYjNOdFN6UnhNVGw2UTBsSlF6UTlYQ0o5SWl3aWJtOXVZMlVpT2lJelRUTjRPWFE0VURaeFZXdEZhVVV4TlRsaWIweEdNV2xhTkRGRWJsSlhaMGc1Vm5sT2JXWlFSRlF6VmpoSmJFZFFhRFFpTENKMWNtd2lPaUpvZEhSd2N6cGNMMXd2WVdOdFpTMXpkR0ZuYVc1bkxYWXdNaTVoY0drdWJHVjBjMlZ1WTNKNWNIUXViM0puWEM5aFkyMWxYQzl1WlhjdFlXTmpkQ0o5ZXlKMFpYSnRjMDltVTJWeWRtbGpaVUZuY21WbFpDSTZkSEoxWlN3aVkyOXVkR0ZqZENJNld5SnRZV2xzZEc4NlkyVnlkQzFoWkcxcGJrQmxlR0Z0Y0d4bExtOXlaeUlzSW0xaGFXeDBienBoWkcxcGJrQmxlR0Z0Y0d4bExtOXlaeUpkZlFNRVVDSUZUVDVHSmRpd2lGdWZHZlZ1VjFFdWVIQWY3TDJzOUxRNzgzb1ZTYkljMHhBaUVBdm9MSXo0NXdlSG1pVmo3NW02NDR4MWtlWjY2bHpPZllGdlcyMlFhcDhTZw

The ACME API still complains Parse error reading JWS.

I think this is looking mostly right, but I'm not sure I have all the Base64 vs Base64Web right and also, all the JSON encoding and base64 encoding and concatenations in the right order.

0

There are 0 answers