def test_gpg_functions(self): """Signing, key export and util functions must raise on missing gpg. """ with self.assertRaises(UnsupportedLibraryError) as ctx: create_signature('bar') self.assertEqual(NO_GPG_MSG, str(ctx.exception)) with self.assertRaises(UnsupportedLibraryError) as ctx: export_pubkey('f00') self.assertEqual(NO_GPG_MSG, str(ctx.exception)) with self.assertRaises(UnsupportedLibraryError) as ctx: get_version() self.assertEqual(NO_GPG_MSG, str(ctx.exception))
def test_export_pubkey(self): """ export a public key and make sure the parameters are the right ones: since there's very little we can do to check rsa key parameters are right we pre-exported the public key to an ssh key, which we can load with cryptography for the sake of comparison """ # export our gpg key, using our functions key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) our_exported_key = dsa_create_pubkey(key_data) # load the equivalent ssh key, and make sure that we get the same RSA key # parameters ssh_key_basename = "{}.ssh".format(self.default_keyid) ssh_key_path = os.path.join(self.gnupg_home, ssh_key_basename) with open(ssh_key_path, "rb") as fp: keydata = fp.read() ssh_key = serialization.load_ssh_public_key(keydata, backends.default_backend()) self.assertEqual(ssh_key.public_numbers().y, our_exported_key.public_numbers().y) self.assertEqual(ssh_key.public_numbers().parameter_numbers.g, our_exported_key.public_numbers().parameter_numbers.g) self.assertEqual(ssh_key.public_numbers().parameter_numbers.q, our_exported_key.public_numbers().parameter_numbers.q) self.assertEqual(ssh_key.public_numbers().parameter_numbers.p, our_exported_key.public_numbers().parameter_numbers.p)
def test_export_pubkey(self): """ export a public key and make sure the parameters are the right ones: since there's very little we can do to check key parameters are right we pre-exported the public key to an x.509 SubjectPublicKeyInfo key, which we can load with cryptography for the sake of comparison """ # export our gpg key, using our functions key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) our_exported_key = dsa_create_pubkey(key_data) # load same key, pre-exported with 3rd-party tooling pem_key_basename = "{}.pem".format(self.default_keyid) pem_key_path = os.path.join(self.gnupg_home, pem_key_basename) with open(pem_key_path, "rb") as fp: keydata = fp.read() pem_key = serialization.load_pem_public_key(keydata, backends.default_backend()) # make sure keys match self.assertEqual(pem_key.public_numbers().y, our_exported_key.public_numbers().y) self.assertEqual(pem_key.public_numbers().parameter_numbers.g, our_exported_key.public_numbers().parameter_numbers.g) self.assertEqual(pem_key.public_numbers().parameter_numbers.q, our_exported_key.public_numbers().parameter_numbers.q) self.assertEqual(pem_key.public_numbers().parameter_numbers.p, our_exported_key.public_numbers().parameter_numbers.p)
def fetch_keyval_from_gpg(fingerprint): """ Retrieve the underlying 32-byte raw ed25519 public key for a GPG key. Given a GPG key fingerprint (40-character hex string), retrieve the GPG key, parse it, and return "q", the 32-byte ed25519 key value. This takes advantage of the GPG key parser in securesystemslib. The fingerprint will be stripped of spaces and lowercased, so you can use the GPG output even if it's in a funky format: 94A3 EED0 806C 1F10 7754 A446 FDAD 11B8 2DD4 0E8C 94A3 EED0 806C 1F10 7754 A446 FDAD 11B8 2DD4 0E8C # <-- No, this is actually not the same as the previous one, which uses \\xa0.... 94A3EED0806C1F107754A446FDAD11B82DD40E8C 94a3eed0806c1f107754a446fdad11b82dd40e8c etc. """ if not SSLIB_AVAILABLE: # TODO✅: Consider a missing-optional-dependency exception class. raise Exception( 'fetch_keyval_from_gpg requires the securesystemslib library, which ' 'appears to be unavailable.') fingerprint = fingerprint.lower().replace(' ', '').replace( '\xa0', '') # \xa0 is another space character that GPG sometimes outputs checkformat_gpg_fingerprint(fingerprint) key_parameters = gpg_funcs.export_pubkey(fingerprint) return key_parameters['keyval']['public']['q']
def test_gpg_sign_and_verify_object_with_default_key(self): """Create a signature using the default key on the keyring """ test_data = b'test_data' wrong_data = b'something malicious' signature = create_signature(test_data, homedir=self.gnupg_home) key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) self.assertTrue(verify_signature(signature, key_data, test_data)) self.assertFalse(verify_signature(signature, key_data, wrong_data))
def test_export_pubkey(self): """ export a public key and make sure the parameters are the right ones: since there's very little we can do to check rsa key parameters are right we pre-exported the public key to an ssh key, which we can load with cryptography for the sake of comparison """ # export our gpg key, using our functions key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) our_exported_key = rsa_create_pubkey(key_data) # load the equivalent ssh key, and make sure that we get the same RSA key # parameters ssh_key_basename = "{}.ssh".format(self.default_keyid) ssh_key_path = os.path.join(self.gnupg_home, ssh_key_basename) with open(ssh_key_path, "rb") as fp: keydata = fp.read() ssh_key = serialization.load_ssh_public_key(keydata, backends.default_backend()) self.assertEqual(ssh_key.public_numbers().n, our_exported_key.public_numbers().n) self.assertEqual(ssh_key.public_numbers().e, our_exported_key.public_numbers().e) subkey_keyids = list(key_data["subkeys"].keys()) # We export the whole master key bundle which must contain the subkeys self.assertTrue(self.signing_subkey_keyid.lower() in subkey_keyids) # Currently we do not exclude encryption subkeys self.assertTrue(self.encryption_subkey_keyid.lower() in subkey_keyids) # However we do exclude subkeys, whose algorithm we do not support self.assertFalse( self.unsupported_subkey_keyid.lower() in subkey_keyids) # When passing the subkey keyid we also export the whole keybundle key_data2 = export_pubkey(self.signing_subkey_keyid, homedir=self.gnupg_home) self.assertDictEqual(key_data, key_data2)
def test_gpg_key_retrieval_with_unknown_fingerprint(): if not SSLIB_AVAILABLE: pytest.skip('--TEST SKIPPED⚠️ : Unable to use GPG key retrieval or ' 'signing without securesystemslib and GPG.') return # TODO✅: Adjust this to use whatever assertRaises() functionality the # testing suite we're using provides. with pytest.raises(securesystemslib.gpg.exceptions.KeyNotFoundError): full_gpg_pubkey = gpg_funcs.export_pubkey(SAMPLE_UNKNOWN_FINGERPRINT) print('--TEST SUCCESS✅: detection of error when we pass an unknown ' 'key fingerprint to GPG for retrieval of the full public key.')
def test_gpg_sign_and_verify_object_default_keyring(self): """Sign/verify using keyring from envvar. """ test_data = b'test_data' gnupg_home_backup = os.environ.get("GNUPGHOME") os.environ["GNUPGHOME"] = self.gnupg_home signature = create_signature(test_data, keyid=self.default_keyid) key_data = export_pubkey(self.default_keyid) self.assertTrue(verify_signature(signature, key_data, test_data)) # Reset GNUPGHOME if gnupg_home_backup: os.environ["GNUPGHOME"] = gnupg_home_backup else: del os.environ["GNUPGHOME"]
def test_verify_short_signature(self): """Correctly verify a special-crafted short signature. """ test_data = b"hello" signature_path = os.path.join(self.gnupg_home, "short.sig") # Read special-crafted raw gpg signature that is one byte too short with open(signature_path, "rb") as f: signature_data = f.read() # Check that the signature is padded upon parsing # NOTE: The returned signature is a hex string and thus twice as long signature = parse_signature_packet(signature_data) self.assertTrue(len(signature["signature"]) == (ED25519_SIG_LENGTH * 2)) # Check that the signature can be successfully verified key = export_pubkey(self.default_keyid, homedir=self.gnupg_home) self.assertTrue(verify_signature(signature, key, test_data))
def test_verify_signature_with_expired_key(self): """Test sig verification with expired key raises KeyExpirationError. """ signature = { "keyid": self.expired_key_keyid, "other_headers": "deadbeef", "signature": "deadbeef", } content = b"livestock" key = export_pubkey(self.expired_key_keyid, homedir=self.gnupg_home) with self.assertRaises(KeyExpirationError) as ctx: verify_signature(signature, key, content) expected = ("GPG key 'e8ac80c924116dabb51d4b987cb07d6d2c199c7c' " "created on '2019-03-25 12:46 UTC' with validity period '1 day, " "0:25:01' expired on '2019-03-26 13:11 UTC'.") self.assertTrue(expected == str(ctx.exception), "\nexpected: {}" "\ngot: {}".format(expected, ctx.exception))
def fetch_keyval_from_gpg(fingerprint): """ Retrieve the underlying 32-byte raw ed25519 public key for a GPG key. Given a GPG key fingerprint (40-character hex string), retrieve the GPG key, parse it, and return "q", the 32-byte ed25519 key value. This takes advantage of the GPG key parser in securesystemslib. """ if not SSLIB_AVAILABLE: # TODO✅: Consider a missing-optional-dependency exception class. raise Exception( 'sign_root_metadata_via_gpg requires the securesystemslib library, which ' 'appears to be unavailable.') checkformat_gpg_fingerprint(fingerprint) key_parameters = gpg_funcs.export_pubkey(fingerprint) return key_parameters['keyval']['public']['q']
def test_export_pubkey_error(self): """Test correct error is raised if function called incorrectly. """ with self.assertRaises(ValueError): export_pubkey("not-a-key-id")
def sign_via_gpg(data_to_sign, gpg_key_fingerprint): """ <Purpose> This is an alternative to the car.authenticate.sign() function, for use with OpenPGP keys, allowing us to use protected keys in YubiKeys (which provide an OpenPGP interface) to sign data. The signature is not simply over data_to_sign, as is the case with the car.authenticate.sign() function, but over an expanded payload with metadata about the signature to be signed, as specified by the OpenPGP standard (RFC 4880). See data_to_sign and Security Note below. This process is nominally deterministic, but varies with the precise time, since there is a timestamp added by GPG into the signed payload. Nonetheless, this process does not depend at any point on the ability to generate random data (unlike key generation). This function requires securesystemslib, which is otherwise an optional dependency. <Arguments> data_to_sign The raw bytes of interest that will be signed by GPG. Note that pursuant to the OpenPGP standard, GPG will add to this data: specifically, it includes metadata about the signature that is about to be made into the data that will be signed. We do not care about that metadata, and we do not want to burden signature verification with its processing, so we essentially ignore it. This should have negligible security impact, but for more information, see "A note on security" below. gpg_key_fingerprint This is a (fairly) unique identifier for an OpenPGP key pair. Also Known as a "long" GPG keyid, a GPG fingerprint is 40-hex-character string representing 20 bytes of raw data, the SHA-1 hash of a collection of the GPG key's properties. Internally, GPG uses the key fingerprint to identify keys the client knows of. Note that an OpenPGP public key is a larger object identified by a fingerprint. GPG keys include two things, from our perspective: - the raw bytes of the actual cryptographic key (in our case the 32-byte value "q" for an ed25519 public key) - lots of data that is totally extraneous to us, including a timestamp, some representations of relationships with other keys (subkeys, signed-by lists, etc.), potential revocations, etc...) We do not care about this extra data because we are using the OpenPGP standard not for its key-to-key semantics or any element of its Public Key Infrastructure features (revocation, vouching for other keys, key relationships, etc.), but simply as a means of asking YubiKeys to sign data for us, with ed25519 keys whose raw public key value ("q") we know to expect. <Returns> Returns two values: - a dictionary representing a GPG signature, conforming to securesystemslib.formats.GPG_SIGNATURE_SCHEMA, and - a gpg public key object, a dictionary conforming to securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA. This is unlike sign(), which returns 64 bytes of raw ed25519 signature. <Security Note> A note on the security implications of this treatment of OpenPGP signatures: TL;DR: It is NOT easier for an attacker to find a collision; however, it IS easier, IF an attacker CAN find a collision, to do so in a way that presents a specific, arbitrary payload. Note that pursuant to the OpenPGP standard, GPG will add to the data we ask it to sign (data_to_sign) before signing it. Specifically, GPG will add, to the payload-to-be-signed, OpenPGP metadata about the signature it is about to create. We do not care about that metadata, and we do not want to burden signature verification with its processing (that is, we do not want to use GPG to verify these signatures; conda will do that with simpler code). As a result, we will ignore this data when parsing the signed payload. This will mean that there will be many different messages that have the same meaning to us: signed: <some raw data we send to GPG: 'ABCDEF...'> <some data GPG adds in: '123456...'> Since we will not be processing the '123456...' above, '654321...' would have the same effect: as long as the signature is verified, we don't care what's in that portion of the payload. Since there are many, many payloads that mean the same thing to us, an attacker has a vast space of options all with the same meaning to us in which to search for (effectively) a useful SHA256 hash collision to find different data that says something *specific* and still *succeeds* in signature verification using the same signature. While that is not ideal, it is difficult enough simply to find a SHA256 collision that this is acceptable. """ if not SSLIB_AVAILABLE: # TODO✅: Consider a missing-optional-dependency exception class. raise Exception( 'sign_via_gpg requires the securesystemslib library, which ' 'appears to be unavailable.') sig = gpg_funcs.create_signature(data_to_sign, gpg_key_fingerprint) full_gpg_pubkey = gpg_funcs.export_pubkey(gpg_key_fingerprint) # 💣💥 Debug only. # 💣💥 Debug only. assert gpg_funcs.verify_signature(sig, full_gpg_pubkey, data_to_sign) return sig, full_gpg_pubkey