Skip to content

Commit

Permalink
Add key revocation functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Dax Harris committed Mar 29, 2024
1 parent 9840106 commit 4d9e7d2
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 7 deletions.
102 changes: 102 additions & 0 deletions gpyg/operators/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from contextlib import contextmanager
from datetime import date, datetime, timedelta
from enum import StrEnum
from tempfile import NamedTemporaryFile
from typing import Any, Literal

from pydantic import Field, PrivateAttr, computed_field
Expand Down Expand Up @@ -615,6 +616,107 @@ def edit(self, user: str | None = None) -> Generator["KeyEditor", Any, Any]:
editor = KeyEditor(self, user if user else self.fingerprint, interactive)
yield editor

def generate_revocation(
self,
passphrase: str | None = None,
reason: KeyRevocationReason = KeyRevocationReason.KEY_COMPROMISED,
description: str = "",
) -> str:
"""Generate a revocation certificate for the specified key.
Args:
passphrase (str | None, optional): The key's passphrase. Defaults to None.
reason (KeyRevocationReason, optional): A revocation reason, if desired. Defaults to KeyRevocationReason.KEY_COMPROMISED.
description (str, optional): A revocation description, if required. Defaults to "".
Raises:
ValueError: If the specified password is invalid
ExecutionError: If another error occurs
Returns:
str: The ASCII-armored representation of the revocation certificate.
"""
with StatusInteractive(
self.session,
f"gpg --status-fd 1 --pinentry-mode loopback --command-fd 0 --no-tty --gen-revoke {self.fingerprint}",
) as inter:
result = []
reading_result = False

for line in inter.readlines():
if line:
if "make_keysig_packet failed" in line.content:
raise ValueError("Bad password.")
if "-BEGIN PGP PUBLIC KEY BLOCK-" in line.content:
reading_result = True
result.append(line.content)

elif "-END PGP PUBLIC KEY BLOCK-" in line.content:
reading_result = False
result.append(line.content)
break

elif reading_result:
result.append(line.content)
elif line.is_status:
if (
line.code == StatusCodes.GET_BOOL
and line.arguments[0] == "gen_revoke.okay"
):
inter.writelines("y")

if (
line.code == StatusCodes.GET_LINE
and line.arguments[0] == "ask_revocation_reason.code"
):
inter.writelines(str(reason))

if (
line.code == StatusCodes.GET_LINE
and line.arguments[0] == "ask_revocation_reason.text"
):
inter.writelines(description)

if (
line.code == StatusCodes.GET_BOOL
and line.arguments[0] == "ask_revocation_reason.okay"
):
inter.writelines("y")

if (
line.code == StatusCodes.GET_HIDDEN
and line.arguments[0] == "passphrase.enter"
):
inter.writelines(passphrase if passphrase else "")
elif inter.code:
inter.seek()
raise ExecutionError("Failed to execute:\n" + inter.read().decode())

return "\n".join(result)

def revoke(
self,
passphrase: str | None = None,
reason: KeyRevocationReason = KeyRevocationReason.KEY_COMPROMISED,
description: str = "",
):
"""Convenience function to generate a revocation certificate and automatically apply it.
Args:
passphrase (str | None, optional): Key passphrase. Defaults to None.
reason (KeyRevocationReason, optional): Revocation reason, if required. Defaults to KeyRevocationReason.KEY_COMPROMISED.
description (str, optional): Revocation description, if required. Defaults to "".
"""
cert = self.generate_revocation(
passphrase=passphrase, reason=reason, description=description
)
with NamedTemporaryFile() as revocfile:
revocfile.write(cert.encode())
revocfile.seek(0)
self.operator.import_key(revocfile.name)

self.reload()


class KeyEditor:

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "gpyg"
version = "0.1.2"
version = "0.2.0"
dependencies = [
"typing-extensions",
"pydantic"
Expand Down
9 changes: 3 additions & 6 deletions scratchpad.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@
os.makedirs("./tmp", exist_ok=True)
with TemporaryDirectory(dir="tmp", delete=False) as tmpdir:
gpg = GPG(homedir=tmpdir, kill_existing_agent=True)
with gpg.smart_card() as card:
print(card.active)
if card.active:
card.reset()
card.set_usage_info("decrypt", True, "12345678")
# print(card.active.model_dump_json(indent=4))
key = gpg.keys.generate_key("Test Key", passphrase="beans")
print(key.revoke(passphrase="beans"))
print(gpg.keys.list_keys())

"""gpg = GPG(homedir=tmpdir, kill_existing_agent=True)
key = gpg.keys.generate_key("Bongus", passphrase="test")
Expand Down
9 changes: 9 additions & 0 deletions tests/test_op_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,12 @@ def test_import_key(environment):
assert len(environment.keys.list_keys()) == 3
environment.keys.import_key(os.path.join(environment.homedir, "export.asc"))
assert len(environment.keys.list_keys()) == 4


def test_key_revocation(environment):
keys = environment.keys.list_keys()
assert len(keys) == 4
assert keys[0].validity == FieldValidity.ULTIMATELY_VALID
keys[0].revoke(passphrase="test-psk-0")
keys = environment.keys.list_keys()
assert keys[0].validity == FieldValidity.REVOKED

0 comments on commit 4d9e7d2

Please sign in to comment.