Commit cbcdfd84 authored by pneubeck's avatar pneubeck Committed by Commit bot

platformKeys: Add per-extension sign permissions.

PlatformKeysService now supports persisting whether an extension is allowed to sign data with a key an unlimited number of times.

Currently, these permissions are only granted in the accompanying browser test and not in production, because UI is still missing.

BUG=450167

Review URL: https://codereview.chromium.org/905523002

Cr-Commit-Position: refs/heads/master@{#317053}
parent e2c9d120
......@@ -113,16 +113,19 @@ void SelectClientCertificates(const ClientCertificateRequest& request,
} // namespace subtle
// Returns the DER encoding of the X.509 Subject Public Key Info of the public
// key in |certificate|.
std::string GetSubjectPublicKeyInfo(
const scoped_refptr<net::X509Certificate>& certificate);
// Obtains information about the public key in |certificate|.
// If |certificate| contains an RSA key, sets |key_size_bits| to the modulus
// length, |public_key_spki_der| to the DER encoding of the X.509 Subject Public
// Key Info, and |key_type| to type RSA and returns true.
// length, and |key_type| to type RSA and returns true.
// If |certificate| contains any other key type, or if the public exponent of
// the RSA key in |certificate| is not F4, returns false and does not update any
// of the output parameters.
// All pointer arguments must not be null.
bool GetPublicKey(const scoped_refptr<net::X509Certificate>& certificate,
std::string* public_key_spki_der,
net::X509Certificate::PublicKeyType* key_type,
size_t* key_size_bits);
......
......@@ -777,12 +777,15 @@ void SelectClientCertificates(const ClientCertificateRequest& request,
} // namespace subtle
std::string GetSubjectPublicKeyInfo(
const scoped_refptr<net::X509Certificate>& certificate) {
const SECItem& spki_der = certificate->os_cert_handle()->derPublicKey;
return std::string(spki_der.data, spki_der.data + spki_der.len);
}
bool GetPublicKey(const scoped_refptr<net::X509Certificate>& certificate,
std::string* public_key_spki_der,
net::X509Certificate::PublicKeyType* key_type,
size_t* key_size_bits) {
const SECItem& spki_der = certificate->os_cert_handle()->derPublicKey;
net::X509Certificate::PublicKeyType key_type_tmp =
net::X509Certificate::kPublicKeyTypeUnknown;
size_t key_size_bits_tmp = 0;
......@@ -810,10 +813,8 @@ bool GetPublicKey(const scoped_refptr<net::X509Certificate>& certificate,
return false;
}
public_key_spki_der->assign(spki_der.data, spki_der.data + spki_der.len);
*key_type = key_type_tmp;
*key_size_bits = key_size_bits_tmp;
return true;
}
......
......@@ -39,6 +39,36 @@ namespace chromeos {
class PlatformKeysService : public KeyedService {
public:
struct KeyEntry;
using KeyEntries = std::vector<KeyEntry>;
// The SelectDelegate is used to select a single certificate from all
// certificates matching a request (see SelectClientCertificates). E.g. this
// can happen by exposing UI to let the user select.
class SelectDelegate {
public:
// TODO(pneubeck): Handle if the selection was aborted, e.g. by the user.
using CertificateSelectedCallback =
base::Callback<void(scoped_refptr<net::X509Certificate> selection)>;
SelectDelegate();
virtual ~SelectDelegate();
// Called on an interactive SelectClientCertificates call with the list of
// matching certificates, |certs|.
// The certificate passed to |callback| will be forwarded to the
// calling extension and the extension will get unlimited sign permission
// for this cert. By passing null to |callback|, no cert will be selected.
// Must eventually call |callback| or be destructed. |callback| must not be
// called after this delegate is destructed.
virtual void Select(const std::string& extension_id,
const net::CertificateList& certs,
const CertificateSelectedCallback& callback) = 0;
private:
DISALLOW_ASSIGN(SelectDelegate);
};
// Stores registration information in |state_store|, i.e. for each extension
// the list of public keys that are valid to be used for signing. Each key can
// be used for signing at most once.
......@@ -50,18 +80,22 @@ class PlatformKeysService : public KeyedService {
extensions::StateStore* state_store);
~PlatformKeysService() override;
// Disables the checks whether an extension is allowed to read client
// certificates or allowed to use the signing function of a key.
// TODO(pneubeck): Remove this once a permissions are implemented.
void DisablePermissionCheckForTesting();
// Sets the delegate which will be used for interactive
// SelectClientCertificates calls.
void SetSelectDelegate(scoped_ptr<SelectDelegate> delegate);
// Grants unlimited sign permission for |cert| to the extension with the ID
// |extension_id|.
void GrantUnlimitedSignPermission(const std::string& extension_id,
scoped_refptr<net::X509Certificate> cert);
// If the generation was successful, |public_key_spki_der| will contain the
// DER encoding of the SubjectPublicKeyInfo of the generated key and
// |error_message| will be empty. If it failed, |public_key_spki_der| will be
// empty and |error_message| contain an error message.
typedef base::Callback<void(const std::string& public_key_spki_der,
const std::string& error_message)>
GenerateKeyCallback;
using GenerateKeyCallback =
base::Callback<void(const std::string& public_key_spki_der,
const std::string& error_message)>;
// Generates an RSA key pair with |modulus_length_bits| and registers the key
// to allow a single sign operation by the given extension. |token_id| is
......@@ -77,8 +111,8 @@ class PlatformKeysService : public KeyedService {
// If signing was successful, |signature| will be contain the signature and
// |error_message| will be empty. If it failed, |signature| will be empty and
// |error_message| contain an error message.
typedef base::Callback<void(const std::string& signature,
const std::string& error_message)> SignCallback;
using SignCallback = base::Callback<void(const std::string& signature,
const std::string& error_message)>;
// Digests |data|, applies PKCS1 padding and afterwards signs the data with
// the private key matching |params.public_key|. If a non empty token id is
......@@ -118,27 +152,35 @@ class PlatformKeysService : public KeyedService {
// contain the list of matching certificates (maybe empty) and |error_message|
// will be empty. If an error occurred, |matches| will be null and
// |error_message| contain an error message.
typedef base::Callback<void(scoped_ptr<net::CertificateList> matches,
const std::string& error_message)>
SelectCertificatesCallback;
// Returns the list of all certificates that match |request|. |callback| will
// be invoked with these matches or an error message.
using SelectCertificatesCallback =
base::Callback<void(scoped_ptr<net::CertificateList> matches,
const std::string& error_message)>;
// Returns a list of certificates matching |request|.
// 1) all certificates that match the request (like being rooted in one of the
// give CAs) are determined. 2) if |interactive| is true, the currently set
// SelectDelegate is used to select a single certificate from these matches
// which will the extension will also be granted access to. 3) only
// certificates, that the extension has unlimited sign permission for, will be
// returned.
// |callback| will be invoked with these certificates or an error message.
// Will only call back during the lifetime of this object.
// TODO(pneubeck): Add the interactive option and integrate the select
// certificate dialog.
void SelectClientCertificates(
const platform_keys::ClientCertificateRequest& request,
bool interactive,
const std::string& extension_id,
const SelectCertificatesCallback& callback);
private:
using GetPlatformKeysCallback =
base::Callback<void(scoped_ptr<base::ListValue> platform_keys)>;
base::Callback<void(scoped_ptr<KeyEntries> platform_keys)>;
enum SignPermission { ONCE, UNLIMITED };
class Task;
class SignTask;
class PermissionUpdateTask;
class SelectTask;
class SignTask;
class Task;
// Starts |task| eventually. To ensure that at most one |Task| is running at a
// time, it queues |task| for later execution if necessary.
......@@ -159,7 +201,7 @@ class PlatformKeysService : public KeyedService {
// Writes |platform_keys| to the state store of the extension with id
// |extension_id|.
void SetPlatformKeysOfExtension(const std::string& extension_id,
scoped_ptr<base::ListValue> platform_keys);
const KeyEntries& platform_keys);
// Callback used by |GenerateRSAKey|.
// If the key generation was successful, registers the generated public key
......@@ -179,17 +221,6 @@ class PlatformKeysService : public KeyedService {
const std::string& public_key_spki_der,
Task* task);
// Calback used by |SelectClientCertificates|.
// If the certificate request could be processed successfully, |matches| will
// contain the list of matching certificates (maybe empty) and |error_message|
// will be empty. If an error occurred, |matches| will be null and
// |error_message| contain an error message.
void SelectClientCertificatesCallback(
const std::string& extension_id,
const SelectCertificatesCallback& callback,
scoped_ptr<net::CertificateList> matches,
const std::string& error_message);
// Callback used by |GetPlatformKeysOfExtension|.
// Is called with |value| set to the PlatformKeys value read from the
// StateStore, which it forwards to |callback|. On error, calls |callback|
......@@ -200,7 +231,7 @@ class PlatformKeysService : public KeyedService {
content::BrowserContext* browser_context_;
extensions::StateStore* state_store_;
bool permission_check_enabled_ = true;
scoped_ptr<SelectDelegate> select_delegate_;
std::queue<linked_ptr<Task>> tasks_;
base::WeakPtrFactory<PlatformKeysService> weak_factory_;
......
......@@ -114,9 +114,10 @@ PlatformKeysInternalGetPublicKeyFunction::Run() {
return RespondNow(Error(kErrorInvalidX509Cert));
PublicKeyInfo key_info;
if (!chromeos::platform_keys::GetPublicKey(
cert_x509, &key_info.public_key_spki_der, &key_info.key_type,
&key_info.key_size_bits) ||
key_info.public_key_spki_der =
chromeos::platform_keys::GetSubjectPublicKeyInfo(cert_x509);
if (!chromeos::platform_keys::GetPublicKey(cert_x509, &key_info.key_type,
&key_info.key_size_bits) ||
key_info.key_type != net::X509Certificate::kPublicKeyTypeRSA) {
return RespondNow(Error(kErrorAlgorithmNotSupported));
}
......@@ -154,7 +155,7 @@ PlatformKeysInternalSelectClientCertificatesFunction::Run() {
}
service->SelectClientCertificates(
request, extension_id(),
request, params->details.interactive, extension_id(),
base::Bind(&PlatformKeysInternalSelectClientCertificatesFunction::
OnSelectedCertificates,
this));
......@@ -173,9 +174,10 @@ void PlatformKeysInternalSelectClientCertificatesFunction::
std::vector<linked_ptr<api_pk::Match>> result_matches;
for (const scoped_refptr<net::X509Certificate>& match : *matches) {
PublicKeyInfo key_info;
if (!chromeos::platform_keys::GetPublicKey(
match, &key_info.public_key_spki_der, &key_info.key_type,
&key_info.key_size_bits)) {
key_info.public_key_spki_der =
chromeos::platform_keys::GetSubjectPublicKeyInfo(match);
if (!chromeos::platform_keys::GetPublicKey(match, &key_info.key_type,
&key_info.key_size_bits)) {
LOG(ERROR) << "Could not retrieve public key info.";
continue;
}
......
......@@ -100,8 +100,8 @@ class PlatformKeysTest : public ExtensionApiTest,
loop.Run();
}
chromeos::PlatformKeysServiceFactory::GetForBrowserContext(
browser()->profile())->DisablePermissionCheckForTesting();
base::FilePath extension_path = test_data_dir_.AppendASCII("platform_keys");
extension_ = LoadExtension(extension_path);
}
void TearDownOnMainThread() override {
......@@ -115,22 +115,47 @@ class PlatformKeysTest : public ExtensionApiTest,
loop.Run();
}
chromeos::PlatformKeysService* GetPlatformKeysService() {
return chromeos::PlatformKeysServiceFactory::GetForBrowserContext(
browser()->profile());
}
bool RunExtensionTest(const std::string& test_suite_name) {
// By default, the system token is not available.
std::string system_token_availability;
// Only if the current user is of the same domain as the device is enrolled
// to, the system token is available to the extension.
if (GetParam().device_status_ == DEVICE_STATUS_ENROLLED &&
GetParam().user_affiliation_ == USER_AFFILIATION_ENROLLED_DOMAIN) {
system_token_availability = "systemTokenEnabled";
}
GURL url = extension_->GetResourceURL(base::StringPrintf(
"basic.html?%s#%s", system_token_availability.c_str(),
test_suite_name.c_str()));
return RunExtensionSubtest("platform_keys", url.spec());
}
protected:
scoped_refptr<net::X509Certificate> client_cert1_;
scoped_refptr<net::X509Certificate> client_cert2_;
const extensions::Extension* extension_;
private:
void SetupTestCerts(const base::Closure& done_callback,
net::NSSCertDatabase* cert_db) {
scoped_refptr<net::X509Certificate> client_cert1 =
net::ImportClientCertAndKeyFromFile(net::GetTestCertsDirectory(),
"client_1.pem", "client_1.pk8",
cert_db->GetPrivateSlot().get());
ASSERT_TRUE(client_cert1.get());
client_cert1_ = net::ImportClientCertAndKeyFromFile(
net::GetTestCertsDirectory(), "client_1.pem", "client_1.pk8",
cert_db->GetPrivateSlot().get());
ASSERT_TRUE(client_cert1_.get());
// Import a second client cert signed by another CA than client_1 into the
// system wide key slot.
scoped_refptr<net::X509Certificate> client_cert2 =
net::ImportClientCertAndKeyFromFile(net::GetTestCertsDirectory(),
"client_2.pem", "client_2.pk8",
test_system_slot_->slot());
ASSERT_TRUE(client_cert2.get());
client_cert2_ = net::ImportClientCertAndKeyFromFile(
net::GetTestCertsDirectory(), "client_2.pem", "client_2.pk8",
test_system_slot_->slot());
ASSERT_TRUE(client_cert2_.get());
done_callback.Run();
}
......@@ -157,22 +182,58 @@ class PlatformKeysTest : public ExtensionApiTest,
scoped_ptr<crypto::ScopedTestSystemNSSKeySlot> test_system_slot_;
};
class TestSelectDelegate
: public chromeos::PlatformKeysService::SelectDelegate {
public:
explicit TestSelectDelegate(
scoped_refptr<net::X509Certificate> cert_to_select)
: cert_to_select_(cert_to_select) {}
~TestSelectDelegate() override {}
void Select(const std::string& extension_id,
const net::CertificateList& certs,
const CertificateSelectedCallback& callback) override {
if (!cert_to_select_) {
callback.Run(nullptr /* no cert */);
return;
}
scoped_refptr<net::X509Certificate> selection;
for (scoped_refptr<net::X509Certificate> cert : certs) {
if (cert->Equals(cert_to_select_.get())) {
selection = cert;
break;
}
}
callback.Run(selection);
}
private:
scoped_refptr<net::X509Certificate> cert_to_select_;
};
} // namespace
// Basic tests that start with already granted permissions for both client_cert1
// and client_cert2.
// On interactive calls, the simulated user does not select any cert.
IN_PROC_BROWSER_TEST_P(PlatformKeysTest, Basic) {
// By default, the system token is not available.
std::string system_token_availability;
// Only if the current user is of the same domain as the device is enrolled
// to, the system token is available to the extension.
if (GetParam().device_status_ == DEVICE_STATUS_ENROLLED &&
GetParam().user_affiliation_ == USER_AFFILIATION_ENROLLED_DOMAIN) {
system_token_availability = "systemTokenEnabled";
}
GetPlatformKeysService()->SetSelectDelegate(
make_scoped_ptr(new TestSelectDelegate(nullptr /* select no cert */)));
GetPlatformKeysService()->GrantUnlimitedSignPermission(extension_->id(),
client_cert1_);
GetPlatformKeysService()->GrantUnlimitedSignPermission(extension_->id(),
client_cert2_);
ASSERT_TRUE(RunExtensionTest("basicTests")) << message_;
}
// This permission test starts without any granted permissions.
// On interactive calls, the simulated user selects client_1, if matching.
IN_PROC_BROWSER_TEST_P(PlatformKeysTest, Permissions) {
GetPlatformKeysService()->SetSelectDelegate(
make_scoped_ptr(new TestSelectDelegate(client_cert1_)));
ASSERT_TRUE(RunExtensionSubtest("platform_keys",
"basic.html?" + system_token_availability))
<< message_;
ASSERT_TRUE(RunExtensionTest("permissionTests")) << message_;
}
INSTANTIATE_TEST_CASE_P(
......
......@@ -4,11 +4,13 @@
'use strict';
var systemTokenEnabled = (location.href.indexOf("systemTokenEnabled") != -1);
var systemTokenEnabled = (location.search.indexOf("systemTokenEnabled") != -1);
var selectedTestSuite = location.hash.slice(1);
console.log('[SELECTED TEST SUITE] ' + selectedTestSuite +
', systemTokenEnable ' + systemTokenEnabled);
var assertEq = chrome.test.assertEq;
var assertTrue = chrome.test.assertTrue;
var assertThrows = chrome.test.assertThrows;
var fail = chrome.test.fail;
var succeed = chrome.test.succeed;
var callbackPass = chrome.test.callbackPass;
......@@ -112,10 +114,9 @@ function sortCerts(certs) {
return certs.sort(compareArrays);
}
function assertCertsSelected(request, expectedCerts, callback) {
function assertCertsSelected(details, expectedCerts, callback) {
chrome.platformKeys.selectClientCertificates(
{interactive: false, request: request},
callbackPass(function(actualMatches) {
details, callbackPass(function(actualMatches) {
assertEq(expectedCerts.length, actualMatches.length,
'Number of stored certs not as expected');
if (expectedCerts.length == actualMatches.length) {
......@@ -190,32 +191,54 @@ function testHasSubtleCryptoMethods(token) {
succeed();
}
function testSelectAllCerts() {
var requestAll = {
var requestAll = {
certificateTypes: [],
certificateAuthorities: []
};
// Depends on |data|, thus it cannot be created immediately.
function requestCA1() {
return {
certificateTypes: [],
certificateAuthorities: []
certificateAuthorities: [data.client_1_issuer_dn.buffer]
};
}
function testSelectAllCerts() {
var expectedCerts = [data.client_1];
if (systemTokenEnabled)
expectedCerts.push(data.client_2);
assertCertsSelected(requestAll, expectedCerts);
assertCertsSelected({interactive: false, request: requestAll}, expectedCerts);
}
function testSelectCA1Certs() {
var requestCA1 = {
certificateTypes: [],
certificateAuthorities: [data.client_1_issuer_dn.buffer]
};
assertCertsSelected(requestCA1, [data.client_1]);
assertCertsSelected({interactive: false, request: requestCA1()},
[data.client_1]);
}
function testSelectAllReturnsNoCerts() {
assertCertsSelected({interactive: false, request: requestAll},
[] /* no certs selected */);
}
function testSelectAllReturnsClient1() {
assertCertsSelected({interactive: false, request: requestAll},
[data.client_1]);
}
function testInteractiveSelectNoCerts() {
assertCertsSelected({interactive: true, request: requestAll},
[] /* no certs selected */);
}
function testInteractiveSelectClient1() {
assertCertsSelected({interactive: true, request: requestAll},
[data.client_1]);
}
function testMatchResult() {
var requestCA1 = {
certificateTypes: [],
certificateAuthorities: [data.client_1_issuer_dn.buffer]
};
chrome.platformKeys.selectClientCertificates(
{interactive: false, request: requestCA1},
{interactive: false, request: requestCA1()},
callbackPass(function(matches) {
var expectedAlgorithm = {
modulusLength: 2048,
......@@ -282,7 +305,7 @@ function testSignNoHash() {
}));
}
function testSignSha1() {
function testSignSha1Client1() {
var keyParams = {
// Algorithm names are case-insensitive.
hash: {name: 'Sha-1'}
......@@ -305,18 +328,86 @@ function testSignSha1() {
}));
}
function runTests() {
var tests = [
testStaticMethods,
testSelectAllCerts,
testSelectCA1Certs,
testMatchResult,
testGetKeyPair,
testSignNoHash,
testSignSha1
];
chrome.test.runTests(tests);
// TODO(pneubeck): Test this by verifying that no private key is returned, once
// that's implemented.
function testSignFails(cert) {
var keyParams = {
hash: {name: 'SHA-1'}
};
var signParams = {
name: 'RSASSA-PKCS1-v1_5'
};
chrome.platformKeys.getKeyPair(
cert.buffer, keyParams, callbackPass(function(publicKey, privateKey) {
chrome.platformKeys.subtleCrypto()
.sign(signParams, privateKey, data.raw_data)
.then(function(signature) { fail('sign was expected to fail.'); },
callbackPass(function(error) {
assertTrue(error instanceof Error);
assertEq(
'The operation failed for an operation-specific reason',
error.message);
}));
}));
}
function testSignClient1Fails() {
testSignFails(data.client_1);
}
setUp(runTests);
function testSignClient2Fails() {
testSignFails(data.client_2);
}
var testSuites = {
// These tests assume already granted permissions for client_1 and client_2.
// On interactive selectClientCertificates calls, the simulated user does not
// select any cert.
basicTests: function() {
var tests = [
testStaticMethods,
testSelectAllCerts,
testSelectCA1Certs,
testInteractiveSelectNoCerts,
testMatchResult,
testGetKeyPair,
testSignNoHash,
testSignSha1Client1,
];
chrome.test.runTests(tests);
},
// This test suite starts without any granted permissions.
// On interactive selectClientCertificates calls, the simulated user selects
// client_1, if matching.
permissionTests: function() {
var tests = [
// Without permissions both sign attempts fail.
testSignClient1Fails,
testSignClient2Fails,
// Without permissions, non-interactive select calls return no certs.
testSelectAllReturnsNoCerts,
testInteractiveSelectClient1,
// Now that the permission for client_1 is granted.
// Verify that signing with client_1 is possible and with client_2 still
// fails.
testSignSha1Client1,
testSignClient2Fails,
// Verify that client_1 can still be selected interactively.
testInteractiveSelectClient1,
// Verify that client_1 but not client_2 is selected in non-interactive
// calls.
testSelectAllReturnsClient1,
];
chrome.test.runTests(tests);
}
};
setUp(testSuites[selectedTestSuite]);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment