Factor out most crypto bits into their own functions and modules, where they can sensibly be tested.
This commit is contained in:
+29
-207
@@ -15,6 +15,7 @@
|
|||||||
# 3. This notice may not be removed or altered from any source distribution.
|
# 3. This notice may not be removed or altered from any source distribution.
|
||||||
#
|
#
|
||||||
# Copyright (c) 2008 Greg Hewgill http://hewgill.com
|
# Copyright (c) 2008 Greg Hewgill http://hewgill.com
|
||||||
|
# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -23,6 +24,14 @@ import time
|
|||||||
|
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
|
|
||||||
|
from dkim.crypto import (
|
||||||
|
DigestTooLargeError,
|
||||||
|
parse_private_key,
|
||||||
|
parse_public_key,
|
||||||
|
RSASSA_PKCS1_v1_5_sign,
|
||||||
|
RSASSA_PKCS1_v1_5_verify,
|
||||||
|
UnparsableKeyError,
|
||||||
|
)
|
||||||
from dkim.util import (
|
from dkim.util import (
|
||||||
InvalidTagValueList,
|
InvalidTagValueList,
|
||||||
parse_tag_value,
|
parse_tag_value,
|
||||||
@@ -99,29 +108,6 @@ def _remove(s, t):
|
|||||||
assert i >= 0
|
assert i >= 0
|
||||||
return s[:i] + s[i+len(t):]
|
return s[:i] + s[i+len(t):]
|
||||||
|
|
||||||
|
|
||||||
def EMSA_PKCS1_v1_5_encode(digest, modlen, hashid):
|
|
||||||
"""Encode a digest with EMSA-PKCS1-v1_5.
|
|
||||||
|
|
||||||
Defined in RFC3447 section 9.2.
|
|
||||||
|
|
||||||
@param digest: A digest value to encode.
|
|
||||||
@param modlen: The desired message length.
|
|
||||||
@param hashid: The ID of the hash used to generate the digest.
|
|
||||||
"""
|
|
||||||
dinfo = asn1_build(
|
|
||||||
(SEQUENCE, [
|
|
||||||
(SEQUENCE, [
|
|
||||||
(OBJECT_IDENTIFIER, hashid),
|
|
||||||
(NULL, None),
|
|
||||||
]),
|
|
||||||
(OCTET_STRING, digest),
|
|
||||||
]))
|
|
||||||
if len(dinfo)+3 > modlen:
|
|
||||||
raise ParameterError("Hash too large for modulus")
|
|
||||||
return "\x00\x01"+"\xff"*(modlen-len(dinfo)-3)+"\x00"+dinfo
|
|
||||||
|
|
||||||
|
|
||||||
def hash_headers(hasher, canonicalize_headers, headers, include_headers,
|
def hash_headers(hasher, canonicalize_headers, headers, include_headers,
|
||||||
sigheaders, sig):
|
sigheaders, sig):
|
||||||
"""Sign message header fields."""
|
"""Sign message header fields."""
|
||||||
@@ -146,22 +132,6 @@ def hash_headers(hasher, canonicalize_headers, headers, include_headers,
|
|||||||
hasher.update(x[1])
|
hasher.update(x[1])
|
||||||
|
|
||||||
|
|
||||||
def parse_public_key(data):
|
|
||||||
"""Parse an RSA public key.
|
|
||||||
|
|
||||||
@param data: A DER-encoded X.509 subjectPublicKeyInfo
|
|
||||||
containing an RFC3447 RSAPublicKey.
|
|
||||||
"""
|
|
||||||
x = asn1_parse(ASN1_Object, data)
|
|
||||||
# Not sure why the [1:] is necessary to skip a byte.
|
|
||||||
pkd = asn1_parse(ASN1_RSAPublicKey, x[0][1][1:])
|
|
||||||
pk = {
|
|
||||||
'modulus': pkd[0][0],
|
|
||||||
'publicExponent': pkd[0][1],
|
|
||||||
}
|
|
||||||
return pk
|
|
||||||
|
|
||||||
|
|
||||||
def validate_signature_fields(sig, debuglog=None):
|
def validate_signature_fields(sig, debuglog=None):
|
||||||
"""Validate DKIM-Signature fields.
|
"""Validate DKIM-Signature fields.
|
||||||
|
|
||||||
@@ -217,148 +187,10 @@ def validate_signature_fields(sig, debuglog=None):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
INTEGER = 0x02
|
|
||||||
BIT_STRING = 0x03
|
|
||||||
OCTET_STRING = 0x04
|
|
||||||
NULL = 0x05
|
|
||||||
OBJECT_IDENTIFIER = 0x06
|
|
||||||
SEQUENCE = 0x30
|
|
||||||
|
|
||||||
ASN1_Object = [
|
|
||||||
(SEQUENCE, [
|
|
||||||
(SEQUENCE, [
|
|
||||||
(OBJECT_IDENTIFIER,),
|
|
||||||
(NULL,),
|
|
||||||
]),
|
|
||||||
(BIT_STRING,),
|
|
||||||
])
|
|
||||||
]
|
|
||||||
|
|
||||||
ASN1_RSAPublicKey = [
|
|
||||||
(SEQUENCE, [
|
|
||||||
(INTEGER,),
|
|
||||||
(INTEGER,),
|
|
||||||
])
|
|
||||||
]
|
|
||||||
|
|
||||||
ASN1_RSAPrivateKey = [
|
|
||||||
(SEQUENCE, [
|
|
||||||
(INTEGER,),
|
|
||||||
(INTEGER,),
|
|
||||||
(INTEGER,),
|
|
||||||
(INTEGER,),
|
|
||||||
(INTEGER,),
|
|
||||||
(INTEGER,),
|
|
||||||
(INTEGER,),
|
|
||||||
(INTEGER,),
|
|
||||||
(INTEGER,),
|
|
||||||
])
|
|
||||||
]
|
|
||||||
|
|
||||||
def asn1_parse(template, data):
|
|
||||||
"""Parse a data structure according to ASN.1 template.
|
|
||||||
|
|
||||||
@param template: A list of tuples comprising the ASN.1 template.
|
|
||||||
@param data: A list of bytes to parse.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
r = []
|
|
||||||
i = 0
|
|
||||||
for t in template:
|
|
||||||
tag = ord(data[i])
|
|
||||||
i += 1
|
|
||||||
if tag == t[0]:
|
|
||||||
length = ord(data[i])
|
|
||||||
i += 1
|
|
||||||
if length & 0x80:
|
|
||||||
n = length & 0x7f
|
|
||||||
length = 0
|
|
||||||
for j in range(n):
|
|
||||||
length = (length << 8) | ord(data[i])
|
|
||||||
i += 1
|
|
||||||
if tag == INTEGER:
|
|
||||||
n = 0
|
|
||||||
for j in range(length):
|
|
||||||
n = (n << 8) | ord(data[i])
|
|
||||||
i += 1
|
|
||||||
r.append(n)
|
|
||||||
elif tag == BIT_STRING:
|
|
||||||
r.append(data[i:i+length])
|
|
||||||
i += length
|
|
||||||
elif tag == NULL:
|
|
||||||
assert length == 0
|
|
||||||
r.append(None)
|
|
||||||
elif tag == OBJECT_IDENTIFIER:
|
|
||||||
r.append(data[i:i+length])
|
|
||||||
i += length
|
|
||||||
elif tag == SEQUENCE:
|
|
||||||
r.append(asn1_parse(t[1], data[i:i+length]))
|
|
||||||
i += length
|
|
||||||
else:
|
|
||||||
raise KeyFormatError("Unexpected tag in template: %02x" % tag)
|
|
||||||
else:
|
|
||||||
raise KeyFormatError("Unexpected tag (got %02x, expecting %02x)" % (tag, t[0]))
|
|
||||||
return r
|
|
||||||
|
|
||||||
def asn1_length(n):
|
|
||||||
"""Return a string representing a field length in ASN.1 format."""
|
|
||||||
assert n >= 0
|
|
||||||
if n < 0x7f:
|
|
||||||
return chr(n)
|
|
||||||
r = ""
|
|
||||||
while n > 0:
|
|
||||||
r = chr(n & 0xff) + r
|
|
||||||
n >>= 8
|
|
||||||
return r
|
|
||||||
|
|
||||||
def asn1_build(node):
|
|
||||||
"""Build an ASN.1 data structure based on pairs of (type, data)."""
|
|
||||||
if node[0] == OCTET_STRING:
|
|
||||||
return chr(OCTET_STRING) + asn1_length(len(node[1])) + node[1]
|
|
||||||
if node[0] == NULL:
|
|
||||||
assert node[1] is None
|
|
||||||
return chr(NULL) + asn1_length(0)
|
|
||||||
elif node[0] == OBJECT_IDENTIFIER:
|
|
||||||
return chr(OBJECT_IDENTIFIER) + asn1_length(len(node[1])) + node[1]
|
|
||||||
elif node[0] == SEQUENCE:
|
|
||||||
r = ""
|
|
||||||
for x in node[1]:
|
|
||||||
r += asn1_build(x)
|
|
||||||
return chr(SEQUENCE) + asn1_length(len(r)) + r
|
|
||||||
else:
|
|
||||||
raise InternalError("Unexpected tag in template: %02x" % node[0])
|
|
||||||
|
|
||||||
# These values come from RFC 3447, section 9.2 Notes, page 43.
|
# These values come from RFC 3447, section 9.2 Notes, page 43.
|
||||||
HASHID_SHA1 = "\x2b\x0e\x03\x02\x1a"
|
HASHID_SHA1 = "\x2b\x0e\x03\x02\x1a"
|
||||||
HASHID_SHA256 = "\x60\x86\x48\x01\x65\x03\x04\x02\x01"
|
HASHID_SHA256 = "\x60\x86\x48\x01\x65\x03\x04\x02\x01"
|
||||||
|
|
||||||
def str2int(s):
|
|
||||||
"""Convert an octet string to an integer. Octet string assumed to represent a positive integer."""
|
|
||||||
r = 0
|
|
||||||
for c in s:
|
|
||||||
r = (r << 8) | ord(c)
|
|
||||||
return r
|
|
||||||
|
|
||||||
def int2str(n, length = -1):
|
|
||||||
"""Convert an integer to an octet string. Number must be positive.
|
|
||||||
|
|
||||||
@param n: Number to convert.
|
|
||||||
@param length: Minimum length, or -1 to return the smallest number of bytes that represent the integer.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert n >= 0
|
|
||||||
r = []
|
|
||||||
while length < 0 or len(r) < length:
|
|
||||||
r.append(chr(n & 0xff))
|
|
||||||
n >>= 8
|
|
||||||
if length < 0 and n == 0: break
|
|
||||||
r.reverse()
|
|
||||||
assert length < 0 or len(r) == length
|
|
||||||
return r
|
|
||||||
|
|
||||||
def rfc822_parse(message):
|
def rfc822_parse(message):
|
||||||
"""Parse a message in RFC822 format.
|
"""Parse a message in RFC822 format.
|
||||||
|
|
||||||
@@ -443,18 +275,10 @@ def sign(message, selector, domain, privkey, identity=None, canonicalize=(Simple
|
|||||||
raise KeyFormatError(str(e))
|
raise KeyFormatError(str(e))
|
||||||
if debuglog is not None:
|
if debuglog is not None:
|
||||||
print >>debuglog, " ".join("%02x" % ord(x) for x in pkdata)
|
print >>debuglog, " ".join("%02x" % ord(x) for x in pkdata)
|
||||||
pka = asn1_parse(ASN1_RSAPrivateKey, pkdata)
|
try:
|
||||||
pk = {
|
pk = parse_private_key(pkdata)
|
||||||
'version': pka[0][0],
|
except UnparsableKeyError, e:
|
||||||
'modulus': pka[0][1],
|
raise KeyFormatError(str(e))
|
||||||
'publicExponent': pka[0][2],
|
|
||||||
'privateExponent': pka[0][3],
|
|
||||||
'prime1': pka[0][4],
|
|
||||||
'prime2': pka[0][5],
|
|
||||||
'exponent1': pka[0][6],
|
|
||||||
'exponent2': pka[0][7],
|
|
||||||
'coefficient': pka[0][8],
|
|
||||||
}
|
|
||||||
|
|
||||||
if identity is not None and not identity.endswith(domain):
|
if identity is not None and not identity.endswith(domain):
|
||||||
raise ParameterError("identity must end with domain")
|
raise ParameterError("identity must end with domain")
|
||||||
@@ -503,10 +327,12 @@ def sign(message, selector, domain, privkey, identity=None, canonicalize=(Simple
|
|||||||
if debuglog is not None:
|
if debuglog is not None:
|
||||||
print >>debuglog, "sign digest:", " ".join("%02x" % ord(x) for x in d)
|
print >>debuglog, "sign digest:", " ".join("%02x" % ord(x) for x in d)
|
||||||
|
|
||||||
modlen = len(int2str(pk['modulus']))
|
try:
|
||||||
encoded = EMSA_PKCS1_v1_5_encode(d, modlen, HASHID_SHA256)
|
sig2 = RSASSA_PKCS1_v1_5_sign(
|
||||||
sig2 = int2str(pow(str2int(encoded), pk['privateExponent'], pk['modulus']), modlen)
|
d, HASHID_SHA256, pk['privateExponent'], pk['modulus'])
|
||||||
sig += base64.b64encode(''.join(sig2))
|
except DigestTooLargeError:
|
||||||
|
raise ParameterError("digest too large for modulus")
|
||||||
|
sig += base64.b64encode(sig2)
|
||||||
|
|
||||||
return sig + "\r\n"
|
return sig + "\r\n"
|
||||||
|
|
||||||
@@ -597,10 +423,12 @@ def verify(message, debuglog=None, dnsfunc=dnstxt):
|
|||||||
pub = parse_tag_value(s)
|
pub = parse_tag_value(s)
|
||||||
except InvalidTagValueList:
|
except InvalidTagValueList:
|
||||||
return False
|
return False
|
||||||
|
try:
|
||||||
pk = parse_public_key(base64.b64decode(pub['p']))
|
pk = parse_public_key(base64.b64decode(pub['p']))
|
||||||
modlen = len(int2str(pk['modulus']))
|
except UnparsableKeyError, e:
|
||||||
if debuglog is not None:
|
if debuglog is not None:
|
||||||
print >>debuglog, "modlen:", modlen
|
print >>debuglog, "could not parse public key: %s" % e
|
||||||
|
return False
|
||||||
|
|
||||||
include_headers = re.split(r"\s*:\s*", sig['h'])
|
include_headers = re.split(r"\s*:\s*", sig['h'])
|
||||||
h = hasher()
|
h = hasher()
|
||||||
@@ -609,17 +437,11 @@ def verify(message, debuglog=None, dnsfunc=dnstxt):
|
|||||||
d = h.digest()
|
d = h.digest()
|
||||||
if debuglog is not None:
|
if debuglog is not None:
|
||||||
print >>debuglog, "verify digest:", " ".join("%02x" % ord(x) for x in d)
|
print >>debuglog, "verify digest:", " ".join("%02x" % ord(x) for x in d)
|
||||||
|
signature = base64.b64decode(re.sub(r"\s+", "", sig['b']))
|
||||||
try:
|
try:
|
||||||
sig2 = EMSA_PKCS1_v1_5_encode(d, modlen, hashid)
|
return RSASSA_PKCS1_v1_5_verify(
|
||||||
except ParameterError:
|
d, hashid, signature, pk['publicExponent'], pk['modulus'])
|
||||||
|
except DigestTooLargeError:
|
||||||
|
if debuglog is not None:
|
||||||
|
print >>debuglog, "digest too large for modulus"
|
||||||
return False
|
return False
|
||||||
if debuglog is not None:
|
|
||||||
print >>debuglog, "sig2:", " ".join("%02x" % ord(x) for x in sig2)
|
|
||||||
print >>debuglog, sig['b']
|
|
||||||
print >>debuglog, re.sub(r"\s+", "", sig['b'])
|
|
||||||
v = int2str(pow(str2int(base64.b64decode(re.sub(r"\s+", "", sig['b']))), pk['publicExponent'], pk['modulus']), modlen)
|
|
||||||
if debuglog is not None:
|
|
||||||
print >>debuglog, "v:", " ".join("%02x" % ord(x) for x in v)
|
|
||||||
assert len(v) == len(sig2)
|
|
||||||
# Byte-by-byte compare of signatures
|
|
||||||
return not [1 for x in zip(v, sig2) if x[0] != x[1]]
|
|
||||||
|
|||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
# This software is provided 'as-is', without any express or implied
|
||||||
|
# warranty. In no event will the author be held liable for any damages
|
||||||
|
# arising from the use of this software.
|
||||||
|
#
|
||||||
|
# Permission is granted to anyone to use this software for any purpose,
|
||||||
|
# including commercial applications, and to alter it and redistribute it
|
||||||
|
# freely, subject to the following restrictions:
|
||||||
|
#
|
||||||
|
# 1. The origin of this software must not be misrepresented; you must not
|
||||||
|
# claim that you wrote the original software. If you use this software
|
||||||
|
# in a product, an acknowledgment in the product documentation would be
|
||||||
|
# appreciated but is not required.
|
||||||
|
# 2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
# misrepresented as being the original software.
|
||||||
|
# 3. This notice may not be removed or altered from any source distribution.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Greg Hewgill http://hewgill.com
|
||||||
|
# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'asn1_build',
|
||||||
|
'asn1_parse',
|
||||||
|
'ASN1FormatError',
|
||||||
|
'BIT_STRING',
|
||||||
|
'INTEGER',
|
||||||
|
'SEQUENCE',
|
||||||
|
'OBJECT_IDENTIFIER',
|
||||||
|
'OCTET_STRING',
|
||||||
|
'NULL',
|
||||||
|
]
|
||||||
|
|
||||||
|
INTEGER = 0x02
|
||||||
|
BIT_STRING = 0x03
|
||||||
|
OCTET_STRING = 0x04
|
||||||
|
NULL = 0x05
|
||||||
|
OBJECT_IDENTIFIER = 0x06
|
||||||
|
SEQUENCE = 0x30
|
||||||
|
|
||||||
|
|
||||||
|
class ASN1FormatError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def asn1_parse(template, data):
|
||||||
|
"""Parse a data structure according to an ASN.1 template.
|
||||||
|
|
||||||
|
@param template: tuples comprising the ASN.1 template
|
||||||
|
@param data: byte string data to parse
|
||||||
|
@return: decoded structure
|
||||||
|
"""
|
||||||
|
|
||||||
|
r = []
|
||||||
|
i = 0
|
||||||
|
for t in template:
|
||||||
|
tag = ord(data[i])
|
||||||
|
i += 1
|
||||||
|
if tag == t[0]:
|
||||||
|
length = ord(data[i])
|
||||||
|
i += 1
|
||||||
|
if length & 0x80:
|
||||||
|
n = length & 0x7f
|
||||||
|
length = 0
|
||||||
|
for j in range(n):
|
||||||
|
length = (length << 8) | ord(data[i])
|
||||||
|
i += 1
|
||||||
|
if tag == INTEGER:
|
||||||
|
n = 0
|
||||||
|
for j in range(length):
|
||||||
|
n = (n << 8) | ord(data[i])
|
||||||
|
i += 1
|
||||||
|
r.append(n)
|
||||||
|
elif tag == BIT_STRING:
|
||||||
|
r.append(data[i:i+length])
|
||||||
|
i += length
|
||||||
|
elif tag == NULL:
|
||||||
|
assert length == 0
|
||||||
|
r.append(None)
|
||||||
|
elif tag == OBJECT_IDENTIFIER:
|
||||||
|
r.append(data[i:i+length])
|
||||||
|
i += length
|
||||||
|
elif tag == SEQUENCE:
|
||||||
|
r.append(asn1_parse(t[1], data[i:i+length]))
|
||||||
|
i += length
|
||||||
|
else:
|
||||||
|
raise ASN1FormatError(
|
||||||
|
"Unexpected tag in template: %02x" % tag)
|
||||||
|
else:
|
||||||
|
raise ASN1FormatError(
|
||||||
|
"Unexpected tag (got %02x, expecting %02x)" % (tag, t[0]))
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def asn1_length(n):
|
||||||
|
"""Return a string representing a field length in ASN.1 format.
|
||||||
|
|
||||||
|
@param n: integer field length
|
||||||
|
@return: ASN.1 field length
|
||||||
|
"""
|
||||||
|
assert n >= 0
|
||||||
|
if n < 0x7f:
|
||||||
|
return chr(n)
|
||||||
|
r = ""
|
||||||
|
while n > 0:
|
||||||
|
r = chr(n & 0xff) + r
|
||||||
|
n >>= 8
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def asn1_build(node):
|
||||||
|
"""Build a DER-encoded ASN.1 data structure.
|
||||||
|
|
||||||
|
@param node: (type, data) tuples comprising the ASN.1 structure
|
||||||
|
@return: DER-encoded ASN.1 byte string
|
||||||
|
"""
|
||||||
|
if node[0] == OCTET_STRING:
|
||||||
|
return chr(OCTET_STRING) + asn1_length(len(node[1])) + node[1]
|
||||||
|
if node[0] == NULL:
|
||||||
|
assert node[1] is None
|
||||||
|
return chr(NULL) + asn1_length(0)
|
||||||
|
elif node[0] == OBJECT_IDENTIFIER:
|
||||||
|
return chr(OBJECT_IDENTIFIER) + asn1_length(len(node[1])) + node[1]
|
||||||
|
elif node[0] == SEQUENCE:
|
||||||
|
r = ""
|
||||||
|
for x in node[1]:
|
||||||
|
r += asn1_build(x)
|
||||||
|
return chr(SEQUENCE) + asn1_length(len(r)) + r
|
||||||
|
else:
|
||||||
|
raise ASN1FormatError("Unexpected tag in template: %02x" % node[0])
|
||||||
+221
@@ -0,0 +1,221 @@
|
|||||||
|
# This software is provided 'as-is', without any express or implied
|
||||||
|
# warranty. In no event will the author be held liable for any damages
|
||||||
|
# arising from the use of this software.
|
||||||
|
#
|
||||||
|
# Permission is granted to anyone to use this software for any purpose,
|
||||||
|
# including commercial applications, and to alter it and redistribute it
|
||||||
|
# freely, subject to the following restrictions:
|
||||||
|
#
|
||||||
|
# 1. The origin of this software must not be misrepresented; you must not
|
||||||
|
# claim that you wrote the original software. If you use this software
|
||||||
|
# in a product, an acknowledgment in the product documentation would be
|
||||||
|
# appreciated but is not required.
|
||||||
|
# 2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
# misrepresented as being the original software.
|
||||||
|
# 3. This notice may not be removed or altered from any source distribution.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Greg Hewgill http://hewgill.com
|
||||||
|
# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'DigestTooLargeError',
|
||||||
|
'parse_private_key',
|
||||||
|
'parse_public_key',
|
||||||
|
'RSASSA_PKCS1_v1_5_sign',
|
||||||
|
'RSASSA_PKCS1_v1_5_verify',
|
||||||
|
'UnparsableKeyError',
|
||||||
|
]
|
||||||
|
|
||||||
|
from dkim.asn1 import (
|
||||||
|
ASN1FormatError,
|
||||||
|
asn1_build,
|
||||||
|
asn1_parse,
|
||||||
|
BIT_STRING,
|
||||||
|
INTEGER,
|
||||||
|
SEQUENCE,
|
||||||
|
OBJECT_IDENTIFIER,
|
||||||
|
OCTET_STRING,
|
||||||
|
NULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ASN1_Object = [
|
||||||
|
(SEQUENCE, [
|
||||||
|
(SEQUENCE, [
|
||||||
|
(OBJECT_IDENTIFIER,),
|
||||||
|
(NULL,),
|
||||||
|
]),
|
||||||
|
(BIT_STRING,),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
|
||||||
|
ASN1_RSAPublicKey = [
|
||||||
|
(SEQUENCE, [
|
||||||
|
(INTEGER,),
|
||||||
|
(INTEGER,),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
|
||||||
|
ASN1_RSAPrivateKey = [
|
||||||
|
(SEQUENCE, [
|
||||||
|
(INTEGER,),
|
||||||
|
(INTEGER,),
|
||||||
|
(INTEGER,),
|
||||||
|
(INTEGER,),
|
||||||
|
(INTEGER,),
|
||||||
|
(INTEGER,),
|
||||||
|
(INTEGER,),
|
||||||
|
(INTEGER,),
|
||||||
|
(INTEGER,),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class DigestTooLargeError(Exception):
|
||||||
|
"""The digest is too large to fit within the requested length."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnparsableKeyError(Exception):
|
||||||
|
"""The data could not be parsed as a key."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_public_key(data):
|
||||||
|
"""Parse an RSA public key.
|
||||||
|
|
||||||
|
@param data: DER-encoded X.509 subjectPublicKeyInfo
|
||||||
|
containing an RFC3447 RSAPublicKey.
|
||||||
|
@return: RSA public key
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Not sure why the [1:] is necessary to skip a byte.
|
||||||
|
x = asn1_parse(ASN1_Object, data)
|
||||||
|
pkd = asn1_parse(ASN1_RSAPublicKey, x[0][1][1:])
|
||||||
|
except ASN1FormatError, e:
|
||||||
|
raise UnparsableKeyError(str(e))
|
||||||
|
pk = {
|
||||||
|
'modulus': pkd[0][0],
|
||||||
|
'publicExponent': pkd[0][1],
|
||||||
|
}
|
||||||
|
return pk
|
||||||
|
|
||||||
|
|
||||||
|
def parse_private_key(data):
|
||||||
|
"""Parse an RSA private key.
|
||||||
|
|
||||||
|
@param data: DER-encoded RFC3447 RSAPrivateKey.
|
||||||
|
@return: RSA private key
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pka = asn1_parse(ASN1_RSAPrivateKey, data)
|
||||||
|
except ASN1FormatError, e:
|
||||||
|
raise UnparsableKeyError(str(e))
|
||||||
|
pk = {
|
||||||
|
'version': pka[0][0],
|
||||||
|
'modulus': pka[0][1],
|
||||||
|
'publicExponent': pka[0][2],
|
||||||
|
'privateExponent': pka[0][3],
|
||||||
|
'prime1': pka[0][4],
|
||||||
|
'prime2': pka[0][5],
|
||||||
|
'exponent1': pka[0][6],
|
||||||
|
'exponent2': pka[0][7],
|
||||||
|
'coefficient': pka[0][8],
|
||||||
|
}
|
||||||
|
return pk
|
||||||
|
|
||||||
|
|
||||||
|
def EMSA_PKCS1_v1_5_encode(digest, mlen, hashid):
|
||||||
|
"""Encode a digest with RFC3447 EMSA-PKCS1-v1_5.
|
||||||
|
|
||||||
|
@param digest: digest byte string to encode
|
||||||
|
@param mlen: desired message length
|
||||||
|
@param hashid: ID of the hash used to generate the digest
|
||||||
|
@return: encoded digest byte string
|
||||||
|
"""
|
||||||
|
dinfo = asn1_build(
|
||||||
|
(SEQUENCE, [
|
||||||
|
(SEQUENCE, [
|
||||||
|
(OBJECT_IDENTIFIER, hashid),
|
||||||
|
(NULL, None),
|
||||||
|
]),
|
||||||
|
(OCTET_STRING, digest),
|
||||||
|
]))
|
||||||
|
if len(dinfo)+3 > mlen:
|
||||||
|
raise DigestTooLargeError()
|
||||||
|
return "\x00\x01"+"\xff"*(mlen-len(dinfo)-3)+"\x00"+dinfo
|
||||||
|
|
||||||
|
|
||||||
|
def str2int(s):
|
||||||
|
"""Convert a byte string to an integer.
|
||||||
|
|
||||||
|
@param s: byte string representing a positive integer to convert
|
||||||
|
@return: converted integer
|
||||||
|
"""
|
||||||
|
r = 0
|
||||||
|
for c in s:
|
||||||
|
r = (r << 8) | ord(c)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def int2str(n, length=-1):
|
||||||
|
"""Convert an integer to a byte string.
|
||||||
|
|
||||||
|
@param n: positive integer to convert
|
||||||
|
@param length: minimum length
|
||||||
|
@return: converted bytestring, of at least the minimum length if it was
|
||||||
|
specified
|
||||||
|
"""
|
||||||
|
assert n >= 0
|
||||||
|
r = []
|
||||||
|
while length < 0 or len(r) < length:
|
||||||
|
r.append(chr(n & 0xff))
|
||||||
|
n >>= 8
|
||||||
|
if length < 0 and n == 0:
|
||||||
|
break
|
||||||
|
r.reverse()
|
||||||
|
assert length < 0 or len(r) == length
|
||||||
|
return ''.join(r)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_rsa(message, exponent, modulus, mlen):
|
||||||
|
"""Perform RSA signing or verification.
|
||||||
|
|
||||||
|
@param message: byte string to operate on
|
||||||
|
@param exponent: public or private key exponent
|
||||||
|
@param modulus: key modulus
|
||||||
|
@param mlen: desired output length
|
||||||
|
@return: byte string result of the operation
|
||||||
|
"""
|
||||||
|
return int2str(pow(str2int(message), exponent, modulus), mlen)
|
||||||
|
|
||||||
|
|
||||||
|
def RSASSA_PKCS1_v1_5_sign(digest, hashid, private_exponent, modulus):
|
||||||
|
"""Sign a digest with RFC3447 RSASSA-PKCS1-v1_5.
|
||||||
|
|
||||||
|
@param digest: digest byte string to sign
|
||||||
|
@param hashid: ID of the hash used to generate the digest
|
||||||
|
@param private_exponent: private key exponent
|
||||||
|
@param modulus: key modulus
|
||||||
|
@return: signed digest byte string
|
||||||
|
"""
|
||||||
|
modlen = len(int2str(modulus))
|
||||||
|
encoded_digest = EMSA_PKCS1_v1_5_encode(digest, modlen, hashid)
|
||||||
|
return perform_rsa(encoded_digest, private_exponent, modulus, modlen)
|
||||||
|
|
||||||
|
|
||||||
|
def RSASSA_PKCS1_v1_5_verify(digest, hashid, signature, public_exponent,
|
||||||
|
modulus):
|
||||||
|
"""Verify a digest signed with RFC3447 RSASSA-PKCS1-v1_5.
|
||||||
|
|
||||||
|
@param digest: digest byte string to check
|
||||||
|
@param hashid: ID of the hash used to generate the digest
|
||||||
|
@param signature: signed digest byte string
|
||||||
|
@param public_exponent: public key exponent
|
||||||
|
@param modulus: key modulus
|
||||||
|
@return: True if the signature is valid, False otherwise
|
||||||
|
"""
|
||||||
|
modlen = len(int2str(modulus))
|
||||||
|
encoded_digest = EMSA_PKCS1_v1_5_encode(digest, modlen, hashid)
|
||||||
|
signed_digest = perform_rsa(signature, public_exponent, modulus, modlen)
|
||||||
|
return encoded_digest == signed_digest
|
||||||
Reference in New Issue
Block a user