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:
- The ACME documentation suggests - ES256as the algorithm, but- hash_hmacdoesn't seem to have that as an option. What function in PHP can use- ES256--OR-- what option will ACME allow that- hash_hmaccan create?
- 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? 
- 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?
- What function(s) should be used to generate the - hash_hmackey value and the JWK values?
- Does - openssl_sign()with- OPENSSL_ALGO_SHA256implement- 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.