Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/securesystemslib/signer/_gpg_signer.py: 41%

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

86 statements  

1"""Signer implementation for OpenPGP""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from typing import Any 

7from urllib import parse 

8 

9from securesystemslib import exceptions 

10from securesystemslib._gpg import constants as gpg_constants 

11from securesystemslib._gpg import exceptions as gpg_exceptions 

12from securesystemslib._gpg import functions as gpg 

13from securesystemslib.signer._key import Key 

14from securesystemslib.signer._signer import SecretsHandler, Signature, Signer 

15 

16logger = logging.getLogger(__name__) 

17 

18 

19class GPGKey(Key): 

20 """OpenPGP Key. 

21 

22 *All parameters named below are not just constructor arguments but also 

23 instance attributes.* 

24 

25 Attributes: 

26 keyid: Key identifier that is unique within the metadata it is used in. 

27 It is also used to identify the GnuPG local user signing key. 

28 ketytype: Key type, e.g. "rsa", "dsa" or "eddsa". 

29 scheme: Signing schemes, e.g. "pgp+rsa-pkcsv1.5", "pgp+dsa-fips-180-2", 

30 "pgp+eddsa-ed25519". 

31 keyval: Opaque key content. 

32 unrecognized_fields: Dictionary of all attributes that are not managed 

33 by Securesystemslib 

34 """ 

35 

36 @classmethod 

37 def from_dict(cls, keyid: str, key_dict: dict[str, Any]) -> GPGKey: 

38 keytype, scheme, keyval = cls._from_dict(key_dict) 

39 return cls(keyid, keytype, scheme, keyval, key_dict) 

40 

41 def to_dict(self) -> dict: 

42 return self._to_dict() 

43 

44 def verify_signature(self, signature: Signature, data: bytes) -> None: 

45 try: 

46 if not gpg.verify_signature( 

47 GPGSigner._sig_to_legacy_dict(signature), 

48 GPGSigner._key_to_legacy_dict(self), 

49 data, 

50 ): 

51 raise exceptions.UnverifiedSignatureError( 

52 f"Failed to verify signature by {self.keyid}" 

53 ) 

54 except (exceptions.UnsupportedLibraryError,) as e: 

55 logger.info("Key %s failed to verify sig: %s", self.keyid, str(e)) 

56 raise exceptions.VerificationError( 

57 f"Unknown failure to verify signature by {self.keyid}" 

58 ) from e 

59 

60 

61class GPGSigner(Signer): 

62 """OpenPGP Signer 

63 

64 Runs command in ``GNUPG`` environment variable to sign. Fallback commands are 

65 ``gpg2`` and ``gpg``. 

66 

67 Supported signing schemes are: "pgp+rsa-pkcsv1.5", "pgp+dsa-fips-180-2" and 

68 "pgp+eddsa-ed25519", with SHA-256 hashing. 

69 

70 GPGSigner can be instantiated with Signer.from_priv_key_uri(). These private key URI 

71 schemes are supported: 

72 

73 * "gnupg:[<GnuPG homedir>]": 

74 Signs with GnuPG key in keyring in home dir. The signing key is 

75 identified with the keyid of the passed public key. If homedir is not 

76 passed, the default homedir is used. 

77 

78 Arguments: 

79 public_key: The related public key instance. 

80 homedir: GnuPG home directory path. If not passed, the default homedir is used. 

81 

82 """ 

83 

84 SCHEME = "gnupg" 

85 

86 def __init__( 

87 self, 

88 public_key: Key, 

89 homedir: str | None = None, 

90 ): 

91 self.homedir = homedir 

92 self._public_key = public_key 

93 

94 @property 

95 def public_key(self) -> Key: 

96 return self._public_key 

97 

98 @classmethod 

99 def from_priv_key_uri( 

100 cls, 

101 priv_key_uri: str, 

102 public_key: Key, 

103 secrets_handler: SecretsHandler | None = None, 

104 ) -> GPGSigner: 

105 if not isinstance(public_key, GPGKey): 

106 raise ValueError(f"expected GPGKey for {priv_key_uri}") 

107 

108 uri = parse.urlparse(priv_key_uri) 

109 

110 if uri.scheme != cls.SCHEME: 

111 raise ValueError(f"GPGSigner does not support {priv_key_uri}") 

112 

113 homedir = uri.path or None 

114 

115 return cls(public_key, homedir) 

116 

117 @staticmethod 

118 def _sig_to_legacy_dict(sig: Signature) -> dict: 

119 """Helper to convert Signature to internal gpg signature dict format.""" 

120 sig_dict = sig.to_dict() 

121 sig_dict["signature"] = sig_dict.pop("sig") 

122 return sig_dict 

123 

124 @staticmethod 

125 def _sig_from_legacy_dict(sig_dict: dict) -> Signature: 

126 """Helper to convert internal gpg signature format to Signature.""" 

127 sig_dict["sig"] = sig_dict.pop("signature") 

128 return Signature.from_dict(sig_dict) 

129 

130 @staticmethod 

131 def _key_to_legacy_dict(key: GPGKey) -> dict[str, Any]: 

132 """Returns legacy dictionary representation of self.""" 

133 return { 

134 "keyid": key.keyid, 

135 "type": key.keytype, 

136 "method": key.scheme, 

137 "hashes": [gpg_constants.GPG_HASH_ALGORITHM_STRING], 

138 "keyval": key.keyval, 

139 } 

140 

141 @staticmethod 

142 def _key_from_legacy_dict(key_dict: dict[str, Any]) -> GPGKey: 

143 """Create GPGKey from legacy dictionary representation.""" 

144 keyid = key_dict["keyid"] 

145 keytype = key_dict["type"] 

146 scheme = key_dict["method"] 

147 keyval = key_dict["keyval"] 

148 

149 return GPGKey(keyid, keytype, scheme, keyval) 

150 

151 @classmethod 

152 def import_(cls, keyid: str, homedir: str | None = None) -> tuple[str, Key]: 

153 """Load key and signer details from GnuPG keyring. 

154 

155 NOTE: Information about the key validity (expiration, revocation, etc.) 

156 is discarded at import and not considered when verifying a signature. 

157 

158 Args: 

159 keyid: GnuPG local user signing key id. 

160 homedir: GnuPG home directory path. If not passed, the default homedir is 

161 used. 

162 

163 Raises: 

164 UnsupportedLibraryError: The gpg command or pyca/cryptography are 

165 not available. 

166 ValueError: No key was found for the passed keyid. 

167 

168 Returns: 

169 Tuple of private key uri and the public key. 

170 

171 """ 

172 uri = f"{cls.SCHEME}:{homedir or ''}" 

173 

174 try: 

175 raw_key = gpg.export_pubkey(keyid, homedir) 

176 

177 except gpg_exceptions.KeyNotFoundError as e: 

178 raise ValueError(e) from e 

179 

180 raw_keys = [raw_key] + list(raw_key.pop("subkeys", {}).values()) 

181 keyids = [] 

182 

183 for key in raw_keys: 

184 if key["keyid"] == keyid: 

185 # TODO: Raise here if key is expired, revoked, incapable, ... 

186 public_key = cls._key_from_legacy_dict(key) 

187 break 

188 keyids.append(key["keyid"]) 

189 

190 else: 

191 raise ValueError( 

192 f"No exact match found for passed keyid {keyid}, found: {keyids}." 

193 ) 

194 

195 return (uri, public_key) 

196 

197 def sign(self, payload: bytes) -> Signature: 

198 """Signs payload with GnuPG. 

199 

200 Arguments: 

201 payload: bytes to be signed. 

202 

203 Raises: 

204 ValueError: gpg command failed to create a valid signature, e.g. 

205 because its keyid does not match the public key keyid. 

206 OSError: gpg command is not present, or non-executable, or returned 

207 a non-zero exit code. 

208 securesystemslib.exceptions.UnsupportedLibraryError: gpg command is not 

209 available, or the cryptography library is not installed. 

210 

211 Returns: 

212 Signature. 

213 

214 """ 

215 try: 

216 raw_sig = gpg.create_signature(payload, self.public_key.keyid, self.homedir) 

217 except gpg_exceptions.KeyNotFoundError as e: 

218 raise ValueError(e) from e 

219 

220 if raw_sig["keyid"] != self.public_key.keyid: 

221 raise ValueError( 

222 f"The signing key {raw_sig['keyid']} does not" 

223 f" match the attached public key {self.public_key.keyid}." 

224 ) 

225 

226 return self._sig_from_legacy_dict(raw_sig)