diff options
Diffstat (limited to 'keymanager')
-rw-r--r-- | keymanager/src/leap/keymanager/__init__.py | 4 | ||||
-rw-r--r-- | keymanager/src/leap/keymanager/openpgp.py | 18 | ||||
-rw-r--r-- | keymanager/src/leap/keymanager/tests/__init__.py | 9 | ||||
-rw-r--r-- | keymanager/src/leap/keymanager/tests/fixtures/private_key.bin | bin | 0 -> 2202 bytes | |||
-rw-r--r-- | keymanager/src/leap/keymanager/tests/fixtures/public_key.bin | bin | 0 -> 2202 bytes | |||
-rw-r--r-- | keymanager/src/leap/keymanager/tests/test_keymanager.py | 68 | ||||
-rw-r--r-- | keymanager/src/leap/keymanager/tests/test_openpgp.py | 38 |
7 files changed, 93 insertions, 44 deletions
diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 1106c23..194a4ee 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -831,7 +831,7 @@ class KeyManager(object): self._assert_supported_key_type(ktype) _keys = self._wrapper_map[ktype] - pubkey, privkey = _keys.parse_ascii_key(key, address) + pubkey, privkey = _keys.parse_key(key, address) if pubkey is None: return defer.fail(KeyNotFound(key)) @@ -875,7 +875,7 @@ class KeyManager(object): ascii_content = yield self._get_with_combined_ca_bundle(uri) # XXX parse binary keys - pubkey, _ = _keys.parse_ascii_key(ascii_content, address) + pubkey, _ = _keys.parse_key(ascii_content, address) if pubkey is None: raise KeyNotFound(uri) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index a843261..82050cc 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -382,9 +382,9 @@ class OpenPGPScheme(EncryptionScheme): d.addCallback(build_key) return d - def parse_ascii_key(self, key_data, address=None): + def parse_key(self, key_data, address=None): """ - Parses an ascii armored key (or key pair) data and returns + Parses a key (or key pair) data and returns the OpenPGPKey keys. :param key_data: the key data to be parsed. @@ -400,9 +400,9 @@ class OpenPGPScheme(EncryptionScheme): # TODO: add more checks for correct key data. leap_assert(key_data is not None, 'Data does not represent a key.') - priv_info, privkey = process_ascii_key( + priv_info, privkey = process_key( key_data, self._gpgbinary, secret=True) - pub_info, pubkey = process_ascii_key( + pub_info, pubkey = process_key( key_data, self._gpgbinary, secret=False) if not pubkey: @@ -421,9 +421,9 @@ class OpenPGPScheme(EncryptionScheme): return (openpgp_pubkey, openpgp_privkey) - def put_ascii_key(self, key_data, address): + def put_raw_key(self, key_data, address): """ - Put key contained in ascii-armored C{key_data} in local storage. + Put key contained in C{key_data} in local storage. :param key_data: The key data to be stored. :type key_data: str or unicode @@ -437,7 +437,7 @@ class OpenPGPScheme(EncryptionScheme): openpgp_privkey = None try: - openpgp_pubkey, openpgp_privkey = self.parse_ascii_key( + openpgp_pubkey, openpgp_privkey = self.parse_key( key_data, address) except (errors.KeyAddressMismatch, errors.KeyFingerprintMismatch) as e: return defer.fail(e) @@ -546,7 +546,7 @@ class OpenPGPScheme(EncryptionScheme): Build an OpenPGPKey for C{address} based on C{key} from local gpg storage. - ASCII armored GPG key data has to be queried independently in this + GPG key data has to be queried independently in this wrapper, so we receive it in C{key_data}. :param address: Active address for the key. @@ -850,7 +850,7 @@ class OpenPGPScheme(EncryptionScheme): return doclist[0] -def process_ascii_key(key_data, gpgbinary, secret=False): +def process_key(key_data, gpgbinary, secret=False): with TempGPGWrapper(gpgbinary=gpgbinary) as gpg: try: gpg.import_keys(key_data) diff --git a/keymanager/src/leap/keymanager/tests/__init__.py b/keymanager/src/leap/keymanager/tests/__init__.py index 20d05e8..2a6a3f1 100644 --- a/keymanager/src/leap/keymanager/tests/__init__.py +++ b/keymanager/src/leap/keymanager/tests/__init__.py @@ -29,6 +29,7 @@ from leap.soledad.client import Soledad from leap.keymanager import KeyManager from leap.keymanager.openpgp import OpenPGPKey +PATH = os.path.dirname(os.path.realpath(__file__)) ADDRESS = 'leap@leap.se' ADDRESS_2 = 'anotheruser@leap.se' @@ -95,6 +96,14 @@ class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest): else: return "/usr/bin/gpg" + def get_public_binary_key(self): + with open(PATH + '/fixtures/public_key.bin', 'r') as binary_public_key: + return binary_public_key.read() + + def get_private_binary_key(self): + with open(PATH + '/fixtures/private_key.bin', 'r') as binary_private_key: + return binary_private_key.read() + # key 24D18DDF: public key "Leap Test Key <leap@leap.se>" KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" diff --git a/keymanager/src/leap/keymanager/tests/fixtures/private_key.bin b/keymanager/src/leap/keymanager/tests/fixtures/private_key.bin Binary files differnew file mode 100644 index 0000000..ab17431 --- /dev/null +++ b/keymanager/src/leap/keymanager/tests/fixtures/private_key.bin diff --git a/keymanager/src/leap/keymanager/tests/fixtures/public_key.bin b/keymanager/src/leap/keymanager/tests/fixtures/public_key.bin Binary files differnew file mode 100644 index 0000000..ab17431 --- /dev/null +++ b/keymanager/src/leap/keymanager/tests/fixtures/public_key.bin diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 6347d56..05c1cdd 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -136,7 +136,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): @defer.inlineCallbacks def test_get_all_keys_in_db(self): km = self._key_manager() - yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_raw_key(PRIVATE_KEY, ADDRESS) # get public keys keys = yield km.get_all_keys(False) self.assertEqual(len(keys), 1, 'Wrong number of keys') @@ -151,7 +151,20 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): @defer.inlineCallbacks def test_get_public_key(self): km = self._key_manager() - yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_raw_key(PRIVATE_KEY, ADDRESS) + # get the key + key = yield km.get_key(ADDRESS, OpenPGPKey, private=False, + fetch_remote=False) + self.assertTrue(key is not None) + self.assertTrue(ADDRESS in key.uids) + self.assertEqual( + key.fingerprint.lower(), KEY_FINGERPRINT.lower()) + self.assertFalse(key.private) + + @defer.inlineCallbacks + def test_get_public_key_with_binary_private_key(self): + km = self._key_manager() + yield km._wrapper_map[OpenPGPKey].put_raw_key(self.get_private_binary_key(), ADDRESS) # get the key key = yield km.get_key(ADDRESS, OpenPGPKey, private=False, fetch_remote=False) @@ -164,7 +177,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): @defer.inlineCallbacks def test_get_private_key(self): km = self._key_manager() - yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_raw_key(PRIVATE_KEY, ADDRESS) # get the key key = yield km.get_key(ADDRESS, OpenPGPKey, private=True, fetch_remote=False) @@ -186,7 +199,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): """ token = "mytoken" km = self._key_manager(token=token) - yield km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_raw_key(PUBLIC_KEY, ADDRESS) km._async_client_pinned.request = Mock(return_value=defer.succeed('')) # the following data will be used on the send km.ca_cert_path = 'capath' @@ -279,6 +292,19 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): self.assertTrue(ADDRESS in key.uids) @defer.inlineCallbacks + def test_put_key_binary(self): + """ + Test that putting binary key works + """ + km = self._key_manager(url=NICKSERVER_URI) + + yield km.put_raw_key(self.get_public_binary_key(), OpenPGPKey, ADDRESS) + key = yield km.get_key(ADDRESS, OpenPGPKey) + + self.assertIsInstance(key, OpenPGPKey) + self.assertTrue(ADDRESS in key.uids) + + @defer.inlineCallbacks def test_fetch_uri_ascii_key(self): """ Test that fetch key downloads the ascii key and gets included in @@ -292,6 +318,20 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): key = yield km.get_key(ADDRESS, OpenPGPKey) self.assertEqual(KEY_FINGERPRINT, key.fingerprint) + @defer.inlineCallbacks + def test_fetch_uri_binary_key(self): + """ + Test that fetch key downloads the binary key and gets included in + the local storage + """ + km = self._key_manager() + + km._async_client.request = Mock(return_value=defer.succeed(self.get_public_binary_key())) + + yield km.fetch_key(ADDRESS, "http://site.domain/key", OpenPGPKey) + key = yield km.get_key(ADDRESS, OpenPGPKey) + self.assertEqual(KEY_FINGERPRINT, key.fingerprint) + def test_fetch_uri_empty_key(self): """ Test that fetch key raises KeyNotFound if no key in the url @@ -391,8 +431,8 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): def test_decrypt_updates_sign_used_for_signer(self): # given km = self._key_manager() - yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) - yield km._wrapper_map[OpenPGPKey].put_ascii_key( + yield km._wrapper_map[OpenPGPKey].put_raw_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_raw_key( PRIVATE_KEY_2, ADDRESS_2) encdata = yield km.encrypt('data', ADDRESS, OpenPGPKey, sign=ADDRESS_2, fetch_remote=False) @@ -409,9 +449,9 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): def test_decrypt_does_not_update_sign_used_for_recipient(self): # given km = self._key_manager() - yield km._wrapper_map[OpenPGPKey].put_ascii_key( + yield km._wrapper_map[OpenPGPKey].put_raw_key( PRIVATE_KEY, ADDRESS) - yield km._wrapper_map[OpenPGPKey].put_ascii_key( + yield km._wrapper_map[OpenPGPKey].put_raw_key( PRIVATE_KEY_2, ADDRESS_2) encdata = yield km.encrypt('data', ADDRESS, OpenPGPKey, sign=ADDRESS_2, fetch_remote=False) @@ -434,8 +474,8 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): def test_keymanager_openpgp_encrypt_decrypt(self): km = self._key_manager() # put raw private key - yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) - yield km._wrapper_map[OpenPGPKey].put_ascii_key( + yield km._wrapper_map[OpenPGPKey].put_raw_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_raw_key( PRIVATE_KEY_2, ADDRESS_2) # encrypt encdata = yield km.encrypt(self.RAW_DATA, ADDRESS, OpenPGPKey, @@ -453,8 +493,8 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): def test_keymanager_openpgp_encrypt_decrypt_wrong_sign(self): km = self._key_manager() # put raw keys - yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) - yield km._wrapper_map[OpenPGPKey].put_ascii_key( + yield km._wrapper_map[OpenPGPKey].put_raw_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_raw_key( PRIVATE_KEY_2, ADDRESS_2) # encrypt encdata = yield km.encrypt(self.RAW_DATA, ADDRESS, OpenPGPKey, @@ -470,7 +510,7 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): def test_keymanager_openpgp_sign_verify(self): km = self._key_manager() # put raw private keys - yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_raw_key(PRIVATE_KEY, ADDRESS) signdata = yield km.sign(self.RAW_DATA, ADDRESS, OpenPGPKey, detach=False) self.assertNotEqual(self.RAW_DATA, signdata) @@ -483,7 +523,7 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): def test_keymanager_encrypt_key_not_found(self): km = self._key_manager() - d = km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + d = km._wrapper_map[OpenPGPKey].put_raw_key(PRIVATE_KEY, ADDRESS) d.addCallback( lambda _: km.encrypt(self.RAW_DATA, ADDRESS_2, OpenPGPKey, sign=ADDRESS, fetch_remote=False)) diff --git a/keymanager/src/leap/keymanager/tests/test_openpgp.py b/keymanager/src/leap/keymanager/tests/test_openpgp.py index 0e5f6be..68fb4e0 100644 --- a/keymanager/src/leap/keymanager/tests/test_openpgp.py +++ b/keymanager/src/leap/keymanager/tests/test_openpgp.py @@ -68,7 +68,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) yield self._assert_key_not_found(pgp, ADDRESS) - yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + yield pgp.put_raw_key(PUBLIC_KEY, ADDRESS) key = yield pgp.get_key(ADDRESS, private=False) yield pgp.delete_key(key) yield self._assert_key_not_found(pgp, ADDRESS) @@ -78,7 +78,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) yield self._assert_key_not_found(pgp, ADDRESS) - yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + yield pgp.put_raw_key(PUBLIC_KEY, ADDRESS) key = yield pgp.get_key(ADDRESS, private=False) self.assertIsInstance(key, openpgp.OpenPGPKey) self.assertTrue( @@ -93,7 +93,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) yield self._assert_key_not_found(pgp, ADDRESS) - yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + yield pgp.put_raw_key(PUBLIC_KEY, ADDRESS) yield self._assert_key_not_found(pgp, ADDRESS, private=True) key = yield pgp.get_key(ADDRESS, private=False) self.assertTrue(ADDRESS in key.address) @@ -109,7 +109,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): self._soledad, gpgbinary=self.gpg_binary_path) # encrypt - yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + yield pgp.put_raw_key(PUBLIC_KEY, ADDRESS) pubkey = yield pgp.get_key(ADDRESS, private=False) cyphertext = yield pgp.encrypt(data, pubkey) @@ -121,7 +121,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): # decrypt yield self._assert_key_not_found(pgp, ADDRESS, private=True) - yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + yield pgp.put_raw_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) decrypted, _ = yield pgp.decrypt(cyphertext, privkey) self.assertEqual(decrypted, data) @@ -136,7 +136,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): data = 'data' pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + yield pgp.put_raw_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) signed = pgp.sign(data, privkey) self.assertRaises( @@ -148,7 +148,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): data = 'data' pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + yield pgp.put_raw_key(PUBLIC_KEY, ADDRESS) self.assertRaises( AssertionError, pgp.sign, data, ADDRESS, OpenPGPKey) @@ -158,10 +158,10 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): data = 'data' pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + yield pgp.put_raw_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) signed = pgp.sign(data, privkey) - yield pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) + yield pgp.put_raw_key(PUBLIC_KEY_2, ADDRESS_2) wrongkey = yield pgp.get_key(ADDRESS_2) self.assertFalse(pgp.verify(signed, wrongkey)) @@ -170,7 +170,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): data = 'data' pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + yield pgp.put_raw_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) pubkey = yield pgp.get_key(ADDRESS, private=False) self.failureResultOf( @@ -182,7 +182,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): data = 'data' pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + yield pgp.put_raw_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) pubkey = yield pgp.get_key(ADDRESS, private=False) encrypted_and_signed = yield pgp.encrypt( @@ -196,11 +196,11 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): data = 'data' pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + yield pgp.put_raw_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) pubkey = yield pgp.get_key(ADDRESS, private=False) encrypted_and_signed = yield pgp.encrypt(data, pubkey, sign=privkey) - yield pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) + yield pgp.put_raw_key(PUBLIC_KEY_2, ADDRESS_2) wrongkey = yield pgp.get_key(ADDRESS_2) decrypted, validsign = yield pgp.decrypt(encrypted_and_signed, privkey, @@ -213,7 +213,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): data = 'data' pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + yield pgp.put_raw_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) signed = pgp.sign(data, privkey, detach=False) pubkey = yield pgp.get_key(ADDRESS, private=False) @@ -225,11 +225,11 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + yield pgp.put_raw_key(PRIVATE_KEY, ADDRESS) pubkey = yield pgp.get_key(ADDRESS, private=False) privkey = yield pgp.get_key(ADDRESS, private=True) - yield pgp.put_ascii_key(PRIVATE_KEY_2, ADDRESS_2) + yield pgp.put_raw_key(PRIVATE_KEY_2, ADDRESS_2) pubkey2 = yield pgp.get_key(ADDRESS_2, private=False) privkey2 = yield pgp.get_key(ADDRESS_2, private=True) @@ -246,7 +246,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): data = 'data' pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + yield pgp.put_raw_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) signature = yield pgp.sign(data, privkey, detach=True) pubkey = yield pgp.get_key(ADDRESS, private=False) @@ -272,7 +272,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_self_repair_no_keys(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + yield pgp.put_raw_key(PUBLIC_KEY, ADDRESS) get_from_index = self._soledad.get_from_index delete_doc = self._soledad.delete_doc @@ -304,7 +304,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + yield pgp.put_raw_key(PUBLIC_KEY, ADDRESS) self.assertEqual(self.count, 2) self._soledad.delete_doc = delete_doc |