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

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

54 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 subprocess 

21import time 

22 

23from securesystemslib import exceptions 

24from securesystemslib._gpg.common import ( 

25 get_pubkey_bundle, 

26 parse_signature_packet, 

27) 

28from securesystemslib._gpg.constants import ( 

29 FULLY_SUPPORTED_MIN_VERSION, 

30 GPG_TIMEOUT, 

31 NO_GPG_MSG, 

32 SHA256, 

33 gpg_export_pubkey_command, 

34 gpg_sign_command, 

35 have_gpg, 

36) 

37from securesystemslib._gpg.exceptions import KeyExpirationError 

38from securesystemslib._gpg.handlers import SIGNATURE_HANDLERS 

39from securesystemslib._gpg.rsa import CRYPTO 

40 

41log = logging.getLogger(__name__) 

42 

43NO_CRYPTO_MSG = "GPG support requires the cryptography library" 

44 

45 

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

47 """ 

48 <Purpose> 

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

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

51 

52 The executed base command is defined in 

53 securesystemslib._gpg.constants.gpg_sign_command. 

54 

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

56 securesystemslib._gpg.constants.FULLY_SUPPORTED_MIN_VERSION the returned 

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

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

59 and add it to the returned signature. 

60 

61 <Arguments> 

62 content: 

63 The content to be signed. (bytes) 

64 

65 keyid: (optional) 

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

67 key in the keyring is used. 

68 

69 homedir: (optional) 

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

71 

72 timeout (optional): 

73 gpg command timeout in seconds. Default is 10. 

74 

75 <Exceptions> 

76 

77 ValueError: 

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

79 

80 OSError: 

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

82 or returned a non-zero exit code 

83 

84 securesystemslib.exceptions.UnsupportedLibraryError: 

85 If the gpg command is not available, or 

86 the cryptography library is not installed. 

87 

88 securesystemslib._gpg.exceptions.KeyNotFoundError: 

89 If the used gpg version is not fully supported 

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

91 

92 <Side Effects> 

93 None. 

94 

95 <Returns> 

96 A signature dict. 

97 

98 """ 

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

100 raise exceptions.UnsupportedLibraryError(NO_GPG_MSG) 

101 

102 if not CRYPTO: # pragma: no cover 

103 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) 

104 

105 keyarg = "" 

106 if keyid: 

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

108 

109 homearg = "" 

110 if homedir: 

111 homearg = f"--homedir {homedir}".replace("\\", "/") 

112 

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

114 

115 gpg_process = subprocess.run( # noqa: S603 

116 command, 

117 input=content, 

118 check=False, 

119 capture_output=True, 

120 timeout=timeout, 

121 ) 

122 

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

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

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

126 if gpg_process.returncode != 0: 

127 raise OSError( 

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

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

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

131 ) 

132 

133 signature_data = gpg_process.stdout 

134 signature = parse_signature_packet(signature_data) 

135 

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

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

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

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

140 # short_keyid. 

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

142 # test environments. 

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

144 log.warning( 

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

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

147 f" <{FULLY_SUPPORTED_MIN_VERSION}." 

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

149 " compute the full keyid." 

150 ) 

151 

152 short_keyid = signature["short_keyid"] 

153 

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

155 public_key_bundle = export_pubkey(short_keyid, homedir) 

156 

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

158 master_key_full_keyid = public_key_bundle["keyid"] 

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

160 signature["keyid"] = master_key_full_keyid 

161 

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

163 else: 

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

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

166 signature["keyid"] = sub_key_full_keyid 

167 break 

168 

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

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

171 raise ValueError( 

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

173 ) 

174 

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

176 signature.pop("short_keyid", None) 

177 

178 return signature 

179 

180 

181def verify_signature(signature_object, pubkey_info, content): 

182 """ 

183 <Purpose> 

184 Verifies the passed signature against the passed content using the 

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

186 keyid. 

187 

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

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

190 

191 <Arguments> 

192 signature_object: 

193 A signature dict. 

194 

195 pubkey_info: 

196 A public key dict. 

197 

198 content: 

199 The content to be verified. (bytes) 

200 

201 <Exceptions> 

202 securesystemslib._gpg.exceptions.KeyExpirationError: 

203 if the passed public key has expired 

204 

205 securesystemslib.exceptions.UnsupportedLibraryError: 

206 if the cryptography module is unavailable 

207 

208 <Side Effects> 

209 None. 

210 

211 <Returns> 

212 True if signature verification passes, False otherwise. 

213 

214 """ 

215 if not CRYPTO: # pragma: no cover 

216 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) 

217 

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

219 sig_keyid = signature_object["keyid"] 

220 

221 verification_key = pubkey_info 

222 

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

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

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

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

227 

228 creation_time = verification_key.get("creation_time") 

229 validity_period = verification_key.get("validity_period") 

230 

231 if ( 

232 creation_time 

233 and validity_period 

234 and creation_time + validity_period < time.time() 

235 ): 

236 raise KeyExpirationError(verification_key) 

237 

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

239 

240 

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

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

243 

244 Arguments: 

245 keyid: An OpenPGP keyid.. 

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

247 default GnuPG home directory is used. 

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

249 

250 Raises: 

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

252 available. 

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

254 

255 Side Effects: 

256 Calls system gpg command in a subprocess. 

257 

258 Returns: 

259 An OpenPGP public key dict. 

260 

261 """ 

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

263 raise exceptions.UnsupportedLibraryError(NO_GPG_MSG) 

264 

265 if not CRYPTO: # pragma: no cover 

266 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) 

267 

268 homearg = "" 

269 if homedir: 

270 homearg = f"--homedir {homedir}".replace("\\", "/") 

271 

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

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

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

275 gpg_process = subprocess.run( # noqa: S603 

276 command, 

277 capture_output=True, 

278 timeout=timeout, 

279 check=True, 

280 ) 

281 

282 key_packet = gpg_process.stdout 

283 key_bundle = get_pubkey_bundle(key_packet, keyid) 

284 

285 return key_bundle 

286 

287 

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

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

290 

291 Arguments: 

292 keyids: A list of OpenPGP keyids. 

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

294 default GnuPG home directory is used. 

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

296 

297 Raises: 

298 TypeError: Keyids is not iterable. 

299 ValueError: A Keyid is not a string. 

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

301 available. 

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

303 

304 Side Effects: 

305 Calls system gpg command in a subprocess. 

306 

307 Returns: 

308 A dict of OpenPGP public key dicts as values, 

309 and their keyids as dict keys. 

310 

311 

312 """ 

313 public_key_dict = {} 

314 for gpg_keyid in keyids: 

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

316 keyid = public_key["keyid"] 

317 public_key_dict[keyid] = public_key 

318 

319 return public_key_dict