Skip to content

API Reference

Signature Verification

sshsig.sshsig.check_signature(msg_in, armored_signature, namespace='git')

Check that an ssh-keygen signature is a digital signature of the input message.

This function implements functionality provided by:

ssh-keygen -Y check-novalidate -n namespace -s armored_signature_file < msg_in

Returns:

Type Description
PublicKey

The cryptographic PublicKey embedded inside the SSHSIG signature.

Raises:

Type Description
InvalidSignature

If signature is not valid for the input message.

NotImplementedError

If a signature encoding feature is not supported.

Source code in sshsig/sshsig.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def check_signature(
    msg_in: str | bytes | BinaryIO,
    armored_signature: str | bytes,
    namespace: str = "git",
) -> PublicKey:
    """Check that an ssh-keygen signature is a digital signature of the input message.

    This function implements functionality provided by:
    ```
    ssh-keygen -Y check-novalidate -n namespace -s armored_signature_file < msg_in
    ```

    Returns:
      The cryptographic PublicKey embedded inside the SSHSIG signature.

    Raises:
      InvalidSignature: If signature is not valid for the input message.
      NotImplementedError: If a signature encoding feature is not supported.
    """
    return cast_or_raise(do_check_signature(msg_in, armored_signature, namespace))

sshsig.sshsig.verify(msg_in, armored_signature, allowed_signers, namespace='git')

Verify a signature generated by ssh-keygen, the OpenSSH authentication key utility.

This function implements a SUBSET of functionality provided by:

ssh-keygen -Y verify \
    -f allowed_signers_file \
    -I '*' \
    -n namespace \
    -s armored_signature_file \
    < msg_in

when the allowed_signers_file is in a sub-format with only lines starting: * namespaces="X" ... where X equals the namespace argument.

Returns:

Type Description
PublicKey

The cryptographic PublicKey embedded inside the SSHSIG signature.

Raises:

Type Description
InvalidSignature

If signature is not valid for the input message.

NotImplementedError

If a signature encoding feature is not supported.

Source code in sshsig/sshsig.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
def verify(
    msg_in: str | bytes | BinaryIO,
    armored_signature: str | bytes,
    allowed_signers: Iterable[PublicKey],
    namespace: str = "git",
) -> PublicKey:
    r"""Verify a signature generated by ssh-keygen, the OpenSSH authentication key utility.

    This function implements a _SUBSET_ of functionality provided by:
    ```sh
    ssh-keygen -Y verify \
        -f allowed_signers_file \
        -I '*' \
        -n namespace \
        -s armored_signature_file \
        < msg_in
    ```
    when the allowed_signers_file is in a sub-format with only lines starting:
    `* namespaces="X" ...`
    where X equals the namespace argument.

    Returns:
      The cryptographic PublicKey embedded inside the SSHSIG signature.

    Raises:
      InvalidSignature: If signature is not valid for the input message.
      NotImplementedError: If a signature encoding feature is not supported.
    """
    return cast_or_raise(
        do_verify(msg_in, armored_signature, allowed_signers, namespace)
    )

SSH Public Key

sshsig.ssh_public_key.PublicKey

Bases: ABC

Source code in sshsig/ssh_public_key.py
 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
126
127
128
129
130
131
132
133
134
135
136
137
class PublicKey(ABC):

    @property
    @abstractmethod
    def algo_name(self) -> str: ...

    def verify(self, signature: bytes, message: bytes) -> None:
        """Verify the signature matches the message.

        Subclasses should override do_verify, not verify.

        Raises:
            Exception: An exception object describing the reason the signature does \
            not match the message.
        """
        return cast_or_raise(self.do_verify(signature, message))

    @abstractmethod
    def do_verify(self, signature: bytes, message: bytes) -> None | Exception:
        """Verify the signature matches the message.

        Call verify if you want an exception raised instead of returned.

        Returns:
            None if the signature is verified to match the message.
            Otherwise, an exception object describing the reason the signature does
            not match the message.

        Raises:
            Exception: Possible exceptions for reasons other than the public key \
            determining the signature does not match the message.
        """
        ...

    @abstractmethod
    def openssh_str(self) -> str: ...

    def __str__(self) -> str:
        return self.openssh_str()

    @classmethod
    def from_openssh_str(cls, line: str) -> PublicKey:
        """Create PublicKey from an OpenSSH format public key string.

        Returns:
            PublicKey

        Raises:
            ValueError: If the input string is not a valid format or encoding.
            NotImplementedError: If the public key algorithm is not supported.
        """
        return cast_or_raise(cls.do_from_openssh_str(line))

    @staticmethod
    def do_from_openssh_str(line: str) -> PublicKey | ValueError | NotImplementedError:
        parts = line.split(maxsplit=2)
        if len(parts) < 2:
            msg = "Not space-separated OpenSSH format public key ('{}')."
            return unexceptional(ValueError(msg.format(line)))
        key_algo_name = parts[0]
        try:
            buf = binascii.a2b_base64(parts[1])
        except binascii.Error as ex:
            return unexceptional(ValueError(), ex)
        ret = PublicKey.do_from_ssh_encoding(buf)
        if not isinstance(ret, PublicKey):
            return ret
        if ret.algo_name != key_algo_name:
            return unexceptional(ValueError("Improperly encoded public key."))
        return ret

    @classmethod
    def from_ssh_encoding(cls, buf: bytes) -> PublicKey:
        return cast_or_raise(cls.do_from_ssh_encoding(buf))

    @staticmethod
    def do_from_ssh_encoding(
        buf: bytes,
    ) -> PublicKey | ValueError | NotImplementedError:
        try:
            pkt = SshReader(buf)
            algo_name = pkt.read_string().decode()
            algo = PublicKeyAlgorithm.do_from_name(algo_name)
            if not isinstance(algo, PublicKeyAlgorithm):
                return algo
            return algo.load_public_key(pkt)
        except ValueError as error:
            return error

from_openssh_str(line) classmethod

Create PublicKey from an OpenSSH format public key string.

Returns:

Type Description
PublicKey

PublicKey

Raises:

Type Description
ValueError

If the input string is not a valid format or encoding.

NotImplementedError

If the public key algorithm is not supported.

Source code in sshsig/ssh_public_key.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
@classmethod
def from_openssh_str(cls, line: str) -> PublicKey:
    """Create PublicKey from an OpenSSH format public key string.

    Returns:
        PublicKey

    Raises:
        ValueError: If the input string is not a valid format or encoding.
        NotImplementedError: If the public key algorithm is not supported.
    """
    return cast_or_raise(cls.do_from_openssh_str(line))

Allowed Signers File Format

Parsing of the ssh-keygen allowed signers format.

load_allowed_signers_file(file)

Read public keys in "allowed signers" format per ssh-keygen.

Raises:

Type Description
ValueError

If the file is not properly formatted.

Source code in sshsig/allowed_signers.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def load_allowed_signers_file(file: TextIO | Path) -> Iterable[AllowedSigner]:
    """Read public keys in "allowed signers" format per ssh-keygen.

    Raises:
        ValueError: If the file is not properly formatted.
    """
    # The intention of this implementation is to reproduce the behaviour of the
    # parse_principals_key_and_options function of the following sshsig.c file:
    # https://archive.softwareheritage.org/
    # swh:1:cnt:470b286a3a982875a48a5262b7057c4710b17fed

    if isinstance(file, Path):
        with open(file, encoding="ascii") as f:
            return load_allowed_signers_file(f)
    ret = list()
    for line in file.readlines():
        if "\f" in line:
            raise ValueError(f"Form feed character not supported: ('{line}').")
        if "\v" in line:
            raise ValueError(f"Vertical tab character not supported: ('{line}').")
        line = line.strip("\n\r")
        if line and line[0] not in ["#", "\0"]:
            ret.append(AllowedSigner.parse(line))
    return ret

for_git_allowed_keys(allowed_signers)

Convert ssh-keygen "allowed signers" entries to "just-a-list-for-git" sub-format.

In the "just-a-list-for-git" sub-format, only the "*" value is accepted in the principles field. The only allowed signers option accepted is 'namespaces="git"'.

Raises:

Type Description
ValueError

If any ssh-keygen "allowed signers" feature is used that is not valid in the "just-a-list-for-git" sub-format.

NotImplementedError

If a public key algorithm is not supported.

Source code in sshsig/allowed_signers.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def for_git_allowed_keys(
    allowed_signers: Iterable[AllowedSigner],
) -> Iterable[PublicKey]:
    """Convert ssh-keygen "allowed signers" entries to "just-a-list-for-git" sub-format.

    In the "just-a-list-for-git" sub-format, only the "*" value is accepted in
    the principles field. The only allowed signers option accepted is 'namespaces="git"'.

    Raises:
        ValueError: If any ssh-keygen "allowed signers" feature is used that is
            not valid in the "just-a-list-for-git" sub-format.
        NotImplementedError: If a public key algorithm is not supported.
    """
    ret = list()
    for allowed in allowed_signers:
        if allowed.principals != "*":
            raise ValueError("Only solitary wildcard principal pattern supported.")
        options = allowed.options or dict()
        only_namespaces = options.get("namespaces")
        if only_namespaces is not None and only_namespaces != "git":
            raise ValueError('Only namespaces="git" is supported.')
        if "cert-authority" in options:
            raise ValueError("Certificate keys not supported.")
        if "valid-before" in options or "valid-after" in options:
            raise ValueError("Allowed signer validation dates not supported.")
        s = " ".join((allowed.key_type, allowed.base64_key))
        ret.append(PublicKey.from_openssh_str(s))
    return ret

save_for_git_allowed_signers_file(src, out)

Save keys for git to "allowed signers" format per ssh-keygen.

Source code in sshsig/allowed_signers.py
176
177
178
179
180
181
182
183
184
185
186
def save_for_git_allowed_signers_file(
    src: Iterable[PublicKey], out: Path | TextIO
) -> None:
    """Save keys for git to "allowed signers" format per ssh-keygen."""

    if isinstance(out, Path):
        with open(out, 'w') as f:
            save_for_git_allowed_signers_file(src, f)
    else:
        for key in src:
            out.write('* namespaces="git" {}\n'.format(key.openssh_str()))