Merge branch 'gpg-init'
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
[MESSAGES CONTROL]
|
||||
disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return
|
||||
|
||||
[SIMILARITIES]
|
||||
min-similarity-lines=5
|
||||
|
||||
@@ -30,11 +30,12 @@ $ pip install --user (trezor|keepkey|ledger)_agent
|
||||
# Quickstart
|
||||
|
||||
## Identity creation
|
||||
[](https://asciinema.org/a/90416)
|
||||
[](https://asciinema.org/a/3iNw2L9QWB8R3EVdYdAxMOLK8)
|
||||
|
||||
In order to use specific device type for GPG indentity creation, use either command:
|
||||
```
|
||||
$ DEVICE=(trezor,ledger) ./scripts/gpg-init "John Doe <john@doe.bit>"
|
||||
$ trezor-gpg init "Roman Zeyde <roman.zeyde@gmail.com>"
|
||||
$ ledger-gpg init "Roman Zeyde <roman.zeyde@gmail.com>"
|
||||
```
|
||||
|
||||
## Sample usage (signature and decryption)
|
||||
@@ -102,7 +103,7 @@ $ GNUPGHOME=~/.gnupg/trezor qtpass
|
||||
```
|
||||
|
||||
## Re-generation of an existing GPG identity
|
||||
[](https://asciinema.org/a/M4lRjEmGJ2RreQiHBGWT9pzp4)
|
||||
[](https://asciinema.org/a/5tIQa5qt5bV134oeOqFyKEU29)
|
||||
|
||||
If you've forgotten the timestamp value, but still have access to the public key, then you can
|
||||
retrieve the timestamp with the following command (substitute "john@doe.bit" for the key's address or id):
|
||||
@@ -119,10 +120,10 @@ pub rsa2048/90C4064B 2017-10-10 [SC]
|
||||
uid [ultimate] foobar
|
||||
sub rsa2048/4DD05FF0 2017-10-10 [E]
|
||||
|
||||
$ ./scripts/gpg-init "foobar" --subkey
|
||||
$ trezor-gpg init "foobar" --subkey
|
||||
```
|
||||
|
||||
[](https://asciinema.org/a/JFazcJfORrz7k9DmVXknaXBfD)
|
||||
[](https://asciinema.org/a/Ick5G724zrZRFsGY7ZUdFSnV1)
|
||||
|
||||
In order to enter existing GPG passphrase, I recommend installing and using a graphical Pinentry:
|
||||
```
|
||||
|
||||
@@ -13,11 +13,14 @@ import contextlib
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import semver
|
||||
|
||||
|
||||
from . import agent, client, encode, keyring, protocol
|
||||
from .. import device, formats, server, util
|
||||
|
||||
@@ -73,23 +76,111 @@ def export_public_key(device_type, args):
|
||||
subkey=subkey,
|
||||
signer_func=signer_func)
|
||||
|
||||
sys.stdout.write(protocol.armor(result, 'PUBLIC KEY BLOCK'))
|
||||
return protocol.armor(result, 'PUBLIC KEY BLOCK')
|
||||
|
||||
|
||||
def run_create(device_type, args):
|
||||
"""Export public GPG key."""
|
||||
def verify_gpg_version():
|
||||
"""Make sure that the installed GnuPG is not too old."""
|
||||
existing_gpg = keyring.gpg_version().decode('ascii')
|
||||
required_gpg = '>=2.1.11'
|
||||
msg = 'Existing GnuPG has version "{}" ({} required)'.format(existing_gpg,
|
||||
required_gpg)
|
||||
assert semver.match(existing_gpg, required_gpg), msg
|
||||
|
||||
|
||||
def check_output(args):
|
||||
"""Runs command and returns the output as string."""
|
||||
log.debug('run: %s', args)
|
||||
out = subprocess.check_output(args=args).decode('utf-8')
|
||||
log.debug('out: %r', out)
|
||||
return out
|
||||
|
||||
|
||||
def check_call(args, stdin=None, env=None):
|
||||
"""Runs command and verifies its success."""
|
||||
log.debug('run: %s', args)
|
||||
subprocess.check_call(args=args, stdin=stdin, env=env)
|
||||
|
||||
|
||||
def write_file(path, data):
|
||||
"""Writes data to specified path."""
|
||||
with open(path, 'w') as f:
|
||||
log.debug('setting %s contents:\n%s', path, data)
|
||||
f.write(data)
|
||||
return f
|
||||
|
||||
|
||||
def run_init(device_type, args):
|
||||
"""Initialize hardware-based GnuPG identity."""
|
||||
util.setup_logging(verbosity=args.verbose)
|
||||
log.warning('This GPG tool is still in EXPERIMENTAL mode, '
|
||||
'so please note that the API and features may '
|
||||
'change without backwards compatibility!')
|
||||
|
||||
existing_gpg = keyring.gpg_version().decode('ascii')
|
||||
required_gpg = '>=2.1.11'
|
||||
if semver.match(existing_gpg, required_gpg):
|
||||
export_public_key(device_type, args)
|
||||
else:
|
||||
log.error('Existing gpg2 has version "%s" (%s required)',
|
||||
existing_gpg, required_gpg)
|
||||
verify_gpg_version()
|
||||
|
||||
# Prepare new GPG home directory for hardware-based identity
|
||||
device_name = os.path.basename(sys.argv[0]).rsplit('-', 1)[0]
|
||||
log.info('device name: %s', device_name)
|
||||
homedir = os.path.expanduser('~/.gnupg/{}'.format(device_name))
|
||||
log.info('GPG home directory: %s', homedir)
|
||||
|
||||
check_call(['rm', '-rf', homedir])
|
||||
check_call(['mkdir', '-p', homedir])
|
||||
check_call(['chmod', '700', homedir])
|
||||
|
||||
# Generate new GPG identity and import into GPG keyring
|
||||
pubkey = write_file(os.path.join(homedir, 'pubkey.asc'),
|
||||
export_public_key(device_type, args))
|
||||
gpg_binary = keyring.get_gnupg_binary()
|
||||
check_call([gpg_binary, '--homedir', homedir, '--quiet',
|
||||
'--import', pubkey.name])
|
||||
check_call(['rm', '-f', os.path.join(homedir, 'S.gpg-agent')])
|
||||
# (otherwise, our agent won't be started automatically)
|
||||
|
||||
# Make new GPG identity with "ultimate" trust (via its fingerprint)
|
||||
out = check_output([gpg_binary, '--homedir', homedir, '--list-public-keys',
|
||||
'--with-fingerprint', '--with-colons'])
|
||||
fpr = re.findall('fpr:::::::::([0-9A-F]+):', out)[0]
|
||||
f = write_file(os.path.join(homedir, 'ownertrust.txt'), fpr + ':6\n')
|
||||
check_call([gpg_binary, '--homedir', homedir,
|
||||
'--import-ownertrust', f.name])
|
||||
|
||||
agent_path = check_output(['which', '{}-gpg-agent'.format(device_name)])
|
||||
agent_path = agent_path.strip()
|
||||
|
||||
# Prepare GPG configuration file
|
||||
with open(os.path.join(homedir, 'gpg.conf'), 'w') as f:
|
||||
f.write("""# Hardware-based GPG configuration
|
||||
agent-program {0}
|
||||
personal-digest-preferences SHA512
|
||||
default-key \"{1}\"
|
||||
""".format(agent_path, args.user_id))
|
||||
|
||||
# Prepare GPG agent configuration file
|
||||
with open(os.path.join(homedir, 'gpg-agent.conf'), 'w') as f:
|
||||
f.write("""# Hardware-based GPG agent emulator
|
||||
log-file {0}/gpg-agent.log
|
||||
verbosity 2
|
||||
""".format(homedir))
|
||||
|
||||
# Prepare a helper script for setting up the new identity
|
||||
with open(os.path.join(homedir, 'env'), 'w') as f:
|
||||
f.write("""#!/bin/bash
|
||||
set -eu
|
||||
export GNUPGHOME={0}
|
||||
COMMAND=$*
|
||||
if [ -z "${{COMMAND}}" ]
|
||||
then
|
||||
${{SHELL}}
|
||||
else
|
||||
${{COMMAND}}
|
||||
fi
|
||||
""".format(homedir))
|
||||
check_call(['chmod', 'u+x', f.name])
|
||||
|
||||
# Load agent and make sure it responds with the new identity
|
||||
check_call([gpg_binary, '--list-secret-keys'], env={'GNUPGHOME': homedir})
|
||||
|
||||
|
||||
def run_unlock(device_type, args):
|
||||
@@ -133,13 +224,14 @@ def main(device_type):
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
p = subparsers.add_parser('create', help='Export public GPG key')
|
||||
p = subparsers.add_parser('init',
|
||||
help='Initialize hardware-based GnuPG identity')
|
||||
p.add_argument('user_id')
|
||||
p.add_argument('-e', '--ecdsa-curve', default='nist256p1')
|
||||
p.add_argument('-t', '--time', type=int, default=int(time.time()))
|
||||
p.add_argument('-v', '--verbose', default=0, action='count')
|
||||
p.add_argument('-s', '--subkey', default=False, action='store_true')
|
||||
p.set_defaults(func=run_create)
|
||||
p.set_defaults(func=run_init)
|
||||
|
||||
p = subparsers.add_parser('unlock', help='Unlock the hardware device')
|
||||
p.add_argument('-v', '--verbose', default=0, action='count')
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
USER_ID="${1}"
|
||||
shift
|
||||
ARGS="$*"
|
||||
|
||||
DEVICE=${DEVICE:="trezor"} # or "ledger"
|
||||
CURVE=${CURVE:="nist256p1"} # or "ed25519"
|
||||
TIMESTAMP=${TIMESTAMP:=`date +%s`} # key creation timestamp
|
||||
HOMEDIR=~/.gnupg/${DEVICE}
|
||||
|
||||
# NOTE: starting from GnuPG 2.2, gpg2 -> gpg
|
||||
GPG_BINARY=$(python -c "import libagent.gpg.keyring as k; print(k.get_gnupg_binary())")
|
||||
${GPG_BINARY} --version # verify that GnuPG 2.1+ is installed
|
||||
|
||||
# Prepare new GPG home directory for hardware-based identity
|
||||
rm -rf "${HOMEDIR}"
|
||||
mkdir -p "${HOMEDIR}"
|
||||
chmod 700 "${HOMEDIR}"
|
||||
|
||||
# Generate new GPG identity and import into GPG keyring
|
||||
$DEVICE-gpg create -v "${USER_ID}" -t "${TIMESTAMP}" -e "${CURVE}" ${ARGS} > "${HOMEDIR}/pubkey.asc"
|
||||
${GPG_BINARY} --homedir "${HOMEDIR}" -q --import < "${HOMEDIR}/pubkey.asc"
|
||||
rm -f "${HOMEDIR}/S.gpg-agent" # (otherwise, our agent won't be started automatically)
|
||||
|
||||
# Make new GPG identity with "ultimate" trust (via its fingerprint)
|
||||
FINGERPRINT=$(${GPG_BINARY} --homedir "${HOMEDIR}" --list-public-keys --with-fingerprint --with-colons | sed -n -E 's/^fpr:::::::::([0-9A-F]+):$/\1/p' | head -n1)
|
||||
echo "${FINGERPRINT}:6" | ${GPG_BINARY} --homedir "${HOMEDIR}" --import-ownertrust 2> /dev/null
|
||||
|
||||
AGENT_PATH="$(which ${DEVICE}-gpg-agent)"
|
||||
|
||||
# Prepare GPG configuration file
|
||||
echo "# Hardware-based GPG configuration
|
||||
agent-program ${AGENT_PATH}
|
||||
personal-digest-preferences SHA512
|
||||
default-key \"${USER_ID}\"
|
||||
" > "${HOMEDIR}/gpg.conf"
|
||||
|
||||
# Prepare GPG agent configuration file
|
||||
echo "# Hardware-based GPG agent emulator
|
||||
log-file ${HOMEDIR}/gpg-agent.log
|
||||
verbosity 2
|
||||
" > "${HOMEDIR}/gpg-agent.conf"
|
||||
|
||||
# Prepare a helper script for setting up the new identity
|
||||
echo "#!/bin/bash
|
||||
set -eu
|
||||
export GNUPGHOME=${HOMEDIR}
|
||||
COMMAND=\$*
|
||||
if [ -z \"\${COMMAND}\" ]
|
||||
then
|
||||
\${SHELL}
|
||||
else
|
||||
\${COMMAND}
|
||||
fi
|
||||
" > "${HOMEDIR}/env"
|
||||
chmod u+x "${HOMEDIR}/env"
|
||||
|
||||
echo "Starting ${DEVICE}-gpg-agent at ${HOMEDIR}..."
|
||||
# Load agent and make sure it responds with the new identity
|
||||
GNUPGHOME="${HOMEDIR}" ${GPG_BINARY} -K 2> /dev/null
|
||||
Reference in New Issue
Block a user