@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow device SSH keys to be trusted

Summary:
Ref T6240. Some discussion in that task. In instance/cluster environments, daemons need to make Conduit calls that bypass policy checks.

We can't just let anyone add SSH keys with this capability to the web directly, because then an adminstrator could just add a key they own and start signing requests with it, bypassing policy checks.

Add a `bin/almanac trust-key --id <x>` workflow for trusting keys. Only trusted keys can sign requests.

Test Plan:
- Generated a user key.
- Generated a device key.
- Trusted a device key.
- Untrusted a device key.
- Hit the various errors on trust/untrust.
- Tried to edit a trusted key.

{F236010}

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T6240

Differential Revision: https://secure.phabricator.com/D10878

+219 -87
+2
resources/sql/autopatches/20141119.sshtrust.sql
··· 1 + ALTER TABLE {$NAMESPACE}_auth.auth_sshkey 2 + ADD isTrusted BOOL NOT NULL;
+4 -4
src/__phutil_library_map__.php
··· 20 20 'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php', 21 21 'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php', 22 22 'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php', 23 - 'AlmanacConduitUtil' => 'applications/almanac/util/AlmanacConduitUtil.php', 24 23 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 25 24 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', 26 25 'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php', ··· 46 45 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 47 46 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 48 47 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 49 - 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 48 + 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', 49 + 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 50 50 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 51 51 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', 52 52 'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php', ··· 3000 3000 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 3001 3001 'AlmanacBindingViewController' => 'AlmanacServiceController', 3002 3002 'AlmanacConduitAPIMethod' => 'ConduitAPIMethod', 3003 - 'AlmanacConduitUtil' => 'Phobject', 3004 3003 'AlmanacConsoleController' => 'AlmanacController', 3005 3004 'AlmanacController' => 'PhabricatorController', 3006 3005 'AlmanacCoreCustomField' => array( ··· 3040 3039 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 3041 3040 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3042 3041 'AlmanacInterfaceTableView' => 'AphrontView', 3043 - 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 3042 + 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', 3043 + 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 3044 3044 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 3045 3045 'AlmanacNames' => 'Phobject', 3046 3046 'AlmanacNamesTestCase' => 'PhabricatorTestCase',
+2
src/applications/almanac/controller/AlmanacDeviceViewController.php
··· 161 161 ->setUser($viewer) 162 162 ->setKeys($keys) 163 163 ->setCanEdit($can_edit) 164 + ->setShowID(true) 165 + ->setShowTrusted(true) 164 166 ->setNoDataString(pht('This device has no associated SSH public keys.')); 165 167 166 168 try {
-64
src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php
··· 1 - <?php 2 - 3 - final class AlmanacManagementRegisterWorkflow 4 - extends AlmanacManagementWorkflow { 5 - 6 - public function didConstruct() { 7 - $this 8 - ->setName('register') 9 - ->setSynopsis(pht('Register this host for authorized Conduit access.')) 10 - ->setArguments(array()); 11 - } 12 - 13 - public function execute(PhutilArgumentParser $args) { 14 - $console = PhutilConsole::getConsole(); 15 - 16 - if (Filesystem::pathExists(AlmanacConduitUtil::getHostPrivateKeyPath())) { 17 - throw new Exception( 18 - 'This host already has a private key for Conduit access.'); 19 - } 20 - 21 - $pair = PhabricatorSSHKeyGenerator::generateKeypair(); 22 - list($public_key, $private_key) = $pair; 23 - 24 - $host = id(new AlmanacDevice()) 25 - ->setName(php_uname('n')) 26 - ->save(); 27 - 28 - id(new AlmanacProperty()) 29 - ->setObjectPHID($host->getPHID()) 30 - ->setName('conduitPublicOpenSSHKey') 31 - ->setValue($public_key) 32 - ->save(); 33 - 34 - id(new AlmanacProperty()) 35 - ->setObjectPHID($host->getPHID()) 36 - ->setName('conduitPublicOpenSSLKey') 37 - ->setValue($this->convertToOpenSSLPublicKey($public_key)) 38 - ->save(); 39 - 40 - Filesystem::writeFile( 41 - AlmanacConduitUtil::getHostPrivateKeyPath(), 42 - $private_key); 43 - 44 - Filesystem::writeFile( 45 - AlmanacConduitUtil::getHostIDPath(), 46 - $host->getID()); 47 - 48 - $console->writeOut("Registered as device %d.\n", $host->getID()); 49 - } 50 - 51 - private function convertToOpenSSLPublicKey($openssh_public_key) { 52 - $ssh_public_key_file = new TempFile(); 53 - Filesystem::writeFile($ssh_public_key_file, $openssh_public_key); 54 - 55 - list($public_key, $stderr) = id(new ExecFuture( 56 - 'ssh-keygen -e -f %s -m pkcs8', 57 - $ssh_public_key_file))->resolvex(); 58 - 59 - unset($ssh_public_key_file); 60 - 61 - return $public_key; 62 - } 63 - 64 - }
+85
src/applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php
··· 1 + <?php 2 + 3 + final class AlmanacManagementTrustKeyWorkflow 4 + extends AlmanacManagementWorkflow { 5 + 6 + public function didConstruct() { 7 + $this 8 + ->setName('trust-key') 9 + ->setSynopsis(pht('Mark a public key as trusted.')) 10 + ->setArguments( 11 + array( 12 + array( 13 + 'name' => 'id', 14 + 'param' => 'id', 15 + 'help' => pht('ID of the key to trust.'), 16 + ), 17 + )); 18 + } 19 + 20 + public function execute(PhutilArgumentParser $args) { 21 + $console = PhutilConsole::getConsole(); 22 + 23 + $id = $args->getArg('id'); 24 + if (!$id) { 25 + throw new PhutilArgumentUsageException( 26 + pht('Specify a public key to trust with --id.')); 27 + } 28 + 29 + $key = id(new PhabricatorAuthSSHKeyQuery()) 30 + ->setViewer($this->getViewer()) 31 + ->withIDs(array($id)) 32 + ->executeOne(); 33 + if (!$key) { 34 + throw new PhutilArgumentUsageException( 35 + pht('No public key exists with ID "%s".', $id)); 36 + } 37 + 38 + if ($key->getIsTrusted()) { 39 + throw new PhutilArgumentUsageException( 40 + pht('Public key with ID %s is already trusted.', $id)); 41 + } 42 + 43 + if (!($key->getObject() instanceof AlmanacDevice)) { 44 + throw new PhutilArgumentUsageException( 45 + pht('You can only trust keys associated with Almanac devices.')); 46 + } 47 + 48 + $handle = id(new PhabricatorHandleQuery()) 49 + ->setViewer($this->getViewer()) 50 + ->withPHIDs(array($key->getObject()->getPHID())) 51 + ->executeOne(); 52 + 53 + $console->writeOut( 54 + "**<bg:red> %s </bg>**\n\n%s\n\n%s\n\n%s", 55 + pht('IMPORTANT!'), 56 + phutil_console_wrap( 57 + pht( 58 + 'Trusting a public key gives anyone holding the corresponding '. 59 + 'private key complete, unrestricted access to all data in '. 60 + 'Phabricator. The private key will be able to sign requests that '. 61 + 'skip policy and security checks.')), 62 + phutil_console_wrap( 63 + pht( 64 + 'This is an advanced feature which should normally be used only '. 65 + 'when building a Phabricator cluster. This feature is very '. 66 + 'dangerous if misused.')), 67 + pht('This key is associated with device "%s".', $handle->getName())); 68 + 69 + $prompt = pht( 70 + 'Really trust this key?'); 71 + if (!phutil_console_confirm($prompt)) { 72 + throw new PhutilArgumentUsageException( 73 + pht('User aborted workflow.')); 74 + } 75 + 76 + $key->setIsTrusted(1); 77 + $key->save(); 78 + 79 + $console->writeOut( 80 + "**<bg:green> %s </bg>** %s\n", 81 + pht('TRUSTED'), 82 + pht('Key %s has been marked as trusted.', $id)); 83 + } 84 + 85 + }
+52
src/applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php
··· 1 + <?php 2 + 3 + final class AlmanacManagementUntrustKeyWorkflow 4 + extends AlmanacManagementWorkflow { 5 + 6 + public function didConstruct() { 7 + $this 8 + ->setName('untrust-key') 9 + ->setSynopsis(pht('Revoke trust of a public key.')) 10 + ->setArguments( 11 + array( 12 + array( 13 + 'name' => 'id', 14 + 'param' => 'id', 15 + 'help' => pht('ID of the key to revoke trust for.'), 16 + ), 17 + )); 18 + } 19 + 20 + public function execute(PhutilArgumentParser $args) { 21 + $console = PhutilConsole::getConsole(); 22 + 23 + $id = $args->getArg('id'); 24 + if (!$id) { 25 + throw new PhutilArgumentUsageException( 26 + pht('Specify a public key to revoke trust for with --id.')); 27 + } 28 + 29 + $key = id(new PhabricatorAuthSSHKeyQuery()) 30 + ->setViewer($this->getViewer()) 31 + ->withIDs(array($id)) 32 + ->executeOne(); 33 + if (!$key) { 34 + throw new PhutilArgumentUsageException( 35 + pht('No public key exists with ID "%s".', $id)); 36 + } 37 + 38 + if (!$key->getIsTrusted()) { 39 + throw new PhutilArgumentUsageException( 40 + pht('Public key with ID %s is not trusted.', $id)); 41 + } 42 + 43 + $key->setIsTrusted(0); 44 + $key->save(); 45 + 46 + $console->writeOut( 47 + "**<bg:green> %s </bg>** %s\n", 48 + pht('TRUST REVOKED'), 49 + pht('Trust has been revoked for public key %s.', $id)); 50 + } 51 + 52 + }
+4
src/applications/almanac/storage/AlmanacDevice.php
··· 170 170 return $this->getURI(); 171 171 } 172 172 173 + public function getSSHKeyDefaultName() { 174 + return $this->getName(); 175 + } 176 + 173 177 174 178 }
-17
src/applications/almanac/util/AlmanacConduitUtil.php
··· 1 - <?php 2 - 3 - final class AlmanacConduitUtil extends Phobject { 4 - 5 - public static function getHostPrivateKeyPath() { 6 - $root = dirname(phutil_get_library_root('phabricator')); 7 - $path = $root.'/conf/local/HOSTKEY'; 8 - return $path; 9 - } 10 - 11 - public static function getHostIDPath() { 12 - $root = dirname(phutil_get_library_root('phabricator')); 13 - $path = $root.'/conf/local/HOSTID'; 14 - return $path; 15 - } 16 - 17 - }
+16
src/applications/auth/controller/PhabricatorAuthSSHKeyEditController.php
··· 32 32 33 33 $cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer); 34 34 35 + if ($key->getIsTrusted()) { 36 + $id = $key->getID(); 37 + 38 + return $this->newDialog() 39 + ->setTitle(pht('Can Not Edit Trusted Key')) 40 + ->appendParagraph( 41 + pht( 42 + 'This key is trusted. Trusted keys can not be edited. '. 43 + 'Use %s to revoke trust before editing the key.', 44 + phutil_tag( 45 + 'tt', 46 + array(), 47 + "bin/almanac untrust-key --id {$id}"))) 48 + ->addCancelButton($cancel_uri, pht('Okay')); 49 + } 50 + 35 51 $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( 36 52 $viewer, 37 53 $request,
+4 -2
src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
··· 19 19 $cancel_uri); 20 20 21 21 if ($request->isFormPost()) { 22 + $default_name = $key->getObject()->getSSHKeyDefaultName(); 23 + 22 24 $keys = PhabricatorSSHKeyGenerator::generateKeypair(); 23 25 list($public_key, $private_key) = $keys; 24 26 25 27 $file = PhabricatorFile::buildFromFileDataOrHash( 26 28 $private_key, 27 29 array( 28 - 'name' => 'id_rsa_phabricator.key', 30 + 'name' => $default_name.'.key', 29 31 'ttl' => time() + (60 * 10), 30 32 'viewPolicy' => $viewer->getPHID(), 31 33 )); ··· 36 38 $body = $public_key->getBody(); 37 39 38 40 $key 39 - ->setName('id_rsa_phabricator') 41 + ->setName($default_name) 40 42 ->setKeyType($type) 41 43 ->setKeyBody($body) 42 44 ->setKeyComment(pht('Generated'))
+6
src/applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php
··· 11 11 */ 12 12 public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer); 13 13 14 + 15 + /** 16 + * Provide a default name for generated SSH keys. 17 + */ 18 + public function getSSHKeyDefaultName(); 19 + 14 20 }
+2
src/applications/auth/storage/PhabricatorAuthSSHKey.php
··· 10 10 protected $keyIndex; 11 11 protected $keyBody; 12 12 protected $keyComment = ''; 13 + protected $isTrusted = 0; 13 14 14 15 private $object = self::ATTACHABLE; 15 16 ··· 21 22 'keyIndex' => 'bytes12', 22 23 'keyBody' => 'text', 23 24 'keyComment' => 'text255', 25 + 'isTrusted' => 'bool', 24 26 ), 25 27 self::CONFIG_KEY_SCHEMA => array( 26 28 'key_object' => array(
+29
src/applications/auth/view/PhabricatorAuthSSHKeyTableView.php
··· 5 5 private $keys; 6 6 private $canEdit; 7 7 private $noDataString; 8 + private $showTrusted; 9 + private $showID; 8 10 9 11 public function setNoDataString($no_data_string) { 10 12 $this->noDataString = $no_data_string; ··· 16 18 return $this; 17 19 } 18 20 21 + public function setShowTrusted($show_trusted) { 22 + $this->showTrusted = $show_trusted; 23 + return $this; 24 + } 25 + 26 + public function setShowID($show_id) { 27 + $this->showID = $show_id; 28 + return $this; 29 + } 30 + 19 31 public function setKeys(array $keys) { 20 32 assert_instances_of($keys, 'PhabricatorAuthSSHKey'); 21 33 $this->keys = $keys; ··· 32 44 $delete_class = 'small grey button disabled'; 33 45 } 34 46 47 + $trusted_icon = id(new PHUIIconView()) 48 + ->setIconFont('fa-star blue'); 49 + $untrusted_icon = id(new PHUIIconView()) 50 + ->setIconFont('fa-times grey'); 51 + 35 52 $rows = array(); 36 53 foreach ($keys as $key) { 37 54 $rows[] = array( 55 + $key->getID(), 38 56 javelin_tag( 39 57 'a', 40 58 array( ··· 42 60 'sigil' => 'workflow', 43 61 ), 44 62 $key->getName()), 63 + $key->getIsTrusted() ? $trusted_icon : $untrusted_icon, 45 64 $key->getKeyComment(), 46 65 $key->getKeyType(), 47 66 phabricator_datetime($key->getDateCreated(), $viewer), ··· 60 79 ->setNoDataString($this->noDataString) 61 80 ->setHeaders( 62 81 array( 82 + pht('ID'), 63 83 pht('Name'), 84 + pht('Trusted'), 64 85 pht('Comment'), 65 86 pht('Type'), 66 87 pht('Added'), 67 88 null, 68 89 )) 90 + ->setColumnVisibility( 91 + array( 92 + $this->showID, 93 + true, 94 + $this->showTrusted, 95 + )) 69 96 ->setColumnClasses( 70 97 array( 98 + '', 71 99 'wide pri', 100 + 'center', 72 101 '', 73 102 '', 74 103 'right',
+9
src/applications/conduit/controller/PhabricatorConduitAPIController.php
··· 268 268 if ($object instanceof PhabricatorUser) { 269 269 $user = $object; 270 270 } else { 271 + if (!$stored_key->getIsTrusted()) { 272 + return array( 273 + 'ERR-INVALID-AUTH', 274 + pht( 275 + 'The key which signed this request is not trusted. Only '. 276 + 'trusted keys can be used to sign API calls.'), 277 + ); 278 + } 279 + 271 280 throw new Exception( 272 281 pht('Not Implemented: Would authenticate Almanac device.')); 273 282 }
+4
src/applications/people/storage/PhabricatorUser.php
··· 943 943 } 944 944 } 945 945 946 + public function getSSHKeyDefaultName() { 947 + return 'id_rsa_phabricator'; 948 + } 949 + 946 950 }