Skip to content

Commit 631ead3

Browse files
committed
Use unrestricted connection for HAVEKEY --list and KEYINFO --list
These commands are forbidden over a restricted connection to the agent, but GnuPG wars if they are not present and Sequoia Chameleon requires them. Fortunately, they are trivial to sanitize input for, so there is near-zero risk of an injection vulnerability. Therefore, use a separate unrestricted agent connection for these commands. Also use a separate function to read agent hello messages sent upon connection.
1 parent 6a0b041 commit 631ead3

1 file changed

Lines changed: 73 additions & 30 deletions

File tree

splitgpg2/__init__.py

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ class GpgServer:
218218
commands: Dict[bytes, 'NoneCallback']
219219
seen_data: bool
220220
config_loaded: bool
221+
agent_unrestricted_socket_path: Optional[str]
221222
agent_socket_path: Optional[str]
222223
agent_reader: Optional[asyncio.StreamReader]
223224
agent_writer: Optional[asyncio.StreamWriter]
@@ -245,6 +246,7 @@ class GpgServer:
245246
'seen_data',
246247
'config_loaded',
247248
'agent_socket_path',
249+
'agent_unrestricted_socket_path',
248250
'agent_reader',
249251
'agent_writer',
250252
'source_keyring_dir',
@@ -275,6 +277,7 @@ def __init__(self, reader: asyncio.StreamReader,
275277

276278
self.log = logging.getLogger('splitgpg2.Server')
277279
self.agent_socket_path = None
280+
self.agent_unrestricted_socket_path = None
278281
self.agent_reader: Optional[asyncio.StreamReader] = None
279282
self.agent_writer: Optional[asyncio.StreamWriter] = None
280283

@@ -463,11 +466,20 @@ async def connect_agent(self) -> None:
463466
# a message.
464467
# The filtering done by split-gpg2 is far stronger than anything the agent does
465468
# internally.
466-
socket_field = b'agent-socket:'
469+
unrestricted_socket_field = b'agent-socket'
470+
socket_field = unrestricted_socket_field if self.allow_keygen else b'agent-extra-socket'
467471
# search for agent-socket:/run/user/1000/gnupg/S.gpg-agent
468-
agent_socket_path = [d.split(b':', 1)[1] for d in dirs.splitlines()
469-
if d.startswith(socket_field)][0]
470-
self.agent_socket_path = agent_socket_path.decode()
472+
for d in dirs.splitlines():
473+
key, value = d.split(b':')
474+
if key == socket_field:
475+
self.agent_socket_path = value.decode("UTF-8", "surrogateescape")
476+
if key == unrestricted_socket_field:
477+
self.agent_unrestricted_socket_path = value.decode("UTF-8", "surrogateescape")
478+
if ((self.agent_unrestricted_socket_path is not None) and
479+
(self.agent_socket_path is not None)):
480+
break
481+
else:
482+
raise RuntimeError("bad output from gpgconf")
471483

472484
self.agent_reader, self.agent_writer = await asyncio.open_unix_connection(
473485
path=self.agent_socket_path)
@@ -476,7 +488,7 @@ async def connect_agent(self) -> None:
476488
self.notify('connected')
477489

478490
# wait for agent hello
479-
await self.handle_agent_response({})
491+
self.client_write(await self.read_hello(self.agent_reader))
480492

481493
def close(self, reason: str, log_level: int = logging.ERROR) -> None:
482494
self.log.log(log_level, '%s; Closing!', reason)
@@ -563,8 +575,8 @@ def default_options() -> Dict[bytes, Tuple[OptionHandlingType, Optional[bytes]]]
563575
b'lc-messages': (OptionHandlingType.fake, b'OK'),
564576
b'putenv': (OptionHandlingType.fake, b'OK'),
565577
b'pinentry-mode': (OptionHandlingType.fake, b'ERR 67108924 Not supported <GPG Agent>'),
566-
b'allow-pinentry-notify': (OptionHandlingType.verify, None),
567-
b'agent-awareness': (OptionHandlingType.verify, b'2.1.0')
578+
b'allow-pinentry-notify': (OptionHandlingType.fake, b'OK'),
579+
b'agent-awareness': (OptionHandlingType.verify, b'2.1.0'),
568580
}
569581

570582
@staticmethod
@@ -742,11 +754,13 @@ async def command_HAVEKEY(self, untrusted_args: Optional[bytes]) -> None:
742754
raise Filtered
743755
# upper keygrip limit is arbitary
744756
args = self.verify_keygrip_arguments(1, 200, untrusted_args, True)
745-
await self.send_agent_command(b'HAVEKEY', args)
757+
unrestricted = args.startswith(b'--list') and not self.allow_keygen
758+
await self.send_agent_command(b'HAVEKEY', args, unrestricted)
746759

747760
async def command_KEYINFO(self, untrusted_args: Optional[bytes]) -> None:
748761
args = self.verify_keygrip_arguments(1, 1, untrusted_args, True)
749-
await self.send_agent_command(b'KEYINFO', args)
762+
unrestricted = args.startswith(b'--list') and not self.allow_keygen
763+
await self.send_agent_command(b'KEYINFO', args, unrestricted)
750764

751765
async def command_GENKEY(self, untrusted_args: Optional[bytes]) -> None:
752766
if not self.allow_keygen:
@@ -821,7 +835,8 @@ async def setkeydesc(self, keygrip: bytes) -> None:
821835
key.fingerprint,
822836
subkey_desc)
823837

824-
self.agent_write(b'SETKEYDESC %s\n' % self.percent_plus_escape(desc))
838+
assert self.agent_writer is not None, "no writer?"
839+
self.agent_write(b'SETKEYDESC %s\n' % self.percent_plus_escape(desc), self.agent_writer)
825840

826841
assert self.agent_reader is not None
827842
untrusted_line = await self.agent_reader.readline()
@@ -1018,43 +1033,68 @@ def get_inquires_for_command(self, command: bytes) -> Dict[bytes, 'ArgCallback']
10181033
}
10191034
return {}
10201035

1021-
async def send_agent_command(self, command: bytes, args: Optional[bytes]) -> None:
1036+
async def send_agent_command(self, command: bytes, args: Optional[bytes],
1037+
unrestricted: bool=False) -> None:
10221038
""" Sends command to local gpg agent and handle the response """
10231039
expected_inquires = self.get_inquires_for_command(command)
1024-
if args:
1025-
if not self.command_argument_regex.match(args):
1026-
raise AssertionError("BUG: corrupt command about to be sent to agent!")
1027-
cmd_with_args = command + b' ' + args + b'\n'
1040+
assert self.agent_reader is not None, "no reader?"
1041+
assert self.agent_writer is not None, "no writer?"
1042+
if unrestricted and not self.allow_keygen:
1043+
reader, writer = await asyncio.open_unix_connection(
1044+
self.agent_unrestricted_socket_path)
1045+
await self.read_hello(reader)
10281046
else:
1029-
cmd_with_args = command + b'\n'
1030-
self.agent_write(cmd_with_args)
1031-
while True:
1032-
more_expected = await self.handle_agent_response(
1033-
expected_inquires=expected_inquires)
1034-
if not more_expected:
1035-
break
1047+
reader, writer = self.agent_reader, self.agent_writer
1048+
try:
1049+
if args:
1050+
if not self.command_argument_regex.match(args):
1051+
raise AssertionError("BUG: corrupt command about to be sent to agent!")
1052+
cmd_with_args = command + b' ' + args + b'\n'
1053+
else:
1054+
cmd_with_args = command + b'\n'
1055+
self.agent_write(cmd_with_args, writer)
1056+
while True:
1057+
more_expected = await self.handle_agent_response(expected_inquires, reader)
1058+
if not more_expected:
1059+
break
1060+
finally:
1061+
if reader is not self.agent_reader:
1062+
writer.close()
10361063

1037-
def agent_write(self, data: bytes) -> None:
1038-
writer = self.agent_writer
1064+
async def read_hello(self, agent_reader: asyncio.StreamReader) -> bytes:
1065+
while True:
1066+
line = await agent_reader.readline()
1067+
if not line.endswith(b'\n'):
1068+
raise ProtocolError("premature EOF from agent connection")
1069+
if b'\n' in line[:-1]:
1070+
raise ProtocolError("newline in readline() result???")
1071+
if line.startswith(b'#'):
1072+
continue
1073+
if line == b'OK' or line.startswith(b'OK '):
1074+
return line
1075+
raise ProtocolError("agent responded with something other than 'OK'"
1076+
" to initial connection")
1077+
1078+
def agent_write(self, data: bytes, writer: asyncio.StreamWriter) -> None:
10391079
assert writer is not None, 'agent_write called with no agent writer?'
10401080
self.log_io('A <<<', data)
10411081
writer.write(data)
10421082

10431083
async def handle_agent_response(self,
1044-
expected_inquires: Dict[bytes, 'ArgCallback']) -> bool:
1084+
expected_inquires: Dict[bytes, 'ArgCallback'],
1085+
agent_reader: asyncio.StreamReader) -> bool:
10451086
""" Receive and handle one agent response. Return whether there are
10461087
more expected """
1047-
assert self.agent_reader is not None
10481088
assert self.client_writer is not None
10491089
if self.client_writer.is_closing():
10501090
# If something went wrong, agent might send back junk.
10511091
# Discard all remaining data from agent and return.
1052-
while await self.agent_reader.read(1024):
1092+
while await agent_reader.read(1024):
10531093
pass
10541094
return False
10551095
# We generally consider the agent as trusted. But since the client can
10561096
# determine part of the response we handle this here as untrusted.
1057-
untrusted_line = await self.agent_reader.readline()
1097+
untrusted_line = await agent_reader.readline()
10581098
untrusted_line = untrusted_line.rstrip(b'\n')
10591099
self.log_io('A >>>', untrusted_line)
10601100
if untrusted_line.startswith(b'#'):
@@ -1295,7 +1335,9 @@ async def inquire_command_D(self, validate_sexp: 'SExprValidator', *,
12951335
raise Filtered from e
12961336
args = untrusted_sexp
12971337

1298-
self.agent_write(b'D ' + self.escape_D(self.serialize_sexpr(args)) + b'\n')
1338+
assert self.agent_writer is not None, "no writer?"
1339+
self.agent_write(b'D ' + self.escape_D(self.serialize_sexpr(args)) + b'\n',
1340+
self.agent_writer)
12991341
self.seen_data = True
13001342
return True
13011343

@@ -1393,7 +1435,8 @@ def serialize_item(item: 'SExpr') -> bytes:
13931435
async def inquire_command_END(self, *, untrusted_args: bytes) -> bool:
13941436
if untrusted_args:
13951437
raise Filtered('unexpected arguments to END')
1396-
self.agent_write(b'END\n')
1438+
assert self.agent_writer is not None, "no writer?"
1439+
self.agent_write(b'END\n', self.agent_writer)
13971440
return False
13981441

13991442
# endregion

0 commit comments

Comments
 (0)