File: gnupg.py

package info (click to toggle)
debsigs 0.2.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 420 kB
  • sloc: python: 1,160; perl: 728; makefile: 12; sh: 9
file content (125 lines) | stat: -rw-r--r-- 3,399 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: GPL-2.0-or-later
"""Parse the output of `gpg --list-keys --with-colons`."""

from __future__ import annotations

import dataclasses
import typing

import pyparsing as pyp

from testsigs import defs


if typing.TYPE_CHECKING:
    from typing import Final


_p_eol = pyp.Char("\r\n").suppress()

_p_ign = pyp.ZeroOrMore(pyp.CharsNotIn(":\n")).suppress() + pyp.Char(":").suppress()

_p_ign_all = pyp.CharsNotIn("\n").suppress()

_p_hex_string = pyp.Word("0123456789ABCDEF")

_p_string = pyp.CharsNotIn(":\n")

_p_ignored_line = pyp.Literal("tru:").suppress() + _p_ign_all + _p_eol

_p_ignored_lines = pyp.ZeroOrMore(_p_ignored_line)

_p_list_pub = (
    pyp.Literal("pub:").suppress() + _p_ign + _p_ign + _p_ign + _p_hex_string + _p_ign_all + _p_eol
)

_p_list_fpr = pyp.Literal("fpr:").suppress() + _p_ign * 8 + _p_hex_string + _p_ign_all + _p_eol

_p_list_uid = pyp.Literal("uid:").suppress() + _p_ign * 8 + _p_string + _p_ign_all + _p_eol

_p_list_sub = (
    pyp.Literal("sub:").suppress()
    + _p_ign
    + _p_ign
    + _p_ign
    + _p_hex_string
    + _p_ign * 7
    + _p_string
    + _p_ign_all
    + _p_eol
)


@_p_list_sub.set_parse_action
def _parse_list_sub(tokens: pyp.ParseResults) -> defs.Subkey:
    """Parse a subkey definition."""
    params: Final = tokens.as_list()
    match params:
        case [key_id, caps]:
            return defs.Subkey(key_id=key_id, fpr="", capabilities=set(caps))

        case _:
            raise RuntimeError(repr(params))


_p_list_subkey = _p_ignored_lines + _p_list_sub + _p_ignored_lines + _p_list_fpr


@_p_list_subkey.set_parse_action
def _parse_list_subkey(tokens: pyp.ParseResults) -> defs.Subkey:
    """Parse a subkey definition."""
    params: Final = tokens.as_list()
    match params:
        case [sub_key_id, fpr]:
            return dataclasses.replace(sub_key_id, fpr=fpr)

        case _:
            raise RuntimeError(repr(params))


_p_list_pubkey = (
    _p_ignored_lines
    + _p_list_pub
    + _p_ignored_lines
    + _p_list_fpr
    + _p_ignored_lines
    + _p_list_uid
    + pyp.OneOrMore(_p_list_subkey)
)


@_p_list_pubkey.set_parse_action
def _parse_list_pubkey(tokens: pyp.ParseResults) -> defs.PublicKey:
    """Parse a subkey definition."""
    params: Final = tokens.as_list()
    match params:
        case [key_id, fpr, uid, *subkeys] if isinstance(key_id, str) and isinstance(
            fpr,
            str,
        ) and isinstance(uid, str) and subkeys and all(
            isinstance(subkey, defs.Subkey) for subkey in subkeys
        ):
            # We did just validate that one...
            return defs.PublicKey(
                key_id=key_id,
                fpr=fpr,
                uid=uid,
                subkeys=subkeys,  # type: ignore[arg-type]
            )

        case _:
            raise RuntimeError(repr(params))


_p_list_keys = pyp.ZeroOrMore(_p_list_pubkey) + _p_ignored_lines

_p_list_keys_complete = _p_list_keys.leave_whitespace().parse_with_tabs()


def parse_list_keys(output: str) -> list[defs.PublicKey]:
    """Parse the output of `gpg --list-keys`, extract the info we care about."""
    res: Final = _p_list_keys_complete.parse_string(output, parse_all=True).as_list()
    if not all(isinstance(pkey, defs.PublicKey) for pkey in res):
        raise RuntimeError(repr(res))
    return res