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

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

15 

16import os 

17import sys 

18from pathlib import Path 

19from typing import Dict 

20 

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 

28 

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 

35 

36 

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

45 

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) 

51 

52 idty_table = idty_tools.display_identity(rev_doc.identity) 

53 click.echo(idty_table.draw()) 

54 

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

60 

61 

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

69 

70 warn_before_dry_run_or_display(ctx) 

71 

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) 

77 

78 if ctx.obj["DRY_RUN"]: 

79 click.echo(rev_doc.signed_raw()) 

80 return 

81 

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

86 

87 warn_before_sending_document() 

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

89 

90 

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

106 

107 

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) 

119 

120 rev_doc = verify_document(file) 

121 if ctx.obj["DRY_RUN"]: 

122 click.echo(rev_doc.signed_raw()) 

123 return 

124 

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

129 

130 warn_before_sending_document() 

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

132 

133 

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

137 

138 

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

147 

148 

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 ) 

165 

166 

167def opener_user_rw(path, flags): 

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

169 

170 

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 ) 

189 

190 

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 ) 

202 

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

204 

205 try: 

206 rev_doc = Revocation.from_signed_raw(original_doc) 

207 except (MalformedDocumentError, IndexError): 

208 sys.exit(error_invalid_doc) 

209 

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) 

213 

214 many_idtys = idty_tools.check_many_identities(rev_doc) 

215 if many_idtys: 

216 return rev_doc 

217 sys.exit(FAILURE_EXIT_STATUS)