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

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/>. 

15 

16import os 

17import sys 

18from pathlib import Path 

19 

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 

27 

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 

35 

36 

37@click.command("create", help="Create and save revocation document") 

38def create() -> None: 

39 currency = bc_tools.get_currency() 

40 

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) 

46 

47 idty_table = idty_tools.display_identity(rev_doc.identity) 

48 click.echo(idty_table.draw()) 

49 

50 revocation_file_path = AccountStorage().revocation_path(check_exist=False) 

51 

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!") 

57 

58 

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() 

66 

67 warn_before_dry_run_or_display(ctx) 

68 

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) 

74 

75 if ctx.obj["DRY_RUN"]: 

76 click.echo(rev_doc.signed_raw()) 

77 return 

78 

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()) 

83 

84 warn_before_sending_document() 

85 network.send_document(bma.wot.revoke, rev_doc) 

86 

87 

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.") 

98 

99 

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) 

108 

109 rev_doc = verify_document(revocation_file_path) 

110 if ctx.obj["DRY_RUN"]: 

111 click.echo(rev_doc.signed_raw()) 

112 return 

113 

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()) 

118 

119 warn_before_sending_document() 

120 network.send_document(bma.wot.revoke, rev_doc) 

121 

122 

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.") 

126 

127 

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") 

136 

137 

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 ) 

154 

155 

156def opener_user_rw(path, flags): 

157 return os.open(path, flags, 0o600) 

158 

159 

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 ) 

177 

178 

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 ) 

190 

191 original_doc = doc.read_text(encoding="utf-8") 

192 

193 try: 

194 rev_doc = Revocation.from_signed_raw(original_doc) 

195 except (MalformedDocumentError, IndexError): 

196 sys.exit(error_invalid_doc) 

197 

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) 

201 

202 many_idtys = idty_tools.check_many_identities(rev_doc) 

203 if many_idtys: 

204 return rev_doc 

205 sys.exit(FAILURE_EXIT_STATUS)