1"""
2<Module Name>
3 rsa.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 RSA-specific handling routines for signature verification and key parsing
16"""
17
18import binascii
19
20CRYPTO = True
21NO_CRYPTO_MSG = "RSA key support for GPG requires the cryptography library"
22try:
23 from cryptography.exceptions import InvalidSignature
24 from cryptography.hazmat import backends
25 from cryptography.hazmat.primitives.asymmetric import padding, rsa, utils
26except ImportError:
27 CRYPTO = False
28
29# ruff: noqa: E402
30from securesystemslib import exceptions
31from securesystemslib._gpg import util as gpg_util
32from securesystemslib._gpg.exceptions import PacketParsingError
33
34
35def create_pubkey(pubkey_info):
36 """
37 <Purpose>
38 Create and return an RSAPublicKey object from the passed pubkey_info
39 using pyca/cryptography.
40
41 <Arguments>
42 pubkey_info:
43 An RSA pubkey dict.
44
45 <Exceptions>
46 securesystemslib.exceptions.UnsupportedLibraryError if
47 the cryptography module is unavailable
48
49 <Returns>
50 A cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey based on the
51 passed pubkey_info.
52
53 """
54 if not CRYPTO: # pragma: no cover
55 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG)
56
57 e = int(pubkey_info["keyval"]["public"]["e"], 16)
58 n = int(pubkey_info["keyval"]["public"]["n"], 16)
59 pubkey = rsa.RSAPublicNumbers(e, n).public_key(backends.default_backend())
60
61 return pubkey
62
63
64def get_pubkey_params(data):
65 """
66 <Purpose>
67 Parse the public key parameters as multi-precision-integers.
68
69 <Arguments>
70 data:
71 the RFC4880-encoded public key parameters data buffer as described
72 in the fifth paragraph of section 5.5.2.
73
74 <Exceptions>
75 securesystemslib._gpg.exceptions.PacketParsingError:
76 if the public key parameters are malformed
77
78 <Side Effects>
79 None.
80
81 <Returns>
82 An RSA public key dict.
83 """
84 ptr = 0
85
86 modulus_length = gpg_util.get_mpi_length(data[ptr : ptr + 2])
87 ptr += 2
88 modulus = data[ptr : ptr + modulus_length]
89 if len(modulus) != modulus_length: # pragma: no cover
90 raise PacketParsingError("This modulus MPI was truncated!")
91 ptr += modulus_length
92
93 exponent_e_length = gpg_util.get_mpi_length(data[ptr : ptr + 2])
94 ptr += 2
95 exponent_e = data[ptr : ptr + exponent_e_length]
96 if len(exponent_e) != exponent_e_length: # pragma: no cover
97 raise PacketParsingError("This e MPI has been truncated!")
98
99 return {
100 "e": binascii.hexlify(exponent_e).decode("ascii"),
101 "n": binascii.hexlify(modulus).decode("ascii"),
102 }
103
104
105def get_signature_params(data):
106 """
107 <Purpose>
108 Parse the signature parameters as multi-precision-integers.
109
110 <Arguments>
111 data:
112 the RFC4880-encoded signature data buffer as described
113 in the third paragraph of section 5.2.2.
114
115 <Exceptions>
116 securesystemslib._gpg.exceptions.PacketParsingError:
117 if the public key parameters are malformed
118
119 <Side Effects>
120 None.
121
122 <Returns>
123 The decoded signature buffer
124 """
125
126 ptr = 0
127 signature_length = gpg_util.get_mpi_length(data[ptr : ptr + 2])
128 ptr += 2
129 signature = data[ptr : ptr + signature_length]
130 if len(signature) != signature_length: # pragma: no cover
131 raise PacketParsingError("This signature was truncated!")
132
133 return signature
134
135
136def verify_signature(signature_object, pubkey_info, content, hash_algorithm_id):
137 """
138 <Purpose>
139 Verify the passed signature against the passed content with the passed
140 RSA public key using pyca/cryptography.
141
142 <Arguments>
143 signature_object:
144 A signature dict.
145
146 pubkey_info:
147 The RSA public key dict.
148
149 content:
150 The signed bytes against which the signature is verified
151
152 hash_algorithm_id:
153 one of SHA1, SHA256, SHA512 (see securesystemslib._gpg.constants)
154 used to verify the signature
155 NOTE: Overrides any hash algorithm specification in "pubkey_info"'s
156 "hashes" or "method" fields.
157
158 <Exceptions>
159 securesystemslib.exceptions.UnsupportedLibraryError if:
160 the cryptography module is unavailable
161
162 ValueError:
163 if the passed hash_algorithm_id is not supported (see
164 securesystemslib._gpg.util.get_hashing_class)
165
166 <Returns>
167 True if signature verification passes and False otherwise
168
169 """
170 if not CRYPTO: # pragma: no cover
171 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG)
172
173 hasher = gpg_util.get_hashing_class(hash_algorithm_id)
174
175 pubkey_object = create_pubkey(pubkey_info)
176
177 # zero-pad the signature due to a discrepancy between the openssl backend
178 # and the gnupg interpretation of PKCSv1.5. Read more at:
179 # https://github.com/in-toto/in-toto/issues/171#issuecomment-440039256
180 # we are skipping this if on the tests because well, how would one test this
181 # deterministically.
182 pubkey_length = len(pubkey_info["keyval"]["public"]["n"])
183 signature_length = len(signature_object["signature"])
184 if pubkey_length != signature_length: # pragma: no cover
185 zero_pad = "0" * (pubkey_length - signature_length)
186 signature_object["signature"] = "{}{}".format(
187 zero_pad, signature_object["signature"]
188 )
189
190 digest = gpg_util.hash_object(
191 binascii.unhexlify(signature_object["other_headers"]), hasher(), content
192 )
193
194 try:
195 pubkey_object.verify(
196 binascii.unhexlify(signature_object["signature"]),
197 digest,
198 padding.PKCS1v15(),
199 utils.Prehashed(hasher()),
200 )
201 return True
202 except InvalidSignature:
203 return False