feature(keyringctl): support query expressions for packet field selection
Instead of simply string matching a line, we now traverse the packet as a tree and match the path based on a depth first search. While traversing, we support logical OR and current depth * wildcard processed as a component based query expression. Callee's are adjusted to specifically select the appropriate Issuer at the correct depth. Fixes #185
This commit is contained in:
parent
9d4c7057f4
commit
099df52a04
@ -125,8 +125,10 @@ def convert_pubkey_signature_packet(
|
|||||||
if not current_packet_fingerprint:
|
if not current_packet_fingerprint:
|
||||||
raise Exception('missing current packet fingerprint for "{packet.name}"')
|
raise Exception('missing current packet fingerprint for "{packet.name}"')
|
||||||
|
|
||||||
signature_type = packet_dump_field(packet=packet, field="Type")
|
signature_type = packet_dump_field(packet=packet, query="Type")
|
||||||
issuer = get_fingerprint_from_partial(fingerprint_filter or set(), Fingerprint(packet_dump_field(packet, "Issuer")))
|
issuer = get_fingerprint_from_partial(
|
||||||
|
fingerprint_filter or set(), Fingerprint(packet_dump_field(packet, "Hashed area|Unhashed area.Issuer"))
|
||||||
|
)
|
||||||
|
|
||||||
if not issuer:
|
if not issuer:
|
||||||
debug(f"failed to resolve partial fingerprint {issuer}, skipping packet")
|
debug(f"failed to resolve partial fingerprint {issuer}, skipping packet")
|
||||||
@ -165,8 +167,10 @@ def convert_uid_signature_packet(
|
|||||||
if not current_packet_uid:
|
if not current_packet_uid:
|
||||||
raise Exception('missing current packet uid for "{packet.name}"')
|
raise Exception('missing current packet uid for "{packet.name}"')
|
||||||
|
|
||||||
signature_type = packet_dump_field(packet=packet, field="Type")
|
signature_type = packet_dump_field(packet=packet, query="Type")
|
||||||
issuer = get_fingerprint_from_partial(fingerprint_filter or set(), Fingerprint(packet_dump_field(packet, "Issuer")))
|
issuer = get_fingerprint_from_partial(
|
||||||
|
fingerprint_filter or set(), Fingerprint(packet_dump_field(packet, "Hashed area|Unhashed area.Issuer"))
|
||||||
|
)
|
||||||
|
|
||||||
if not issuer:
|
if not issuer:
|
||||||
debug(f"failed to resolve partial fingerprint {issuer}, skipping packet")
|
debug(f"failed to resolve partial fingerprint {issuer}, skipping packet")
|
||||||
@ -207,8 +211,10 @@ def convert_subkey_signature_packet(
|
|||||||
if not current_packet_fingerprint:
|
if not current_packet_fingerprint:
|
||||||
raise Exception('missing current packet fingerprint for "{packet.name}"')
|
raise Exception('missing current packet fingerprint for "{packet.name}"')
|
||||||
|
|
||||||
signature_type = packet_dump_field(packet=packet, field="Type")
|
signature_type = packet_dump_field(packet=packet, query="Type")
|
||||||
issuer = get_fingerprint_from_partial(fingerprint_filter or set(), Fingerprint(packet_dump_field(packet, "Issuer")))
|
issuer = get_fingerprint_from_partial(
|
||||||
|
fingerprint_filter or set(), Fingerprint(packet_dump_field(packet, "Hashed area|Unhashed area.Issuer"))
|
||||||
|
)
|
||||||
|
|
||||||
if not issuer:
|
if not issuer:
|
||||||
debug(f"failed to resolve partial fingerprint {issuer}, skipping packet")
|
debug(f"failed to resolve partial fingerprint {issuer}, skipping packet")
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -176,13 +177,23 @@ def packet_dump(packet: Path) -> str:
|
|||||||
return system(["sq", "packet", "dump", str(packet)])
|
return system(["sq", "packet", "dump", str(packet)])
|
||||||
|
|
||||||
|
|
||||||
def packet_dump_field(packet: Path, field: str) -> str:
|
def packet_dump_field(packet: Path, query: str) -> str:
|
||||||
"""Retrieve the value of a field from a PGP packet
|
"""Retrieve the value of a field from a PGP packet
|
||||||
|
|
||||||
|
Field queries are possible with the following notation during tree traversal:
|
||||||
|
- Use '.' to separate the parent section
|
||||||
|
- Use '*' as a wildcard for the current section
|
||||||
|
- Use '|' inside the current level as a logical OR
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- Version
|
||||||
|
- Hashed area|Unhashed area.Issuer
|
||||||
|
- *.Issuer
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
packet: The path to the PGP packet to retrieve the value from
|
packet: The path to the PGP packet to retrieve the value from
|
||||||
field: The name of the field
|
query: The name of the field as a query notation
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
@ -194,11 +205,49 @@ def packet_dump_field(packet: Path, field: str) -> str:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
dump = packet_dump(packet)
|
dump = packet_dump(packet)
|
||||||
lines = [line.strip() for line in dump.splitlines()]
|
|
||||||
lines = list(filter(lambda line: line.strip().startswith(f"{field}: "), lines))
|
queries = deque(query.split("."))
|
||||||
if not lines:
|
path = [queries.popleft()]
|
||||||
raise Exception(f'Packet has no field "{field}"')
|
depth = 0
|
||||||
return lines[0].split(sep=": ", maxsplit=1)[1]
|
|
||||||
|
# remove leading 4 space indention
|
||||||
|
lines = list(filter(lambda line: line.startswith(" "), dump.splitlines()))
|
||||||
|
lines = [sub(r"^ {4}", "", line, count=1) for line in lines]
|
||||||
|
# filter empty lines
|
||||||
|
lines = list(filter(lambda line: line.strip(), lines))
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
# determine current line depth by counting whitespace pairs
|
||||||
|
depth_line = int((len(line) - len(line.lstrip(" "))) / 2)
|
||||||
|
line = line.lstrip(" ")
|
||||||
|
|
||||||
|
# skip nodes that are deeper as our currently matched path
|
||||||
|
if depth < depth_line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# unwind the current query path until reaching previous match depth
|
||||||
|
while depth > depth_line:
|
||||||
|
queries.appendleft(path.pop())
|
||||||
|
depth -= 1
|
||||||
|
matcher = path[-1].split("|")
|
||||||
|
|
||||||
|
# check if current field matches the query expression
|
||||||
|
field = line.split(sep=":", maxsplit=1)[0]
|
||||||
|
if field not in matcher and "*" not in matcher:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# next depth is one level deeper as the current line
|
||||||
|
depth = depth_line + 1
|
||||||
|
|
||||||
|
# check if matcher is not the leaf of the query expression
|
||||||
|
if queries:
|
||||||
|
path.append(queries.popleft())
|
||||||
|
continue
|
||||||
|
|
||||||
|
# return final match
|
||||||
|
return line.split(sep=": ", maxsplit=1)[1] if ": " in line else line
|
||||||
|
|
||||||
|
raise Exception(f"Packet '{packet}' did not match the query '{query}'")
|
||||||
|
|
||||||
|
|
||||||
def packet_signature_creation_time(packet: Path) -> datetime:
|
def packet_signature_creation_time(packet: Path) -> datetime:
|
||||||
@ -212,7 +261,7 @@ def packet_signature_creation_time(packet: Path) -> datetime:
|
|||||||
-------
|
-------
|
||||||
The signature creation time as datetime
|
The signature creation time as datetime
|
||||||
"""
|
"""
|
||||||
field = packet_dump_field(packet, "Signature creation time")
|
field = packet_dump_field(packet, "Hashed area.Signature creation time")
|
||||||
field = " ".join(field.split(" ", 3)[0:3])
|
field = " ".join(field.split(" ", 3)[0:3])
|
||||||
return datetime.strptime(field, "%Y-%m-%d %H:%M:%S %Z")
|
return datetime.strptime(field, "%Y-%m-%d %H:%M:%S %Z")
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ def verify_integrity(certificate: Path, all_fingerprints: Set[Fingerprint]) -> N
|
|||||||
|
|
||||||
assert_packet_kind(path=uid_path, expected="User")
|
assert_packet_kind(path=uid_path, expected="User")
|
||||||
|
|
||||||
uid_value = simplify_uid(Uid(packet_dump_field(packet=uid_path, field="Value")))
|
uid_value = simplify_uid(Uid(packet_dump_field(packet=uid_path, query="Value")))
|
||||||
if uid_value != uid.name:
|
if uid_value != uid.name:
|
||||||
raise Exception(f"Unexpected uid in file {str(uid_path)}: {uid_value}")
|
raise Exception(f"Unexpected uid in file {str(uid_path)}: {uid_value}")
|
||||||
elif not uid_path.is_dir():
|
elif not uid_path.is_dir():
|
||||||
@ -139,7 +139,9 @@ def verify_integrity(certificate: Path, all_fingerprints: Set[Fingerprint]) -> N
|
|||||||
|
|
||||||
issuer = get_fingerprint_from_partial(
|
issuer = get_fingerprint_from_partial(
|
||||||
fingerprints=all_fingerprints,
|
fingerprints=all_fingerprints,
|
||||||
fingerprint=Fingerprint(packet_dump_field(packet=sig, field="Issuer")),
|
fingerprint=Fingerprint(
|
||||||
|
packet_dump_field(packet=sig, query="Hashed area|Unhashed area.Issuer")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if issuer != sig.stem:
|
if issuer != sig.stem:
|
||||||
raise Exception(f"Unexpected issuer in file {str(sig)}: {issuer}")
|
raise Exception(f"Unexpected issuer in file {str(sig)}: {issuer}")
|
||||||
@ -155,7 +157,9 @@ def verify_integrity(certificate: Path, all_fingerprints: Set[Fingerprint]) -> N
|
|||||||
|
|
||||||
issuer = get_fingerprint_from_partial(
|
issuer = get_fingerprint_from_partial(
|
||||||
fingerprints=all_fingerprints,
|
fingerprints=all_fingerprints,
|
||||||
fingerprint=Fingerprint(packet_dump_field(packet=sig, field="Issuer")),
|
fingerprint=Fingerprint(
|
||||||
|
packet_dump_field(packet=sig, query="Hashed area|Unhashed area.Issuer")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if issuer != sig.stem:
|
if issuer != sig.stem:
|
||||||
raise Exception(f"Unexpected issuer in file {str(sig)}: {issuer}")
|
raise Exception(f"Unexpected issuer in file {str(sig)}: {issuer}")
|
||||||
@ -236,13 +240,13 @@ def assert_packet_kind(path: Path, expected: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def assert_signature_type(path: Path, expected: str) -> None:
|
def assert_signature_type(path: Path, expected: str) -> None:
|
||||||
sig_type = packet_dump_field(packet=path, field="Type")
|
sig_type = packet_dump_field(packet=path, query="Type")
|
||||||
if sig_type != expected:
|
if sig_type != expected:
|
||||||
raise Exception(f"Unexpected packet type in file {str(path)} type: {sig_type} expected: {expected}")
|
raise Exception(f"Unexpected packet type in file {str(path)} type: {sig_type} expected: {expected}")
|
||||||
|
|
||||||
|
|
||||||
def assert_signature_type_certification(path: Path) -> None:
|
def assert_signature_type_certification(path: Path) -> None:
|
||||||
sig_type = packet_dump_field(packet=path, field="Type")
|
sig_type = packet_dump_field(packet=path, query="Type")
|
||||||
if sig_type not in ["GenericCertification", "PersonaCertification", "CasualCertification", "PositiveCertification"]:
|
if sig_type not in ["GenericCertification", "PersonaCertification", "CasualCertification", "PositiveCertification"]:
|
||||||
raise Exception(f"Unexpected packet certification type in file {str(path)} type: {sig_type}")
|
raise Exception(f"Unexpected packet certification type in file {str(path)} type: {sig_type}")
|
||||||
|
|
||||||
@ -253,13 +257,13 @@ def assert_is_pgp_fingerprint(path: Path, _str: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def assert_filename_matches_packet_issuer_fingerprint(path: Path, check: str) -> None:
|
def assert_filename_matches_packet_issuer_fingerprint(path: Path, check: str) -> None:
|
||||||
fingerprint = packet_dump_field(packet=path, field="Issuer Fingerprint")
|
fingerprint = packet_dump_field(packet=path, query="Unhashed area|Hashed area.Issuer Fingerprint")
|
||||||
if not fingerprint == check:
|
if not fingerprint == check:
|
||||||
raise Exception(f"Unexpected packet fingerprint in file {str(path)}: {fingerprint}")
|
raise Exception(f"Unexpected packet fingerprint in file {str(path)}: {fingerprint}")
|
||||||
|
|
||||||
|
|
||||||
def assert_filename_matches_packet_fingerprint(path: Path, check: str) -> None:
|
def assert_filename_matches_packet_fingerprint(path: Path, check: str) -> None:
|
||||||
fingerprint = packet_dump_field(packet=path, field="Fingerprint")
|
fingerprint = packet_dump_field(packet=path, query="Fingerprint")
|
||||||
if not fingerprint == check:
|
if not fingerprint == check:
|
||||||
raise Exception(f"Unexpected packet fingerprint in file {str(path)}: {fingerprint}")
|
raise Exception(f"Unexpected packet fingerprint in file {str(path)}: {fingerprint}")
|
||||||
|
|
||||||
|
@ -170,16 +170,127 @@ def test_packet_dump(system_mock: Mock) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@mark.parametrize(
|
@mark.parametrize(
|
||||||
"packet_dump_return, field, expectation",
|
"packet_dump_return, query, result, expectation",
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"foo: bar",
|
"""
|
||||||
"foo",
|
Signature Packet
|
||||||
|
Version: 4
|
||||||
|
Type: SubkeyBinding
|
||||||
|
Hash algo: SHA512
|
||||||
|
""",
|
||||||
|
"Type",
|
||||||
|
"SubkeyBinding",
|
||||||
does_not_raise(),
|
does_not_raise(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"foo: bar",
|
"""
|
||||||
"baz",
|
Signature Packet
|
||||||
|
Version: 4
|
||||||
|
Type: SubkeyBinding
|
||||||
|
Hash algo: SHA512
|
||||||
|
Hashed area:
|
||||||
|
Signature creation time: 2022-12-31 15:53:59 UTC
|
||||||
|
Issuer: BBBBBB
|
||||||
|
Unhashed area:
|
||||||
|
Issuer: 42424242
|
||||||
|
""",
|
||||||
|
"Unhashed area.Issuer",
|
||||||
|
"42424242",
|
||||||
|
does_not_raise(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
Signature Packet
|
||||||
|
Version: 4
|
||||||
|
Type: SubkeyBinding
|
||||||
|
Hash algo: SHA512
|
||||||
|
Hashed area:
|
||||||
|
Signature creation time: 2022-12-31 15:53:59 UTC
|
||||||
|
Unhashed area:
|
||||||
|
Issuer: 42424242
|
||||||
|
""",
|
||||||
|
"Hashed area|Unhashed area.Issuer",
|
||||||
|
"42424242",
|
||||||
|
does_not_raise(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
Signature Packet
|
||||||
|
Version: 4
|
||||||
|
Type: SubkeyBinding
|
||||||
|
Hash algo: SHA1
|
||||||
|
Hashed area:
|
||||||
|
Signature creation time: 2022-12-31
|
||||||
|
""",
|
||||||
|
"*.Signature creation time",
|
||||||
|
"2022-12-31",
|
||||||
|
does_not_raise(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
Signature Packet
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
x: foo
|
||||||
|
b:
|
||||||
|
b:
|
||||||
|
c: bar
|
||||||
|
""",
|
||||||
|
"*.b.c",
|
||||||
|
"bar",
|
||||||
|
does_not_raise(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
Signature Packet
|
||||||
|
a:
|
||||||
|
b:
|
||||||
|
x:
|
||||||
|
y:
|
||||||
|
z: foo
|
||||||
|
b:
|
||||||
|
b:
|
||||||
|
x:
|
||||||
|
y:
|
||||||
|
z: foo
|
||||||
|
w:
|
||||||
|
w: foo
|
||||||
|
k:
|
||||||
|
i:
|
||||||
|
c: bar
|
||||||
|
""",
|
||||||
|
"*.b.*.*.c",
|
||||||
|
"bar",
|
||||||
|
does_not_raise(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
Signature Packet
|
||||||
|
a:
|
||||||
|
c:
|
||||||
|
b: foo
|
||||||
|
a:
|
||||||
|
b: bar
|
||||||
|
""",
|
||||||
|
"a.b",
|
||||||
|
"bar",
|
||||||
|
does_not_raise(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"""
|
||||||
|
Signature Packet
|
||||||
|
Version: 4
|
||||||
|
Type: SubkeyBinding
|
||||||
|
Hash algo: SHA512
|
||||||
|
Hashed area:
|
||||||
|
Signature creation time: 2022-12-31 15:53:59 UTC
|
||||||
|
Unhashed area:
|
||||||
|
Issuer: 42424242
|
||||||
|
Issuer: BBBBBBBB
|
||||||
|
""",
|
||||||
|
"Hashed area.Issuer",
|
||||||
|
None,
|
||||||
raises(Exception),
|
raises(Exception),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -188,13 +299,14 @@ def test_packet_dump(system_mock: Mock) -> None:
|
|||||||
def test_packet_dump_field(
|
def test_packet_dump_field(
|
||||||
packet_dump_mock: Mock,
|
packet_dump_mock: Mock,
|
||||||
packet_dump_return: str,
|
packet_dump_return: str,
|
||||||
field: str,
|
query: str,
|
||||||
|
result: str,
|
||||||
expectation: ContextManager[str],
|
expectation: ContextManager[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
packet_dump_mock.return_value = packet_dump_return
|
packet_dump_mock.return_value = packet_dump_return
|
||||||
|
|
||||||
with expectation:
|
with expectation:
|
||||||
sequoia.packet_dump_field(packet=Path("packet"), field=field)
|
assert sequoia.packet_dump_field(packet=Path("packet"), query=query) == result
|
||||||
|
|
||||||
|
|
||||||
@patch("libkeyringctl.sequoia.packet_dump_field")
|
@patch("libkeyringctl.sequoia.packet_dump_field")
|
||||||
|
Loading…
Reference in New Issue
Block a user