diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,8 @@ /conf/local/local.json /conf/local/ENVIRONMENT /conf/local/VERSION -/conf/local/HOSTKEY -/conf/local/HOSTID +/conf/keys/device.pub +/conf/keys/device.key # Impact Font /resources/font/impact.ttf diff --git a/conf/keys/.keep b/conf/keys/.keep new file mode 100644 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 @@ -49,7 +49,9 @@ 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', + 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', + 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', @@ -3091,7 +3093,9 @@ 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacInterfaceTableView' => 'AphrontView', + 'AlmanacKeys' => 'Phobject', 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', + 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', diff --git a/src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php b/src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php new file mode 100644 --- /dev/null +++ b/src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php @@ -0,0 +1,190 @@ +setName('register') + ->setSynopsis(pht('Register this host as an Almanac device.')) + ->setArguments( + array( + array( + 'name' => 'device', + 'param' => 'name', + 'help' => pht('Almanac device name to register.'), + ), + array( + 'name' => 'private-key', + 'param' => 'key', + 'help' => pht('Path to a private key for the host.'), + ), + array( + 'name' => 'allow-key-reuse', + 'help' => pht( + 'Register even if another host is already registered with this '. + 'keypair.'), + ), + array( + 'name' => 'force', + 'help' => pht( + 'Register this host even if keys already exist.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $device_name = $args->getArg('device'); + if (!strlen($device_name)) { + throw new PhutilArgumentUsageException( + pht('Specify a device with --device.')); + } + + $device = id(new AlmanacDeviceQuery()) + ->setViewer($this->getViewer()) + ->withNames(array($device_name)) + ->executeOne(); + if (!$device) { + throw new PhutilArgumentUsageException( + pht('No such device "%s" exists!', $device_name)); + } + + $private_key_path = $args->getArg('private-key'); + if (!strlen($private_key_path)) { + throw new PhutilArgumentUsageException( + pht('Specify a private key with --private-key.')); + } + + if (!Filesystem::pathExists($private_key_path)) { + throw new PhutilArgumentUsageException( + pht('Private key "%s" does not exist!', $private_key_path)); + } + + $raw_private_key = Filesystem::readFile($private_key_path); + + $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); + if (!$phd_user) { + throw new PhutilArgumentUsageException( + pht( + 'Config option "phd.user" is not set. You must set this option '. + 'so the private key can be stored with the correct permissions.')); + } + + $tmp = new TempFile(); + list($err) = exec_manual('chown %s %s', $phd_user, $tmp); + if ($err) { + throw new PhutilArgumentUsageException( + pht( + 'Unable to change ownership of a file to daemon user "%s". Run '. + 'this command as %s or root.', + $phd_user, + $phd_user)); + } + + $stored_public_path = AlmanacKeys::getKeyPath('device.pub'); + $stored_private_path = AlmanacKeys::getKeyPath('device.key'); + + if (!$args->getArg('force')) { + if (Filesystem::pathExists($stored_public_path)) { + throw new PhutilArgumentUsageException( + pht( + 'This host already has a registered public key ("%s"). '. + 'Remove this key before registering the host, or use '. + '--force to overwrite it.', + Filesystem::readablePath($stored_public_path))); + } + + if (Filesystem::pathExists($stored_private_path)) { + throw new PhutilArgumentUsageException( + pht( + 'This host already has a registered private key ("%s"). '. + 'Remove this key before registering the host, or use '. + '--force to overwrite it.', + Filesystem::readablePath($stored_private_path))); + } + } + + list($raw_public_key) = execx('ssh-keygen -y -f %s', $private_key_path); + + $key_object = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_public_key); + + $public_key = id(new PhabricatorAuthSSHKeyQuery()) + ->setViewer($this->getViewer()) + ->withKeys(array($key_object)) + ->executeOne(); + + if ($public_key) { + if ($public_key->getObjectPHID() !== $device->getPHID()) { + throw new PhutilArgumentUsageException( + pht( + 'The public key corresponding to the given private key is '. + 'already associated with an object other than the specified '. + 'device. You can not use a single private key to identify '. + 'multiple devices or users.')); + } else if (!$public_key->getIsTrusted()) { + throw new PhutilArgumentUsageException( + pht( + 'The public key corresponding to the given private key is '. + 'already associated with the device, but is not trusted. '. + 'Registering this key would trust the other entities which '. + 'hold it. Use a unique key, or explicitly enable trust for the '. + 'current key.')); + } else if (!$args->getArg('allow-key-reuse')) { + throw new PhutilArgumentUsageException( + pht( + 'The public key corresponding to the given private key is '. + 'already associated with the device. If you do not want to '. + 'use a unique key, use --allow-key-reuse to permit '. + 'reassociation.')); + } + } else { + $public_key = id(new PhabricatorAuthSSHKey()) + ->setObjectPHID($device->getPHID()) + ->attachObject($device) + ->setName($device->getSSHKeyDefaultName()) + ->setKeyType($key_object->getType()) + ->setKeyBody($key_object->getBody()) + ->setKeyComment(pht('Registered')) + ->setIsTrusted(1); + } + + + $console->writeOut( + "%s\n", + pht('Installing public key...')); + + $tmp_public = new TempFile(); + Filesystem::changePermissions($tmp_public, 0600); + execx('chown %s %s', $phd_user, $tmp_public); + Filesystem::writeFile($tmp_public, $raw_public_key); + execx('mv -f %s %s', $tmp_public, $stored_public_path); + + $console->writeOut( + "%s\n", + pht('Installing private key...')); + + $tmp_private = new TempFile(); + Filesystem::changePermissions($tmp_private, 0600); + execx('chown %s %s', $phd_user, $tmp_private); + Filesystem::writeFile($tmp_private, $raw_private_key); + execx('mv -f %s %s', $tmp_private, $stored_private_path); + + if (!$public_key->getID()) { + $console->writeOut( + "%s\n", + pht('Registering device key...')); + $public_key->save(); + } + + $console->writeOut( + "** %s ** %s\n", + pht('HOST REGISTERED'), + pht( + 'This host has been registered as "%s" and a trusted keypair '. + 'has been installed.', + $device_name)); + } + +} diff --git a/src/applications/almanac/util/AlmanacKeys.php b/src/applications/almanac/util/AlmanacKeys.php new file mode 100644 --- /dev/null +++ b/src/applications/almanac/util/AlmanacKeys.php @@ -0,0 +1,12 @@ +getBranch(); } + // If the method we're calling doesn't actually take some of the implicit + // parameters we derive from the DiffusionRequest, omit them. + $method_object = ConduitAPIMethod::getConduitMethod($method); + $method_params = $method_object->defineParamTypes(); + foreach ($core_params as $key => $value) { + if (empty($method_params[$key])) { + unset($core_params[$key]); + } + } + $params = $params + $core_params; $service_phid = $repository->getAlmanacServicePHID(); @@ -123,9 +133,48 @@ $client = id(new ConduitClient($uri)) ->setHost($domain); - $token = PhabricatorConduitToken::loadClusterTokenForUser($user); - if ($token) { - $client->setConduitToken($token->getToken()); + if ($user->isOmnipotent()) { + // If the caller is the omnipotent user (normally, a daemon), we will + // sign the request with this host's asymmetric keypair. + + $public_path = AlmanacKeys::getKeyPath('device.pub'); + try { + $public_key = Filesystem::readFile($public_path); + } catch (Exception $ex) { + throw new PhutilAggregateException( + pht( + 'Unable to read device public key while attempting to make '. + 'authenticated method call within the Phabricator cluster. '. + 'Use `bin/almanac register` to register keys for this device. '. + 'Exception: %s', + $ex->getMessage()), + array($ex)); + } + + $private_path = AlmanacKeys::getKeyPath('device.key'); + try { + $private_key = Filesystem::readFile($private_path); + $private_key = new PhutilOpaqueEnvelope($private_key); + } catch (Exception $ex) { + throw new PhutilAggregateException( + pht( + 'Unable to read device private key while attempting to make '. + 'authenticated method call within the Phabricator cluster. '. + 'Use `bin/almanac register` to register keys for this device. '. + 'Exception: %s', + $ex->getMessage()), + array($ex)); + } + + $client->setSigningKeys($public_key, $private_key); + } else { + // If the caller is a normal user, we generate or retrieve a cluster + // API token. + + $token = PhabricatorConduitToken::loadClusterTokenForUser($user); + if ($token) { + $client->setConduitToken($token->getToken()); + } } return $client->callMethodSynchronous($method, $params);