Package dkim :: Module crypto
[hide private]
[frames] | no frames]

Source Code for Module dkim.crypto

  1  # This software is provided 'as-is', without any express or implied 
  2  # warranty.  In no event will the author be held liable for any damages 
  3  # arising from the use of this software. 
  4  # 
  5  # Permission is granted to anyone to use this software for any purpose, 
  6  # including commercial applications, and to alter it and redistribute it 
  7  # freely, subject to the following restrictions: 
  8  # 
  9  # 1. The origin of this software must not be misrepresented; you must not 
 10  #    claim that you wrote the original software. If you use this software 
 11  #    in a product, an acknowledgment in the product documentation would be 
 12  #    appreciated but is not required. 
 13  # 2. Altered source versions must be plainly marked as such, and must not be 
 14  #    misrepresented as being the original software. 
 15  # 3. This notice may not be removed or altered from any source distribution. 
 16  # 
 17  # Copyright (c) 2008 Greg Hewgill http://hewgill.com 
 18  # 
 19  # This has been modified from the original software. 
 20  # Copyright (c) 2011 William Grant <me@williamgrant.id.au> 
 21  # Copyright (c) 2018 Scott Kitterman <scott@kitterman.com> 
 22   
 23  __all__ = [ 
 24      'DigestTooLargeError', 
 25      'HASH_ALGORITHMS', 
 26      'parse_pem_private_key', 
 27      'parse_private_key', 
 28      'parse_public_key', 
 29      'RSASSA_PKCS1_v1_5_sign', 
 30      'RSASSA_PKCS1_v1_5_verify', 
 31      'UnparsableKeyError', 
 32      ] 
 33   
 34  import base64 
 35  import hashlib 
 36  import re 
 37   
 38  from dkim.asn1 import ( 
 39      ASN1FormatError, 
 40      asn1_build, 
 41      asn1_parse, 
 42      BIT_STRING, 
 43      INTEGER, 
 44      SEQUENCE, 
 45      OBJECT_IDENTIFIER, 
 46      OCTET_STRING, 
 47      NULL, 
 48      ) 
 49   
 50   
 51  ASN1_Object = [ 
 52      (SEQUENCE, [ 
 53          (SEQUENCE, [ 
 54              (OBJECT_IDENTIFIER,), 
 55              (NULL,), 
 56          ]), 
 57          (BIT_STRING,), 
 58      ]) 
 59  ] 
 60   
 61  ASN1_RSAPublicKey = [ 
 62      (SEQUENCE, [ 
 63          (INTEGER,), 
 64          (INTEGER,), 
 65      ]) 
 66  ] 
 67   
 68  ASN1_RSAPrivateKey = [ 
 69      (SEQUENCE, [ 
 70          (INTEGER,), 
 71          (INTEGER,), 
 72          (INTEGER,), 
 73          (INTEGER,), 
 74          (INTEGER,), 
 75          (INTEGER,), 
 76          (INTEGER,), 
 77          (INTEGER,), 
 78          (INTEGER,), 
 79      ]) 
 80  ] 
 81   
 82  HASH_ALGORITHMS = { 
 83      b'rsa-sha1': hashlib.sha1, 
 84      b'rsa-sha256': hashlib.sha256, 
 85      b'ed25519-sha256': hashlib.sha256 
 86      } 
 87   
 88  # These values come from RFC 8017, section 9.2 Notes, page 46. 
 89  HASH_ID_MAP = { 
 90      'sha1': b"\x2b\x0e\x03\x02\x1a", 
 91      'sha256': b"\x60\x86\x48\x01\x65\x03\x04\x02\x01", 
 92      } 
 93   
 94   
95 -class DigestTooLargeError(Exception):
96 """The digest is too large to fit within the requested length.""" 97 pass
98 99
100 -class UnparsableKeyError(Exception):
101 """The data could not be parsed as a key.""" 102 pass
103 104
105 -def parse_public_key(data):
106 """Parse an RSA public key. 107 108 @param data: DER-encoded X.509 subjectPublicKeyInfo 109 containing an RFC8017 RSAPublicKey. 110 @return: RSA public key 111 """ 112 try: 113 # Not sure why the [1:] is necessary to skip a byte. 114 x = asn1_parse(ASN1_Object, data) 115 pkd = asn1_parse(ASN1_RSAPublicKey, x[0][1][1:]) 116 except ASN1FormatError as e: 117 raise UnparsableKeyError('Unparsable public key: ' + str(e)) 118 pk = { 119 'modulus': pkd[0][0], 120 'publicExponent': pkd[0][1], 121 } 122 return pk
123 124
125 -def parse_private_key(data):
126 """Parse an RSA private key. 127 128 @param data: DER-encoded RFC8017 RSAPrivateKey. 129 @return: RSA private key 130 """ 131 try: 132 pka = asn1_parse(ASN1_RSAPrivateKey, data) 133 except ASN1FormatError as e: 134 raise UnparsableKeyError('Unparsable private key: ' + str(e)) 135 pk = { 136 'version': pka[0][0], 137 'modulus': pka[0][1], 138 'publicExponent': pka[0][2], 139 'privateExponent': pka[0][3], 140 'prime1': pka[0][4], 141 'prime2': pka[0][5], 142 'exponent1': pka[0][6], 143 'exponent2': pka[0][7], 144 'coefficient': pka[0][8], 145 } 146 return pk
147 148
149 -def parse_pem_private_key(data):
150 """Parse a PEM RSA private key. 151 152 @param data: RFC8017 RSAPrivateKey in PEM format. 153 @return: RSA private key 154 """ 155 m = re.search(b"--\n(.*?)\n--", data, re.DOTALL) 156 if m is None: 157 raise UnparsableKeyError("Private key not found") 158 try: 159 pkdata = base64.b64decode(m.group(1)) 160 except TypeError as e: 161 raise UnparsableKeyError(str(e)) 162 return parse_private_key(pkdata)
163 164
165 -def EMSA_PKCS1_v1_5_encode(hash, mlen):
166 """Encode a digest with RFC8017 EMSA-PKCS1-v1_5. 167 168 @param hash: hash object to encode 169 @param mlen: desired message length 170 @return: encoded digest byte string 171 """ 172 dinfo = asn1_build( 173 (SEQUENCE, [ 174 (SEQUENCE, [ 175 (OBJECT_IDENTIFIER, HASH_ID_MAP[hash.name.lower()]), 176 (NULL, None), 177 ]), 178 (OCTET_STRING, hash.digest()), 179 ])) 180 if len(dinfo) + 11 > mlen: 181 raise DigestTooLargeError() 182 return b"\x00\x01"+b"\xff"*(mlen-len(dinfo)-3)+b"\x00"+dinfo
183 184
185 -def str2int(s):
186 """Convert a byte string to an integer. 187 188 @param s: byte string representing a positive integer to convert 189 @return: converted integer 190 """ 191 s = bytearray(s) 192 r = 0 193 for c in s: 194 r = (r << 8) | c 195 return r
196 197
198 -def int2str(n, length=-1):
199 """Convert an integer to a byte string. 200 201 @param n: positive integer to convert 202 @param length: minimum length 203 @return: converted bytestring, of at least the minimum length if it was 204 specified 205 """ 206 assert n >= 0 207 r = bytearray() 208 while length < 0 or len(r) < length: 209 r.append(n & 0xff) 210 n >>= 8 211 if length < 0 and n == 0: 212 break 213 r.reverse() 214 assert length < 0 or len(r) == length 215 return r
216 217
218 -def rsa_decrypt(message, pk, mlen):
219 """Perform RSA decryption/signing 220 221 @param message: byte string to operate on 222 @param pk: private key data 223 @param mlen: desired output length 224 @return: byte string result of the operation 225 """ 226 c = str2int(message) 227 228 m1 = pow(c, pk['exponent1'], pk['prime1']) 229 m2 = pow(c, pk['exponent2'], pk['prime2']) 230 231 if m1 < m2: 232 h = pk['coefficient'] * (m1 + pk['prime1'] - m2) % pk['prime1'] 233 else: 234 h = pk['coefficient'] * (m1 - m2) % pk['prime1'] 235 236 return int2str(m2 + h * pk['prime2'], mlen)
237 238
239 -def rsa_encrypt(message, pk, mlen):
240 """Perform RSA encryption/verification 241 242 @param message: byte string to operate on 243 @param pk: public key data 244 @param mlen: desired output length 245 @return: byte string result of the operation 246 """ 247 m = str2int(message) 248 return int2str(pow(m, pk['publicExponent'], pk['modulus']), mlen)
249 250
251 -def RSASSA_PKCS1_v1_5_sign(hash, private_key):
252 """Sign a digest with RFC8017 RSASSA-PKCS1-v1_5. 253 254 @param hash: hash object to sign 255 @param private_key: private key data 256 @return: signed digest byte string 257 """ 258 modlen = len(int2str(private_key['modulus'])) 259 encoded_digest = EMSA_PKCS1_v1_5_encode(hash, modlen) 260 return rsa_decrypt(encoded_digest, private_key, modlen)
261 262
263 -def RSASSA_PKCS1_v1_5_verify(hash, signature, public_key):
264 """Verify a digest signed with RFC8017 RSASSA-PKCS1-v1_5. 265 266 @param hash: hash object to check 267 @param signature: signed digest byte string 268 @param public_key: public key data 269 @return: True if the signature is valid, False otherwise 270 """ 271 modlen = len(int2str(public_key['modulus'])) 272 encoded_digest = EMSA_PKCS1_v1_5_encode(hash, modlen) 273 signed_digest = rsa_encrypt(signature, public_key, modlen) 274 return encoded_digest == signed_digest
275