Skip to content

Commit 2129a12

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 ebf46f9 commit 2129a12

1 file changed

Lines changed: 73 additions & 33 deletions

File tree

splitgpg2/__init__.py

Lines changed: 73 additions & 33 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

@@ -456,14 +459,20 @@ async def connect_agent(self) -> None:
456459

457460
dirs = subprocess.check_output(
458461
['gpgconf', *self.homedir_opts(), '--list-dirs', '-o/dev/stdout'])
459-
if self.allow_keygen:
460-
socket_field = b'agent-socket:'
461-
else:
462-
socket_field = b'agent-extra-socket'
462+
unrestricted_socket_field = b'agent-socket'
463+
socket_field = unrestricted_socket_field if self.allow_keygen else b'agent-extra-socket'
463464
# search for agent-socket:/run/user/1000/gnupg/S.gpg-agent
464-
agent_socket_path = [d.split(b':', 1)[1] for d in dirs.splitlines()
465-
if d.startswith(socket_field)][0]
466-
self.agent_socket_path = agent_socket_path.decode()
465+
for d in dirs.splitlines():
466+
key, value = d.split(b':')
467+
if key == socket_field:
468+
self.agent_socket_path = value.decode("UTF-8", "surrogateescape")
469+
if key == unrestricted_socket_field:
470+
self.agent_unrestricted_socket_path = value.decode("UTF-8", "surrogateescape")
471+
if ((self.agent_unrestricted_socket_path is not None) and
472+
(self.agent_socket_path is not None)):
473+
break
474+
else:
475+
raise RuntimeError("bad output from gpgconf")
467476

468477
self.agent_reader, self.agent_writer = await asyncio.open_unix_connection(
469478
path=self.agent_socket_path)
@@ -472,7 +481,7 @@ async def connect_agent(self) -> None:
472481
self.notify('connected')
473482

474483
# wait for agent hello
475-
await self.handle_agent_response({})
484+
self.client_write(await self.read_hello(self.agent_reader))
476485

477486
def close(self, reason: str, log_level: int = logging.ERROR) -> None:
478487
self.log.log(log_level, '%s; Closing!', reason)
@@ -559,8 +568,8 @@ def default_options() -> Dict[bytes, Tuple[OptionHandlingType, Optional[bytes]]]
559568
b'lc-messages': (OptionHandlingType.fake, b'OK'),
560569
b'putenv': (OptionHandlingType.fake, b'OK'),
561570
b'pinentry-mode': (OptionHandlingType.fake, b'ERR 67108924 Not supported <GPG Agent>'),
562-
b'allow-pinentry-notify': (OptionHandlingType.verify, None),
563-
b'agent-awareness': (OptionHandlingType.verify, b'2.1.0')
571+
b'allow-pinentry-notify': (OptionHandlingType.fake, b'OK'),
572+
b'agent-awareness': (OptionHandlingType.verify, b'2.1.0'),
564573
}
565574

566575
@staticmethod
@@ -738,11 +747,13 @@ async def command_HAVEKEY(self, untrusted_args: Optional[bytes]) -> None:
738747
raise Filtered
739748
# upper keygrip limit is arbitary
740749
args = self.verify_keygrip_arguments(1, 200, untrusted_args, True)
741-
await self.send_agent_command(b'HAVEKEY', args)
750+
unrestricted = args.startswith(b'--list') and not self.allow_keygen
751+
await self.send_agent_command(b'HAVEKEY', args, unrestricted)
742752

743753
async def command_KEYINFO(self, untrusted_args: Optional[bytes]) -> None:
744754
args = self.verify_keygrip_arguments(1, 1, untrusted_args, True)
745-
await self.send_agent_command(b'KEYINFO', args)
755+
unrestricted = args.startswith(b'--list') and not self.allow_keygen
756+
await self.send_agent_command(b'KEYINFO', args, unrestricted)
746757

747758
async def command_GENKEY(self, untrusted_args: Optional[bytes]) -> None:
748759
if not self.allow_keygen:
@@ -817,7 +828,8 @@ async def setkeydesc(self, keygrip: bytes) -> None:
817828
key.fingerprint,
818829
subkey_desc)
819830

820-
self.agent_write(b'SETKEYDESC %s\n' % self.percent_plus_escape(desc))
831+
assert self.agent_writer is not None, "no writer?"
832+
self.agent_write(b'SETKEYDESC %s\n' % self.percent_plus_escape(desc), self.agent_writer)
821833

822834
assert self.agent_reader is not None
823835
untrusted_line = await self.agent_reader.readline()
@@ -1014,43 +1026,68 @@ def get_inquires_for_command(self, command: bytes) -> Dict[bytes, 'ArgCallback']
10141026
}
10151027
return {}
10161028

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

1033-
def agent_write(self, data: bytes) -> None:
1034-
writer = self.agent_writer
1057+
async def read_hello(self, agent_reader: asyncio.StreamReader) -> bytes:
1058+
while True:
1059+
line = await agent_reader.readline()
1060+
if not line.endswith(b'\n'):
1061+
raise ProtocolError("premature EOF from agent connection")
1062+
if b'\n' in line[:-1]:
1063+
raise ProtocolError("newline in readline() result???")
1064+
if line.startswith(b'#'):
1065+
continue
1066+
if line == b'OK' or line.startswith(b'OK '):
1067+
return line
1068+
raise ProtocolError("agent responded with something other than 'OK'"
1069+
" to initial connection")
1070+
1071+
def agent_write(self, data: bytes, writer: asyncio.StreamWriter) -> None:
10351072
assert writer is not None, 'agent_write called with no agent writer?'
10361073
self.log_io('A <<<', data)
10371074
writer.write(data)
10381075

10391076
async def handle_agent_response(self,
1040-
expected_inquires: Dict[bytes, 'ArgCallback']) -> bool:
1077+
expected_inquires: Dict[bytes, 'ArgCallback'],
1078+
agent_reader: asyncio.StreamReader) -> bool:
10411079
""" Receive and handle one agent response. Return whether there are
10421080
more expected """
1043-
assert self.agent_reader is not None
10441081
assert self.client_writer is not None
10451082
if self.client_writer.is_closing():
10461083
# If something went wrong, agent might send back junk.
10471084
# Discard all remaining data from agent and return.
1048-
while await self.agent_reader.read(1024):
1085+
while await agent_reader.read(1024):
10491086
pass
10501087
return False
10511088
# We generally consider the agent as trusted. But since the client can
10521089
# determine part of the response we handle this here as untrusted.
1053-
untrusted_line = await self.agent_reader.readline()
1090+
untrusted_line = await agent_reader.readline()
10541091
untrusted_line = untrusted_line.rstrip(b'\n')
10551092
self.log_io('A >>>', untrusted_line)
10561093
if untrusted_line.startswith(b'#'):
@@ -1291,7 +1328,9 @@ async def inquire_command_D(self, validate_sexp: 'SExprValidator', *,
12911328
raise Filtered from e
12921329
args = untrusted_sexp
12931330

1294-
self.agent_write(b'D ' + self.escape_D(self.serialize_sexpr(args)) + b'\n')
1331+
assert self.agent_writer is not None, "no writer?"
1332+
self.agent_write(b'D ' + self.escape_D(self.serialize_sexpr(args)) + b'\n',
1333+
self.agent_writer)
12951334
self.seen_data = True
12961335
return True
12971336

@@ -1389,7 +1428,8 @@ def serialize_item(item: 'SExpr') -> bytes:
13891428
async def inquire_command_END(self, *, untrusted_args: bytes) -> bool:
13901429
if untrusted_args:
13911430
raise Filtered('unexpected arguments to END')
1392-
self.agent_write(b'END\n')
1431+
assert self.agent_writer is not None, "no writer?"
1432+
self.agent_write(b'END\n', self.agent_writer)
13931433
return False
13941434

13951435
# endregion

0 commit comments

Comments
 (0)