Skip to content

Commit a87f5a0

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 zero risk of an injection vulnerability. Therefore, use a separate unrestricted agent connection for these commands.
1 parent 5b7b711 commit a87f5a0

1 file changed

Lines changed: 64 additions & 31 deletions

File tree

splitgpg2/__init__.py

Lines changed: 64 additions & 31 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+
await self.handle_agent_response({}, 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
@@ -656,7 +668,9 @@ def verify_keygrip_arguments(min_count: int, max_count: int,
656668
pass
657669
elif untrusted_args[6] == 61: # ASCII '='
658670
# 1000 is the default value used by gpg2
659-
sanitize_int(untrusted_args[7:], 1, 1000)
671+
if untrusted_args != b'--list=%d' % sanitize_int(untrusted_args[7:], 1, 1000):
672+
assert False, "bug in filtering"
673+
raise Filtered
660674
else:
661675
raise Filtered
662676
else:
@@ -744,11 +758,13 @@ async def command_HAVEKEY(self, untrusted_args: Optional[bytes]) -> None:
744758
raise Filtered
745759
# upper keygrip limit is arbitary
746760
args = self.verify_keygrip_arguments(1, 200, untrusted_args, True)
747-
await self.send_agent_command(b'HAVEKEY', args)
761+
unrestricted = args.startswith(b'--list') and not self.allow_keygen
762+
await self.send_agent_command(b'HAVEKEY', args, unrestricted)
748763

749764
async def command_KEYINFO(self, untrusted_args: Optional[bytes]) -> None:
750765
args = self.verify_keygrip_arguments(1, 1, untrusted_args, True)
751-
await self.send_agent_command(b'KEYINFO', args)
766+
unrestricted = args.startswith(b'--list') and not self.allow_keygen
767+
await self.send_agent_command(b'KEYINFO', args, unrestricted)
752768

753769
async def command_GENKEY(self, untrusted_args: Optional[bytes]) -> None:
754770
if not self.allow_keygen:
@@ -823,7 +839,8 @@ async def setkeydesc(self, keygrip: bytes) -> None:
823839
key.fingerprint,
824840
subkey_desc)
825841

826-
self.agent_write(b'SETKEYDESC %s\n' % self.percent_plus_escape(desc))
842+
assert self.agent_writer is not None, "no writer?"
843+
self.agent_write(b'SETKEYDESC %s\n' % self.percent_plus_escape(desc), self.agent_writer)
827844

828845
assert self.agent_reader is not None
829846
untrusted_line = await self.agent_reader.readline()
@@ -1020,43 +1037,56 @@ def get_inquires_for_command(self, command: bytes) -> Dict[bytes, 'ArgCallback']
10201037
}
10211038
return {}
10221039

1023-
async def send_agent_command(self, command: bytes, args: Optional[bytes]) -> None:
1040+
async def send_agent_command(self, command: bytes, args: Optional[bytes],
1041+
unrestricted: bool=False) -> None:
10241042
""" Sends command to local gpg agent and handle the response """
10251043
expected_inquires = self.get_inquires_for_command(command)
1026-
if args:
1027-
if not self.command_argument_regex.match(args):
1028-
raise AssertionError("BUG: corrupt command about to be sent to agent!")
1029-
cmd_with_args = command + b' ' + args + b'\n'
1044+
assert self.agent_reader is not None, "no reader?"
1045+
assert self.agent_writer is not None, "no writer?"
1046+
if unrestricted and not self.allow_keygen:
1047+
reader, writer = await asyncio.open_unix_connection(
1048+
self.agent_unrestricted_socket_path)
1049+
line = await reader.readline()
1050+
if not line.startswith(b'OK '):
1051+
raise Filtered # TODO: better handle error
10301052
else:
1031-
cmd_with_args = command + b'\n'
1032-
self.agent_write(cmd_with_args)
1033-
while True:
1034-
more_expected = await self.handle_agent_response(
1035-
expected_inquires=expected_inquires)
1036-
if not more_expected:
1037-
break
1053+
reader, writer = self.agent_reader, self.agent_writer
1054+
try:
1055+
if args:
1056+
if not self.command_argument_regex.match(args):
1057+
raise AssertionError("BUG: corrupt command about to be sent to agent!")
1058+
cmd_with_args = command + b' ' + args + b'\n'
1059+
else:
1060+
cmd_with_args = command + b'\n'
1061+
self.agent_write(cmd_with_args, writer)
1062+
while True:
1063+
more_expected = await self.handle_agent_response(expected_inquires, reader)
1064+
if not more_expected:
1065+
break
1066+
finally:
1067+
if reader is not self.agent_reader:
1068+
writer.close()
10381069

1039-
def agent_write(self, data: bytes) -> None:
1040-
writer = self.agent_writer
1070+
def agent_write(self, data: bytes, writer: asyncio.StreamWriter) -> None:
10411071
assert writer is not None, 'agent_write called with no agent writer?'
10421072
self.log_io('A <<<', data)
10431073
writer.write(data)
10441074

10451075
async def handle_agent_response(self,
1046-
expected_inquires: Dict[bytes, 'ArgCallback']) -> bool:
1076+
expected_inquires: Dict[bytes, 'ArgCallback'],
1077+
agent_reader: asyncio.StreamReader) -> bool:
10471078
""" Receive and handle one agent response. Return whether there are
10481079
more expected """
1049-
assert self.agent_reader is not None
10501080
assert self.client_writer is not None
10511081
if self.client_writer.is_closing():
10521082
# If something went wrong, agent might send back junk.
10531083
# Discard all remaining data from agent and return.
1054-
while await self.agent_reader.read(1024):
1084+
while await agent_reader.read(1024):
10551085
pass
10561086
return False
10571087
# We generally consider the agent as trusted. But since the client can
10581088
# determine part of the response we handle this here as untrusted.
1059-
untrusted_line = await self.agent_reader.readline()
1089+
untrusted_line = await agent_reader.readline()
10601090
untrusted_line = untrusted_line.rstrip(b'\n')
10611091
self.log_io('A >>>', untrusted_line)
10621092
untrusted_res, untrusted_args = extract_args(untrusted_line)
@@ -1294,7 +1324,9 @@ async def inquire_command_D(self, validate_sexp: 'SExprValidator', *,
12941324
raise Filtered from e
12951325
args = untrusted_sexp
12961326

1297-
self.agent_write(b'D ' + self.escape_D(self.serialize_sexpr(args)) + b'\n')
1327+
assert self.agent_writer is not None, "no writer?"
1328+
self.agent_write(b'D ' + self.escape_D(self.serialize_sexpr(args)) + b'\n',
1329+
self.agent_writer)
12981330
self.seen_data = True
12991331
return True
13001332

@@ -1392,7 +1424,8 @@ def serialize_item(item: 'SExpr') -> bytes:
13921424
async def inquire_command_END(self, *, untrusted_args: bytes) -> bool:
13931425
if untrusted_args:
13941426
raise Filtered('unexpected arguments to END')
1395-
self.agent_write(b'END\n')
1427+
assert self.agent_writer is not None, "no writer?"
1428+
self.agent_write(b'END\n', self.agent_writer)
13961429
return False
13971430

13981431
# endregion

0 commit comments

Comments
 (0)