Coverage for silkaj/wot/revocation.py: 100%
113 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-17 17:03 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-17 17:03 +0000
1# Copyright 2016-2024 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
19from typing import Dict
21import rich_click as click
22from duniterpy.api import bma
23from duniterpy.documents.block_id import BlockID
24from duniterpy.documents.document import MalformedDocumentError
25from duniterpy.documents.identity import Identity
26from duniterpy.documents.revocation import Revocation
27from duniterpy.key.verifying_key import VerifyingKey
29from silkaj import auth, network, tui
30from silkaj.blockchain.tools import get_currency
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 a revocation document")
38@click.argument(
39 "file",
40 type=click.Path(dir_okay=False, writable=True, path_type=Path),
41)
42@click.pass_context
43def create(ctx: click.Context, file: Path) -> None:
44 currency = get_currency()
46 key = auth.auth_method()
47 gen_pubkey_checksum(key.pubkey)
48 _id = (w_tools.choose_identity(key.pubkey))[0]
49 rev_doc = create_revocation_doc(_id, key.pubkey, currency)
50 rev_doc.sign(key)
52 idty_table = idty_tools.display_identity(rev_doc.identity)
53 click.echo(idty_table.draw())
55 confirm_message = "Do you want to save the revocation document for this identity?"
56 if click.confirm(confirm_message):
57 save_doc(file, rev_doc.signed_raw(), key.pubkey)
58 else:
59 click.echo("Ok, goodbye!")
62@click.command(
63 "revoke",
64 help="Create and publish revocation document. Will revoke the identity immediately.",
65)
66@click.pass_context
67def revoke_now(ctx: click.Context) -> None:
68 currency = get_currency()
70 warn_before_dry_run_or_display(ctx)
72 key = auth.auth_method()
73 gen_pubkey_checksum(key.pubkey)
74 _id = (w_tools.choose_identity(key.pubkey))[0]
75 rev_doc = create_revocation_doc(_id, key.pubkey, currency)
76 rev_doc.sign(key)
78 if ctx.obj["DRY_RUN"]:
79 click.echo(rev_doc.signed_raw())
80 return
82 idty_table = idty_tools.display_identity(rev_doc.identity)
83 click.echo(idty_table.draw())
84 if ctx.obj["DISPLAY_DOCUMENT"]:
85 click.echo(rev_doc.signed_raw())
87 warn_before_sending_document()
88 network.send_document(bma.wot.revoke, rev_doc)
91@click.command(
92 "verify",
93 help="Verifies that a revocation document is correctly formatted and matches an \
94existing identity",
95)
96@click.argument(
97 "file",
98 type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
99)
100@click.pass_context
101def verify(ctx: click.Context, file: Path) -> None:
102 rev_doc = verify_document(file)
103 idty_table = idty_tools.display_identity(rev_doc.identity)
104 click.echo(idty_table.draw())
105 click.echo("Revocation document is valid.")
108@click.command(
109 "publish",
110 help="Publish revocation document. Identity will be revoked immediately",
111)
112@click.argument(
113 "file",
114 type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
115)
116@click.pass_context
117def publish(ctx: click.Context, file: Path) -> None:
118 warn_before_dry_run_or_display(ctx)
120 rev_doc = verify_document(file)
121 if ctx.obj["DRY_RUN"]:
122 click.echo(rev_doc.signed_raw())
123 return
125 idty_table = idty_tools.display_identity(rev_doc.identity)
126 click.echo(idty_table.draw())
127 if ctx.obj["DISPLAY_DOCUMENT"]:
128 click.echo(rev_doc.signed_raw())
130 warn_before_sending_document()
131 network.send_document(bma.wot.revoke, rev_doc)
134def warn_before_dry_run_or_display(ctx: click.Context) -> None:
135 if ctx.obj["DRY_RUN"]:
136 click.echo("WARNING: the document will only be displayed and will not be sent.")
139def warn_before_sending_document() -> None:
140 click.secho("/!\\WARNING/!\\", blink=True, fg="red")
141 click.echo(
142 "This identity will be revoked.\n\
143It will cease to be member and to create the Universal Dividend.\n\
144All currently sent certifications will remain valid until they expire.",
145 )
146 tui.send_doc_confirmation("revocation document immediately")
149def create_revocation_doc(_id: Dict, pubkey: str, currency: str) -> Revocation:
150 """
151 Creates an unsigned revocation document.
152 _id is the dict object containing id infos from request wot.requirements
153 """
154 idty = Identity(
155 currency=currency,
156 pubkey=pubkey,
157 uid=_id["uid"],
158 block_id=BlockID.from_str(_id["meta"]["timestamp"]),
159 )
160 idty.signature = _id["self"]
161 return Revocation(
162 currency=currency,
163 identity=idty,
164 )
167def opener_user_rw(path, flags):
168 return os.open(path, flags, 0o600)
171def save_doc(rev_path: Path, content: str, pubkey: str) -> None:
172 pubkey_cksum = gen_pubkey_checksum(pubkey)
173 # Ask confirmation if the file exists
174 if rev_path.is_file():
175 if click.confirm(
176 f"Would you like to erase existing file `{rev_path} \
177 ` with the \
178gene rated revocation document corresponding to {pubkey_cksum} public key?",
179 ):
180 rev_path.unlink()
181 else:
182 click.echo("Ok, goodbye!")
183 sys.exit(SUCCESS_EXIT_STATUS)
184 with open(rev_path, "w", encoding="utf-8", opener=opener_user_rw) as fh:
185 fh.write(content)
186 click.echo(
187 f"Revocation document file stored into `{rev_path}` for following public key: {pubkey_cksum}",
188 )
191def verify_document(doc: Path) -> Revocation:
192 """
193 This checks that:
194 - that the revocation signature is valid.
195 - if the identity is unique (warns the user)
196 It returns the revocation document or exits.
197 """
198 error_invalid_sign = "Error: the signature of the revocation document is invalid."
199 error_invalid_doc = (
200 f"Error: {doc} is not a revocation document, or is not correctly formatted."
201 )
203 original_doc = doc.read_text(encoding="utf-8")
205 try:
206 rev_doc = Revocation.from_signed_raw(original_doc)
207 except (MalformedDocumentError, IndexError):
208 sys.exit(error_invalid_doc)
210 verif_key = VerifyingKey(rev_doc.pubkey)
211 if not verif_key.check_signature(rev_doc.raw(), rev_doc.signature):
212 sys.exit(error_invalid_sign)
214 many_idtys = idty_tools.check_many_identities(rev_doc)
215 if many_idtys:
216 return rev_doc
217 sys.exit(FAILURE_EXIT_STATUS)