Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14079605
D10402.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
7 KB
Referenced Files
None
Subscribers
None
D10402.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Sat, Nov 23, 9:05 AM (18 h, 14 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6778009
Default Alt Text
D10402.diff (7 KB)
Attached To
Mode
D10402: Allow Conduit requests to be signed with a public/private keypair
Attached
Detach File
Event Timeline
Log In to Comment