Page MenuHomePhabricator

D10402.diff
No OneTemporary

D10402.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -41,6 +41,7 @@
'CommandException' => 'future/exec/CommandException.php',
'ConduitClient' => 'conduit/ConduitClient.php',
'ConduitClientException' => 'conduit/ConduitClientException.php',
+ 'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php',
'ConduitFuture' => 'conduit/ConduitFuture.php',
'ExecFuture' => 'future/exec/ExecFuture.php',
'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php',
@@ -493,6 +494,7 @@
'BaseHTTPFuture' => 'Future',
'CommandException' => 'Exception',
'ConduitClientException' => 'Exception',
+ 'ConduitClientTestCase' => 'PhutilTestCase',
'ConduitFuture' => 'FutureProxy',
'ExecFuture' => 'Future',
'ExecFutureTestCase' => 'PhutilTestCase',
diff --git a/src/conduit/ConduitClient.php b/src/conduit/ConduitClient.php
--- a/src/conduit/ConduitClient.php
+++ b/src/conduit/ConduitClient.php
@@ -8,6 +8,12 @@
private $timeout = 300.0;
private $username;
private $password;
+ private $publicKey;
+ private $privateKey;
+
+ const AUTH_ASYMMETRIC = 'asymmetric';
+
+ const SIGNATURE_CONSIGN_1 = 'Consign1.0/';
public function getConnectionID() {
return $this->connectionID;
@@ -37,6 +43,15 @@
return $this;
}
+ public function setSigningKeys(
+ $public_key,
+ PhutilOpaqueEnvelope $private_key) {
+
+ $this->publicKey = $public_key;
+ $this->privateKey = $private_key;
+ return $this;
+ }
+
public function callMethod($method, array $params) {
$meta = array();
@@ -59,6 +74,15 @@
unset($params['certificate']);
}
+ if ($this->privateKey && $this->publicKey) {
+ $meta['auth.type'] = self::AUTH_ASYMMETRIC;
+ $meta['auth.key'] = $this->publicKey;
+ $meta['auth.host'] = $this->getHostString();
+
+ $signature = $this->signRequest($method, $params, $meta);
+ $meta['auth.signature'] = $signature;
+ }
+
if ($meta) {
$params['__conduit__'] = $meta;
}
@@ -102,4 +126,191 @@
return $this;
}
+ private function getHostString() {
+ $uri = new PhutilURI($this->uri);
+ $host = $uri->getDomain();
+ $port = $uri->getPort();
+ if (!$port) {
+ switch ($uri->getProtocol()) {
+ case 'https':
+ $port = 443;
+ break;
+ default:
+ $port = 80;
+ break;
+ }
+ }
+
+ return $host.':'.$port;
+ }
+
+ private function signRequest(
+ $method,
+ array $params,
+ array $meta) {
+
+ $input = self::encodeRequestDataForSignature(
+ $method,
+ $params,
+ $meta);
+
+ $signature = null;
+ $result = openssl_sign(
+ $input,
+ $signature,
+ $this->privateKey->openEnvelope());
+ if (!$result) {
+ throw new Exception('Unable to sign Conduit request with signing key.');
+ }
+
+ return self::SIGNATURE_CONSIGN_1.base64_encode($signature);
+ }
+
+ public static function verifySignature(
+ $method,
+ array $params,
+ array $meta,
+ $openssl_public_key) {
+
+ $auth_type = idx($meta, 'auth.type');
+ switch ($auth_type) {
+ case self::AUTH_ASYMMETRIC:
+ break;
+ default:
+ throw new Exception(
+ pht(
+ 'Unable to verify request signature, specified "auth.type" '.
+ '("%s") is unknown.',
+ $auth_type));
+ }
+
+ $public_key = idx($meta, 'auth.key');
+ if (!strlen($public_key)) {
+ throw new Exception(
+ pht(
+ 'Unable to verify request signature, no "auth.key" present in '.
+ 'request protocol information.'));
+ }
+
+ $signature = idx($meta, 'auth.signature');
+ if (!strlen($signature)) {
+ throw new Exception(
+ pht(
+ 'Unable to verify request signature, no "auth.signature" present '.
+ 'in request protocol information.'));
+ }
+
+ $prefix = self::SIGNATURE_CONSIGN_1;
+ if (strncmp($signature, $prefix, strlen($prefix)) !== 0) {
+ throw new Exception(
+ pht(
+ 'Unable to verify request signature, signature format is not '.
+ 'known.'));
+ }
+ $signature = substr($signature, strlen($prefix));
+
+ $input = self::encodeRequestDataForSignature(
+ $method,
+ $params,
+ $meta);
+
+ $signature = base64_decode($signature);
+
+ $trap = new PhutilErrorTrap();
+ $result = @openssl_verify(
+ $input,
+ $signature,
+ $openssl_public_key);
+ $err = $trap->getErrorsAsString();
+ $trap->destroy();
+
+ if ($result === 1) {
+ // Signature is good.
+ return true;
+ } else if ($result === 0) {
+ // Signature is bad.
+ throw new Exception(
+ pht(
+ 'Request signature verification failed: signature is not correct.'));
+ } else {
+ // Some kind of error.
+ if (strlen($err)) {
+ throw new Exception(
+ pht(
+ 'OpenSSL encountered an error verifying the request signature: '.
+ '%s',
+ $err));
+ } else {
+ throw new Exception(
+ pht(
+ 'OpenSSL encountered an unknown error verifying the request.',
+ $err));
+ }
+ }
+ }
+
+ private static function encodeRequestDataForSignature(
+ $method,
+ array $params,
+ array $meta) {
+
+ unset($meta['auth.signature']);
+
+ $structure = array(
+ 'method' => $method,
+ 'protocol' => $meta,
+ 'parameters' => $params,
+ );
+
+ return self::encodeRawDataForSignature($structure);
+ }
+
+ public static function encodeRawDataForSignature($data) {
+ $out = array();
+
+ if (is_array($data)) {
+ if (!$data || (array_keys($data) == range(0, count($data) - 1))) {
+ $out[] = 'A';
+ $out[] = count($data);
+ $out[] = ':';
+ foreach ($data as $value) {
+ $out[] = self::encodeRawDataForSignature($value);
+ }
+ } else {
+ ksort($data);
+ $out[] = 'O';
+ $out[] = count($data);
+ $out[] = ':';
+ foreach ($data as $key => $value) {
+ $out[] = self::encodeRawDataForSignature($key);
+ $out[] = self::encodeRawDataForSignature($value);
+ }
+ }
+ } else if (is_string($data)) {
+ $out[] = 'S';
+ $out[] = strlen($data);
+ $out[] = ':';
+ $out[] = $data;
+ } else if (is_integer($data)) {
+ $out[] = 'I';
+ $out[] = strlen((string)$data);
+ $out[] = ':';
+ $out[] = (string)$data;
+ } else if (is_null($data)) {
+ $out[] = 'N';
+ $out[] = ':';
+ } else if ($data === true) {
+ $out[] = 'B1:';
+ } else if ($data === false) {
+ $out[] = 'B0:';
+ } else {
+ throw new Exception(
+ pht(
+ 'Unexpected data type in request data: %s.',
+ gettype($data)));
+ }
+
+ return implode('', $out);
+ }
+
}
diff --git a/src/conduit/__tests__/ConduitClientTestCase.php b/src/conduit/__tests__/ConduitClientTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/conduit/__tests__/ConduitClientTestCase.php
@@ -0,0 +1,34 @@
+<?php
+
+final class ConduitClientTestCase extends PhutilTestCase {
+
+ public function testConduitRequestEncoding() {
+ $input = array(
+ 'z' => array(
+ 'nothing' => null,
+ 'emptystring' => '',
+ ),
+ 'empty' => array(
+ ),
+ 'list' => array(
+ 15,
+ 'quack',
+ true,
+ false,
+ ),
+ 'a' => array(
+ 'key' => 'value',
+ 'key2' => 'value2',
+ ),
+ );
+
+ $expect =
+ 'O4:S1:aO2:S3:keyS5:valueS4:key2S6:value2S5:emptyA0:S4:listA4:I2:15'.
+ 'S5:quackB1:B0:S1:zO2:S11:emptystringS0:S7:nothingN:';
+
+ $this->assertEqual(
+ $expect,
+ ConduitClient::encodeRawDataForSignature($input));
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Sat, May 11, 9:12 AM (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6275518
Default Alt Text
D10402.diff (7 KB)

Event Timeline