def import_key(key): """Import an ASCII Armor key. A Radix64 format keyid is also supported for backwards compatibility, but should never be used; the key retrieval mechanism is insecure and subject to man-in-the-middle attacks voiding all signature checks using that key. :param keyid: The key in ASCII armor format, including BEGIN and END markers. :raises: GPGKeyError if the key could not be imported """ key = key.strip() if '-' in key or '\n' in key: # Send everything not obviously a keyid to GPG to import, as # we trust its validation better than our own. eg. handling # comments before the key. log("PGP key found (looks like ASCII Armor format)", level=DEBUG) if ('-----BEGIN PGP PUBLIC KEY BLOCK-----' in key and '-----END PGP PUBLIC KEY BLOCK-----' in key): log("Importing ASCII Armor PGP key", level=DEBUG) with NamedTemporaryFile() as keyfile: with open(keyfile.name, 'w') as fd: fd.write(key) fd.write("\n") cmd = ['apt-key', 'add', keyfile.name] try: subprocess.check_call(cmd) except subprocess.CalledProcessError: error = "Error importing PGP key '{}'".format(key) log(error) raise GPGKeyError(error) else: raise GPGKeyError("ASCII armor markers missing from GPG key") else: # We should only send things obviously not a keyid offsite # via this unsecured protocol, as it may be a secret or part # of one. log("PGP key found (looks like Radix64 format)", level=WARNING) log( "INSECURLY importing PGP key from keyserver; " "full key not provided.", level=WARNING) cmd = [ 'apt-key', 'adv', '--keyserver', 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key ] try: _run_with_retries(cmd) except subprocess.CalledProcessError: error = "Error importing PGP key '{}'".format(key) log(error) raise GPGKeyError(error)
def _get_keyid_by_gpg_key(key_material): """Get a GPG key fingerprint by GPG key material. Gets a GPG key fingerprint (40-digit, 160-bit) by the ASCII armor-encoded or binary GPG key material. Can be used, for example, to generate file names for keys passed via charm options. :param key_material: ASCII armor-encoded or binary GPG key material :type key_material: bytes :raises: GPGKeyError if invalid key material has been provided :returns: A GPG key fingerprint :rtype: str """ # Use the same gpg command for both Xenial and Bionic cmd = 'gpg --with-colons --with-fingerprint' ps = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) out, err = ps.communicate(input=key_material) if six.PY3: out = out.decode('utf-8') err = err.decode('utf-8') if 'gpg: no valid OpenPGP data found.' in err: raise GPGKeyError('Invalid GPG key material provided') # from gnupg2 docs: fpr :: Fingerprint (fingerprint is in field 10) return re.search(r"^fpr:{9}([0-9A-F]{40}):$", out, re.MULTILINE).group(1)
def _get_keyid_by_gpg_key(key_material): """Get a GPG key fingerprint by GPG key material. Gets a GPG key fingerprint (40-digit, 160-bit) by the ASCII armor-encoded or binary GPG key material. Can be used, for example, to generate file names for keys passed via charm options. :param key_material: ASCII armor-encoded or binary GPG key material :type key_material: bytes :raises: GPGKeyError if invalid key material has been provided :returns: A GPG key fingerprint :rtype: str """ # trusty, xenial and bionic handling differs due to gpg 1.x to 2.x change release = get_distrib_codename() is_gpgv2_distro = CompareHostReleases(release) >= "bionic" if is_gpgv2_distro: # --import is mandatory, otherwise fingerprint is not printed cmd = 'gpg --with-colons --import-options show-only --import --dry-run' else: cmd = 'gpg --with-colons --with-fingerprint' ps = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) out, err = ps.communicate(input=key_material) if six.PY3: out = out.decode('utf-8') err = err.decode('utf-8') if 'gpg: no valid OpenPGP data found.' in err: raise GPGKeyError('Invalid GPG key material provided') # from gnupg2 docs: fpr :: Fingerprint (fingerprint is in field 10) return re.search(r"^fpr:{9}([0-9A-F]{40}):$", out, re.MULTILINE).group(1)
def import_key(keyid): """Import a key in either ASCII Armor or Radix64 format. `keyid` is either the keyid to fetch from a PGP server, or the key in ASCII armor foramt. :param keyid: String of key (or key id). :raises: GPGKeyError if the key could not be imported """ key = keyid.strip() if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and key.endswith('-----END PGP PUBLIC KEY BLOCK-----')): log("PGP key found (looks like ASCII Armor format)", level=DEBUG) log("Importing ASCII Armor PGP key", level=DEBUG) with NamedTemporaryFile() as keyfile: with open(keyfile.name, 'w') as fd: fd.write(key) fd.write("\n") cmd = ['apt-key', 'add', keyfile.name] try: subprocess.check_call(cmd) except subprocess.CalledProcessError: error = "Error importing PGP key '{}'".format(key) log(error) raise GPGKeyError(error) else: log("PGP key found (looks like Radix64 format)", level=DEBUG) log("Importing PGP key from keyserver", level=DEBUG) cmd = [ 'apt-key', 'adv', '--keyserver', 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key ] try: subprocess.check_call(cmd) except subprocess.CalledProcessError: error = "Error importing PGP key '{}'".format(key) log(error) raise GPGKeyError(error)
def import_key(key): """Import an ASCII Armor key. A Radix64 format keyid is also supported for backwards compatibility. In this case Ubuntu keyserver will be queried for a key via HTTPS by its keyid. This method is less preferable because https proxy servers may require traffic decryption which is equivalent to a man-in-the-middle attack (a proxy server impersonates keyserver TLS certificates and has to be explicitly trusted by the system). :param key: A GPG key in ASCII armor format, including BEGIN and END markers or a keyid. :type key: (bytes, str) :raises: GPGKeyError if the key could not be imported """ key = key.strip() if '-' in key or '\n' in key: # Send everything not obviously a keyid to GPG to import, as # we trust its validation better than our own. eg. handling # comments before the key. log("PGP key found (looks like ASCII Armor format)", level=DEBUG) if ('-----BEGIN PGP PUBLIC KEY BLOCK-----' in key and '-----END PGP PUBLIC KEY BLOCK-----' in key): log("Writing provided PGP key in the binary format", level=DEBUG) if six.PY3: key_bytes = key.encode('utf-8') else: key_bytes = key key_name = _get_keyid_by_gpg_key(key_bytes) key_gpg = _dearmor_gpg_key(key_bytes) _write_apt_gpg_keyfile(key_name=key_name, key_material=key_gpg) else: raise GPGKeyError("ASCII armor markers missing from GPG key") else: log("PGP key found (looks like Radix64 format)", level=WARNING) log( "SECURELY importing PGP key from keyserver; " "full key not provided.", level=WARNING) # as of bionic add-apt-repository uses curl with an HTTPS keyserver URL # to retrieve GPG keys. `apt-key adv` command is deprecated as is # apt-key in general as noted in its manpage. See lp:1433761 for more # history. Instead, /etc/apt/trusted.gpg.d is used directly to drop # gpg key_asc = _get_key_by_keyid(key) # write the key in GPG format so that apt-key list shows it key_gpg = _dearmor_gpg_key(key_asc) _write_apt_gpg_keyfile(key_name=key, key_material=key_gpg)
def _dearmor_gpg_key(key_asc): """Converts a GPG key in the ASCII armor format to the binary format. :param key_asc: A GPG key in ASCII armor format. :type key_asc: (str, bytes) :returns: A GPG key in binary format :rtype: (str, bytes) :raises: GPGKeyError """ ps = subprocess.Popen(['gpg', '--dearmor'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) out, err = ps.communicate(input=key_asc) # no need to decode output as it is binary (invalid utf-8), only error err = err.decode('utf-8') if 'gpg: no valid OpenPGP data found.' in err: raise GPGKeyError('Invalid GPG key material. Check your network setup' ' (MTU, routing, DNS) and/or proxy server settings' ' as well as destination keyserver status.') else: return out