|
|
@ -1,5 +1,13 @@
|
|
|
|
#!/usr/bin/env python
|
|
|
|
"""
|
|
|
|
"""Create signatures and export public keys for GPG using TREZOR."""
|
|
|
|
TREZOR support for ECDSA GPG signatures.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
See these links for more details:
|
|
|
|
|
|
|
|
- https://www.gnupg.org/faq/whats-new-in-2.1.html
|
|
|
|
|
|
|
|
- https://tools.ietf.org/html/rfc4880
|
|
|
|
|
|
|
|
- https://tools.ietf.org/html/rfc6637
|
|
|
|
|
|
|
|
- https://tools.ietf.org/html/draft-irtf-cfrg-eddsa-05
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import argparse
|
|
|
|
import contextlib
|
|
|
|
import contextlib
|
|
|
|
import logging
|
|
|
|
import logging
|
|
|
@ -15,14 +23,15 @@ from .. import device, formats, server, util
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def export_public_key(args):
|
|
|
|
def export_public_key(device_type, args):
|
|
|
|
"""Generate a new pubkey for a new/existing GPG identity."""
|
|
|
|
"""Generate a new pubkey for a new/existing GPG identity."""
|
|
|
|
log.warning('NOTE: in order to re-generate the exact same GPG key later, '
|
|
|
|
log.warning('NOTE: in order to re-generate the exact same GPG key later, '
|
|
|
|
'run this command with "--time=%d" commandline flag (to set '
|
|
|
|
'run this command with "--time=%d" commandline flag (to set '
|
|
|
|
'the timestamp of the GPG key manually).', args.time)
|
|
|
|
'the timestamp of the GPG key manually).', args.time)
|
|
|
|
d = client.Client(user_id=args.user_id, curve_name=args.ecdsa_curve)
|
|
|
|
c = client.Client(user_id=args.user_id, curve_name=args.ecdsa_curve,
|
|
|
|
verifying_key = d.pubkey(ecdh=False)
|
|
|
|
device_type=device_type)
|
|
|
|
decryption_key = d.pubkey(ecdh=True)
|
|
|
|
verifying_key = c.pubkey(ecdh=False)
|
|
|
|
|
|
|
|
decryption_key = c.pubkey(ecdh=True)
|
|
|
|
|
|
|
|
|
|
|
|
if args.subkey: # add as subkey
|
|
|
|
if args.subkey: # add as subkey
|
|
|
|
log.info('adding %s GPG subkey for "%s" to existing key',
|
|
|
|
log.info('adding %s GPG subkey for "%s" to existing key',
|
|
|
@ -38,10 +47,10 @@ def export_public_key(args):
|
|
|
|
primary_bytes = keyring.export_public_key(args.user_id)
|
|
|
|
primary_bytes = keyring.export_public_key(args.user_id)
|
|
|
|
result = encode.create_subkey(primary_bytes=primary_bytes,
|
|
|
|
result = encode.create_subkey(primary_bytes=primary_bytes,
|
|
|
|
subkey=signing_key,
|
|
|
|
subkey=signing_key,
|
|
|
|
signer_func=d.sign)
|
|
|
|
signer_func=c.sign)
|
|
|
|
result = encode.create_subkey(primary_bytes=result,
|
|
|
|
result = encode.create_subkey(primary_bytes=result,
|
|
|
|
subkey=encryption_key,
|
|
|
|
subkey=encryption_key,
|
|
|
|
signer_func=d.sign)
|
|
|
|
signer_func=c.sign)
|
|
|
|
else: # add as primary
|
|
|
|
else: # add as primary
|
|
|
|
log.info('creating new %s GPG primary key for "%s"',
|
|
|
|
log.info('creating new %s GPG primary key for "%s"',
|
|
|
|
args.ecdsa_curve, args.user_id)
|
|
|
|
args.ecdsa_curve, args.user_id)
|
|
|
@ -56,15 +65,15 @@ def export_public_key(args):
|
|
|
|
|
|
|
|
|
|
|
|
result = encode.create_primary(user_id=args.user_id,
|
|
|
|
result = encode.create_primary(user_id=args.user_id,
|
|
|
|
pubkey=primary,
|
|
|
|
pubkey=primary,
|
|
|
|
signer_func=d.sign)
|
|
|
|
signer_func=c.sign)
|
|
|
|
result = encode.create_subkey(primary_bytes=result,
|
|
|
|
result = encode.create_subkey(primary_bytes=result,
|
|
|
|
subkey=subkey,
|
|
|
|
subkey=subkey,
|
|
|
|
signer_func=d.sign)
|
|
|
|
signer_func=c.sign)
|
|
|
|
|
|
|
|
|
|
|
|
sys.stdout.write(protocol.armor(result, 'PUBLIC KEY BLOCK'))
|
|
|
|
sys.stdout.write(protocol.armor(result, 'PUBLIC KEY BLOCK'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_create(args):
|
|
|
|
def run_create(device_type, args):
|
|
|
|
"""Export public GPG key."""
|
|
|
|
"""Export public GPG key."""
|
|
|
|
util.setup_logging(verbosity=args.verbose)
|
|
|
|
util.setup_logging(verbosity=args.verbose)
|
|
|
|
log.warning('This GPG tool is still in EXPERIMENTAL mode, '
|
|
|
|
log.warning('This GPG tool is still in EXPERIMENTAL mode, '
|
|
|
@ -74,26 +83,27 @@ def run_create(args):
|
|
|
|
existing_gpg = keyring.gpg_version().decode('ascii')
|
|
|
|
existing_gpg = keyring.gpg_version().decode('ascii')
|
|
|
|
required_gpg = '>=2.1.11'
|
|
|
|
required_gpg = '>=2.1.11'
|
|
|
|
if semver.match(existing_gpg, required_gpg):
|
|
|
|
if semver.match(existing_gpg, required_gpg):
|
|
|
|
export_public_key(args)
|
|
|
|
export_public_key(device_type, args)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.error('Existing gpg2 has version "%s" (%s required)',
|
|
|
|
log.error('Existing gpg2 has version "%s" (%s required)',
|
|
|
|
existing_gpg, required_gpg)
|
|
|
|
existing_gpg, required_gpg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_unlock(args):
|
|
|
|
def run_unlock(device_type, args):
|
|
|
|
"""Unlock hardware device (for future interaction)."""
|
|
|
|
"""Unlock hardware device (for future interaction)."""
|
|
|
|
util.setup_logging(verbosity=args.verbose)
|
|
|
|
util.setup_logging(verbosity=args.verbose)
|
|
|
|
d = device.detect()
|
|
|
|
with device_type() as d:
|
|
|
|
log.info('unlocked %s device', d)
|
|
|
|
log.info('unlocked %s device', d)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_agent(_):
|
|
|
|
def run_agent(device_type):
|
|
|
|
"""Run a simple GPG-agent server."""
|
|
|
|
"""Run a simple GPG-agent server."""
|
|
|
|
home_dir = os.environ.get('GNUPGHOME', os.path.expanduser('~/.gnupg/trezor'))
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
config_file = os.path.join(home_dir, 'gpg-agent.conf')
|
|
|
|
parser.add_argument('--homedir', default=os.environ.get('GNUPGHOME'))
|
|
|
|
if not os.path.exists(config_file):
|
|
|
|
args, _ = parser.parse_known_args()
|
|
|
|
msg = 'No configuration file found: {}'.format(config_file)
|
|
|
|
|
|
|
|
raise IOError(msg)
|
|
|
|
assert args.homedir
|
|
|
|
|
|
|
|
config_file = os.path.join(args.homedir, 'gpg-agent.conf')
|
|
|
|
|
|
|
|
|
|
|
|
lines = (line.strip() for line in open(config_file))
|
|
|
|
lines = (line.strip() for line in open(config_file))
|
|
|
|
lines = (line for line in lines if line and not line.startswith('#'))
|
|
|
|
lines = (line for line in lines if line and not line.startswith('#'))
|
|
|
@ -106,7 +116,7 @@ def run_agent(_):
|
|
|
|
for conn in agent.yield_connections(sock):
|
|
|
|
for conn in agent.yield_connections(sock):
|
|
|
|
with contextlib.closing(conn):
|
|
|
|
with contextlib.closing(conn):
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
agent.handle_connection(conn)
|
|
|
|
agent.handle_connection(conn=conn, device_type=device_type)
|
|
|
|
except StopIteration:
|
|
|
|
except StopIteration:
|
|
|
|
log.info('stopping gpg-agent')
|
|
|
|
log.info('stopping gpg-agent')
|
|
|
|
return
|
|
|
|
return
|
|
|
@ -114,14 +124,11 @@ def run_agent(_):
|
|
|
|
log.exception('gpg-agent failed: %s', e)
|
|
|
|
log.exception('gpg-agent failed: %s', e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
def main(device_type):
|
|
|
|
"""Parse command-line arguments."""
|
|
|
|
"""Parse command-line arguments."""
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
subparsers = parser.add_subparsers()
|
|
|
|
subparsers = parser.add_subparsers()
|
|
|
|
|
|
|
|
|
|
|
|
p = subparsers.add_parser('agent', help='Run GPG agent using a hardware device')
|
|
|
|
|
|
|
|
p.set_defaults(func=run_agent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p = subparsers.add_parser('create', help='Export public GPG key')
|
|
|
|
p = subparsers.add_parser('create', help='Export public GPG key')
|
|
|
|
p.add_argument('user_id')
|
|
|
|
p.add_argument('user_id')
|
|
|
|
p.add_argument('-e', '--ecdsa-curve', default='nist256p1')
|
|
|
|
p.add_argument('-e', '--ecdsa-curve', default='nist256p1')
|
|
|
@ -135,8 +142,4 @@ def main():
|
|
|
|
p.set_defaults(func=run_unlock)
|
|
|
|
p.set_defaults(func=run_unlock)
|
|
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
args = parser.parse_args()
|
|
|
|
return args.func(args)
|
|
|
|
return args.func(device_type=device_type, args=args)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
|
|
main()
|
|
|
|
|