Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/securesystemslib/_gpg/functions.py: 29%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

66 statements  

1""" 

2<Module Name> 

3 functions.py 

4 

5<Author> 

6 Santiago Torres-Arias <santiago@nyu.edu> 

7 

8<Started> 

9 Nov 15, 2017 

10 

11<Copyright> 

12 See LICENSE for licensing information. 

13 

14<Purpose> 

15 publicly-usable functions for exporting public-keys, signing data and 

16 verifying signatures. 

17""" 

18 

19import logging 

20import os 

21import subprocess 

22import time 

23from pathlib import PureWindowsPath 

24 

25from securesystemslib import exceptions 

26from securesystemslib._gpg.common import ( 

27 get_pubkey_bundle, 

28 parse_signature_packet, 

29) 

30from securesystemslib._gpg.constants import ( 

31 FULLY_SUPPORTED_MIN_VERSION, 

32 GPG_TIMEOUT, 

33 NO_GPG_MSG, 

34 SHA256, 

35 gpg_command, 

36 gpg_export_pubkey_command, 

37 gpg_sign_command, 

38 have_gpg, 

39) 

40from securesystemslib._gpg.exceptions import KeyExpirationError 

41from securesystemslib._gpg.handlers import SIGNATURE_HANDLERS 

42from securesystemslib._gpg.rsa import CRYPTO 

43 

44log = logging.getLogger(__name__) 

45 

46NO_CRYPTO_MSG = "GPG support requires the cryptography library" 

47 

48 

49def _homedir_to_gpg_arg(homedir: str) -> str: 

50 """Convert a homedir path to a GPG-compatible --homedir argument. 

51 

52 On Windows, path format depends on the GPG binary: 

53 - Native Gpg4win (.exe): accepts forward-slash Windows paths (C:/path) 

54 - Cygwin/MSYS2 GPG: requires cygwin-style paths (/c/path) 

55 See https://github.com/secure-systems-lab/securesystemslib/issues/517 

56 """ 

57 if gpg_command().endswith(".exe"): 

58 return homedir.replace("\\", "/") 

59 if os.name == "nt": 

60 p = PureWindowsPath(homedir) 

61 if p.drive: 

62 drive_letter = p.drive[0].lower() 

63 rest = p.as_posix()[len(p.drive) :] 

64 return f"/{drive_letter}{rest}" 

65 return homedir.replace("\\", "/") 

66 

67 

68def create_signature(content, keyid=None, homedir=None, timeout=GPG_TIMEOUT): 

69 """ 

70 <Purpose> 

71 Calls the gpg command line utility to sign the passed content with the key 

72 identified by the passed keyid from the gpg keyring at the passed homedir. 

73 

74 The executed base command is defined in 

75 securesystemslib._gpg.constants.gpg_sign_command. 

76 

77 NOTE: On not fully supported versions of GPG, i.e. versions below 

78 securesystemslib._gpg.constants.FULLY_SUPPORTED_MIN_VERSION the returned 

79 signature does not contain the full keyid. As a work around, we export the 

80 public key bundle identified by the short keyid to compute the full keyid 

81 and add it to the returned signature. 

82 

83 <Arguments> 

84 content: 

85 The content to be signed. (bytes) 

86 

87 keyid: (optional) 

88 The keyid of the gpg signing keyid. If not passed the default 

89 key in the keyring is used. 

90 

91 homedir: (optional) 

92 Path to the gpg keyring. If not passed the default keyring is used. 

93 

94 timeout (optional): 

95 gpg command timeout in seconds. Default is 10. 

96 

97 <Exceptions> 

98 

99 ValueError: 

100 If the gpg command failed to create a valid signature. 

101 

102 OSError: 

103 If the gpg command is not present, or non-executable, 

104 or returned a non-zero exit code 

105 

106 securesystemslib.exceptions.UnsupportedLibraryError: 

107 If the gpg command is not available, or 

108 the cryptography library is not installed. 

109 

110 securesystemslib._gpg.exceptions.KeyNotFoundError: 

111 If the used gpg version is not fully supported 

112 and no public key can be found for short keyid. 

113 

114 <Side Effects> 

115 None. 

116 

117 <Returns> 

118 A signature dict. 

119 

120 """ 

121 if not have_gpg(): # pragma: no cover 

122 raise exceptions.UnsupportedLibraryError(NO_GPG_MSG) 

123 

124 if not CRYPTO: # pragma: no cover 

125 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) 

126 

127 keyarg = "" 

128 if keyid: 

129 keyarg = f"--local-user {keyid}" 

130 

131 homearg = "" 

132 if homedir: 

133 homearg = f"--homedir {_homedir_to_gpg_arg(homedir)}" 

134 

135 command = gpg_sign_command(keyarg=keyarg, homearg=homearg) 

136 

137 gpg_process = subprocess.run( # noqa: S603 

138 command, 

139 input=content, 

140 check=False, 

141 capture_output=True, 

142 timeout=timeout, 

143 ) 

144 

145 # TODO: It's suggested to take a look at `--status-fd` for proper error 

146 # reporting, as there is no clear distinction between the return codes 

147 # https://lists.gnupg.org/pipermail/gnupg-devel/2005-December/022559.html 

148 if gpg_process.returncode != 0: 

149 raise OSError( 

150 f"Command '{gpg_process.args}' returned " 

151 f"non-zero exit status '{gpg_process.returncode}', " 

152 f"stderr was:\n{gpg_process.stderr.decode()}." 

153 ) 

154 

155 signature_data = gpg_process.stdout 

156 signature = parse_signature_packet(signature_data) 

157 

158 # On GPG < 2.1 we cannot derive the full keyid from the signature data. 

159 # Instead we try to compute the keyid from the public part of the signing 

160 # key or its subkeys, identified by the short keyid. 

161 # parse_signature_packet is guaranteed to return at least one of keyid or 

162 # short_keyid. 

163 # Exclude the following code from coverage for consistent coverage across 

164 # test environments. 

165 if not signature["keyid"]: # pragma: no cover 

166 log.warning( 

167 "The created signature does not include the hashed subpacket" 

168 " '33' (full keyid). You probably have a gpg version" 

169 f" <{FULLY_SUPPORTED_MIN_VERSION}." 

170 " We will export the public keys associated with the short keyid to" 

171 " compute the full keyid." 

172 ) 

173 

174 short_keyid = signature["short_keyid"] 

175 

176 # Export public key bundle (master key including with optional subkeys) 

177 public_key_bundle = export_pubkey(short_keyid, homedir) 

178 

179 # Test if the short keyid matches the master key ... 

180 master_key_full_keyid = public_key_bundle["keyid"] 

181 if master_key_full_keyid.endswith(short_keyid.lower()): 

182 signature["keyid"] = master_key_full_keyid 

183 

184 # ... or one of the subkeys, and add the full keyid to the signature dict. 

185 else: 

186 for sub_key_full_keyid in list(public_key_bundle.get("subkeys", {}).keys()): 

187 if sub_key_full_keyid.endswith(short_keyid.lower()): 

188 signature["keyid"] = sub_key_full_keyid 

189 break 

190 

191 # If there is still no full keyid something went wrong 

192 if not signature["keyid"]: # pragma: no cover 

193 raise ValueError( 

194 f"Full keyid could not be determined for signature '{signature}'" 

195 ) 

196 

197 # It is okay now to remove the optional short keyid to save space 

198 signature.pop("short_keyid", None) 

199 

200 return signature 

201 

202 

203def verify_signature(signature_object, pubkey_info, content): 

204 """ 

205 <Purpose> 

206 Verifies the passed signature against the passed content using the 

207 passed public key, or one of its subkeys, associated by the signature's 

208 keyid. 

209 

210 The function selects the appropriate verification algorithm (rsa or dsa) 

211 based on the "type" field in the passed public key object. 

212 

213 <Arguments> 

214 signature_object: 

215 A signature dict. 

216 

217 pubkey_info: 

218 A public key dict. 

219 

220 content: 

221 The content to be verified. (bytes) 

222 

223 <Exceptions> 

224 securesystemslib._gpg.exceptions.KeyExpirationError: 

225 if the passed public key has expired 

226 

227 securesystemslib.exceptions.UnsupportedLibraryError: 

228 if the cryptography module is unavailable 

229 

230 <Side Effects> 

231 None. 

232 

233 <Returns> 

234 True if signature verification passes, False otherwise. 

235 

236 """ 

237 if not CRYPTO: # pragma: no cover 

238 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) 

239 

240 handler = SIGNATURE_HANDLERS[pubkey_info["type"]] 

241 sig_keyid = signature_object["keyid"] 

242 

243 verification_key = pubkey_info 

244 

245 # If the keyid on the signature matches a subkey of the passed key, 

246 # we use that subkey for verification instead of the master key. 

247 if sig_keyid in list(pubkey_info.get("subkeys", {}).keys()): 

248 verification_key = pubkey_info["subkeys"][sig_keyid] 

249 

250 creation_time = verification_key.get("creation_time") 

251 validity_period = verification_key.get("validity_period") 

252 

253 if ( 

254 creation_time 

255 and validity_period 

256 and creation_time + validity_period < time.time() 

257 ): 

258 raise KeyExpirationError(verification_key) 

259 

260 return handler.verify_signature(signature_object, verification_key, content, SHA256) 

261 

262 

263def export_pubkey(keyid, homedir=None, timeout=GPG_TIMEOUT): 

264 """Exports a public key from a GnuPG keyring. 

265 

266 Arguments: 

267 keyid: An OpenPGP keyid.. 

268 homedir (optional): A path to the GnuPG home directory. If not set the 

269 default GnuPG home directory is used. 

270 timeout (optional): gpg command timeout in seconds. Default is 10. 

271 

272 Raises: 

273 UnsupportedLibraryError: The gpg command or pyca/cryptography are not 

274 available. 

275 KeyNotFoundError: No key or subkey was found for that keyid. 

276 

277 Side Effects: 

278 Calls system gpg command in a subprocess. 

279 

280 Returns: 

281 An OpenPGP public key dict. 

282 

283 """ 

284 if not have_gpg(): # pragma: no cover 

285 raise exceptions.UnsupportedLibraryError(NO_GPG_MSG) 

286 

287 if not CRYPTO: # pragma: no cover 

288 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) 

289 

290 homearg = "" 

291 if homedir: 

292 homearg = f"--homedir {_homedir_to_gpg_arg(homedir)}" 

293 

294 # TODO: Consider adopting command error handling from `create_signature` 

295 # above, e.g. in a common 'run gpg command' utility function 

296 command = gpg_export_pubkey_command(keyid=keyid, homearg=homearg) 

297 gpg_process = subprocess.run( # noqa: S603 

298 command, 

299 capture_output=True, 

300 timeout=timeout, 

301 check=True, 

302 ) 

303 

304 key_packet = gpg_process.stdout 

305 key_bundle = get_pubkey_bundle(key_packet, keyid) 

306 

307 return key_bundle 

308 

309 

310def export_pubkeys(keyids, homedir=None, timeout=GPG_TIMEOUT): 

311 """Exports multiple public keys from a GnuPG keyring. 

312 

313 Arguments: 

314 keyids: A list of OpenPGP keyids. 

315 homedir (optional): A path to the GnuPG home directory. If not set the 

316 default GnuPG home directory is used. 

317 timeout (optional): gpg command timeout in seconds. Default is 10. 

318 

319 Raises: 

320 TypeError: Keyids is not iterable. 

321 ValueError: A Keyid is not a string. 

322 UnsupportedLibraryError: The gpg command or pyca/cryptography are not 

323 available. 

324 KeyNotFoundError: No key or subkey was found for that keyid. 

325 

326 Side Effects: 

327 Calls system gpg command in a subprocess. 

328 

329 Returns: 

330 A dict of OpenPGP public key dicts as values, 

331 and their keyids as dict keys. 

332 

333 

334 """ 

335 public_key_dict = {} 

336 for gpg_keyid in keyids: 

337 public_key = export_pubkey(gpg_keyid, homedir=homedir, timeout=timeout) 

338 keyid = public_key["keyid"] 

339 public_key_dict[keyid] = public_key 

340 

341 return public_key_dict