'not_attempted','erasure_complete'=>false); public function __construct(#[\SensitiveParameter] string $key) { if (preg_match('/^[\x21-\x7e]{1,512}$/D',$key)!==1) { throw new \InvalidArgumentException('transport_credentials_invalid'); } $this->key=$key; } public function result(): array { return $this->result; } /** Local configuration check only, no network and no claim consumed. * The future coordinator must persist/route a refusal before attempting work. */ public function preflight(): array { $refuse=static fn($reason)=>array('status'=>'transport_attention_required','reason'=>$reason,'erasure_complete'=>false); if (!extension_loaded('curl') || !(\curl_version()['features'] & CURL_VERSION_ASYNCHDNS)) { return $refuse('transport_unavailable'); } if (!defined('ABSPATH') || !defined('WPINC') || !extension_loaded('openssl')) { return $refuse('wordpress_ca_unavailable'); } $path=ABSPATH.WPINC.'/certificates/ca-bundle.crt'; if (!is_file($path) || !is_readable($path)) { return $refuse('wordpress_ca_unavailable'); } $size=@filesize($path); if ($size===false || $size<1 || $size>1048576) { return $refuse('wordpress_ca_invalid'); } $pem=@file_get_contents($path,false,null,0,1048577); if (!is_string($pem) || strlen($pem)>1048576 || !preg_match_all('/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/s',$pem,$matches)) { return $refuse('wordpress_ca_invalid'); } foreach ($matches[0] as $certificate) { if (@openssl_x509_read($certificate)===false) { return $refuse('wordpress_ca_invalid'); } } return array('status'=>'transport_preflight_ready','erasure_complete'=>false); } /** Advisory only: scheduler must combine this with its own durable backoff. * Invalid/duplicate values do not authorize an immediate retry. */ public static function retryDelay(string $value,int $now): ?int { if (preg_match('/^(0|[1-9][0-9]{0,9})$/D',$value)===1) { return (int)$value<=2147483647 ? (int)$value : null; } $date=\DateTimeImmutable::createFromFormat('!D, d M Y H:i:s \G\M\T',$value,new \DateTimeZone('UTC')); if (!$date || $date->format('D, d M Y H:i:s \G\M\T')!==$value) { return null; } return max(0,$date->getTimestamp()-$now); } public function __invoke(string $payload): ?string { $this->result=array('status'=>'remote_unknown','reason'=>'transport_unavailable', 'retry_same_operation'=>true,'retry_after_seconds'=>null,'erasure_complete'=>false); try { if (strlen($payload)>\OG_Native_Remote_Binding::MAX_BYTES || \OG_Native_Remote_Binding::encode(json_decode($payload,true,32,JSON_THROW_ON_ERROR))!==$payload) { $this->result['reason']='request_invalid'; return null; } } catch (\Throwable $e) { $this->result['reason']='request_invalid'; return null; } return $this->exchange(self::ENDPOINT,$payload); } /** Fixed capability endpoint (server rolls back its write probe); caller verifies the signed challenge. */ public function readiness(string $nonce): ?string { $this->result=array('status'=>'remote_unknown','reason'=>'transport_unavailable', 'retry_same_operation'=>false,'retry_after_seconds'=>null,'erasure_complete'=>false); try {$payload=\OG_Native_Readiness_Proof::request($nonce);} catch(\Throwable $e){$this->result['reason']='request_invalid';return null;} return $this->exchange(self::READINESS_ENDPOINT,$payload); } /** Only accepts a payload obtained by the durable receipt read-back path. * An ACK response is transport confirmation, NEVER an erasure receipt. * On failure the caller must retain the same durable receipt for retry. */ public function acknowledge(string $payload): bool { $this->result=array('status'=>'ack_unknown','reason'=>'transport_unavailable', 'retry_same_operation'=>true,'retry_after_seconds'=>null,'erasure_complete'=>false); try {$request=self::ackRequest($payload);} catch(\Throwable $e){$this->result['reason']='request_invalid';return false;} $body=$this->exchange(self::ACK_ENDPOINT,$payload); if($body===null){return false;} if(!self::ackResponse($request,$body)){ $this->result['status']='ack_unknown';$this->result['reason']='ack_response_invalid';return false; } $this->result['status']='acknowledged';$this->result['reason']='durable_receipt_acknowledged'; return true; } /** Proof transport only. Caller must verify both signatures and all bindings. * A successful HTTP response never authorizes cleanup or completion. */ public function coreProof(array $request,string $nonce): ?string { $this->result=array('status'=>'remote_unknown','reason'=>'transport_unavailable', 'retry_same_operation'=>true,'retry_after_seconds'=>null,'erasure_complete'=>false); try { \OG_Native_Remote_Binding::encode($request); if(preg_match('/^[0-9a-f]{64}$/D',$nonce)!==1){throw new \InvalidArgumentException('nonce');} $payload=\OG_Native_Remote_Binding::canonical(array( 'version'=>'og-native-core-proof-v1','nonce'=>$nonce,'request'=>$request)); }catch(\Throwable $e){$this->result['reason']='request_invalid';return null;} $body=$this->exchange(self::CORE_ENDPOINT,$payload); if($body===null&&in_array($this->result['http_status']??null,array(400,401,403,404,409,410,413,415,422),true)){ // Missing is unknown, never proof that the erasure did not happen. $this->result['status']='core_attention_required'; $this->result['retry_same_operation']=false; $this->result['automatic_retry']=false; $this->result['restart_authorized']=false; } return $body; } /** Owner-approval queue transport. The reply is NOT authority: the store * verifies any signed approval against its own operation and cutoff. A 404 * or 409 here is a refusal of this restart request, never proof of erasure. */ public function restart(string $payload): ?string { $this->result=array('status'=>'restart_unknown','reason'=>'transport_unavailable', 'retry_same_operation'=>false,'retry_after_seconds'=>null,'erasure_complete'=>false,'restart_authorized'=>false); try {\OG_Native_Restart_Approval::checkPayload($payload);} catch(\Throwable $e){$this->result['reason']='request_invalid';return null;} $body=$this->exchange(self::RESTART_ENDPOINT,$payload); if($body===null){$this->result['restart_authorized']=false;} return $body; } public static function ackRequest(string $payload): array { if(strlen($payload)>\OG_Native_Remote_Binding::MAX_BYTES+256){throw new \InvalidArgumentException('ack_capacity');} $body=json_decode($payload,true,32,JSON_THROW_ON_ERROR); if(!is_array($body)||array_keys($body)!==array('version','request','receipt_digest') ||$body['version']!=='og-native-receipt-ack-v1'||!is_string($body['receipt_digest']) ||preg_match('/^[0-9a-f]{64}$/D',$body['receipt_digest'])!==1 ||\OG_Native_Remote_Binding::canonical($body)!==$payload){throw new \InvalidArgumentException('ack_invalid');} \OG_Native_Remote_Binding::encode($body['request']); return $body; } public static function ackResponse(array $request,string $body): bool { try { if(strlen($body)>512){return false;} $expected=array('acknowledged'=>true,'operation_id'=>$request['request']['operation_id'], 'receipt_digest'=>$request['receipt_digest']); return hash_equals(\OG_Native_Remote_Binding::canonical($expected),$body); }catch(\Throwable $e){return false;} } private function exchange(string $endpoint,string $payload): ?string { // An asynchronous resolver is required so DNS is included in cURL's total deadline. $preflight=$this->preflight(); if ($preflight['status']!=='transport_preflight_ready') { $this->result['reason']=$preflight['reason']; return null; } $body=''; $headerBytes=0; $retry=array(); $encoding=array(); $overflow=false; $handle=\curl_init($endpoint); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init -- dedicated no-redirect, pinned TLS privacy transport. if ($handle===false) { return null; } try { $options=array( CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>$payload, CURLOPT_HTTPHEADER=>array('Content-Type: application/json','Accept: application/json', 'Accept-Encoding: identity','Expect:','X-OrderGuard-Key: '.$this->key), CURLOPT_CONNECTTIMEOUT_MS=>self::CONNECT_MS, CURLOPT_TIMEOUT_MS=>self::TOTAL_MS, CURLOPT_NOSIGNAL=>true, CURLOPT_FOLLOWLOCATION=>false, CURLOPT_MAXREDIRS=>0, CURLOPT_SSL_VERIFYPEER=>true, CURLOPT_SSL_VERIFYHOST=>2, CURLOPT_CAINFO=>ABSPATH.WPINC.'/certificates/ca-bundle.crt', CURLOPT_SSLVERSION=>CURL_SSLVERSION_TLSv1_2, CURLOPT_PROXY=>'', CURLOPT_NETRC=>CURL_NETRC_IGNORED, CURLOPT_WRITEFUNCTION=>static function($h,string $chunk) use (&$body,&$overflow): int { if (strlen($body)+strlen($chunk)>\OG_Native_Remote_Binding::MAX_RECEIPT_BYTES) { $overflow=true; return 0; } $body.=$chunk; return strlen($chunk); }, CURLOPT_HEADERFUNCTION=>static function($h,string $line) use (&$headerBytes,&$retry,&$encoding,&$overflow): int { $headerBytes+=strlen($line); if ($headerBytes>16384) { $overflow=true; return 0; } if (preg_match('/^HTTP\//i',$line)) { $retry=array(); $encoding=array(); } if (stripos($line,'Retry-After:')===0) { $retry[]=trim(substr($line,12)); } if (stripos($line,'Content-Encoding:')===0) { $encoding[]=strtolower(trim(substr($line,17))); } return strlen($line); } ); if (defined('CURLOPT_PROTOCOLS_STR')) { $options[CURLOPT_PROTOCOLS_STR]='https'; $options[CURLOPT_REDIR_PROTOCOLS_STR]='https'; } else { $options[CURLOPT_PROTOCOLS]=CURLPROTO_HTTPS; $options[CURLOPT_REDIR_PROTOCOLS]=CURLPROTO_HTTPS; } if (!\curl_setopt_array($handle,$options)) { return null; } // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt_array -- see transport boundary above. $ok=\curl_exec($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec -- see transport boundary above. $status=(int)\curl_getinfo($handle,CURLINFO_RESPONSE_CODE); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo -- see transport boundary above. $this->result['http_status']=$status; if ($ok===false || $overflow) { $this->result['reason']=$overflow?'response_capacity':'transport_error'; return null; } if ($encoding && $encoding!==array('identity')) { $this->result['reason']='response_encoding'; return null; } $this->result['retry_after_seconds']=count($retry)===1?self::retryDelay($retry[0],time()):null; if ($status!==200) { $this->result['status']=in_array($status,array(429,503),true)?'retry_later':'remote_unknown'; $this->result['reason']='http_status'; return null; } $this->result['status']='response_received'; $this->result['reason']='requires_signature_verification'; return $body; // Only the existing bound signature verifier may accept it. } catch (\Throwable $e) { $this->result['reason']='transport_error'; return null; } finally { unset($handle); } } }