python-sshsig_0.2.2.orig/LICENSE0000644000000000000000000000206114716662702013320 0ustar00MIT License Copyright (c) 2024 Castedo Ellerman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-sshsig_0.2.2.orig/Makefile0000644000000000000000000000003714716662702013754 0ustar00.PHONY: help help: ./justfile python-sshsig_0.2.2.orig/README.md0000644000000000000000000000041314736566116013575 0ustar00SSHSIG ====== Python port of the lightweight SSH Signature format of `ssh-keygen`. [Documentation](https://castedo.github.io/sshsig/) Repositories ------------ * (primary) * (secondary) python-sshsig_0.2.2.orig/compat/0000755000000000000000000000000014736014255013573 5ustar00python-sshsig_0.2.2.orig/contrib/0000755000000000000000000000000014720172244013744 5ustar00python-sshsig_0.2.2.orig/docs/0000755000000000000000000000000014736073003013234 5ustar00python-sshsig_0.2.2.orig/integration/0000755000000000000000000000000014731262252014630 5ustar00python-sshsig_0.2.2.orig/justfile0000755000000000000000000000065414736236006014070 0ustar00#!/usr/bin/env -S just --justfile default: just --list test-runtime: python3 -m unittest discover -t . -s tests --buffer test: && test-runtime ruff check sshsig || true mypy --strict sshsig cd tests && mypy --ignore-missing-imports . # cd for separate mypy cache+config integration-test: integration/test-all build-docs: mkdocs build --strict --verbose check: test check-runtime: test-runtime python-sshsig_0.2.2.orig/mkdocs.yml0000644000000000000000000000064214746505521014317 0ustar00site_name: Python SSHSIG Documentation theme: name: material features: - content.code.copy - navigation.footer plugins: - search - mkdocstrings nav: - 'Home': index.md - 'Tutorials': - 'Check Signature': tutorial/check_signature.md - 'Verify Signature': tutorial/verify_signature.md - 'How-To Guides': - 'Check Git Commit': howto/check_commit.md - reference.md - related.md python-sshsig_0.2.2.orig/pyproject.toml0000644000000000000000000000325314736566116015237 0ustar00[build-system] requires = ["setuptools>=64", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project] name = "sshsig" dynamic = ["version"] description = "SSH signature verification" license = {file = "LICENSE"} readme = "README.md" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", ] maintainers = [ {name = "Castedo Ellerman", email = "castedo@castedo.com"}, {name = "Jelmer Vernooij", email = "jelmer@jelmer.uk"}, ] requires-python = ">=3.9" dependencies = ["cryptography"] [project.urls] GitHub = "https://github.com/castedo/sshsig" GitLab = "https://gitlab.com/perm.pub/sshsiglib" [tool.setuptools] packages = ["sshsig"] [tool.setuptools_scm] version_file = "sshsig/_version.py" [tool.ruff.lint] select = [ "ANN", "D", "E", "F", "PIE", "UP", "RSE", "RUF", ] ignore = [ "ANN001", "ANN002", "ANN003", "ANN101", # missing-type-self "ANN102", "ANN201", "ANN202", "ANN204", "ANN205", "ANN206", "ANN401", # Dynamically typed expressions (typing.Any) are disallowed "D100", "D101", "D102", "D103", "D104", "D105", "D107", "D202", # No blank lines allowed after function docstring "D204", "D205", "D417", "E501", # line too long "E741", # ambiguous variable name "UP007", # Use `X | Y` for type annotations "UP032", # Use f-string instead of `format` call ] [tool.ruff.lint.pydocstyle] convention = "google" [tool.ruff.format] quote-style = "preserve" [tool.black] skip_string_normalization = true [tool.yapf] column_limit = 88 based_on_style = "facebook" python-sshsig_0.2.2.orig/sshsig/0000755000000000000000000000000014716662702013614 5ustar00python-sshsig_0.2.2.orig/testdata/0000755000000000000000000000000014716662702014125 5ustar00python-sshsig_0.2.2.orig/tests/0000755000000000000000000000000014716662702013456 5ustar00python-sshsig_0.2.2.orig/compat/ssh_keygen.py0000644000000000000000000000602414737300106016300 0ustar00from __future__ import annotations import argparse import sys from pathlib import Path from typing import BinaryIO from sshsig import InvalidSignature, check_signature, verify from sshsig.allowed_signers import load_for_git_allowed_signers_file def cli_subcmd_check_novalidate( msg_in: BinaryIO, signature_file: Path, namespace: str, ) -> int: try: with open(signature_file) as f: check_signature(msg_in, f.read(), namespace) return 0 except InvalidSignature as ex: print(ex, file=sys.stderr) return 255 def cli_subcmd_verify( msg_in: BinaryIO, signature_file: Path, allowed_signers_file: Path, ) -> int: allowed = load_for_git_allowed_signers_file(allowed_signers_file) try: with open(signature_file) as f: verify(msg_in, f.read(), allowed, "git") return 0 except InvalidSignature as ex: print(ex, file=sys.stderr) return 255 def main(stdin: BinaryIO, args: list[str] | None = None) -> int: parser = argparse.ArgumentParser( description="Test reimplementation of ssh-keygen -Y" ) parser.add_argument("-Y", action="store_true", required=True) subparsers = parser.add_subparsers(dest="subcmd", required=True) check_parser = subparsers.add_parser( "check-novalidate", help="Check signature has valid structure." ) check_parser.add_argument("-O", dest="option", help="not implemented") check_parser.add_argument("-n", dest="namespace", required=True) check_parser.add_argument("-s", dest="signature_file", type=Path, required=True) verify_parser = subparsers.add_parser("verify", help="verify a signature") verify_parser.add_argument("-O", dest='option', help="not implemented") verify_parser.add_argument( "-f", dest='allowed_signers_file', type=Path, required=True ) verify_parser.add_argument("-I", dest='signer_identity') verify_parser.add_argument("-n", dest='namespace', required=True) verify_parser.add_argument("-s", dest='signature_file', type=Path, required=True) verify_parser.add_argument("-r", dest='revocation_file', help="not implemented") noms = parser.parse_args(args) if noms.option: print("ssh-keygen -O option is not implemented.", file=sys.stderr) return 2 if noms.subcmd == "check-novalidate": return cli_subcmd_check_novalidate(stdin, noms.signature_file, noms.namespace) if noms.subcmd == "verify": if noms.namespace != "git": msg = 'Only namespace "git" supported by verify in this implementation.' print(msg, file=sys.stderr) return 2 if noms.revocation_file: print("ssh-keygen verify -r option is not implemented.", file=sys.stderr) return 2 return cli_subcmd_verify(stdin, noms.signature_file, noms.allowed_signers_file) errmsg = "Only verify and check-novalidate subcommands are supported." print(errmsg, file=sys.stderr) return 2 if __name__ == "__main__": exit(main(sys.stdin.buffer)) python-sshsig_0.2.2.orig/contrib/sshsig_criterion.py0000644000000000000000000000376314720172244017705 0ustar00# Copyright (c) 2024 E. Castedo Ellerman # Released under the MIT License (https://spdx.org/licenses/MIT) # fmt: off from pathlib import Path from dulwich.objects import InvalidSignature, SignatureCriterion from sshsig import sshsig from sshsig.allowed_signers import load_for_git_allowed_signers_file class SshsigCheckCriterion(SignatureCriterion): """Checks signature using sshsig.""" def check(self, crypto_msg: bytes, signature: bytes, verify_time: int) -> None: try: sshsig.check_signature(crypto_msg, signature.decode()) except sshsig.InvalidSignature as ex: raise InvalidSignature from ex class SshsigVerifyCriterion(SshsigCheckCriterion): """Verifies signature using sshsig and just-a-list-for-git allowed signers file.""" def __init__(self, allowed_signers: Path): self.allowed = load_for_git_allowed_signers_file(allowed_signers) def check(self, crypto_msg: bytes, signature: bytes, verify_time: int) -> None: try: sshsig.verify(crypto_msg, signature.decode(), self.allowed) except sshsig.InvalidSignature as ex: raise InvalidSignature from ex if __name__ == "__main__": import argparse, dulwich.repo parser = argparse.ArgumentParser() parser.add_argument("git_object", default="HEAD", nargs="?") parser.add_argument( "--allow", type=Path, help="Hidos DSGL just-a-list-for-git allowed signers file" ) args = parser.parse_args() if args.allow is None: criterion = SshsigCheckCriterion() else: criterion = SshsigVerifyCriterion(args.allow) repo = dulwich.repo.Repo(".") commit = repo[args.git_object.encode()] print("commit", commit.id.decode()) try: commit.check_signature(criterion) if commit.gpgsig: print("Valid signature") except InvalidSignature: print("Invalid Signature") print("Author:", commit.author.decode()) print("\n ", commit.message.decode()) python-sshsig_0.2.2.orig/docs/howto/0000755000000000000000000000000014736553024014402 5ustar00python-sshsig_0.2.2.orig/docs/index.md0000644000000000000000000000104214746505521014670 0ustar00sshsig ====== A Python library implementing the SSHSIG [digital signature](https://en.wikipedia.org/wiki/Digital_signature) format of [`ssh-keygen`](https://en.wikipedia.org/wiki/Ssh-keygen), one of the two digital signature formats supported by [Git](https://en.wikipedia.org/wiki/Git). [Check Signature Tutorial](tutorial/check_signature.md) [Verify Signature Tutorial](tutorial/verify_signature.md) [How to Check a Git Commit Signature](howto/check_commit.md) [API Reference](reference.md) Install ------- ``` pip install sshsig ``` python-sshsig_0.2.2.orig/docs/reference.md0000644000000000000000000000126614746505521015527 0ustar00API Reference ============= Signature Verification ---------------------- ::: sshsig.sshsig.check_signature options: heading_level: 3 show_root_heading: true ::: sshsig.sshsig.verify options: heading_level: 3 show_root_heading: true SSH Public Key -------------- ::: sshsig.ssh_public_key.PublicKey options: heading_level: 3 show_root_heading: true members: - from_openssh_str Allowed Signers File Format --------------------------- ::: sshsig.allowed_signers options: heading_level: 3 members: - load_allowed_signers_file - for_git_allowed_keys - save_for_git_allowed_signers_file python-sshsig_0.2.2.orig/docs/related.md0000644000000000000000000000133214736566116015211 0ustar00Related Resources ================= SSHSIG specification -------------------- SSHSIG armored, blob, and signed data formats are documented in a file named `PROTOCOL.sshsig` which is archived from at . Related software ---------------- * [ssh-keygen](https://en.wikipedia.org/wiki/Ssh-keygen) (the functionality basis of this Python port) * [sshkey-tools](https://pypi.org/project/sshkey-tools/) (Note: "SSH key signature" in this library is at a lower level than SSHSIG signatures) History ------- * [ssh-datasign](https://github.com/grawity/ssh-datasign) (starting Python codebase) python-sshsig_0.2.2.orig/docs/tutorial/0000755000000000000000000000000014736531555015112 5ustar00python-sshsig_0.2.2.orig/docs/howto/check_commit.md0000644000000000000000000000277414746505521017363 0ustar00How to Check a Git Commit Signature =================================== Objective --------- This guide demonstrates how to check that a Git commit is signed with an SSH key and get the signing public key for further verification. Prerequisites ------------- Python packages: * sshsig * dulwich Reviewing the [Check Signature Tutorial](../tutorial/check_signature.md) may provide useful background information. Steps ----- ### 1. Switch to a Git commit with an SSH signature For this guide, you can switch to any Git commit that has an SSH signature. One of many ways to do this is by cloning the `0.2.2` release of `sshsig`: ```bash git clone https://github.com/castedo/sshsig.git -b 0.2.2 cd sshsig ``` ### 2. Get the Git commit that was signed From within a Python interpreter or script: ```python import dulwich.repo repo = dulwich.repo.Repo('.') commit = repo[b'HEAD'] ``` ### 3. Check the signature against the original message signed With `commit` defined: ```python import sshsig message = commit.raw_without_sig() signature = commit.gpgsig pub_key = sshsig.check_signature(message, signature) ``` If no exception is raised, then `pub_key` is the SSH public key used to sign the Git commit. ### 4. Do something with the signing public key ```python print(f"HEAD commit signed with public key {pub_key}") ``` Calling `check_signature` does not verify that a particular person used the public/private key pair to sign the commit. Additional steps are necessary to verify the public key is acceptable. python-sshsig_0.2.2.orig/docs/tutorial/check_signature.md0000644000000000000000000000436014740554260020566 0ustar00Check Signature Tutorial ======================== Objective --------- In this tutorial, you will check that an SSH signature is for a specific message and print the public key that was used to sign the message. Prerequisites ------------- You need to install the `sshsig` Python package. A popular way to do this is by running: ``` pip install sshsig ``` The steps in this tutorial include lines of Python code to be run within a Python interpreter. A popular way to run the Python interpreter is to run: ``` python3 ``` from the command line. Steps ----- ### 1. Get the message that was signed. ```python message = "Hello World.\n" ``` The author of this tutorial signed the above message using a [public/private key pair](https://en.wikipedia.org/wiki/Public-key_cryptography). ### 2. Get the plain-text encoded signature. ```python signature = """ -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAghB1C63jrmh3eWRXJVbrTfw9wP/ BIZf/aKPdFxBlMCq0AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQCK1pq8IPKuxTat0RJbZ0LjElEgXNowFdFKElzuq9gunl2b4DHrla0FTkUJKp1eOk1 +GOGN+EejKcsFnrfmZ1gE= -----END SSH SIGNATURE----- """ ``` The above SSH signature was generated by the `ssh-keygen` utility using the message and the author's public/private key pair. ### 3. Check the signature with the message. ```python import sshsig pub_key = sshsig.check_signature(message, signature) ``` Since `check_signature` returns without raising an exception, we know that the signature resulted from signing `message`. The public key returned is from the public/private key pair used to sign the message. ### 4. Print the public key used to sign. We can also print out a textual representation of the signing public key in OpenSSH format. ``` str(pub_key) ``` ### 5. Check with a different message. The signature is specifically for `message`. Checking a different message will fail. ```python sshsig.check_signature("A different message", signature) ``` An `sshsig.sshsig.InvalidSignature` exception is raised, indicating that the signature is not for a different message. ### Conclusion You have successfully checked that the signature is for the message in this tutorial. You have also printed the public key that was used to sign the message. python-sshsig_0.2.2.orig/docs/tutorial/verify_signature.md0000644000000000000000000000745114746505521021023 0ustar00Verify Signature Tutorial ========================= Objective --------- In this tutorial, you will verify a signature against a list of allowed signing keys. You will see the distinction between merely checking a signature, as shown in the [Check Signature Tutorial](check_signature.md), and verifying a signature, as demonstrated in this tutorial. Prerequisites ------------- You need to install the `sshsig` Python package. A popular way to do this is by running: ``` pip install sshsig ``` This tutorial involves executing lines of Python code within a Python interpreter. A popular way to run the Python interpreter is to run: ``` python3 ``` in the command line. Steps ----- ### 1. Get the message and plain-text encoded signature To verify (or check) a signature, we need the message that was signed and its corresponding signature. ```python message = """\ tree 8d602ce92adf2a598552736e97f07e5b8ab2b0a8 parent 06b3e55161aae343d23453f7443904512599a513 author Castedo Ellerman 1736017937 -0500 committer Castedo Ellerman 1736017937 -0500 add py.typed marker """ ``` This message is from a Git commit, as demonstrated in the [Check a Git Commit how-to guide](../howto/check_commit.md) using the `raw_without_sig()` function from [`dulwich`](https://pypi.org/project/dulwich/). This Git commit includes an identity in the form of a personal name and email address. ```python signature = """\ -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAghB1C63jrmh3eWRXJVbrTfw9wP/ BIZf/aKPdFxBlMCq0AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQMroG89bt34Civt2ftnKSDj/qpskASeCBHUfc8KQCUl1LAq4gAy4xQ1orAtiEaj5EM yMvtlcxbEImHo4KtbOewA= -----END SSH SIGNATURE----- """ ``` ### 2. Get and check the signing public key We get the public key from the signature and check that it was used to sign the message. ```python import sshsig pub_key = sshsig.check_signature(message, signature) print(pub_key) ``` Although `check_signature` confirms that the public key was used to sign the message, it does not validate whether the person identified by name and email address in the commit is the same person who holds the signing key and used it to sign the message. ### 3. An allowed signers file One of many possible forms of additional verification is to only accept signatures that are performed with a signing key listed in an _allow list_ of acceptable public keys. The utility [ssh-keygen](https://en.wikipedia.org/wiki/Ssh-keygen) uses an "allowed signers" file as an allow list for signature verification. Git uses this utility to verify commits with SSH signatures. The `sshsig.allowed_signers` module supports a limited sub-format of the `ssh-keygen` allowed signers file format. This sub-format only supports lines starting with `* namespaces="git"` preceding public keys in OpenSSH format. Below, we create such a file: ```python import io file = io.StringIO(f"""\ * namespaces="git" {pub_key} * namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMlKQFTcI28lrqcq8goeL2p1cxdHhm4/reBgjKDp1Ise """) ``` ### 4. Load an allow list Next, we get an allow list of public keys by loading the example file. ```python from sshsig.allowed_signers import load_allowed_signers_file, for_git_allowed_keys allow_list = for_git_allowed_keys(load_allowed_signers_file(file)) print(*allow_list, sep="\n") ``` Note that in this example, we've included the public key found in the signature. ### 5. Verify the key in the allow list Checking whether the returned public key is in the returned allow list is a trivial one-liner. ```python pub_key in allow_list ``` Alternatively, one can use the trivial helper function `verify` to call `check_signature` and verify whether the returned public key is in the allow list. ```python pub_key = sshsig.verify(message, signature, allow_list) ``` python-sshsig_0.2.2.orig/integration/jobs/0000755000000000000000000000000014731262252015565 5ustar00python-sshsig_0.2.2.orig/integration/run-on-distro0000755000000000000000000000035014731262252017274 0ustar00#!/usr/bin/bash set -o errexit -o nounset DISTRO=$1 shift if [[ ! -v REPO_NAMESPACE ]]; then REPO_NAMESPACE="registry.gitlab.com/perm.pub/dock" fi podman run --rm -it -v $PWD:/mnt -w /mnt $REPO_NAMESPACE/hidos-dev/$DISTRO "$@" python-sshsig_0.2.2.orig/integration/test-all0000755000000000000000000000015514735775263016323 0ustar00#!/usr/bin/bash set -o errexit -o nounset for JOB in integration/jobs/test-*; do echo $JOB $JOB done python-sshsig_0.2.2.orig/integration/jobs/build-docs0000755000000000000000000000010114736073003017527 0ustar00#!/usr/bin/bash integration/run-on-distro fedora just build-docs python-sshsig_0.2.2.orig/integration/jobs/test-on-centos-90000755000000000000000000000010514731262252020537 0ustar00#!/usr/bin/bash integration/run-on-distro centos-9 just test-runtime python-sshsig_0.2.2.orig/integration/jobs/test-on-debian-120000755000000000000000000000010614731262252020541 0ustar00#!/usr/bin/bash integration/run-on-distro debian-12 just test-runtime python-sshsig_0.2.2.orig/integration/jobs/test-on-fedora0000755000000000000000000000007314731262252020342 0ustar00#!/usr/bin/bash integration/run-on-distro fedora just test python-sshsig_0.2.2.orig/integration/jobs/test-on-ubuntu-240000755000000000000000000000010114731262252020637 0ustar00#!/usr/bin/bash integration/run-on-distro ubuntu-24.04 just test python-sshsig_0.2.2.orig/sshsig/__init__.py0000644000000000000000000000030114736014255015713 0ustar00from .ssh_public_key import PublicKey from .sshsig import InvalidSignature, check_signature, verify __all__ = [ 'InvalidSignature', 'PublicKey', 'check_signature', 'verify', ] python-sshsig_0.2.2.orig/sshsig/allowed_signers.py0000644000000000000000000001470214737271436017356 0ustar00# (c) 2024 E. Castedo Ellerman # Released under the MIT License (https://spdx.org/licenses/MIT) """Parsing of the ssh-keygen allowed signers format.""" from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, TextIO from .ssh_public_key import PublicKey if TYPE_CHECKING: AllowedSignerOptions = dict[str, str] @dataclass class AllowedSigner: principals: str options: AllowedSignerOptions | None key_type: str base64_key: str comment: str | None = None # "patterned after" sshd authorized keys file format @staticmethod def parse(line: str) -> AllowedSigner: """Parse a line of an ssh-keygen "allowed signers" file. Raises: ValueError: If the line is not properly formatted. NotImplementedError: If the public key algorithm is not supported. """ (principals, line) = lop_principals(line) options = None if detect_options(line): (options, line) = lop_options(line) parts = line.split(maxsplit=2) if len(parts) < 2: msg = "Not space-separated OpenSSH format public key ('{}')." raise ValueError(msg.format(line)) return AllowedSigner(principals, options, *parts) def lop_principals(line: str) -> tuple[str, str]: """Return (principals, rest_of_line).""" if line[0] == '"': (principals, _, line) = line[1:].partition('"') if not line: msg = "No matching double quote character for line ('{}')." raise ValueError(msg.format(line)) return (principals, line.lstrip()) parts = line.split(maxsplit=1) if len(parts) < 2: raise ValueError(f"Invalid line ('{line}').") return (parts[0], parts[1]) def detect_options(line: str) -> bool: start = line.split(maxsplit=1)[0] return "=" in start or "," in start or start.lower() == "cert-authority" def lop_options(line: str) -> tuple[AllowedSignerOptions, str]: """Return (options, rest_of_line). Raises: ValueError """ options: AllowedSignerOptions = dict() while line and not line[0].isspace(): line = lop_one_option(options, line) return (options, line) def lop_one_option(options: AllowedSignerOptions, line: str) -> str: if lopped := lop_flag(options, line, "cert-authority"): return lopped if lopped := lop_option(options, line, "namespaces"): return lopped if lopped := lop_option(options, line, "valid-after"): return lopped if lopped := lop_option(options, line, "valid-before"): return lopped raise ValueError(f"Invalid option ('{line}').") def lop_flag(options: AllowedSignerOptions, line: str, opt_name: str) -> str | None: i = len(opt_name) if line[:i].lower() != opt_name: return None options[opt_name] = "" if line[i : i + 1] == ",": i += 1 return line[i:] def lop_option(options: AllowedSignerOptions, line: str, opt_name: str) -> str | None: i = len(opt_name) if line[:i].lower() != opt_name: return None if opt_name in options: raise ValueError(f"Multiple '{opt_name}' clauses ('{line}')") if line[i : i + 2] != '="': raise ValueError(f"Option '{opt_name}' missing '=\"' ('{line}')") (value, _, line) = line[i + 2 :].partition('"') if not line: raise ValueError(f"No matching quote for option '{opt_name}' ('{line}')") options[opt_name] = value return line[1:] if line[0] == "," else line 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 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 def load_for_git_allowed_signers_file(file: TextIO | Path) -> Iterable[PublicKey]: return for_git_allowed_keys(load_allowed_signers_file(file)) 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())) python-sshsig_0.2.2.orig/sshsig/binary_io.py0000644000000000000000000000550214720403742016133 0ustar00# (c) 2018 Mantas Mikulėnas # (c) 2024 E. Castedo Ellerman # Released under the MIT License (https://spdx.org/licenses/MIT) # fmt: off from __future__ import annotations import io import struct from typing import Any, BinaryIO, TYPE_CHECKING, cast if TYPE_CHECKING: BytesLike = bytes | bytearray | memoryview class SshReader: """All read_ methods may raise ValueError.""" def __init__(self, ins: BinaryIO | BytesLike): if isinstance(ins, (bytes, bytearray, memoryview)): ins = io.BytesIO(ins) self.input_fh = ins @staticmethod def from_bytes(buf: BytesLike) -> SshReader: return SshReader(buf) def read(self, length: int = -1) -> bytes: buf = self.input_fh.read(length) if (not buf) and (length is not None) and (length != 0): raise ValueError("Unexpected end of input.") return buf def read_byte(self) -> int: return cast(int, self._read_and_unpack(1, "!B")) def read_uint32(self) -> int: return cast(int, self._read_and_unpack(4, "!L")) def read_bool(self) -> bool: return cast(bool, self._read_and_unpack(1, "!?")) def read_string(self) -> bytes: length = self.read_uint32() return self.read(length) def read_string_pkt(self) -> SshReader: return SshReader(self.read_string()) def read_mpint(self) -> int: buf = self.read_string() return int.from_bytes(buf, byteorder="big", signed=False) def _read_and_unpack(self, length: int, frmt: str) -> Any: try: return struct.unpack(frmt, self.read(length))[0] except struct.error as ex: raise ValueError from ex def ssh_read_string_pair(buf: BinaryIO | BytesLike) -> tuple[bytes, bytes]: pkt = SshReader(buf) return (pkt.read_string(), pkt.read_string()) class SshWriter: def __init__(self, output_fh: io.BytesIO): self.output_fh = output_fh def write(self, b: BytesLike) -> int: return self.output_fh.write(b) def flush(self) -> None: self.output_fh.flush() def write_byte(self, val: int) -> int: buf = struct.pack("!B", val) return self.write(buf) def write_uint32(self, val: int) -> int: buf = struct.pack("!L", val) return self.write(buf) def write_bool(self, val: bool) -> int: buf = struct.pack("!?", val) return self.write(buf) def write_string(self, val: BytesLike) -> int: buf = struct.pack("!L", len(val)) + val return self.write(buf) def write_mpint(self, val: int) -> int: length = val.bit_length() if length & 0xFF: length |= 0xFF length += 1 length >>= 8 buf = val.to_bytes(length, "big", signed=False) return self.write_string(buf) python-sshsig_0.2.2.orig/sshsig/py.typed0000644000000000000000000000000014736304021015266 0ustar00python-sshsig_0.2.2.orig/sshsig/ssh_public_key.py0000644000000000000000000001740314746505521017174 0ustar00# (c) 2018 Mantas Mikulėnas # (c) 2024 E. Castedo Ellerman # Released under the MIT License (https://spdx.org/licenses/MIT) # fmt: off from __future__ import annotations import binascii from abc import ABC, abstractmethod from typing import Any, ClassVar import cryptography.exceptions from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ed25519, rsa, padding from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from .binary_io import SshReader, ssh_read_string_pair from .unexceptional import cast_or_raise, unexceptional class PublicKeyAlgorithm(ABC): supported: ClassVar[dict[str, PublicKeyAlgorithm]] = dict() @staticmethod def supported_algos() -> list[PublicKeyAlgorithm]: return [Ed25519Algorithm(), RsaAlgorithm()] @property @abstractmethod def name(self) -> str: ... @abstractmethod def load_public_key(self, pkt: SshReader) -> PublicKey: ... @classmethod def from_name(cls, algo_name: str) -> PublicKeyAlgorithm: return cast_or_raise(cls.do_from_name(algo_name)) @classmethod def do_from_name(cls, algo_name: str) -> PublicKeyAlgorithm | NotImplementedError: if not cls.supported: cls.supported = {a.name: a for a in cls.supported_algos()} algo = PublicKeyAlgorithm.supported.get(algo_name) if algo is None: msg = f"Public key algorithm not supported: {algo_name}." return unexceptional(NotImplementedError(msg)) return algo 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 ############################################################################## # Ed25519 Public Key Algo # # https://tools.ietf.org/html/draft-ietf-curdle-ssh-ed25519-ed448-00#section-4 class Ed25519Algorithm(PublicKeyAlgorithm): @property def name(self) -> str: return "ssh-ed25519" def load_public_key(self, pkt: SshReader) -> PublicKey: return Ed25519PublicKey(pkt.read_string()) class Ed25519PublicKey(PublicKey): def __init__(self, raw_key: bytes): self._impl = ed25519.Ed25519PublicKey.from_public_bytes(raw_key) ## python cryptography 36.0 does not do equality properly ## hold on to raw key to perform correct equality function self._raw_key = raw_key @property def algo_name(self) -> str: return "ssh-ed25519" def do_verify(self, signature: bytes, message: bytes) -> None | Exception: sig_algo, raw_signature = ssh_read_string_pair(signature) assert sig_algo == b"ssh-ed25519" try: self._impl.verify(raw_signature, message) return None except cryptography.exceptions.InvalidSignature as ex: return ex def openssh_str(self) -> str: return self._impl.public_bytes(Encoding.OpenSSH, PublicFormat.OpenSSH).decode() def __eq__(self, other: Any) -> bool: if isinstance(other, Ed25519PublicKey): return self._raw_key == other._raw_key return False def __hash__(self) -> int: return hash(self._raw_key) ############################################################################## # RSA Public Key Algo # # https://tools.ietf.org/html/rfc4253#section-6.6 class RsaAlgorithm(PublicKeyAlgorithm): @property def name(self) -> str: return "ssh-rsa" def load_public_key(self, pkt: SshReader) -> PublicKey: e = pkt.read_mpint() n = pkt.read_mpint() return RsaPublicKey(e, n) class RsaPublicKey(PublicKey): def __init__(self, e: int, n: int): self._impl = rsa.RSAPublicNumbers(e, n).public_key() ## python cryptography 36.0 does not do equality properly ## hold on to raw numbers to perform correct equality function self._e = e self._n = n @property def algo_name(self) -> str: return "ssh-rsa" def do_verify(self, signature: bytes, message: bytes) -> None | Exception: sig_algo, raw_signature = ssh_read_string_pair(signature) if sig_algo not in [b"rsa-sha2-512", b"rsa-sha2-256"]: msg = f"Unsupported RSA signature hash algorithm: {sig_algo!r}" return unexceptional(ValueError(msg)) hash_algo = hashes.SHA512() if sig_algo == b"rsa-sha2-512" else hashes.SHA256() try: self._impl.verify(raw_signature, message, padding.PKCS1v15(), hash_algo) return None except cryptography.exceptions.InvalidSignature as ex: return ex def openssh_str(self) -> str: return self._impl.public_bytes(Encoding.OpenSSH, PublicFormat.OpenSSH).decode() def __eq__(self, other: Any) -> bool: if isinstance(other, RsaPublicKey): return self._e == other._e and self._n == other._n return False def __hash__(self) -> int: return hash((self._e, self._n)) python-sshsig_0.2.2.orig/sshsig/sshsig.py0000644000000000000000000002251214746505521015466 0ustar00# (c) 2018 Mantas Mikulėnas # (c) 2024 E. Castedo Ellerman # Released under the MIT License (https://spdx.org/licenses/MIT) from __future__ import annotations import binascii import hashlib import io from collections.abc import ByteString, Iterable from typing import BinaryIO, ClassVar from .binary_io import SshReader, SshWriter from .ssh_public_key import PublicKey from .unexceptional import cast_or_raise, unexceptional # SSHSIG armored, blob, and signed data formats are documented in a file named # `PROTOCOL.sshsig` which is archived from https://github.com/openssh/openssh-portable at # https://archive.softwareheritage.org/swh:1:cnt:78457ddfc653519c056e36c79525712dafba4e6e class InvalidSignature(Exception): pass def ssh_enarmor_sshsig(raw: bytes) -> str: lines = ["-----BEGIN SSH SIGNATURE-----"] buf = binascii.b2a_base64(raw, newline=False).decode() for i in range(0, len(buf), 76): lines.append(buf[i : i + 76]) lines += ["-----END SSH SIGNATURE-----", ""] return "\n".join(lines) def ssh_dearmor_sshsig(buf: str | bytes) -> bytes: if isinstance(buf, bytes): buf = buf.decode('ascii') acc = "" match = False # TODO: stricter format check for line in buf.splitlines(): if line == "-----BEGIN SSH SIGNATURE-----": match = True elif line == "-----END SSH SIGNATURE-----": break elif line and match: acc += line return binascii.a2b_base64(acc) class SshsigWrapper: """The inner 'to-be-signed' data.""" def __init__( self, *, namespace: bytes = b"", reserved: bytes = b"", hash_algo: bytes, hash: bytes, ) -> None: self.namespace = namespace self.reserved = reserved self.hash_algo = hash_algo self.hash = hash @staticmethod def from_bytes(buf: ByteString) -> SshsigWrapper: pkt = SshReader.from_bytes(buf) magic = pkt.read(6) if magic != b"SSHSIG": raise ValueError("magic preamble not found") return SshsigWrapper( namespace=pkt.read_string(), reserved=pkt.read_string(), hash_algo=pkt.read_string(), hash=pkt.read_string(), ) def to_bytes(self) -> bytes: pkt = SshWriter(io.BytesIO()) pkt.write(b"SSHSIG") pkt.write_string(self.namespace) pkt.write_string(self.reserved) pkt.write_string(self.hash_algo) pkt.write_string(self.hash) return pkt.output_fh.getvalue() def __bytes__(self) -> bytes: return self.to_bytes() class SshsigSignature: VERSION: ClassVar[int] = 0x1 public_key: bytes namespace: bytes hash_algo: bytes signature: bytes def __init__(self, buf: ByteString): pkt = SshReader.from_bytes(buf) if pkt.read(6) != b"SSHSIG": raise ValueError("SSH Signature magic preamble not found.") version = pkt.read_uint32() if version != SshsigSignature.VERSION: raise NotImplementedError(f"SSH Signature format version {version}.") self.public_key = pkt.read_string() self.namespace = pkt.read_string() pkt.read_string() # reserved field to be ignored self.hash_algo = pkt.read_string() self.signature = pkt.read_string() def __bytes__(self) -> bytes: pkt = SshWriter(io.BytesIO()) pkt.write(b"SSHSIG") pkt.write_uint32(SshsigSignature.VERSION) pkt.write_string(self.public_key) pkt.write_string(self.namespace) pkt.write_string(b"") # reserved field to be ignored pkt.write_string(self.hash_algo) pkt.write_string(self.signature) return pkt.output_fh.getvalue() @staticmethod def from_armored(buf: str | bytes) -> SshsigSignature: return SshsigSignature(ssh_dearmor_sshsig(buf)) def to_armored(self) -> str: return ssh_enarmor_sshsig(bytes(self)) def hash_file(msg_file: BinaryIO, hash_algo_name: str | bytes) -> bytes: return cast_or_raise(do_hash_file(msg_file, hash_algo_name)) def do_hash_file( msg_file: BinaryIO, hash_algo_name: str | bytes ) -> bytes | NotImplementedError: if isinstance(hash_algo_name, bytes): hash_algo_name = hash_algo_name.decode("ascii") hash_algo = hash_algo_name.lower() if hash_algo not in hashlib.algorithms_guaranteed: msg = "Signature hash algo '{}' not supported across platforms by Python." return unexceptional(NotImplementedError(msg.format(hash_algo))) hobj = hashlib.new(hash_algo) while data := msg_file.read(8192): hobj.update(data) return hobj.digest() def do_sshsig_verify( sshsig_outer: SshsigSignature, msg_file: BinaryIO, namespace: str, ) -> PublicKey | InvalidSignature | NotImplementedError: """Verify the SSHSIG signature is for the input message and namespace. The SSHSIG signature is verified to be for the namespace and the embedded public key signature is valid for the provided input message. Returns: If no error, the cryptographic PublicKey embedded inside the SSHSIG signature. ValueError: If the input string is not a valid format or encoding. NotImplementedError: If a signature encoding feature is not supported. """ # The intention of this implementation is to reproduce (approximately) # the behaviour of the sshsig_verify_fd function of the ssh-keygen C file: # sshsig.c # https://archive.softwareheritage.org/ # swh:1:cnt:470b286a3a982875a48a5262b7057c4710b17fed _namespace = namespace.encode("ascii") if _namespace != sshsig_outer.namespace: errmsg = "Namespace of signature {} != {}" return unexceptional( InvalidSignature(errmsg.format(sshsig_outer.namespace, _namespace)) ) msg_hash = do_hash_file(msg_file, sshsig_outer.hash_algo) if isinstance(msg_hash, NotImplementedError): return msg_hash toverify = SshsigWrapper( namespace=_namespace, hash_algo=sshsig_outer.hash_algo, hash=msg_hash ).to_bytes() pub_key = PublicKey.do_from_ssh_encoding(sshsig_outer.public_key) if isinstance(pub_key, NotImplementedError): return pub_key if isinstance(pub_key, ValueError): return unexceptional(InvalidSignature(pub_key)) if err := pub_key.do_verify(sshsig_outer.signature, toverify): return unexceptional(InvalidSignature(err)) return pub_key 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)) def do_check_signature( msg_in: str | bytes | BinaryIO, armored_signature: str | bytes, namespace: str = "git", ) -> PublicKey | InvalidSignature | NotImplementedError: """Implementation of check_signature returning unexceptional Exception objects.""" if isinstance(msg_in, str): msg_in = msg_in.encode() msg_file = io.BytesIO(msg_in) if isinstance(msg_in, bytes) else msg_in try: sshsig_outer = SshsigSignature.from_armored(armored_signature) except ValueError as ex: return unexceptional(InvalidSignature(ex)) return do_sshsig_verify(sshsig_outer, msg_file, namespace) 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) ) def do_verify( msg_in: str | bytes | BinaryIO, armored_signature: str | bytes, allowed_signers: Iterable[PublicKey], namespace: str = "git", ) -> PublicKey | InvalidSignature | NotImplementedError: """Implementation of verify returning unexceptional Exception objects.""" ret = do_check_signature(msg_in, armored_signature, namespace) if not isinstance(ret, PublicKey): return ret if all(key != ret for key in allowed_signers): msg = "Signature public key not of allowed signer." return unexceptional(InvalidSignature(msg)) return ret python-sshsig_0.2.2.orig/sshsig/unexceptional.py0000644000000000000000000000367714721665730017061 0ustar00# (c) 2024 E. Castedo Ellerman # Released under the MIT License (https://spdx.org/licenses/MIT) """Helper functions for returning and raising unexceptional exceptions. Unexceptional exceptions are Exception objects that are returned as "normal" error objects, with return type hints, for "normal" non-try-except execution flow in some, but not necessarily all, layers of a Python application. With return type hints, type checkers, such as Mypy, can identify when unexceptional exceptions are not being handled properly. See the README.md in https://gitlab.com/castedo/unexceptional for more information. """ from __future__ import annotations from types import TracebackType from typing import TYPE_CHECKING, TypeVar, cast if TYPE_CHECKING: ExceptionT = TypeVar('ExceptionT', bound='Exception') NonExceptionT = TypeVar('NonExceptionT') def cast_or_raise(ret: NonExceptionT | Exception) -> NonExceptionT: """Cast a value to a non-Exception type or raise an Exception. Example: def do_fancy_math(x: float) -> float | ValueError: ... try: ... y = cast_or_raise(do_fancy_math(x)) # mypy knows y is a float here ... except ValueEror: ... """ if isinstance(ret, Exception): raise ret return ret def unexceptional(ex: ExceptionT, cause: Exception | None = None) -> ExceptionT: """Return an Exception object with a stack trace and optional cause. Example: if bad_case: return unexceptional(ValueError("Bad case")) """ try: raise ex except Exception as ret: ret.__cause__ = cause if not ret.__traceback__: return cast('ExceptionT', ret) frame = ret.__traceback__.tb_frame frame = frame.f_back or frame tb = TracebackType(None, frame, frame.f_lasti, frame.f_lineno) return cast('ExceptionT', ret.with_traceback(tb)) python-sshsig_0.2.2.orig/testdata/hola.txt0000644000000000000000000000001314716764203015602 0ustar00Hola Mundo python-sshsig_0.2.2.orig/testdata/hola.txt.sig0000644000000000000000000000227314716764203016375 0ustar00-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAJXDk57H4TBAsZIkZpI7WS kyBohbjgBnHNGzOA/pbCwXcYVJQmqgrBSdLv9yWD1JeL51c7ZTAuzZ5IFVe03pg/MaGKdX 5uD7qTfg268lDVrekMUQTvEnDfLRpj0nWT6QMQu2Ux69EyXtMo6dEupFe6gIJgmh/fzJxD g8tr+YeYk67gxgI6zTNqS27UJGvOSL+aHeGfM4/XvmHnxGD0aP89ab1Aii6aS1e+ReK1Xd oQfuyaEy9D3T80ggLYwzjpLEStaJ9HYDsDtPPugbeZJPWrdMvt9NDuw+uRCnrgb4//jonw VxI1fG5pgjy7vD5ZD9SRU7tepebI0tnvaitDZT465e9Bcc5R5ReSU0KGCgCH6NTw+nu8VO d4IM/ncBym5llF2yqT2Tq26gpIDPqbATpoHrnsYCtyTiWKggWk/2Es+ibJjoUKNedv1bBP aorffM6nqWAC3eGzXDgvIyKeiLSzNG234d+mkzqLB4hruco8L1FcYW7OW3sqWfI6aoomip 5QAAAANnaXQAAAAAAAAABnNoYTUxMgAAAZQAAAAMcnNhLXNoYTItNTEyAAABgIY1pbSqXK FJ8/9GaFAuCkcSJ7YwW1MuidrWXqsDH4IE4j10u3LnQhGd8qKDH9dm93sP15IRuRnbJXg3 qVVFrnxS//EO8BmXHDekLo8yp11CqzYfRrboIvzMRufLdZ8Kt7d+p+jvJuDqN9WHDSyifI D8o7X4tenND+QtELzi2aNrqaAtJYlNBQzxLrUqXSMHdDTDqwkuQaBWCHSmykxi84F9qb6q K5ogklBOJKekyMFXxgPwu+uBXMkCo18QIlUS+J76H4hmJVsIrHOZZntfLOa1dDU8lRZmJP wzCztwzaShx4OTASnJ/wKXtqXFvbz+asPnCqngoYO1aWDg5OzvEFG3DMUpTKRthMuYUWAB TuWmFFLbjPQf1f9r1+/bwquZxapoKNVJfg7IYTfKFI9zKve3z3NLZ52+7eoqYaZsuzjN/a 4r4pfG9OMr60XApWaJkTO0K0RjrCKy9/bsXz5pJTCM5Tm4E3xLQW70GbTVfceDX713lnvx 8ZPva0b19v/BgA== -----END SSH SIGNATURE----- python-sshsig_0.2.2.orig/testdata/lost-key.pub0000644000000000000000000000013514716662702016403 0ustar00ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJY08ynqE/VoH690nSN+MUxMzAbfNcMdUQr+5ltIskMt nobody@lost python-sshsig_0.2.2.orig/testdata/only_lost_allowed_signer0000644000000000000000000000016014716662702021145 0ustar00* namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJY08ynqE/VoH690nSN+MUxMzAbfNcMdUQr+5ltIskMt nobody@lost python-sshsig_0.2.2.orig/testdata/rsa_key0000644000000000000000000000505214716662702015507 0ustar00-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn NhAAAAAwEAAQAAAYEAlcOTnsfhMECxkiRmkjtZKTIGiFuOAGcc0bM4D+lsLBdxhUlCaqCs FJ0u/3JYPUl4vnVztlMC7NnkgVV7TemD8xoYp1fm4PupN+DbryUNWt6QxRBO8ScN8tGmPS dZPpAxC7ZTHr0TJe0yjp0S6kV7qAgmCaH9/MnEODy2v5h5iTruDGAjrNM2pLbtQka85Iv5 od4Z8zj9e+YefEYPRo/z1pvUCKLppLV75F4rVd2hB+7JoTL0PdPzSCAtjDOOksRK1on0dg OwO08+6Bt5kk9at0y+300O7D65EKeuBvj/+OifBXEjV8bmmCPLu8PlkP1JFTu16l5sjS2e 9qK0NlPjrl70FxzlHlF5JTQoYKAIfo1PD6e7xU53ggz+dwHKbmWUXbKpPZOrbqCkgM+psB OmgeuexgK3JOJYqCBaT/YSz6JsmOhQo152/VsE9qit98zqepYALd4bNcOC8jIp6ItLM0bb fh36aTOosHiGu5yjwvUVxhbs5beypZ8jpqiiaKnlAAAFiEnHINVJxyDVAAAAB3NzaC1yc2 EAAAGBAJXDk57H4TBAsZIkZpI7WSkyBohbjgBnHNGzOA/pbCwXcYVJQmqgrBSdLv9yWD1J eL51c7ZTAuzZ5IFVe03pg/MaGKdX5uD7qTfg268lDVrekMUQTvEnDfLRpj0nWT6QMQu2Ux 69EyXtMo6dEupFe6gIJgmh/fzJxDg8tr+YeYk67gxgI6zTNqS27UJGvOSL+aHeGfM4/Xvm HnxGD0aP89ab1Aii6aS1e+ReK1XdoQfuyaEy9D3T80ggLYwzjpLEStaJ9HYDsDtPPugbeZ JPWrdMvt9NDuw+uRCnrgb4//jonwVxI1fG5pgjy7vD5ZD9SRU7tepebI0tnvaitDZT465e 9Bcc5R5ReSU0KGCgCH6NTw+nu8VOd4IM/ncBym5llF2yqT2Tq26gpIDPqbATpoHrnsYCty TiWKggWk/2Es+ibJjoUKNedv1bBPaorffM6nqWAC3eGzXDgvIyKeiLSzNG234d+mkzqLB4 hruco8L1FcYW7OW3sqWfI6aoomip5QAAAAMBAAEAAAGAGFvgsVU6Y9JJ74a7JjK//ErWNC yMCrW6wsLfLftd7Ef931t8kYJDqvCr+cF757ABEx1gbjnPQn3oRD8GmLQQEaTfeKx78YEN U3jf0nt40hxcOKk/5cpf4uxFmTHCusHSMGD94QSIzkTn0blrn64ggEKfCTcPbCPXJfx+Du i6b98lY97pPo12GFFmgr6lr5iyyCirWNueox3ZPIZaqrP0SaY71RHGZdNc7NaCPuuW6wbk RbNXdYz5R3RwZBroqCeD8uzBPLOCxJOcnp6EKuzYXyNeAu22tXPrLC+/QbiAGqwX5Up8fh h+74hxEhT680fGq15IrI0NG7iYPyVlUswSHtoLQg2408EWd8+kgN9x4OMJUiGjhvKd3wO4 90sTD0LGJ5Pv9VED9Fv8QTYdKHTOW7ZK9X6xqJhFgIEu9O9YIIdW/879m13Y1i3keWJuqT A1KwQUwy/HGfaLPs1B70pkSegtGi1iFoOcOUemoBxqvYkErI+3fr9CXNt2Aut/B+l7AAAA wDvtTpVBiSyLGkpPbWbneyFtL8WdbCRUHWdHc6XkpC8hc5DW3R9EPfP16crXHB/B+7o5pr DQj7eCcfWcO4Yr0qrM0LbpWrDvcnTbG+1FFzRcb75Wk/ZTuD86zKX4PtuMTOyCOj5VNwwT u4K4VHNIa7Nc+8o27EUm59710iqLDOA+RPrFUryrM/g+8v5b7JqhK5PEuaKFG2Xhzt73oX 7B5gMTmV5l1OZIjReHhErLIsOwYXoyeLPQoLzBca5oqBbM3wAAAMEAyNvZWr/dXByZhC/1 TGyAimuO1/oX9BTk+Y8A8dxN6K3D4KR12/PMBlMYtDHGh4pwknnlZDBeBcUka83TNo+KAj 7RObPxnkJq6VrNH/gR4N8Sr1BHJ9qfEkXCryhYRL1nN1E/Pu2uOmp1y9qd8WauzbpBVCuI t2/hwtVb6jngXVWZUieIkguVlAslW3GP877GZM9hE04gnE8KCcJN8JyZ+lJH/IHItibbBE BjH5x57tSKW48YV6LN3FpwY/Bye1xXAAAAwQC+4NYJgtSu2B/hCnFNOQbq8xmdTKg9kSST 6QFkUrlcM0DXxUYdxAVZw7dT8N025MqgQAGd+Xg3qFzEvjhs2c+Ijb5CSkwk9KT/XJvtej sHqnXXyW6rQiLrZikn4H+LLa8g+YT1cFewEHZdxRCVF7MjKvLXl98C4JBn+/tI0xIo62pJ SXiseymmuk8tul1FfYf/svUlqz4H7PrycIm8b8PdJHz4fYf612v9X3GbhPCu8aXnbdsBE6 d7csQuz1cHBiMAAAARY2FzdGVkb0Bvbnl4LmhvbWUBAg== -----END OPENSSH PRIVATE KEY----- python-sshsig_0.2.2.orig/testdata/rsa_key.pub0000644000000000000000000000106214716662702016271 0ustar00ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCVw5Oex+EwQLGSJGaSO1kpMgaIW44AZxzRszgP6WwsF3GFSUJqoKwUnS7/clg9SXi+dXO2UwLs2eSBVXtN6YPzGhinV+bg+6k34NuvJQ1a3pDFEE7xJw3y0aY9J1k+kDELtlMevRMl7TKOnRLqRXuoCCYJof38ycQ4PLa/mHmJOu4MYCOs0zaktu1CRrzki/mh3hnzOP175h58Rg9Gj/PWm9QIoumktXvkXitV3aEH7smhMvQ90/NIIC2MM46SxErWifR2A7A7Tz7oG3mST1q3TL7fTQ7sPrkQp64G+P/46J8FcSNXxuaYI8u7w+WQ/UkVO7XqXmyNLZ72orQ2U+OuXvQXHOUeUXklNChgoAh+jU8Pp7vFTneCDP53AcpuZZRdsqk9k6tuoKSAz6mwE6aB657GArck4lioIFpP9hLPomyY6FCjXnb9WwT2qK33zOp6lgAt3hs1w4LyMinoi0szRtt+HfppM6iweIa7nKPC9RXGFuzlt7KlnyOmqKJoqeU= foo@b.ar python-sshsig_0.2.2.orig/testdata/sshsig/0000755000000000000000000000000014716662702015425 5ustar00python-sshsig_0.2.2.orig/testdata/sshsig/0/0000755000000000000000000000000014716662702015564 5ustar00python-sshsig_0.2.2.orig/testdata/sshsig/0/allowed_signers0000644000000000000000000000014414716662702020667 0ustar00* namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIQdQut465od3lkVyVW6038PcD/wSGX/2ij3RcQZTAqt python-sshsig_0.2.2.orig/testdata/sshsig/0/message0000644000000000000000000000042314716662702017132 0ustar00tree fd7187a49a7b26eb36d782d34c672245b94b2e30 parent b0162c139e4ea2e4783402de26617c912eef1e19 author Castedo Ellerman 1729263976 -0400 committer Castedo Ellerman 1729263976 -0400 use vite-plugin-static-copy in doc example of Vite python-sshsig_0.2.2.orig/testdata/sshsig/0/message.sig0000644000000000000000000000044614716662702017720 0ustar00-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAghB1C63jrmh3eWRXJVbrTfw9wP/ BIZf/aKPdFxBlMCq0AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQJoJUglNVFSaWhnbCl4WImCRLUEo6ymS9WBVOqpoH4kJVgIAMmCIAq9yDOL4fGYmJ0 GsF7btQ1wFf6sbMzq9nwA= -----END SSH SIGNATURE----- python-sshsig_0.2.2.orig/tests/__init__.py0000644000000000000000000000000014716662702015555 0ustar00python-sshsig_0.2.2.orig/tests/reject-with-real-ssh-keygen.sh0000755000000000000000000000112614716662702021236 0ustar00#/usr/bin/env bash set -o errexit -o nounset cd $(dirname "$0") for C in ../testdata/sshsig/*; do echo Checking $C ! ssh-keygen -Y verify \ -f "$C/allowed_signers" \ -I "nobody@example.com" \ -n git \ -s "$C/message.sig" \ < "$C/message" ! ssh-keygen -Y verify \ -f "$C/allowed_signers" \ -I $(cat $C/signer_identity) \ -n notgit \ -s "$C/message.sig" \ < "$C/message" ! ssh-keygen -Y verify \ -f "$C/allowed_signers" \ -I $(cat $C/signer_identity) \ -n git \ -s "$C/message.sig" \ < <(echo bad message) done echo All done. python-sshsig_0.2.2.orig/tests/test_allowed_signers.py0000644000000000000000000001077414737300106020247 0ustar00# (c) 2024 E. Castedo Ellerman # Released under the MIT License (https://spdx.org/licenses/MIT) # fmt: off from io import StringIO from pathlib import Path from unittest import TestCase from sshsig import PublicKey from sshsig.allowed_signers import AllowedSigner, load_allowed_signers_file TESTDATA_DIR = Path(__file__).parent.parent / "testdata" SSHSIG_CASES = list((TESTDATA_DIR / "sshsig").iterdir()) key0 = [ "ssh-ed25519", "AAAAC3NzaC1lZDI1NTE5AAAAIJY08ynqE/VoH690nSN+MUxMzAbfNcMdUQr+5ltIskMt", ] key1 = [ "ssh-ed25519", "AAAAC3NzaC1lZDI1NTE5AAAAIIQdQut465od3lkVyVW6038PcD/wSGX/2ij3RcQZTAqt", ] with open(TESTDATA_DIR / "rsa_key.pub") as f: rsa_key = f.read().strip().split() openssh_keys = [key0, key1, rsa_key] class PublicKeyParseTests(TestCase): def test_bad_base64(self): with self.assertRaises(ValueError): PublicKey.from_openssh_str("ssh-rsa AAAAB") with self.assertRaises(ValueError): PublicKey.from_openssh_str("ssh-rsa AAAA") def test_parse(self): for key in openssh_keys: with self.subTest(key=key): PublicKey.from_openssh_str(" ".join(key)) def test_roundtrip(self): for key in openssh_keys: with self.subTest(key=key): key_obj = PublicKey.from_openssh_str(" ".join(key)) s = key_obj.openssh_str() self.assertEqual(key_obj, PublicKey.from_openssh_str(s)) class FileCaseParseTests(TestCase): def test_case_0(self): load_allowed_signers_file(SSHSIG_CASES[0] / "allowed_signers") # Many test cases are from the ssh-keygen test code: # https://archive.softwareheritage.org/ # swh:1:cnt:dae03706d8f0cb09fa8f8cd28f86d06c4693f0c9 class ParseTests(TestCase): def test_man_page_example(self): # Example "ALLOWED SIGNERS" file from ssh-keygen man page. Man page source: # https://archive.softwareheritage.org/ # swh:1:cnt:06f0555a4ec01caf8daed84b8409dd8cb3278740 text = StringIO( """\ # Comments allowed at start of line user1@example.com,user2@example.com {} {} {} # A certificate authority, trusted for all principals in a domain. *@example.com cert-authority {} {} # A key that is accepted only for file signing. user2@example.com namespaces="file" {} {} """.format(*rsa_key, *key0, *key1) ) expect = [ AllowedSigner("user1@example.com,user2@example.com", None, *rsa_key), AllowedSigner("*@example.com", {'cert-authority': ''}, *key0), AllowedSigner("user2@example.com", {'namespaces': "file"}, *key1), ] got = load_allowed_signers_file(text) self.assertEqual(expect, got) def test_no_options_and_quotes(self): text = StringIO( """\ foo@example.com {} {} "foo@example.com" {} {} """.format(*key0, *key0) ) same = AllowedSigner("foo@example.com", None, *key0) expect = [same, same] self.assertEqual(expect, load_allowed_signers_file(text)) def test_space_in_quotes(self): text = StringIO( """\ "ssh-keygen parses this" {} {} """.format(*key0) ) expect = [ AllowedSigner("ssh-keygen parses this", None, *key0), ] self.assertEqual(expect, load_allowed_signers_file(text)) def test_with_comments(self): text = StringIO( """\ foo@bar {} {} even without options ssh-keygen will ignore the end """.format(*key1) ) expect = [ AllowedSigner( "foo@bar", None, *key1, "even without options ssh-keygen will ignore the end", ) ] self.assertEqual(expect, load_allowed_signers_file(text)) def test_two_namespaces(self): text = StringIO( """\ foo@b.ar namespaces="git,got" {} {} """.format(*key1) ) expect = [ AllowedSigner( "foo@b.ar", {'namespaces': "git,got"}, *key1, ), ] self.assertEqual(expect, load_allowed_signers_file(text)) def test_dates(self): text = StringIO( """\ foo@b.ar valid-after="19801201",valid-before="20010201" {} {} """.format(*key0) ) expect = [ AllowedSigner( "foo@b.ar", {"valid-after": "19801201", "valid-before": "20010201"}, *key0, ), ] self.assertEqual(expect, load_allowed_signers_file(text)) python-sshsig_0.2.2.orig/tests/test_cli.py0000644000000000000000000000337414736014255015641 0ustar00import io import subprocess import tempfile from pathlib import Path from compat import ssh_keygen from .test_sshsig import SshKeygenCheckNoValidate from unittest import TestCase TESTDATA_DIR = Path(__file__).parent.parent / "testdata" SSHSIG_CASES = list((TESTDATA_DIR / "sshsig").iterdir()) class CompatTestSshKeygenCheckNoValidate(SshKeygenCheckNoValidate): def good_check_novalidate( self, message: str, signature: str, namespace: str = "git" ) -> bool: cmdline = ["ssh-keygen", "-Y", "check-novalidate", "-n", namespace] with tempfile.NamedTemporaryFile() as sig_file: sig_file.write(signature.encode()) sig_file.flush() cmdline += ["-s", sig_file.name] result = subprocess.run(cmdline, input=message.encode()) return result.returncode == 0 class SimCliCheckNoValidate(SshKeygenCheckNoValidate): def good_check_novalidate( self, message: str, signature: str, namespace: str = "git" ) -> bool: args = ["-Y", "check-novalidate", "-n", namespace] with tempfile.NamedTemporaryFile() as sig_file: sig_file.write(signature.encode()) sig_file.flush() args += ["-s", sig_file.name] msg_in = io.BytesIO(message.encode()) return 0 == ssh_keygen.main(msg_in, args) class CLITests(TestCase): def verify(self, case): args = ["-Y", "verify"] args += ["-f", str(case / "allowed_signers")] args += ["-n", "git"] args += ["-s", str(case / "message.sig")] args += ["-I", '*'] with open(case / "message", "rb") as msgin: self.assertEqual(0, ssh_keygen.main(msgin, args)) def test_verify_case_0(self): self.verify(SSHSIG_CASES[0]) python-sshsig_0.2.2.orig/tests/test_sshsig.py0000644000000000000000000001343414737300106016362 0ustar00# (c) 2024 E. Castedo Ellerman # Released under the MIT License (https://spdx.org/licenses/MIT) # fmt: off from pathlib import Path from unittest import TestCase from sshsig import sshsig from sshsig.allowed_signers import load_for_git_allowed_signers_file from compat import ssh_keygen TESTDATA_DIR = Path(__file__).parent.parent / "testdata" SSHSIG_CASES = list((TESTDATA_DIR / "sshsig").iterdir()) msg_sig_pair_of_commit = ( """\ tree fd7187a49a7b26eb36d782d34c672245b94b2e30 parent b0162c139e4ea2e4783402de26617c912eef1e19 author Castedo Ellerman 1729263976 -0400 committer Castedo Ellerman 1729263976 -0400 use vite-plugin-static-copy in doc example of Vite """, """\ -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAghB1C63jrmh3eWRXJVbrTfw9wP/ BIZf/aKPdFxBlMCq0AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQJoJUglNVFSaWhnbCl4WImCRLUEo6ymS9WBVOqpoH4kJVgIAMmCIAq9yDOL4fGYmJ0 GsF7btQ1wFf6sbMzq9nwA= -----END SSH SIGNATURE----- """, ) ### The following test case produced by ### echo Hola Mundo > hola.txt ### ssh-keygen -Y sign -f test_sign_key -n git hola.txt ### using test_sign_key in ../testdata msg_sig_pair_hola_mundo = ( """\ Hola Mundo """, """\ -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZz+kZMT7JBP0t1l1HQ0K8CduhZ XTBP/l3sXkZMqTtAkAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQDFSdQINV271MZ5VwFecGD8oJRob5Nb04r06oVVVCflwgbDLcezjmHJQo41/H3/HXj pQWO8AJXrx7gcPAcCFGQ0= -----END SSH SIGNATURE----- """, ) ### The following test case produced by ### cd ../testdata ### echo Hola Mundo > hola.txt ### chmod 600 rsa_key ### ssh-keygen -Y sign -f rsa_key -n git hola.txt msg_sig_pair_rsa = ( """\ Hola Mundo """, """\ -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAJXDk57H4TBAsZIkZpI7WS kyBohbjgBnHNGzOA/pbCwXcYVJQmqgrBSdLv9yWD1JeL51c7ZTAuzZ5IFVe03pg/MaGKdX 5uD7qTfg268lDVrekMUQTvEnDfLRpj0nWT6QMQu2Ux69EyXtMo6dEupFe6gIJgmh/fzJxD g8tr+YeYk67gxgI6zTNqS27UJGvOSL+aHeGfM4/XvmHnxGD0aP89ab1Aii6aS1e+ReK1Xd oQfuyaEy9D3T80ggLYwzjpLEStaJ9HYDsDtPPugbeZJPWrdMvt9NDuw+uRCnrgb4//jonw VxI1fG5pgjy7vD5ZD9SRU7tepebI0tnvaitDZT465e9Bcc5R5ReSU0KGCgCH6NTw+nu8VO d4IM/ncBym5llF2yqT2Tq26gpIDPqbATpoHrnsYCtyTiWKggWk/2Es+ibJjoUKNedv1bBP aorffM6nqWAC3eGzXDgvIyKeiLSzNG234d+mkzqLB4hruco8L1FcYW7OW3sqWfI6aoomip 5QAAAANnaXQAAAAAAAAABnNoYTUxMgAAAZQAAAAMcnNhLXNoYTItNTEyAAABgIY1pbSqXK FJ8/9GaFAuCkcSJ7YwW1MuidrWXqsDH4IE4j10u3LnQhGd8qKDH9dm93sP15IRuRnbJXg3 qVVFrnxS//EO8BmXHDekLo8yp11CqzYfRrboIvzMRufLdZ8Kt7d+p+jvJuDqN9WHDSyifI D8o7X4tenND+QtELzi2aNrqaAtJYlNBQzxLrUqXSMHdDTDqwkuQaBWCHSmykxi84F9qb6q K5ogklBOJKekyMFXxgPwu+uBXMkCo18QIlUS+J76H4hmJVsIrHOZZntfLOa1dDU8lRZmJP wzCztwzaShx4OTASnJ/wKXtqXFvbz+asPnCqngoYO1aWDg5OzvEFG3DMUpTKRthMuYUWAB TuWmFFLbjPQf1f9r1+/bwquZxapoKNVJfg7IYTfKFI9zKve3z3NLZ52+7eoqYaZsuzjN/a 4r4pfG9OMr60XApWaJkTO0K0RjrCKy9/bsXz5pJTCM5Tm4E3xLQW70GbTVfceDX713lnvx 8ZPva0b19v/BgA== -----END SSH SIGNATURE----- """, ) msg_sig_pair_cases = [ msg_sig_pair_of_commit, msg_sig_pair_hola_mundo, msg_sig_pair_rsa, ] crazy_ascii = "Nobody expects the Spanish ..." crazy_unicode = "Nobody expects 🥘💃🐂 ..." def good_check_novalidate(message: str, signature: str, namespace: str = "git") -> bool: try: ssh_keygen.check_signature(message, signature, namespace) return True except sshsig.InvalidSignature: return False class SshKeygenCheckNoValidate(TestCase): def test_good_msg_sig_pairs(self): for case in msg_sig_pair_cases: with self.subTest(case=case): (msg, sig) = case self.assertTrue(good_check_novalidate(msg, sig)) def test_hola_mundo(self): (msg, sig) = msg_sig_pair_hola_mundo self.assertTrue(good_check_novalidate(msg, sig)) def test_reject_mixed_msg_sig_pairs(self): (msg1, sig1) = msg_sig_pair_of_commit (msg2, sig2) = msg_sig_pair_hola_mundo self.assertFalse(good_check_novalidate(msg1, sig2)) self.assertFalse(good_check_novalidate(msg2, sig1)) def test_reject_subcmd_check_novalidate(self): (msg, sig) = msg_sig_pair_of_commit self.assertFalse(good_check_novalidate(msg, crazy_ascii)) self.assertFalse(good_check_novalidate(msg, crazy_unicode)) self.assertFalse(good_check_novalidate(crazy_ascii, sig)) self.assertFalse(good_check_novalidate(crazy_unicode, sig)) # the signature was signed with namespace "git" self.assertFalse(good_check_novalidate(msg, sig, "not-git")) def good_verify(message: str, signers, signature: str) -> bool: try: ssh_keygen.verify(message, signature, signers) return True except sshsig.InvalidSignature: return False class VerifyTests(TestCase): def verify(self, case): with open(case / "message", "rb") as f: msg = f.read() with open(case / "message.sig") as f: armored = f.read() signers = load_for_git_allowed_signers_file(case / "allowed_signers") self.assertTrue(good_verify(msg, signers, armored)) bad = b"Corrupt" + msg self.assertFalse(good_verify(bad, signers, armored)) nobody = load_for_git_allowed_signers_file( TESTDATA_DIR / "only_lost_allowed_signer" ) self.assertFalse(good_verify(msg, nobody, armored)) def test_case_0(self): self.verify(SSHSIG_CASES[0]) class ParseSignature(TestCase): def test_ascii_armor(self): for case in msg_sig_pair_cases: armored = case[1] with self.subTest(armored=armored): buf = sshsig.ssh_dearmor_sshsig(armored) sig = sshsig.SshsigSignature(buf) buf2 = sshsig.ssh_dearmor_sshsig(sig.to_armored()) self.assertEqual(buf, buf2) python-sshsig_0.2.2.orig/tests/verify-with-real-ssh-keygen.sh0000755000000000000000000000043714716662702021272 0ustar00#/usr/bin/env bash set -o errexit -o nounset cd $(dirname "$0") for C in ../testdata/sshsig/*; do echo Checking $C ssh-keygen -Y verify \ -f "$C/allowed_signers" \ -I $(cat $C/signer_identity) \ -n git \ -s "$C/message.sig" \ < "$C/message" done echo All done.