Coverage for silkaj/wot/revocation.py: 100%
111 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-22 12:04 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-22 12:04 +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.account_storage import AccountStorage
30from silkaj.blockchain import tools as bc_tools
31from silkaj.constants import FAILURE_EXIT_STATUS, SUCCESS_EXIT_STATUS
32from silkaj.public_key import gen_pubkey_checksum
33from silkaj.wot import idty_tools
34from silkaj.wot import tools as w_tools
37@click.command("create", help="Create and save revocation document")
38def create() -> None:
39 currency = bc_tools.get_currency()
41 key = auth.auth_method()
42 gen_pubkey_checksum(key.pubkey)
43 _id = (w_tools.choose_identity(key.pubkey))[0]
44 rev_doc = create_revocation_doc(_id, key.pubkey, currency)
45 rev_doc.sign(key)
47 idty_table = idty_tools.display_identity(rev_doc.identity)
48 click.echo(idty_table.draw())
50 revocation_file_path = AccountStorage().revocation_path(check_exist=False)
52 confirm_message = "Do you want to save the revocation document for this identity?"
53 if click.confirm(confirm_message):
54 save_doc(revocation_file_path, rev_doc.signed_raw(), key.pubkey)
55 else:
56 click.echo("Ok, goodbye!")
59@click.command(
60 "revoke",
61 help="Create and publish revocation document. Will immediately revoke the identity.",
62)
63@click.pass_context
64def revoke_now(ctx: click.Context) -> None:
65 currency = bc_tools.get_currency()
67 warn_before_dry_run_or_display(ctx)
69 key = auth.auth_method()
70 gen_pubkey_checksum(key.pubkey)
71 _id = (w_tools.choose_identity(key.pubkey))[0]
72 rev_doc = create_revocation_doc(_id, key.pubkey, currency)
73 rev_doc.sign(key)
75 if ctx.obj["DRY_RUN"]:
76 click.echo(rev_doc.signed_raw())
77 return
79 idty_table = idty_tools.display_identity(rev_doc.identity)
80 click.echo(idty_table.draw())
81 if ctx.obj["DISPLAY_DOCUMENT"]:
82 click.echo(rev_doc.signed_raw())
84 warn_before_sending_document()
85 network.send_document(bma.wot.revoke, rev_doc)
88@click.command(
89 "verify",
90 help="Verifies that the revocation document is correctly formatted and matches an existing identity",
91)
92def verify() -> None:
93 revocation_file_path = AccountStorage().revocation_path()
94 rev_doc = verify_document(revocation_file_path)
95 idty_table = idty_tools.display_identity(rev_doc.identity)
96 click.echo(idty_table.draw())
97 click.echo("Revocation document is valid.")
100@click.command(
101 "publish",
102 help="Publish previously created revocation document. Identity will be immediately revoked.",
103)
104@click.pass_context
105def publish(ctx: click.Context) -> None:
106 revocation_file_path = AccountStorage().revocation_path()
107 warn_before_dry_run_or_display(ctx)
109 rev_doc = verify_document(revocation_file_path)
110 if ctx.obj["DRY_RUN"]:
111 click.echo(rev_doc.signed_raw())
112 return
114 idty_table = idty_tools.display_identity(rev_doc.identity)
115 click.echo(idty_table.draw())
116 if ctx.obj["DISPLAY_DOCUMENT"]:
117 click.echo(rev_doc.signed_raw())
119 warn_before_sending_document()
120 network.send_document(bma.wot.revoke, rev_doc)
123def warn_before_dry_run_or_display(ctx: click.Context) -> None:
124 if ctx.obj["DRY_RUN"]:
125 click.echo("WARNING: the document will only be displayed and will not be sent.")
128def warn_before_sending_document() -> None:
129 click.secho("/!\\WARNING/!\\", blink=True, fg="red")
130 click.echo(
131 "This identity will be revoked.\n\
132It will cease to be member and to create the Universal Dividend.\n\
133All currently sent certifications will remain valid until they expire.",
134 )
135 tui.send_doc_confirmation("revocation document immediately")
138def create_revocation_doc(_id: dict, pubkey: str, currency: str) -> Revocation:
139 """
140 Creates an unsigned revocation document.
141 _id is the dict object containing id infos from request wot.requirements
142 """
143 idty = Identity(
144 currency=currency,
145 pubkey=pubkey,
146 uid=_id["uid"],
147 block_id=BlockID.from_str(_id["meta"]["timestamp"]),
148 )
149 idty.signature = _id["self"]
150 return Revocation(
151 currency=currency,
152 identity=idty,
153 )
156def opener_user_rw(path, flags):
157 return os.open(path, flags, 0o600)
160def save_doc(rev_path: Path, content: str, pubkey: str) -> None:
161 pubkey_cksum = gen_pubkey_checksum(pubkey)
162 # Ask confirmation if the file exists
163 if rev_path.is_file():
164 if click.confirm(
165 f"Would you like to erase existing file `{rev_path}` with the \
166generated revocation document corresponding to {pubkey_cksum} public key?",
167 ):
168 rev_path.unlink()
169 else:
170 click.echo("Ok, goodbye!")
171 sys.exit(SUCCESS_EXIT_STATUS)
172 with open(rev_path, "w", encoding="utf-8", opener=opener_user_rw) as fh:
173 fh.write(content)
174 click.echo(
175 f"Revocation document file stored into `{rev_path}` for following public key: {pubkey_cksum}",
176 )
179def verify_document(doc: Path) -> Revocation:
180 """
181 This checks that:
182 - that the revocation signature is valid.
183 - if the identity is unique (warns the user)
184 It returns the revocation document or exits.
185 """
186 error_invalid_sign = "Error: the signature of the revocation document is invalid."
187 error_invalid_doc = (
188 f"Error: {doc} is not a revocation document, or is not correctly formatted."
189 )
191 original_doc = doc.read_text(encoding="utf-8")
193 try:
194 rev_doc = Revocation.from_signed_raw(original_doc)
195 except (MalformedDocumentError, IndexError):
196 sys.exit(error_invalid_doc)
198 verif_key = VerifyingKey(rev_doc.pubkey)
199 if not verif_key.check_signature(rev_doc.raw(), rev_doc.signature):
200 sys.exit(error_invalid_sign)
202 many_idtys = idty_tools.check_many_identities(rev_doc)
203 if many_idtys:
204 return rev_doc
205 sys.exit(FAILURE_EXIT_STATUS)