Coverage for silkaj/wot/revocation.py: 100%

112 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-18 04:28 +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 

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

34 

35 

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

44 

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) 

50 

51 idty_table = idty_tools.display_identity(rev_doc.identity) 

52 click.echo(idty_table.draw()) 

53 

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

59 

60 

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

68 

69 warn_before_dry_run_or_display(ctx) 

70 

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) 

76 

77 if ctx.obj["DRY_RUN"]: 

78 click.echo(rev_doc.signed_raw()) 

79 return 

80 

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

85 

86 warn_before_sending_document() 

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

88 

89 

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

105 

106 

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) 

118 

119 rev_doc = verify_document(file) 

120 if ctx.obj["DRY_RUN"]: 

121 click.echo(rev_doc.signed_raw()) 

122 return 

123 

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

128 

129 warn_before_sending_document() 

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

131 

132 

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

136 

137 

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

146 

147 

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 ) 

164 

165 

166def opener_user_rw(path, flags): 

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

168 

169 

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 ) 

187 

188 

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 ) 

200 

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

202 

203 try: 

204 rev_doc = Revocation.from_signed_raw(original_doc) 

205 except (MalformedDocumentError, IndexError): 

206 sys.exit(error_invalid_doc) 

207 

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) 

211 

212 many_idtys = idty_tools.check_many_identities(rev_doc) 

213 if many_idtys: 

214 return rev_doc 

215 sys.exit(FAILURE_EXIT_STATUS)