Coverage for silkaj/wot/revocation.py: 100%
112 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-20 12:29 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-20 12:29 +0000
1# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
2#
3# Silkaj is free software: you can redistribute it and/or modify
4# it under the terms of the GNU Affero General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# Silkaj is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Affero General Public License for more details.
12#
13# You should have received a copy of the GNU Affero General Public License
14# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
16import os
17import sys
18from pathlib import Path
20import rich_click as click
21from duniterpy.api import bma
22from duniterpy.documents.block_id import BlockID
23from duniterpy.documents.document import MalformedDocumentError
24from duniterpy.documents.identity import Identity
25from duniterpy.documents.revocation import Revocation
26from duniterpy.key.verifying_key import VerifyingKey
28from silkaj import auth, network, tui
29from silkaj.blockchain.tools import get_currency
30from silkaj.constants import FAILURE_EXIT_STATUS, SUCCESS_EXIT_STATUS
31from silkaj.public_key import gen_pubkey_checksum
32from silkaj.wot import idty_tools
33from silkaj.wot import tools as w_tools
36@click.command("create", help="Create and save a revocation document")
37@click.argument(
38 "file",
39 type=click.Path(dir_okay=False, writable=True, path_type=Path),
40)
41@click.pass_context
42def create(ctx: click.Context, file: Path) -> None:
43 currency = get_currency()
45 key = auth.auth_method()
46 gen_pubkey_checksum(key.pubkey)
47 _id = (w_tools.choose_identity(key.pubkey))[0]
48 rev_doc = create_revocation_doc(_id, key.pubkey, currency)
49 rev_doc.sign(key)
51 idty_table = idty_tools.display_identity(rev_doc.identity)
52 click.echo(idty_table.draw())
54 confirm_message = "Do you want to save the revocation document for this identity?"
55 if click.confirm(confirm_message):
56 save_doc(file, rev_doc.signed_raw(), key.pubkey)
57 else:
58 click.echo("Ok, goodbye!")
61@click.command(
62 "revoke",
63 help="Create and publish revocation document. Will revoke the identity immediately.",
64)
65@click.pass_context
66def revoke_now(ctx: click.Context) -> None:
67 currency = get_currency()
69 warn_before_dry_run_or_display(ctx)
71 key = auth.auth_method()
72 gen_pubkey_checksum(key.pubkey)
73 _id = (w_tools.choose_identity(key.pubkey))[0]
74 rev_doc = create_revocation_doc(_id, key.pubkey, currency)
75 rev_doc.sign(key)
77 if ctx.obj["DRY_RUN"]:
78 click.echo(rev_doc.signed_raw())
79 return
81 idty_table = idty_tools.display_identity(rev_doc.identity)
82 click.echo(idty_table.draw())
83 if ctx.obj["DISPLAY_DOCUMENT"]:
84 click.echo(rev_doc.signed_raw())
86 warn_before_sending_document()
87 network.send_document(bma.wot.revoke, rev_doc)
90@click.command(
91 "verify",
92 help="Verifies that a revocation document is correctly formatted and matches an \
93existing identity",
94)
95@click.argument(
96 "file",
97 type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
98)
99@click.pass_context
100def verify(ctx: click.Context, file: Path) -> None:
101 rev_doc = verify_document(file)
102 idty_table = idty_tools.display_identity(rev_doc.identity)
103 click.echo(idty_table.draw())
104 click.echo("Revocation document is valid.")
107@click.command(
108 "publish",
109 help="Publish revocation document. Identity will be revoked immediately",
110)
111@click.argument(
112 "file",
113 type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
114)
115@click.pass_context
116def publish(ctx: click.Context, file: Path) -> None:
117 warn_before_dry_run_or_display(ctx)
119 rev_doc = verify_document(file)
120 if ctx.obj["DRY_RUN"]:
121 click.echo(rev_doc.signed_raw())
122 return
124 idty_table = idty_tools.display_identity(rev_doc.identity)
125 click.echo(idty_table.draw())
126 if ctx.obj["DISPLAY_DOCUMENT"]:
127 click.echo(rev_doc.signed_raw())
129 warn_before_sending_document()
130 network.send_document(bma.wot.revoke, rev_doc)
133def warn_before_dry_run_or_display(ctx: click.Context) -> None:
134 if ctx.obj["DRY_RUN"]:
135 click.echo("WARNING: the document will only be displayed and will not be sent.")
138def warn_before_sending_document() -> None:
139 click.secho("/!\\WARNING/!\\", blink=True, fg="red")
140 click.echo(
141 "This identity will be revoked.\n\
142It will cease to be member and to create the Universal Dividend.\n\
143All currently sent certifications will remain valid until they expire.",
144 )
145 tui.send_doc_confirmation("revocation document immediately")
148def create_revocation_doc(_id: dict, pubkey: str, currency: str) -> Revocation:
149 """
150 Creates an unsigned revocation document.
151 _id is the dict object containing id infos from request wot.requirements
152 """
153 idty = Identity(
154 currency=currency,
155 pubkey=pubkey,
156 uid=_id["uid"],
157 block_id=BlockID.from_str(_id["meta"]["timestamp"]),
158 )
159 idty.signature = _id["self"]
160 return Revocation(
161 currency=currency,
162 identity=idty,
163 )
166def opener_user_rw(path, flags):
167 return os.open(path, flags, 0o600)
170def save_doc(rev_path: Path, content: str, pubkey: str) -> None:
171 pubkey_cksum = gen_pubkey_checksum(pubkey)
172 # Ask confirmation if the file exists
173 if rev_path.is_file():
174 if click.confirm(
175 f"Would you like to erase existing file `{rev_path}` with the \
176gene rated revocation document corresponding to {pubkey_cksum} public key?",
177 ):
178 rev_path.unlink()
179 else:
180 click.echo("Ok, goodbye!")
181 sys.exit(SUCCESS_EXIT_STATUS)
182 with open(rev_path, "w", encoding="utf-8", opener=opener_user_rw) as fh:
183 fh.write(content)
184 click.echo(
185 f"Revocation document file stored into `{rev_path}` for following public key: {pubkey_cksum}",
186 )
189def verify_document(doc: Path) -> Revocation:
190 """
191 This checks that:
192 - that the revocation signature is valid.
193 - if the identity is unique (warns the user)
194 It returns the revocation document or exits.
195 """
196 error_invalid_sign = "Error: the signature of the revocation document is invalid."
197 error_invalid_doc = (
198 f"Error: {doc} is not a revocation document, or is not correctly formatted."
199 )
201 original_doc = doc.read_text(encoding="utf-8")
203 try:
204 rev_doc = Revocation.from_signed_raw(original_doc)
205 except (MalformedDocumentError, IndexError):
206 sys.exit(error_invalid_doc)
208 verif_key = VerifyingKey(rev_doc.pubkey)
209 if not verif_key.check_signature(rev_doc.raw(), rev_doc.signature):
210 sys.exit(error_invalid_sign)
212 many_idtys = idty_tools.check_many_identities(rev_doc)
213 if many_idtys:
214 return rev_doc
215 sys.exit(FAILURE_EXIT_STATUS)