Drop Python 2.5 support, add 3.1 and 3.2.
This commit is contained in:
@@ -12,8 +12,8 @@ This is pydkim 0.3.
|
|||||||
|
|
||||||
REQUIREMENTS
|
REQUIREMENTS
|
||||||
|
|
||||||
Python 2.5 or later is required.
|
- Python 2.x >= 2.6, or Python 3.x >= 3.1.
|
||||||
The dnspython library (http://www.dnspython.org) library is required.
|
- dnspython or pydns. dnspython is preferred if both are present.
|
||||||
|
|
||||||
INSTALLATION
|
INSTALLATION
|
||||||
|
|
||||||
|
|||||||
+92
-82
@@ -54,7 +54,7 @@ __all__ = [
|
|||||||
class Simple:
|
class Simple:
|
||||||
"""Class that represents the "simple" canonicalization algorithm."""
|
"""Class that represents the "simple" canonicalization algorithm."""
|
||||||
|
|
||||||
name = "simple"
|
name = b"simple"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def canonicalize_headers(headers):
|
def canonicalize_headers(headers):
|
||||||
@@ -64,12 +64,12 @@ class Simple:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def canonicalize_body(body):
|
def canonicalize_body(body):
|
||||||
# Ignore all empty lines at the end of the message body.
|
# Ignore all empty lines at the end of the message body.
|
||||||
return re.sub("(\r\n)*$", "\r\n", body)
|
return re.sub(b"(\r\n)*$", b"\r\n", body)
|
||||||
|
|
||||||
class Relaxed:
|
class Relaxed:
|
||||||
"""Class that represents the "relaxed" canonicalization algorithm."""
|
"""Class that represents the "relaxed" canonicalization algorithm."""
|
||||||
|
|
||||||
name = "relaxed"
|
name = b"relaxed"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def canonicalize_headers(headers):
|
def canonicalize_headers(headers):
|
||||||
@@ -77,14 +77,14 @@ class Relaxed:
|
|||||||
# Unfold all header lines.
|
# Unfold all header lines.
|
||||||
# Compress WSP to single space.
|
# Compress WSP to single space.
|
||||||
# Remove all WSP at the start or end of the field value (strip).
|
# Remove all WSP at the start or end of the field value (strip).
|
||||||
return [(x[0].lower(), re.sub(r"\s+", " ", re.sub("\r\n", "", x[1])).strip()+"\r\n") for x in headers]
|
return [(x[0].lower(), re.sub(br"\s+", b" ", re.sub(b"\r\n", b"", x[1])).strip()+b"\r\n") for x in headers]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def canonicalize_body(body):
|
def canonicalize_body(body):
|
||||||
# Remove all trailing WSP at end of lines.
|
# Remove all trailing WSP at end of lines.
|
||||||
# Compress non-line-ending WSP to single space.
|
# Compress non-line-ending WSP to single space.
|
||||||
# Ignore all empty lines at the end of the message body.
|
# Ignore all empty lines at the end of the message body.
|
||||||
return re.sub("(\r\n)*$", "\r\n", re.sub(r"[\x09\x20]+", " ", re.sub("[\\x09\\x20]+\r\n", "\r\n", body)))
|
return re.sub(b"(\r\n)*$", b"\r\n", re.sub(br"[\x09\x20]+", b" ", re.sub(b"[\\x09\\x20]+\r\n", b"\r\n", body)))
|
||||||
|
|
||||||
class DKIMException(Exception):
|
class DKIMException(Exception):
|
||||||
"""Base class for DKIM errors."""
|
"""Base class for DKIM errors."""
|
||||||
@@ -131,11 +131,11 @@ def hash_headers(hasher, canonicalize_headers, headers, include_headers,
|
|||||||
# The call to _remove() assumes that the signature b= only appears
|
# The call to _remove() assumes that the signature b= only appears
|
||||||
# once in the signature header
|
# once in the signature header
|
||||||
cheaders = canonicalize_headers.canonicalize_headers(
|
cheaders = canonicalize_headers.canonicalize_headers(
|
||||||
[(sigheaders[0][0], _remove(sigheaders[0][1], sig['b']))])
|
[(sigheaders[0][0], _remove(sigheaders[0][1], sig[b'b']))])
|
||||||
sign_headers += [(x[0], x[1].rstrip()) for x in cheaders]
|
sign_headers += [(x[0], x[1].rstrip()) for x in cheaders]
|
||||||
for x in sign_headers:
|
for x in sign_headers:
|
||||||
hasher.update(x[0])
|
hasher.update(x[0])
|
||||||
hasher.update(":")
|
hasher.update(b":")
|
||||||
hasher.update(x[1])
|
hasher.update(x[1])
|
||||||
|
|
||||||
|
|
||||||
@@ -147,40 +147,43 @@ def validate_signature_fields(sig):
|
|||||||
|
|
||||||
@param sig: A dict mapping field keys to values.
|
@param sig: A dict mapping field keys to values.
|
||||||
"""
|
"""
|
||||||
mandatory_fields = ('v', 'a', 'b', 'bh', 'd', 'h', 's')
|
mandatory_fields = (b'v', b'a', b'b', b'bh', b'd', b'h', b's')
|
||||||
for field in mandatory_fields:
|
for field in mandatory_fields:
|
||||||
if field not in sig:
|
if field not in sig:
|
||||||
raise ValidationError("signature missing %s=" % field)
|
raise ValidationError("signature missing %s=" % field)
|
||||||
|
|
||||||
if sig['v'] != "1":
|
if sig[b'v'] != b"1":
|
||||||
raise ValidationError("v= value is not 1 (%s)" % sig['v'])
|
raise ValidationError("v= value is not 1 (%s)" % sig[b'v'])
|
||||||
if re.match(r"[\s0-9A-Za-z+/]+=*$", sig['b']) is None:
|
if re.match(br"[\s0-9A-Za-z+/]+=*$", sig[b'b']) is None:
|
||||||
raise ValidationError("b= value is not valid base64 (%s)" % sig['b'])
|
raise ValidationError("b= value is not valid base64 (%s)" % sig[b'b'])
|
||||||
if re.match(r"[\s0-9A-Za-z+/]+=*$", sig['bh']) is None:
|
if re.match(br"[\s0-9A-Za-z+/]+=*$", sig[b'bh']) is None:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"bh= value is not valid base64 (%s)" % sig['bh'])
|
"bh= value is not valid base64 (%s)" % sig[b'bh'])
|
||||||
if 'i' in sig and (
|
# Nasty hack to support both str and bytes... check for both the
|
||||||
not sig['i'].endswith(sig['d']) or
|
# character and integer values.
|
||||||
sig['i'][-len(sig['d'])-1] not in "@."):
|
if b'i' in sig and (
|
||||||
|
not sig[b'i'].endswith(sig[b'd']) or
|
||||||
|
sig[b'i'][-len(sig[b'd'])-1] not in ('@', '.', 64, 46)):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"i= domain is not a subdomain of d= (i=%s d=%d)" %
|
"i= domain is not a subdomain of d= (i=%s d=%d)" %
|
||||||
(sig['i'], sig['d']))
|
(sig[b'i'], sig[b'd']))
|
||||||
if 'l' in sig and re.match(r"\d{,76}$", sig['l']) is None:
|
if b'l' in sig and re.match(br"\d{,76}$", sig['l']) is None:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"l= value is not a decimal integer (%s)" % sig['l'])
|
"l= value is not a decimal integer (%s)" % sig[b'l'])
|
||||||
if 'q' in sig and sig['q'] != "dns/txt":
|
if b'q' in sig and sig[b'q'] != b"dns/txt":
|
||||||
raise ValidationError("q= value is not dns/txt (%s)" % sig['q'])
|
raise ValidationError("q= value is not dns/txt (%s)" % sig[b'q'])
|
||||||
if 't' in sig and re.match(r"\d+$", sig['t']) is None:
|
if b't' in sig and re.match(br"\d+$", sig[b't']) is None:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"t= value is not a decimal integer (%s)" % sig['t'])
|
"t= value is not a decimal integer (%s)" % sig[b't'])
|
||||||
if 'x' in sig:
|
if b'x' in sig:
|
||||||
if re.match(r"\d+$", sig['x']) is None:
|
if re.match(br"\d+$", sig[b'x']) is None:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"x= value is not a decimal integer (%s)" % sig['x'])
|
"x= value is not a decimal integer (%s)" % sig[b'x'])
|
||||||
if int(sig['x']) < int(sig['t']):
|
if int(sig[b'x']) < int(sig[b't']):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"x= value is less than t= value (x=%s t=%s)" %
|
"x= value is less than t= value (x=%s t=%s)" %
|
||||||
(sig['x'], sig['t']))
|
(sig[b'x'], sig[b't']))
|
||||||
|
|
||||||
|
|
||||||
def rfc822_parse(message):
|
def rfc822_parse(message):
|
||||||
"""Parse a message in RFC822 format.
|
"""Parse a message in RFC822 format.
|
||||||
@@ -191,27 +194,27 @@ def rfc822_parse(message):
|
|||||||
The body is a CRLF-separated string.
|
The body is a CRLF-separated string.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
headers = []
|
headers = []
|
||||||
lines = re.split("\r?\n", message)
|
lines = re.split(b"\r?\n", message)
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(lines):
|
while i < len(lines):
|
||||||
if len(lines[i]) == 0:
|
if len(lines[i]) == 0:
|
||||||
# End of headers, return what we have plus the body, excluding the blank line.
|
# End of headers, return what we have plus the body, excluding the blank line.
|
||||||
i += 1
|
i += 1
|
||||||
break
|
break
|
||||||
if re.match(r"[\x09\x20]", lines[i][0]):
|
if lines[i][0] in ("\x09", "\x20", 0x09, 0x20):
|
||||||
headers[-1][1] += lines[i]+"\r\n"
|
headers[-1][1] += lines[i]+b"\r\n"
|
||||||
else:
|
else:
|
||||||
m = re.match(r"([\x21-\x7e]+?):", lines[i])
|
m = re.match(br"([\x21-\x7e]+?):", lines[i])
|
||||||
if m is not None:
|
if m is not None:
|
||||||
headers.append([m.group(1), lines[i][m.end(0):]+"\r\n"])
|
headers.append([m.group(1), lines[i][m.end(0):]+b"\r\n"])
|
||||||
elif lines[i].startswith("From "):
|
elif lines[i].startswith(b"From "):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise MessageFormatError("Unexpected characters in RFC822 header: %s" % lines[i])
|
raise MessageFormatError("Unexpected characters in RFC822 header: %s" % lines[i])
|
||||||
i += 1
|
i += 1
|
||||||
return (headers, "\r\n".join(lines[i:]))
|
return (headers, b"\r\n".join(lines[i:]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def dnstxt_dnspython(name):
|
def dnstxt_dnspython(name):
|
||||||
@@ -219,7 +222,7 @@ def dnstxt_dnspython(name):
|
|||||||
a = dns.resolver.query(name, dns.rdatatype.TXT)
|
a = dns.resolver.query(name, dns.rdatatype.TXT)
|
||||||
for r in a.response.answer:
|
for r in a.response.answer:
|
||||||
if r.rdtype == dns.rdatatype.TXT:
|
if r.rdtype == dns.rdatatype.TXT:
|
||||||
return "".join(r.items[0].strings)
|
return b"".join(r.items[0].strings)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -246,20 +249,20 @@ except ImportError:
|
|||||||
|
|
||||||
def fold(header):
|
def fold(header):
|
||||||
"""Fold a header line into multiple crlf-separated lines at column 72."""
|
"""Fold a header line into multiple crlf-separated lines at column 72."""
|
||||||
i = header.rfind("\r\n ")
|
i = header.rfind(b"\r\n ")
|
||||||
if i == -1:
|
if i == -1:
|
||||||
pre = ""
|
pre = b""
|
||||||
else:
|
else:
|
||||||
i += 3
|
i += 3
|
||||||
pre = header[:i]
|
pre = header[:i]
|
||||||
header = header[i:]
|
header = header[i:]
|
||||||
while len(header) > 72:
|
while len(header) > 72:
|
||||||
i = header[:72].rfind(" ")
|
i = header[:72].rfind(b" ")
|
||||||
if i == -1:
|
if i == -1:
|
||||||
j = i
|
j = i
|
||||||
else:
|
else:
|
||||||
j = i + 1
|
j = i + 1
|
||||||
pre += header[:i] + "\r\n "
|
pre += header[:i] + b"\r\n "
|
||||||
header = header[j:]
|
header = header[j:]
|
||||||
return pre + header
|
return pre + header
|
||||||
|
|
||||||
@@ -286,7 +289,7 @@ def sign(message, selector, domain, privkey, identity=None,
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
pk = parse_pem_private_key(privkey)
|
pk = parse_pem_private_key(privkey)
|
||||||
except UnparsableKeyError, e:
|
except UnparsableKeyError as e:
|
||||||
raise KeyFormatError(str(e))
|
raise KeyFormatError(str(e))
|
||||||
|
|
||||||
if identity is not None and not identity.endswith(domain):
|
if identity is not None and not identity.endswith(domain):
|
||||||
@@ -307,26 +310,26 @@ def sign(message, selector, domain, privkey, identity=None,
|
|||||||
bodyhash = base64.b64encode(h.digest())
|
bodyhash = base64.b64encode(h.digest())
|
||||||
|
|
||||||
sigfields = [x for x in [
|
sigfields = [x for x in [
|
||||||
('v', "1"),
|
(b'v', b"1"),
|
||||||
('a', "rsa-sha256"),
|
(b'a', b"rsa-sha256"),
|
||||||
('c', "%s/%s" % (canonicalize[0].name, canonicalize[1].name)),
|
(b'c', b"/".join((canonicalize[0].name, canonicalize[1].name))),
|
||||||
('d', domain),
|
(b'd', domain),
|
||||||
('i', identity or "@"+domain),
|
(b'i', identity or b"@"+domain),
|
||||||
length and ('l', len(body)),
|
length and (b'l', len(body)),
|
||||||
('q', "dns/txt"),
|
(b'q', b"dns/txt"),
|
||||||
('s', selector),
|
(b's', selector),
|
||||||
('t', str(int(time.time()))),
|
(b't', str(int(time.time())).encode('ascii')),
|
||||||
('h', " : ".join(x[0] for x in sign_headers)),
|
(b'h', b" : ".join(x[0] for x in sign_headers)),
|
||||||
('bh', bodyhash),
|
(b'bh', bodyhash),
|
||||||
('b', ""),
|
(b'b', b""),
|
||||||
] if x]
|
] if x]
|
||||||
|
|
||||||
sig_value = fold("; ".join("%s=%s" % x for x in sigfields))
|
sig_value = fold(b"; ".join(b"=".join(x) for x in sigfields))
|
||||||
dkim_header = canonicalize[0].canonicalize_headers([
|
dkim_header = canonicalize[0].canonicalize_headers([
|
||||||
['DKIM-Signature', ' ' + sig_value]])[0]
|
[b'DKIM-Signature', b' ' + sig_value]])[0]
|
||||||
# the dkim sig is hashed with no trailing crlf, even if the
|
# the dkim sig is hashed with no trailing crlf, even if the
|
||||||
# canonicalization algorithm would add one.
|
# canonicalization algorithm would add one.
|
||||||
if dkim_header[1][-2:] == '\r\n':
|
if dkim_header[1][-2:] == b'\r\n':
|
||||||
dkim_header = (dkim_header[0], dkim_header[1][:-2])
|
dkim_header = (dkim_header[0], dkim_header[1][:-2])
|
||||||
sign_headers.append(dkim_header)
|
sign_headers.append(dkim_header)
|
||||||
|
|
||||||
@@ -334,7 +337,7 @@ def sign(message, selector, domain, privkey, identity=None,
|
|||||||
h = hashlib.sha256()
|
h = hashlib.sha256()
|
||||||
for x in sign_headers:
|
for x in sign_headers:
|
||||||
h.update(x[0])
|
h.update(x[0])
|
||||||
h.update(":")
|
h.update(b":")
|
||||||
h.update(x[1])
|
h.update(x[1])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -342,9 +345,9 @@ def sign(message, selector, domain, privkey, identity=None,
|
|||||||
h, pk['privateExponent'], pk['modulus'])
|
h, pk['privateExponent'], pk['modulus'])
|
||||||
except DigestTooLargeError:
|
except DigestTooLargeError:
|
||||||
raise ParameterError("digest too large for modulus")
|
raise ParameterError("digest too large for modulus")
|
||||||
sig_value += base64.b64encode(sig2)
|
sig_value += base64.b64encode(bytes(sig2))
|
||||||
|
|
||||||
return 'DKIM-Signature: ' + sig_value + "\r\n"
|
return b'DKIM-Signature: ' + sig_value + b"\r\n"
|
||||||
|
|
||||||
|
|
||||||
def verify(message, logger=None, dnsfunc=dnstxt):
|
def verify(message, logger=None, dnsfunc=dnstxt):
|
||||||
@@ -359,7 +362,7 @@ def verify(message, logger=None, dnsfunc=dnstxt):
|
|||||||
|
|
||||||
(headers, body) = rfc822_parse(message)
|
(headers, body) = rfc822_parse(message)
|
||||||
|
|
||||||
sigheaders = [x for x in headers if x[0].lower() == "dkim-signature"]
|
sigheaders = [x for x in headers if x[0].lower() == b"dkim-signature"]
|
||||||
if len(sigheaders) < 1:
|
if len(sigheaders) < 1:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -372,24 +375,24 @@ def verify(message, logger=None, dnsfunc=dnstxt):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
validate_signature_fields(sig)
|
validate_signature_fields(sig)
|
||||||
except ValidationError, e:
|
except ValidationError as e:
|
||||||
logger.error("signature fields failed to validate: %s" % e)
|
logger.error("signature fields failed to validate: %s" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
m = re.match("(\w+)(?:/(\w+))?$", sig['c'])
|
m = re.match(b"(\w+)(?:/(\w+))?$", sig[b'c'])
|
||||||
if m is None:
|
if m is None:
|
||||||
logger.error(
|
logger.error(
|
||||||
"c= value is not in format method/method (%s)" % sig['c'])
|
"c= value is not in format method/method (%s)" % sig[b'c'])
|
||||||
return False
|
return False
|
||||||
can_headers = m.group(1)
|
can_headers = m.group(1)
|
||||||
if m.group(2) is not None:
|
if m.group(2) is not None:
|
||||||
can_body = m.group(2)
|
can_body = m.group(2)
|
||||||
else:
|
else:
|
||||||
can_body = "simple"
|
can_body = b"simple"
|
||||||
|
|
||||||
if can_headers == "simple":
|
if can_headers == b"simple":
|
||||||
canonicalize_headers = Simple
|
canonicalize_headers = Simple
|
||||||
elif can_headers == "relaxed":
|
elif can_headers == b"relaxed":
|
||||||
canonicalize_headers = Relaxed
|
canonicalize_headers = Relaxed
|
||||||
else:
|
else:
|
||||||
logger.error("unknown header canonicalization (%s)" % can_headers)
|
logger.error("unknown header canonicalization (%s)" % can_headers)
|
||||||
@@ -397,36 +400,43 @@ def verify(message, logger=None, dnsfunc=dnstxt):
|
|||||||
|
|
||||||
headers = canonicalize_headers.canonicalize_headers(headers)
|
headers = canonicalize_headers.canonicalize_headers(headers)
|
||||||
|
|
||||||
if can_body == "simple":
|
if can_body == b"simple":
|
||||||
body = Simple.canonicalize_body(body)
|
body = Simple.canonicalize_body(body)
|
||||||
elif can_body == "relaxed":
|
elif can_body == b"relaxed":
|
||||||
body = Relaxed.canonicalize_body(body)
|
body = Relaxed.canonicalize_body(body)
|
||||||
else:
|
else:
|
||||||
logger.error("unknown body canonicalization (%s)" % can_body)
|
logger.error("unknown body canonicalization (%s)" % can_body)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if sig['a'] == "rsa-sha1":
|
if sig[b'a'] == b"rsa-sha1":
|
||||||
hasher = hashlib.sha1
|
hasher = hashlib.sha1
|
||||||
elif sig['a'] == "rsa-sha256":
|
elif sig[b'a'] == b"rsa-sha256":
|
||||||
hasher = hashlib.sha256
|
hasher = hashlib.sha256
|
||||||
else:
|
else:
|
||||||
logger.error("unknown signature algorithm (%s)" % sig['a'])
|
logger.error("unknown signature algorithm (%s)" % sig[b'a'])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if 'l' in sig:
|
if b'l' in sig:
|
||||||
body = body[:int(sig['l'])]
|
body = body[:int(sig[b'l'])]
|
||||||
|
|
||||||
h = hasher()
|
h = hasher()
|
||||||
h.update(body)
|
h.update(body)
|
||||||
bodyhash = h.digest()
|
bodyhash = h.digest()
|
||||||
logger.debug("bh: %s" % base64.b64encode(bodyhash))
|
logger.debug("bh: %s" % base64.b64encode(bodyhash))
|
||||||
if bodyhash != base64.b64decode(re.sub(r"\s+", "", sig['bh'])):
|
if bodyhash != base64.b64decode(re.sub(br"\s+", b"", sig[b'bh'])):
|
||||||
logger.error(
|
logger.error(
|
||||||
"body hash mismatch (got %s, expected %s)" %
|
"body hash mismatch (got %s, expected %s)" %
|
||||||
(base64.b64encode(bodyhash), sig['bh']))
|
(base64.b64encode(bodyhash), sig[b'bh']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
s = dnsfunc(sig['s']+"._domainkey."+sig['d']+".")
|
# dnstxt wants Unicode
|
||||||
|
try:
|
||||||
|
selector = sig[b's'].decode('ascii')
|
||||||
|
domain = sig[b'd'].decode('ascii')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return False
|
||||||
|
name = "%s._domainkey.%s." % (selector, domain)
|
||||||
|
s = dnsfunc(name).encode('utf-8')
|
||||||
if not s:
|
if not s:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
@@ -434,16 +444,16 @@ def verify(message, logger=None, dnsfunc=dnstxt):
|
|||||||
except InvalidTagValueList:
|
except InvalidTagValueList:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
pk = parse_public_key(base64.b64decode(pub['p']))
|
pk = parse_public_key(base64.b64decode(pub[b'p']))
|
||||||
except UnparsableKeyError, e:
|
except UnparsableKeyError as e:
|
||||||
logger.error("could not parse public key: %s" % e)
|
logger.error("could not parse public key: %s" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
include_headers = re.split(r"\s*:\s*", sig['h'])
|
include_headers = re.split(br"\s*:\s*", sig[b'h'])
|
||||||
h = hasher()
|
h = hasher()
|
||||||
hash_headers(
|
hash_headers(
|
||||||
h, canonicalize_headers, headers, include_headers, sigheaders, sig)
|
h, canonicalize_headers, headers, include_headers, sigheaders, sig)
|
||||||
signature = base64.b64decode(re.sub(r"\s+", "", sig['b']))
|
signature = base64.b64decode(re.sub(br"\s+", b"", sig[b'b']))
|
||||||
try:
|
try:
|
||||||
return RSASSA_PKCS1_v1_5_verify(
|
return RSASSA_PKCS1_v1_5_verify(
|
||||||
h, signature, pk['publicExponent'], pk['modulus'])
|
h, signature, pk['publicExponent'], pk['modulus'])
|
||||||
|
|||||||
+20
-13
@@ -50,25 +50,25 @@ def asn1_parse(template, data):
|
|||||||
@param data: byte string data to parse
|
@param data: byte string data to parse
|
||||||
@return: decoded structure
|
@return: decoded structure
|
||||||
"""
|
"""
|
||||||
|
data = bytearray(data)
|
||||||
r = []
|
r = []
|
||||||
i = 0
|
i = 0
|
||||||
for t in template:
|
for t in template:
|
||||||
tag = ord(data[i])
|
tag = data[i]
|
||||||
i += 1
|
i += 1
|
||||||
if tag == t[0]:
|
if tag == t[0]:
|
||||||
length = ord(data[i])
|
length = data[i]
|
||||||
i += 1
|
i += 1
|
||||||
if length & 0x80:
|
if length & 0x80:
|
||||||
n = length & 0x7f
|
n = length & 0x7f
|
||||||
length = 0
|
length = 0
|
||||||
for j in range(n):
|
for j in range(n):
|
||||||
length = (length << 8) | ord(data[i])
|
length = (length << 8) | data[i]
|
||||||
i += 1
|
i += 1
|
||||||
if tag == INTEGER:
|
if tag == INTEGER:
|
||||||
n = 0
|
n = 0
|
||||||
for j in range(length):
|
for j in range(length):
|
||||||
n = (n << 8) | ord(data[i])
|
n = (n << 8) | data[i]
|
||||||
i += 1
|
i += 1
|
||||||
r.append(n)
|
r.append(n)
|
||||||
elif tag == BIT_STRING:
|
elif tag == BIT_STRING:
|
||||||
@@ -100,14 +100,21 @@ def asn1_length(n):
|
|||||||
"""
|
"""
|
||||||
assert n >= 0
|
assert n >= 0
|
||||||
if n < 0x7f:
|
if n < 0x7f:
|
||||||
return chr(n)
|
return bytearray([n])
|
||||||
r = ""
|
r = bytearray()
|
||||||
while n > 0:
|
while n > 0:
|
||||||
r = chr(n & 0xff) + r
|
r.insert(n & 0xff)
|
||||||
n >>= 8
|
n >>= 8
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def asn1_encode(type, data):
|
||||||
|
length = asn1_length(len(data))
|
||||||
|
length.insert(0, type)
|
||||||
|
length.extend(data)
|
||||||
|
return length
|
||||||
|
|
||||||
|
|
||||||
def asn1_build(node):
|
def asn1_build(node):
|
||||||
"""Build a DER-encoded ASN.1 data structure.
|
"""Build a DER-encoded ASN.1 data structure.
|
||||||
|
|
||||||
@@ -115,16 +122,16 @@ def asn1_build(node):
|
|||||||
@return: DER-encoded ASN.1 byte string
|
@return: DER-encoded ASN.1 byte string
|
||||||
"""
|
"""
|
||||||
if node[0] == OCTET_STRING:
|
if node[0] == OCTET_STRING:
|
||||||
return chr(OCTET_STRING) + asn1_length(len(node[1])) + node[1]
|
return asn1_encode(OCTET_STRING, node[1])
|
||||||
if node[0] == NULL:
|
if node[0] == NULL:
|
||||||
assert node[1] is None
|
assert node[1] is None
|
||||||
return chr(NULL) + asn1_length(0)
|
return asn1_encode(NULL, b'')
|
||||||
elif node[0] == OBJECT_IDENTIFIER:
|
elif node[0] == OBJECT_IDENTIFIER:
|
||||||
return chr(OBJECT_IDENTIFIER) + asn1_length(len(node[1])) + node[1]
|
return asn1_encode(OBJECT_IDENTIFIER, node[1])
|
||||||
elif node[0] == SEQUENCE:
|
elif node[0] == SEQUENCE:
|
||||||
r = ""
|
r = bytearray()
|
||||||
for x in node[1]:
|
for x in node[1]:
|
||||||
r += asn1_build(x)
|
r += asn1_build(x)
|
||||||
return chr(SEQUENCE) + asn1_length(len(r)) + r
|
return asn1_encode(SEQUENCE, r)
|
||||||
else:
|
else:
|
||||||
raise ASN1FormatError("Unexpected tag in template: %02x" % node[0])
|
raise ASN1FormatError("Unexpected tag in template: %02x" % node[0])
|
||||||
|
|||||||
+12
-11
@@ -79,8 +79,8 @@ ASN1_RSAPrivateKey = [
|
|||||||
|
|
||||||
# These values come from RFC 3447, section 9.2 Notes, page 43.
|
# These values come from RFC 3447, section 9.2 Notes, page 43.
|
||||||
HASH_ID_MAP = {
|
HASH_ID_MAP = {
|
||||||
'sha1': "\x2b\x0e\x03\x02\x1a",
|
'sha1': b"\x2b\x0e\x03\x02\x1a",
|
||||||
'sha256': "\x60\x86\x48\x01\x65\x03\x04\x02\x01",
|
'sha256': b"\x60\x86\x48\x01\x65\x03\x04\x02\x01",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ def parse_public_key(data):
|
|||||||
# Not sure why the [1:] is necessary to skip a byte.
|
# Not sure why the [1:] is necessary to skip a byte.
|
||||||
x = asn1_parse(ASN1_Object, data)
|
x = asn1_parse(ASN1_Object, data)
|
||||||
pkd = asn1_parse(ASN1_RSAPublicKey, x[0][1][1:])
|
pkd = asn1_parse(ASN1_RSAPublicKey, x[0][1][1:])
|
||||||
except ASN1FormatError, e:
|
except ASN1FormatError as e:
|
||||||
raise UnparsableKeyError(str(e))
|
raise UnparsableKeyError(str(e))
|
||||||
pk = {
|
pk = {
|
||||||
'modulus': pkd[0][0],
|
'modulus': pkd[0][0],
|
||||||
@@ -122,7 +122,7 @@ def parse_private_key(data):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
pka = asn1_parse(ASN1_RSAPrivateKey, data)
|
pka = asn1_parse(ASN1_RSAPrivateKey, data)
|
||||||
except ASN1FormatError, e:
|
except ASN1FormatError as e:
|
||||||
raise UnparsableKeyError(str(e))
|
raise UnparsableKeyError(str(e))
|
||||||
pk = {
|
pk = {
|
||||||
'version': pka[0][0],
|
'version': pka[0][0],
|
||||||
@@ -144,12 +144,12 @@ def parse_pem_private_key(data):
|
|||||||
@param data: RFC3447 RSAPrivateKey in PEM format.
|
@param data: RFC3447 RSAPrivateKey in PEM format.
|
||||||
@return: RSA private key
|
@return: RSA private key
|
||||||
"""
|
"""
|
||||||
m = re.search("--\n(.*?)\n--", data, re.DOTALL)
|
m = re.search(b"--\n(.*?)\n--", data, re.DOTALL)
|
||||||
if m is None:
|
if m is None:
|
||||||
raise UnparsableKeyError("Private key not found")
|
raise UnparsableKeyError("Private key not found")
|
||||||
try:
|
try:
|
||||||
pkdata = base64.b64decode(m.group(1))
|
pkdata = base64.b64decode(m.group(1))
|
||||||
except TypeError, e:
|
except TypeError as e:
|
||||||
raise UnparsableKeyError(str(e))
|
raise UnparsableKeyError(str(e))
|
||||||
return parse_private_key(pkdata)
|
return parse_private_key(pkdata)
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ def EMSA_PKCS1_v1_5_encode(hash, mlen):
|
|||||||
]))
|
]))
|
||||||
if len(dinfo) + 11 > mlen:
|
if len(dinfo) + 11 > mlen:
|
||||||
raise DigestTooLargeError()
|
raise DigestTooLargeError()
|
||||||
return "\x00\x01"+"\xff"*(mlen-len(dinfo)-3)+"\x00"+dinfo
|
return b"\x00\x01"+b"\xff"*(mlen-len(dinfo)-3)+b"\x00"+dinfo
|
||||||
|
|
||||||
|
|
||||||
def str2int(s):
|
def str2int(s):
|
||||||
@@ -180,9 +180,10 @@ def str2int(s):
|
|||||||
@param s: byte string representing a positive integer to convert
|
@param s: byte string representing a positive integer to convert
|
||||||
@return: converted integer
|
@return: converted integer
|
||||||
"""
|
"""
|
||||||
|
s = bytearray(s)
|
||||||
r = 0
|
r = 0
|
||||||
for c in s:
|
for c in s:
|
||||||
r = (r << 8) | ord(c)
|
r = (r << 8) | c
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
@@ -195,15 +196,15 @@ def int2str(n, length=-1):
|
|||||||
specified
|
specified
|
||||||
"""
|
"""
|
||||||
assert n >= 0
|
assert n >= 0
|
||||||
r = []
|
r = bytearray()
|
||||||
while length < 0 or len(r) < length:
|
while length < 0 or len(r) < length:
|
||||||
r.append(chr(n & 0xff))
|
r.append(n & 0xff)
|
||||||
n >>= 8
|
n >>= 8
|
||||||
if length < 0 and n == 0:
|
if length < 0 and n == 0:
|
||||||
break
|
break
|
||||||
r.reverse()
|
r.reverse()
|
||||||
assert length < 0 or len(r) == length
|
assert length < 0 or len(r) == length
|
||||||
return ''.join(r)
|
return r
|
||||||
|
|
||||||
|
|
||||||
def perform_rsa(message, exponent, modulus, mlen):
|
def perform_rsa(message, exponent, modulus, mlen):
|
||||||
|
|||||||
@@ -30,5 +30,5 @@ def test_suite():
|
|||||||
test_dkim,
|
test_dkim,
|
||||||
test_util,
|
test_util,
|
||||||
]
|
]
|
||||||
suites = map(lambda x: x.test_suite(), modules)
|
suites = [x.test_suite() for x in modules]
|
||||||
return unittest.TestSuite(suites)
|
return unittest.TestSuite(suites)
|
||||||
|
|||||||
+30
-30
@@ -17,6 +17,7 @@
|
|||||||
# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
|
# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import binascii
|
||||||
import hashlib
|
import hashlib
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@@ -54,13 +55,13 @@ TEST_KEY_PRIVATE_EXPONENT = int(
|
|||||||
class TestStrIntConversion(unittest.TestCase):
|
class TestStrIntConversion(unittest.TestCase):
|
||||||
|
|
||||||
def test_str2int(self):
|
def test_str2int(self):
|
||||||
self.assertEquals(1234, str2int('\x04\xd2'))
|
self.assertEqual(1234, str2int(b'\x04\xd2'))
|
||||||
|
|
||||||
def test_int2str(self):
|
def test_int2str(self):
|
||||||
self.assertEquals('\x04\xd2', int2str(1234))
|
self.assertEqual(b'\x04\xd2', int2str(1234))
|
||||||
|
|
||||||
def test_int2str_with_length(self):
|
def test_int2str_with_length(self):
|
||||||
self.assertEquals('\x00\x00\x04\xd2', int2str(1234, 4))
|
self.assertEqual(b'\x00\x00\x04\xd2', int2str(1234, 4))
|
||||||
|
|
||||||
def test_int2str_fails_on_negative(self):
|
def test_int2str_fails_on_negative(self):
|
||||||
self.assertRaises(AssertionError, int2str, -1)
|
self.assertRaises(AssertionError, int2str, -1)
|
||||||
@@ -70,39 +71,39 @@ class TestParseKeys(unittest.TestCase):
|
|||||||
|
|
||||||
def test_parse_pem_private_key(self):
|
def test_parse_pem_private_key(self):
|
||||||
key = parse_pem_private_key(read_test_data('test.private'))
|
key = parse_pem_private_key(read_test_data('test.private'))
|
||||||
self.assertEquals(key['modulus'], TEST_KEY_MODULUS)
|
self.assertEqual(key['modulus'], TEST_KEY_MODULUS)
|
||||||
self.assertEquals(key['publicExponent'], TEST_KEY_PUBLIC_EXPONENT)
|
self.assertEqual(key['publicExponent'], TEST_KEY_PUBLIC_EXPONENT)
|
||||||
self.assertEquals(key['privateExponent'], TEST_KEY_PRIVATE_EXPONENT)
|
self.assertEqual(key['privateExponent'], TEST_KEY_PRIVATE_EXPONENT)
|
||||||
|
|
||||||
def test_parse_public_key(self):
|
def test_parse_public_key(self):
|
||||||
data = read_test_data('test.txt')
|
data = read_test_data('test.txt')
|
||||||
key = parse_public_key(base64.b64decode(parse_tag_value(data)['p']))
|
key = parse_public_key(base64.b64decode(parse_tag_value(data)[b'p']))
|
||||||
self.assertEquals(key['modulus'], TEST_KEY_MODULUS)
|
self.assertEqual(key['modulus'], TEST_KEY_MODULUS)
|
||||||
self.assertEquals(key['publicExponent'], TEST_KEY_PUBLIC_EXPONENT)
|
self.assertEqual(key['publicExponent'], TEST_KEY_PUBLIC_EXPONENT)
|
||||||
|
|
||||||
|
|
||||||
class TestEMSA_PKCS1_v1_5(unittest.TestCase):
|
class TestEMSA_PKCS1_v1_5(unittest.TestCase):
|
||||||
|
|
||||||
def test_encode_sha256(self):
|
def test_encode_sha256(self):
|
||||||
hash = hashlib.sha256('message')
|
hash = hashlib.sha256(b'message')
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
'\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00'
|
b'\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00'
|
||||||
'010\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04 '
|
b'010\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04'
|
||||||
+ hash.digest(),
|
b' ' + hash.digest(),
|
||||||
EMSA_PKCS1_v1_5_encode(hash, 62))
|
EMSA_PKCS1_v1_5_encode(hash, 62))
|
||||||
|
|
||||||
def test_encode_sha1(self):
|
def test_encode_sha1(self):
|
||||||
hash = hashlib.sha1('message')
|
hash = hashlib.sha1(b'message')
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
'\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00'
|
b'\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00'
|
||||||
'0!0\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
|
b'0!0\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
|
||||||
+ hash.digest(),
|
+ hash.digest(),
|
||||||
EMSA_PKCS1_v1_5_encode(hash, 46))
|
EMSA_PKCS1_v1_5_encode(hash, 46))
|
||||||
|
|
||||||
def test_encode_forbids_too_short(self):
|
def test_encode_forbids_too_short(self):
|
||||||
# PKCS#1 requires at least 8 bytes of padding, so there must be
|
# PKCS#1 requires at least 8 bytes of padding, so there must be
|
||||||
# at least that much space.
|
# at least that much space.
|
||||||
hash = hashlib.sha1('message')
|
hash = hashlib.sha1(b'message')
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
DigestTooLargeError,
|
DigestTooLargeError,
|
||||||
EMSA_PKCS1_v1_5_encode, hash, 45)
|
EMSA_PKCS1_v1_5_encode, hash, 45)
|
||||||
@@ -110,7 +111,7 @@ class TestEMSA_PKCS1_v1_5(unittest.TestCase):
|
|||||||
|
|
||||||
class TestRSA(unittest.TestCase):
|
class TestRSA(unittest.TestCase):
|
||||||
|
|
||||||
message = '0004fb'.decode('hex')
|
message = binascii.unhexlify(b'0004fb')
|
||||||
modulus = 186101
|
modulus = 186101
|
||||||
modlen = 3
|
modlen = 3
|
||||||
public_exponent = 907
|
public_exponent = 907
|
||||||
@@ -119,14 +120,14 @@ class TestRSA(unittest.TestCase):
|
|||||||
def test_perform(self):
|
def test_perform(self):
|
||||||
signed = perform_rsa(
|
signed = perform_rsa(
|
||||||
self.message, self.private_exponent, self.modulus, self.modlen)
|
self.message, self.private_exponent, self.modulus, self.modlen)
|
||||||
self.assertEquals('01f140'.decode('hex'), signed)
|
self.assertEqual(binascii.unhexlify(b'01f140'), signed)
|
||||||
|
|
||||||
def test_sign_and_verify(self):
|
def test_sign_and_verify(self):
|
||||||
signed = perform_rsa(
|
signed = perform_rsa(
|
||||||
self.message, self.private_exponent, self.modulus, self.modlen)
|
self.message, self.private_exponent, self.modulus, self.modlen)
|
||||||
unsigned = perform_rsa(
|
unsigned = perform_rsa(
|
||||||
signed, self.public_exponent, self.modulus, self.modlen)
|
signed, self.public_exponent, self.modulus, self.modlen)
|
||||||
self.assertEquals(self.message, unsigned)
|
self.assertEqual(self.message, unsigned)
|
||||||
|
|
||||||
|
|
||||||
class TestRSASSA(unittest.TestCase):
|
class TestRSASSA(unittest.TestCase):
|
||||||
@@ -135,18 +136,17 @@ class TestRSASSA(unittest.TestCase):
|
|||||||
self.key = parse_pem_private_key(read_test_data('test.private'))
|
self.key = parse_pem_private_key(read_test_data('test.private'))
|
||||||
self.hash = hashlib.sha1(self.test_digest)
|
self.hash = hashlib.sha1(self.test_digest)
|
||||||
|
|
||||||
test_digest = '0123456789abcdef0123'
|
test_digest = b'0123456789abcdef0123'
|
||||||
test_signature = (
|
test_signature = binascii.unhexlify(
|
||||||
'cc8d3647d64dd3bc12984947a27bdfbb565041fcc9db781afb4b60d29d288d8d60de'
|
b'cc8d3647d64dd3bc12984947a27bdfbb565041fcc9db781afb4b60d29d288d8d60d'
|
||||||
'9e1916d6f81569c3e72af442538dd6aecb50a6de9a14565fdd679c46ff7842482e15'
|
b'e9e1916d6f81569c3e72af442538dd6aecb50a6de9a14565fdd679c46ff7842482e'
|
||||||
'e5aa078549621b6f12ca8cd57ecfad95b18e53581e131c6c3c7cd01cb153adeb439d'
|
b'15e5aa078549621b6f12ca8cd57ecfad95b18e53581e131c6c3c7cd01cb153adeb4'
|
||||||
'2d6ab8b215b19be0e69ef490885004a474eb26d747a219693e8c').decode('hex')
|
b'39d2d6ab8b215b19be0e69ef490885004a474eb26d747a219693e8c')
|
||||||
|
|
||||||
def test_sign_and_verify(self):
|
def test_sign_and_verify(self):
|
||||||
signature = RSASSA_PKCS1_v1_5_sign(
|
signature = RSASSA_PKCS1_v1_5_sign(
|
||||||
self.hash, TEST_KEY_PRIVATE_EXPONENT, TEST_KEY_MODULUS)
|
self.hash, TEST_KEY_PRIVATE_EXPONENT, TEST_KEY_MODULUS)
|
||||||
self.assertEquals(
|
self.assertEqual(self.test_signature, signature)
|
||||||
self.test_signature, signature)
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
RSASSA_PKCS1_v1_5_verify(
|
RSASSA_PKCS1_v1_5_verify(
|
||||||
self.hash, signature, TEST_KEY_PUBLIC_EXPONENT,
|
self.hash, signature, TEST_KEY_PUBLIC_EXPONENT,
|
||||||
|
|||||||
+14
-7
@@ -28,20 +28,21 @@ def read_test_data(filename):
|
|||||||
The files live in dkim/tests/data.
|
The files live in dkim/tests/data.
|
||||||
"""
|
"""
|
||||||
path = os.path.join(os.path.dirname(__file__), 'data', filename)
|
path = os.path.join(os.path.dirname(__file__), 'data', filename)
|
||||||
return open(path).read()
|
with open(path, 'rb') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
class TestFold(unittest.TestCase):
|
class TestFold(unittest.TestCase):
|
||||||
|
|
||||||
def test_short_line(self):
|
def test_short_line(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"foo", dkim.fold("foo"))
|
b"foo", dkim.fold(b"foo"))
|
||||||
|
|
||||||
def DISABLED_test_long_line(self):
|
def DISABLED_test_long_line(self):
|
||||||
# The function is terribly broken, not passing even this simple
|
# The function is terribly broken, not passing even this simple
|
||||||
# test.
|
# test.
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"foo"*24 + "\r\n foo", dkim.fold("foo" * 25))
|
b"foo" * 24 + b"\r\n foo", dkim.fold(b"foo" * 25))
|
||||||
|
|
||||||
|
|
||||||
class TestSignAndVerify(unittest.TestCase):
|
class TestSignAndVerify(unittest.TestCase):
|
||||||
@@ -53,18 +54,24 @@ class TestSignAndVerify(unittest.TestCase):
|
|||||||
|
|
||||||
def dnsfunc(self, domain):
|
def dnsfunc(self, domain):
|
||||||
self.assertEqual('test._domainkey.example.com.', domain)
|
self.assertEqual('test._domainkey.example.com.', domain)
|
||||||
return read_test_data("test.txt")
|
return read_test_data("test.txt").decode('utf-8')
|
||||||
|
|
||||||
def test_verifies(self):
|
def test_verifies(self):
|
||||||
# A message verifies after being signed.
|
# A message verifies after being signed.
|
||||||
sig = dkim.sign(self.message, "test", "example.com", self.key)
|
sig = dkim.sign(self.message, b"test", b"example.com", self.key)
|
||||||
res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc)
|
res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc)
|
||||||
self.assertTrue(res)
|
self.assertTrue(res)
|
||||||
|
|
||||||
def test_altered_body_fails(self):
|
def test_altered_body_fails(self):
|
||||||
# An altered body fails verification.
|
# An altered body fails verification.
|
||||||
sig = dkim.sign(self.message, "test", "example.com", self.key)
|
sig = dkim.sign(self.message, b"test", b"example.com", self.key)
|
||||||
res = dkim.verify(sig + self.message + "foo", dnsfunc=self.dnsfunc)
|
res = dkim.verify(sig + self.message + b"foo", dnsfunc=self.dnsfunc)
|
||||||
|
self.assertFalse(res)
|
||||||
|
|
||||||
|
def test_badly_encoded_domain_fails(self):
|
||||||
|
# Domains should be ASCII. Bad ASCII causes verification to fail.
|
||||||
|
sig = dkim.sign(self.message, b"test", b"example.com\xe9", self.key)
|
||||||
|
res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc)
|
||||||
self.assertFalse(res)
|
self.assertFalse(res)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
-16
@@ -30,38 +30,36 @@ class TestParseTagValue(unittest.TestCase):
|
|||||||
|
|
||||||
def test_single(self):
|
def test_single(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{'foo': 'bar'},
|
{b'foo': b'bar'},
|
||||||
parse_tag_value('foo=bar'))
|
parse_tag_value(b'foo=bar'))
|
||||||
|
|
||||||
def test_trailing_separator_ignored(self):
|
def test_trailing_separator_ignored(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{'foo': 'bar'},
|
{b'foo': b'bar'},
|
||||||
parse_tag_value('foo=bar;'))
|
parse_tag_value(b'foo=bar;'))
|
||||||
|
|
||||||
def test_multiple(self):
|
def test_multiple(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{'foo': 'bar', 'baz': 'foo'},
|
{b'foo': b'bar', b'baz': b'foo'},
|
||||||
parse_tag_value('foo=bar;baz=foo'))
|
parse_tag_value(b'foo=bar;baz=foo'))
|
||||||
|
|
||||||
def test_value_with_equals(self):
|
def test_value_with_equals(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{'foo': 'bar', 'baz': 'foo=bar'},
|
{b'foo': b'bar', b'baz': b'foo=bar'},
|
||||||
parse_tag_value('foo=bar;baz=foo=bar'))
|
parse_tag_value(b'foo=bar;baz=foo=bar'))
|
||||||
|
|
||||||
def test_whitespace_is_stripped(self):
|
def test_whitespace_is_stripped(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{'foo': 'bar', 'baz': 'f oo=bar'},
|
{b'foo': b'bar', b'baz': b'f oo=bar'},
|
||||||
parse_tag_value(' foo \t= bar;\tbaz= f oo=bar '))
|
parse_tag_value(b' foo \t= bar;\tbaz= f oo=bar '))
|
||||||
|
|
||||||
def test_missing_value_is_an_error(self):
|
def test_missing_value_is_an_error(self):
|
||||||
self.assertRaisesRegexp(
|
self.assertRaises(
|
||||||
InvalidTagSpec, 'baz',
|
InvalidTagSpec, parse_tag_value, b'foo=bar;baz')
|
||||||
parse_tag_value, 'foo=bar;baz')
|
|
||||||
|
|
||||||
def test_duplicate_tag_is_an_error(self):
|
def test_duplicate_tag_is_an_error(self):
|
||||||
self.assertRaisesRegexp(
|
self.assertRaises(
|
||||||
DuplicateTag, 'foo',
|
DuplicateTag, parse_tag_value, b'foo=bar;foo=baz')
|
||||||
parse_tag_value, 'foo=bar;foo=baz')
|
|
||||||
|
|
||||||
|
|
||||||
def test_suite():
|
def test_suite():
|
||||||
|
|||||||
+2
-2
@@ -55,13 +55,13 @@ def parse_tag_value(tag_list):
|
|||||||
@param tag_list: A string containing a DKIM Tag=Value list.
|
@param tag_list: A string containing a DKIM Tag=Value list.
|
||||||
"""
|
"""
|
||||||
tags = {}
|
tags = {}
|
||||||
tag_specs = tag_list.split(';')
|
tag_specs = tag_list.split(b';')
|
||||||
# Trailing semicolons are valid.
|
# Trailing semicolons are valid.
|
||||||
if not tag_specs[-1]:
|
if not tag_specs[-1]:
|
||||||
tag_specs.pop()
|
tag_specs.pop()
|
||||||
for tag_spec in tag_specs:
|
for tag_spec in tag_specs:
|
||||||
try:
|
try:
|
||||||
key, value = tag_spec.split('=', 1)
|
key, value = tag_spec.split(b'=', 1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise InvalidTagSpec(tag_spec)
|
raise InvalidTagSpec(tag_spec)
|
||||||
if key.strip() in tags:
|
if key.strip() in tags:
|
||||||
|
|||||||
+20
-10
@@ -3,11 +3,11 @@
|
|||||||
# This software is provided 'as-is', without any express or implied
|
# This software is provided 'as-is', without any express or implied
|
||||||
# warranty. In no event will the author be held liable for any damages
|
# warranty. In no event will the author be held liable for any damages
|
||||||
# arising from the use of this software.
|
# arising from the use of this software.
|
||||||
#
|
#
|
||||||
# Permission is granted to anyone to use this software for any purpose,
|
# Permission is granted to anyone to use this software for any purpose,
|
||||||
# including commercial applications, and to alter it and redistribute it
|
# including commercial applications, and to alter it and redistribute it
|
||||||
# freely, subject to the following restrictions:
|
# freely, subject to the following restrictions:
|
||||||
#
|
#
|
||||||
# 1. The origin of this software must not be misrepresented; you must not
|
# 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
|
# claim that you wrote the original software. If you use this software
|
||||||
# in a product, an acknowledgment in the product documentation would be
|
# in a product, an acknowledgment in the product documentation would be
|
||||||
@@ -15,30 +15,40 @@
|
|||||||
# 2. Altered source versions must be plainly marked as such, and must not be
|
# 2. Altered source versions must be plainly marked as such, and must not be
|
||||||
# misrepresented as being the original software.
|
# misrepresented as being the original software.
|
||||||
# 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
|
||||||
|
#
|
||||||
|
# This has been modified from the original software.
|
||||||
|
# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import dkim
|
import dkim
|
||||||
|
|
||||||
if len(sys.argv) < 4 or len(sys.argv) > 5:
|
if len(sys.argv) < 4 or len(sys.argv) > 5:
|
||||||
print >>sys.stderr, "Usage: dkimsign.py selector domain privatekeyfile [identity]"
|
print("Usage: dkimsign.py selector domain privatekeyfile [identity]", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
selector = sys.argv[1]
|
if sys.version_info[0] >= 3:
|
||||||
domain = sys.argv[2]
|
# Make sys.stdin and stdout binary streams.
|
||||||
|
sys.stdin = sys.stdin.detach()
|
||||||
|
sys.stdout = sys.stdout.detach()
|
||||||
|
|
||||||
|
selector = sys.argv[1].encode('ascii')
|
||||||
|
domain = sys.argv[2].encode('ascii')
|
||||||
privatekeyfile = sys.argv[3]
|
privatekeyfile = sys.argv[3]
|
||||||
if len(sys.argv) > 5:
|
if len(sys.argv) > 5:
|
||||||
identity = sys.argv[4]
|
identity = sys.argv[4].encode('ascii')
|
||||||
else:
|
else:
|
||||||
identity = None
|
identity = None
|
||||||
|
|
||||||
message = sys.stdin.read()
|
message = sys.stdin.read()
|
||||||
try:
|
try:
|
||||||
sig = dkim.sign(message, selector, domain, open(privatekeyfile, "r").read(), identity = identity)
|
sig = dkim.sign(message, selector, domain, open(privatekeyfile, "rb").read(), identity = identity)
|
||||||
sys.stdout.write(sig)
|
sys.stdout.write(sig)
|
||||||
sys.stdout.write(message)
|
sys.stdout.write(message)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
print >>sys.stderr, e
|
print(e, file=sys.stderr)
|
||||||
sys.stdout.write(message)
|
sys.stdout.write(message)
|
||||||
|
|||||||
+14
-5
@@ -3,11 +3,11 @@
|
|||||||
# This software is provided 'as-is', without any express or implied
|
# This software is provided 'as-is', without any express or implied
|
||||||
# warranty. In no event will the author be held liable for any damages
|
# warranty. In no event will the author be held liable for any damages
|
||||||
# arising from the use of this software.
|
# arising from the use of this software.
|
||||||
#
|
#
|
||||||
# Permission is granted to anyone to use this software for any purpose,
|
# Permission is granted to anyone to use this software for any purpose,
|
||||||
# including commercial applications, and to alter it and redistribute it
|
# including commercial applications, and to alter it and redistribute it
|
||||||
# freely, subject to the following restrictions:
|
# freely, subject to the following restrictions:
|
||||||
#
|
#
|
||||||
# 1. The origin of this software must not be misrepresented; you must not
|
# 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
|
# claim that you wrote the original software. If you use this software
|
||||||
# in a product, an acknowledgment in the product documentation would be
|
# in a product, an acknowledgment in the product documentation would be
|
||||||
@@ -15,15 +15,24 @@
|
|||||||
# 2. Altered source versions must be plainly marked as such, and must not be
|
# 2. Altered source versions must be plainly marked as such, and must not be
|
||||||
# misrepresented as being the original software.
|
# misrepresented as being the original software.
|
||||||
# 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
|
||||||
|
#
|
||||||
|
# This has been modified from the original software.
|
||||||
|
# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import dkim
|
import dkim
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
# Make sys.stdin a binary stream.
|
||||||
|
sys.stdin = sys.stdin.detach()
|
||||||
|
|
||||||
message = sys.stdin.read()
|
message = sys.stdin.read()
|
||||||
if not dkim.verify(message):
|
if not dkim.verify(message):
|
||||||
print "signature verification failed"
|
print("signature verification failed")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
print "signature ok"
|
print("signature ok")
|
||||||
|
|||||||
Reference in New Issue
Block a user