From 37510a2d75ed0bcc426cb4156ad33fb89a3186a0 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 16 Oct 2021 19:15:10 +0300 Subject: [PATCH 1/4] Fix FakeDevice close() and pubkey() --- libagent/device/fake_device.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libagent/device/fake_device.py b/libagent/device/fake_device.py index 8edeecd..82bfe7b 100644 --- a/libagent/device/fake_device.py +++ b/libagent/device/fake_device.py @@ -39,13 +39,17 @@ class FakeDevice(interface.Device): self.vk = self.sk.get_verifying_key() return self + def close(self): + """Close the device.""" + def pubkey(self, identity, ecdh=False): """Return public key.""" _verify_support(identity) data = self.vk.to_string() x, y = data[:32], data[32:] prefix = bytearray([2 + (bytearray(y)[0] & 1)]) - return bytes(prefix) + x + pubkey = bytes(prefix) + x + return formats.decompress_pubkey(pubkey=pubkey, curve_name=identity.curve_name) def sign(self, identity, blob): """Sign given blob and return the signature (as bytes).""" From 6c2b880b7d6a29342d618eebccf521e4a6e8d480 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 16 Oct 2021 20:58:30 +0300 Subject: [PATCH 2/4] Support daemonization of GPG agent --- libagent/gpg/__init__.py | 11 +++++++++++ setup.py | 1 + 2 files changed, 12 insertions(+) diff --git a/libagent/gpg/__init__.py b/libagent/gpg/__init__.py index 5ed6af9..51d50fb 100644 --- a/libagent/gpg/__init__.py +++ b/libagent/gpg/__init__.py @@ -226,6 +226,8 @@ def run_agent(device_type): p.add_argument('-v', '--verbose', default=0, action='count') p.add_argument('--server', default=False, action='store_true', help='Use stdin/stdout for communication with GPG.') + p.add_argument('--daemon', default=False, action='store_true', + help='Daemonize the agent.') p.add_argument('--pin-entry-binary', type=str, default='pinentry', help='Path to PIN entry UI helper.') @@ -236,6 +238,15 @@ def run_agent(device_type): args, _ = p.parse_known_args() + if args.daemon: + with daemon.DaemonContext(): + run_agent_internal(args, device_type) + else: + run_agent_internal(args, device_type) + + +def run_agent_internal(args, device_type): + """Actually run the server.""" assert args.homedir log_file = os.path.join(args.homedir, 'gpg-agent.log') diff --git a/setup.py b/setup.py index 97b184a..e3396df 100755 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ setup( ], install_requires=[ 'docutils>=0.14', + 'python-daemon>=2.3.0', 'wheel>=0.32.3', 'backports.shutil_which>=3.5.1', 'ConfigArgParse>=0.12.1', From b9db213912964cf2eb074f64a846ed6fb01aab12 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 16 Oct 2021 21:55:51 +0300 Subject: [PATCH 3/4] Use Popen.communicate to get stdout from subprocess --- libagent/gpg/keyring.py | 5 ++++- libagent/gpg/tests/test_keyring.py | 17 +++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/libagent/gpg/keyring.py b/libagent/gpg/keyring.py index 736ab5c..2260b82 100644 --- a/libagent/gpg/keyring.py +++ b/libagent/gpg/keyring.py @@ -17,8 +17,11 @@ log = logging.getLogger(__name__) def check_output(args, env=None, sp=subprocess): """Call an external binary and return its stdout.""" log.debug('calling %s with env %s', args, env) - output = sp.check_output(args=args, env=env) + p = sp.Popen(args=args, env=env, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) + (output, error) = p.communicate() log.debug('output: %r', output) + if error: + log.debug('error: %r', error) return output diff --git a/libagent/gpg/tests/test_keyring.py b/libagent/gpg/tests/test_keyring.py index e83616e..6faa446 100644 --- a/libagent/gpg/tests/test_keyring.py +++ b/libagent/gpg/tests/test_keyring.py @@ -53,6 +53,14 @@ class FakeSocket: self.tx.write(data) +def mock_subprocess(output, error=b''): + sp = mock.Mock(spec=['Popen', 'PIPE']) + p = mock.Mock(spec=['communicate']) + sp.Popen.return_value = p + p.communicate.return_value = (output, error) + return sp + + def test_sign_digest(): sock = FakeSocket() sock.rx.write(b'OK Pleased to meet you, process XYZ\n') @@ -61,10 +69,8 @@ def test_sign_digest(): sock.rx.seek(0) keygrip = '1234' digest = b'A' * 32 - sp = mock.Mock(spec=['check_output']) - sp.check_output.return_value = '/dev/pts/0' sig = keyring.sign_digest(sock=sock, keygrip=keygrip, - digest=digest, sp=sp, + digest=digest, sp=mock_subprocess('/dev/pts/0'), environ={'DISPLAY': ':0'}) assert sig == (0x30313233343536373839414243444546,) assert sock.tx.getvalue() == b'''RESET @@ -85,8 +91,7 @@ def test_iterlines(): def test_get_agent_sock_path(): - sp = mock.Mock(spec=['check_output']) - sp.check_output.return_value = b'''sysconfdir:/usr/local/etc/gnupg + sp = mock_subprocess(b'''sysconfdir:/usr/local/etc/gnupg bindir:/usr/local/bin libexecdir:/usr/local/libexec libdir:/usr/local/lib/gnupg @@ -96,6 +101,6 @@ dirmngr-socket:/run/user/1000/gnupg/S.dirmngr agent-ssh-socket:/run/user/1000/gnupg/S.gpg-agent.ssh agent-socket:/run/user/1000/gnupg/S.gpg-agent homedir:/home/roman/.gnupg -''' +''') expected = b'/run/user/1000/gnupg/S.gpg-agent' assert keyring.get_agent_sock_path(sp=sp) == expected From 69c5c574899d4b12cd75370b972037675f233521 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 16 Oct 2021 19:15:33 +0300 Subject: [PATCH 4/4] Support "fast-path" key listing https://dev.gnupg.org/rG40da61b89b62dcb77847dc79eb159e885f52f817#change-o4DEJvEV1Dx2 Also, refactor decoding and add a few tests. --- libagent/gpg/agent.py | 12 ++++++++++-- libagent/gpg/decode.py | 21 ++++++++++++++++----- libagent/gpg/tests/romanz-pubkey.gpg | Bin 0 -> 626 bytes libagent/gpg/tests/test_decode.py | 24 ++++++++++++++++++++---- 4 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 libagent/gpg/tests/romanz-pubkey.gpg diff --git a/libagent/gpg/agent.py b/libagent/gpg/agent.py index a4cb99c..63bc157 100644 --- a/libagent/gpg/agent.py +++ b/libagent/gpg/agent.py @@ -99,7 +99,7 @@ class Handler: b'SETHASH': lambda _, args: self.set_hash(*args), b'PKSIGN': lambda conn, _: self.pksign(conn), b'PKDECRYPT': lambda conn, _: self.pkdecrypt(conn), - b'HAVEKEY': lambda _, args: self.have_key(*args), + b'HAVEKEY': lambda conn, args: self.have_key(conn, *args), b'KEYINFO': _key_info, b'SCD': self.handle_scd, b'GET_PASSPHRASE': self.handle_get_passphrase, @@ -198,8 +198,16 @@ class Handler: ec_point = self.client.ecdh(identity=identity, pubkey=remote_pubkey) keyring.sendline(conn, b'D ' + _serialize_point(ec_point)) - def have_key(self, *keygrips): + def have_key(self, conn, *keygrips): """Check if any keygrip corresponds to a TREZOR-based key.""" + if len(keygrips) == 1 and keygrips[0].startswith(b"--list="): + # Support "fast-path" key listing: + # https://dev.gnupg.org/rG40da61b89b62dcb77847dc79eb159e885f52f817 + keygrips = list(decode.iter_keygrips(pubkey_bytes=self.pubkey_bytes)) + log.debug('keygrips: %r', keygrips) + keyring.sendline(conn, b'D ' + util.assuan_serialize(b''.join(keygrips))) + return + for keygrip in keygrips: try: self.get_identity(keygrip=keygrip) diff --git a/libagent/gpg/decode.py b/libagent/gpg/decode.py index 8694dff..1d03b4b 100644 --- a/libagent/gpg/decode.py +++ b/libagent/gpg/decode.py @@ -282,18 +282,20 @@ HASH_ALGORITHMS = { } -def load_by_keygrip(pubkey_bytes, keygrip): - """Return public key and first user ID for specified keygrip.""" +def _parse_pubkey_packets(pubkey_bytes): stream = io.BytesIO(pubkey_bytes) - packets = list(parse_packets(stream)) packets_per_pubkey = [] - for p in packets: + for p in parse_packets(stream): if p['type'] == 'pubkey': # Add a new packet list for each pubkey. packets_per_pubkey.append([]) packets_per_pubkey[-1].append(p) + return packets_per_pubkey - for packets in packets_per_pubkey: + +def load_by_keygrip(pubkey_bytes, keygrip): + """Return public key and first user ID for specified keygrip.""" + for packets in _parse_pubkey_packets(pubkey_bytes): user_ids = [p for p in packets if p['type'] == 'user_id'] for p in packets: if p.get('keygrip') == keygrip: @@ -301,6 +303,15 @@ def load_by_keygrip(pubkey_bytes, keygrip): raise KeyError('{} keygrip not found'.format(util.hexlify(keygrip))) +def iter_keygrips(pubkey_bytes): + """Iterate over all keygrips in this pubkey.""" + for packets in _parse_pubkey_packets(pubkey_bytes): + for p in packets: + keygrip = p.get('keygrip') + if keygrip: + yield keygrip + + def load_signature(stream, original_data): """Load signature from stream, and compute GPG digest for verification.""" signature, = list(parse_packets((stream))) diff --git a/libagent/gpg/tests/romanz-pubkey.gpg b/libagent/gpg/tests/romanz-pubkey.gpg new file mode 100644 index 0000000000000000000000000000000000000000..b80a32fe6d5cfb73a17c61a70a86f88193bbd151 GIT binary patch literal 626 zcmbOc#1bJAwn3OftIgw_Ei)rK6Elm&rJct+e{7R-J{tJ-Xphf^aLt)pY|c+gR%_6Z zuU55aE@n^^u6;c9C9XB;LRz z$H*YYDbRlE=`Xj5HM_Z`LV{eQ{DXAe1KjOTGBUJo-}bTQSW4{rJ826vWT(m)bgbmg zZ`i&eq3QMStqU%+G5mL1SM)sS=CZ#smY2@kU7A~?e{QzLj-aca%t7b7JtgJOZjnK8 zW^Sqj*rh;+>ZPRGbxZ)e&q7R*MfAksaIYsHAI0079)&rFl_{2u!w~3ZR&GuXb|yAa zE=~?^CWzbRK+b32Lb&~?8N)yIIqxhh3SUo{`|^~N-{R}tsS*o*3uFm7cg?T6_(h=p zEW;nZ!nGTE0@oTpV0BIU_;4HJr!6+wExlzI7cuuK`rY@Pupu@5o}SlP(!a4*I>dX*8pbV$ zKbKFln(Db(n3<7-y(5=J0vHpV(3p_s!4>R_KQjDNnA=pV?egTzo{FHam-if~-PbPA zwM*oHWWnS8@|)_KPBHuyO3VDV^h0ITyzWVPug@7*Z', + b'Roman Zeyde ', + ]