1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 __all__ = [
24 'DigestTooLargeError',
25 'HASH_ALGORITHMS',
26 'ARC_HASH_ALGORITHMS',
27 'parse_pem_private_key',
28 'parse_private_key',
29 'parse_public_key',
30 'RSASSA_PKCS1_v1_5_sign',
31 'RSASSA_PKCS1_v1_5_verify',
32 'UnparsableKeyError',
33 ]
34
35 import base64
36 import hashlib
37 import re
38
39 from dkim.asn1 import (
40 ASN1FormatError,
41 asn1_build,
42 asn1_parse,
43 BIT_STRING,
44 INTEGER,
45 SEQUENCE,
46 OBJECT_IDENTIFIER,
47 OCTET_STRING,
48 NULL,
49 )
50
51
52 ASN1_Object = [
53 (SEQUENCE, [
54 (SEQUENCE, [
55 (OBJECT_IDENTIFIER,),
56 (NULL,),
57 ]),
58 (BIT_STRING,),
59 ])
60 ]
61
62 ASN1_RSAPublicKey = [
63 (SEQUENCE, [
64 (INTEGER,),
65 (INTEGER,),
66 ])
67 ]
68
69 ASN1_RSAPrivateKey = [
70 (SEQUENCE, [
71 (INTEGER,),
72 (INTEGER,),
73 (INTEGER,),
74 (INTEGER,),
75 (INTEGER,),
76 (INTEGER,),
77 (INTEGER,),
78 (INTEGER,),
79 (INTEGER,),
80 ])
81 ]
82
83 HASH_ALGORITHMS = {
84 b'rsa-sha1': hashlib.sha1,
85 b'rsa-sha256': hashlib.sha256,
86 b'ed25519-sha256': hashlib.sha256
87 }
88
89 ARC_HASH_ALGORITHMS = {
90 b'rsa-sha256': hashlib.sha256,
91 }
92
93
94 HASH_ID_MAP = {
95 'sha1': b"\x2b\x0e\x03\x02\x1a",
96 'sha256': b"\x60\x86\x48\x01\x65\x03\x04\x02\x01",
97 }
98
99
103
104
106 """The data could not be parsed as a key."""
107 pass
108
109
111 """Parse an RSA public key.
112
113 @param data: DER-encoded X.509 subjectPublicKeyInfo
114 containing an RFC8017 RSAPublicKey.
115 @return: RSA public key
116 """
117 try:
118
119 x = asn1_parse(ASN1_Object, data)
120 pkd = asn1_parse(ASN1_RSAPublicKey, x[0][1][1:])
121 except ASN1FormatError as e_spki:
122 try:
123 pkd = asn1_parse(ASN1_RSAPublicKey, data)
124 except ASN1FormatError as e_rsa:
125 raise UnparsableKeyError('Unparsable public key; SubjectPublicKeyInfo: ' + str(e_spki) + '; RSAPublicKey: ' + str(e_rsa))
126 pk = {
127 'modulus': pkd[0][0],
128 'publicExponent': pkd[0][1],
129 }
130 return pk
131
132
134 """Parse an RSA private key.
135
136 @param data: DER-encoded RFC8017 RSAPrivateKey.
137 @return: RSA private key
138 """
139 try:
140 pka = asn1_parse(ASN1_RSAPrivateKey, data)
141 except ASN1FormatError as e:
142 raise UnparsableKeyError('Unparsable private key: ' + str(e))
143 pk = {
144 'version': pka[0][0],
145 'modulus': pka[0][1],
146 'publicExponent': pka[0][2],
147 'privateExponent': pka[0][3],
148 'prime1': pka[0][4],
149 'prime2': pka[0][5],
150 'exponent1': pka[0][6],
151 'exponent2': pka[0][7],
152 'coefficient': pka[0][8],
153 }
154 return pk
155
156
158 """Parse a PEM RSA private key.
159
160 @param data: RFC8017 RSAPrivateKey in PEM format.
161 @return: RSA private key
162 """
163 m = re.search(b"--\n(.*?)\n--", data, re.DOTALL)
164 if m is None:
165 raise UnparsableKeyError("Private key not found")
166 try:
167 pkdata = base64.b64decode(m.group(1))
168 except TypeError as e:
169 raise UnparsableKeyError(str(e))
170 return parse_private_key(pkdata)
171
172
174 """Encode a digest with RFC8017 EMSA-PKCS1-v1_5.
175
176 @param hash: hash object to encode
177 @param mlen: desired message length
178 @return: encoded digest byte string
179 """
180 dinfo = asn1_build(
181 (SEQUENCE, [
182 (SEQUENCE, [
183 (OBJECT_IDENTIFIER, HASH_ID_MAP[hash.name.lower()]),
184 (NULL, None),
185 ]),
186 (OCTET_STRING, hash.digest()),
187 ]))
188 if len(dinfo) + 11 > mlen:
189 raise DigestTooLargeError()
190 return b"\x00\x01"+b"\xff"*(mlen-len(dinfo)-3)+b"\x00"+dinfo
191
192
194 """Convert a byte string to an integer.
195
196 @param s: byte string representing a positive integer to convert
197 @return: converted integer
198 """
199 s = bytearray(s)
200 r = 0
201 for c in s:
202 r = (r << 8) | c
203 return r
204
205
207 """Convert an integer to a byte string.
208
209 @param n: positive integer to convert
210 @param length: minimum length
211 @return: converted bytestring, of at least the minimum length if it was
212 specified
213 """
214 assert n >= 0
215 r = bytearray()
216 while length < 0 or len(r) < length:
217 r.append(n & 0xff)
218 n >>= 8
219 if length < 0 and n == 0:
220 break
221 r.reverse()
222 assert length < 0 or len(r) == length
223 return r
224
225
227 """Perform RSA decryption/signing
228
229 @param message: byte string to operate on
230 @param pk: private key data
231 @param mlen: desired output length
232 @return: byte string result of the operation
233 """
234 c = str2int(message)
235
236 m1 = pow(c, pk['exponent1'], pk['prime1'])
237 m2 = pow(c, pk['exponent2'], pk['prime2'])
238
239 if m1 < m2:
240 h = pk['coefficient'] * (m1 + pk['prime1'] - m2) % pk['prime1']
241 else:
242 h = pk['coefficient'] * (m1 - m2) % pk['prime1']
243
244 return int2str(m2 + h * pk['prime2'], mlen)
245
246
248 """Perform RSA encryption/verification
249
250 @param message: byte string to operate on
251 @param pk: public key data
252 @param mlen: desired output length
253 @return: byte string result of the operation
254 """
255 m = str2int(message)
256 return int2str(pow(m, pk['publicExponent'], pk['modulus']), mlen)
257
258
260 """Sign a digest with RFC8017 RSASSA-PKCS1-v1_5.
261
262 @param hash: hash object to sign
263 @param private_key: private key data
264 @return: signed digest byte string
265 """
266 modlen = len(int2str(private_key['modulus']))
267 encoded_digest = EMSA_PKCS1_v1_5_encode(hash, modlen)
268 return rsa_decrypt(encoded_digest, private_key, modlen)
269
270
272 """Verify a digest signed with RFC8017 RSASSA-PKCS1-v1_5.
273
274 @param hash: hash object to check
275 @param signature: signed digest byte string
276 @param public_key: public key data
277 @return: True if the signature is valid, False otherwise
278 """
279 modlen = len(int2str(public_key['modulus']))
280 encoded_digest = EMSA_PKCS1_v1_5_encode(hash, modlen)
281 signed_digest = rsa_encrypt(signature, public_key, modlen)
282 return encoded_digest == signed_digest
283