- Add generation of rsafp DKIM signatures per
draft-ietf-dcrup-dkim-crypto-02
This commit is contained in:
@@ -9,7 +9,8 @@ UNRELEASED Version 0.7.0
|
|||||||
- Update dknewkey.py to use argparse. Add --ktype option to specify
|
- Update dknewkey.py to use argparse. Add --ktype option to specify
|
||||||
different key type options in anticipation of the DCRUP WG output.
|
different key type options in anticipation of the DCRUP WG output.
|
||||||
- Add generation of rsafp DNS records per draft-ietf-dcrup-dkim-crypto-02
|
- Add generation of rsafp DNS records per draft-ietf-dcrup-dkim-crypto-02
|
||||||
|
- Add generation of rsafp DKIM signatures per
|
||||||
|
draft-ietf-dcrup-dkim-crypto-02
|
||||||
2017-05-30 Version 0.6.2
|
2017-05-30 Version 0.6.2
|
||||||
- Fixed problem with header folding that caused the first line to be
|
- Fixed problem with header folding that caused the first line to be
|
||||||
folded too long (Updated test test_add_body_length since l= tag is no
|
folded too long (Updated test test_add_body_length since l= tag is no
|
||||||
|
|||||||
+16
-5
@@ -49,6 +49,7 @@ from dkim.crypto import (
|
|||||||
HASH_ALGORITHMS,
|
HASH_ALGORITHMS,
|
||||||
parse_pem_private_key,
|
parse_pem_private_key,
|
||||||
parse_public_key,
|
parse_public_key,
|
||||||
|
get_rsa_pubkey,
|
||||||
RSASSA_PKCS1_v1_5_sign,
|
RSASSA_PKCS1_v1_5_sign,
|
||||||
RSASSA_PKCS1_v1_5_verify,
|
RSASSA_PKCS1_v1_5_verify,
|
||||||
UnparsableKeyError,
|
UnparsableKeyError,
|
||||||
@@ -361,7 +362,7 @@ class DomainSigner(object):
|
|||||||
#: @param logger: a logger to which debug info will be written (default None)
|
#: @param logger: a logger to which debug info will be written (default None)
|
||||||
#: @param signature_algorithm: the signing algorithm to use when signing
|
#: @param signature_algorithm: the signing algorithm to use when signing
|
||||||
def __init__(self,message=None,logger=None,signature_algorithm=b'rsa-sha256',
|
def __init__(self,message=None,logger=None,signature_algorithm=b'rsa-sha256',
|
||||||
minkey=1024):
|
minkey=1024,ktype=b'rsa'):
|
||||||
self.set_message(message)
|
self.set_message(message)
|
||||||
if logger is None:
|
if logger is None:
|
||||||
logger = get_default_logger()
|
logger = get_default_logger()
|
||||||
@@ -382,6 +383,8 @@ class DomainSigner(object):
|
|||||||
#: Minimum public key size. Shorter keys raise KeyFormatError. The
|
#: Minimum public key size. Shorter keys raise KeyFormatError. The
|
||||||
#: default is 1024
|
#: default is 1024
|
||||||
self.minkey = minkey
|
self.minkey = minkey
|
||||||
|
#: Key type to use. rsa, rsafp (DCRUP)
|
||||||
|
self.ktype = ktype
|
||||||
|
|
||||||
#: Header fields to protect from additions by default.
|
#: Header fields to protect from additions by default.
|
||||||
#:
|
#:
|
||||||
@@ -620,12 +623,16 @@ class DKIM(DomainSigner):
|
|||||||
#: @raise DKIMException: when the message, include_headers, or key are badly
|
#: @raise DKIMException: when the message, include_headers, or key are badly
|
||||||
#: formed.
|
#: formed.
|
||||||
def sign(self, selector, domain, privkey, identity=None,
|
def sign(self, selector, domain, privkey, identity=None,
|
||||||
canonicalize=(b'relaxed',b'simple'), include_headers=None, length=False):
|
canonicalize=(b'relaxed',b'simple'), include_headers=None,
|
||||||
|
length=False):
|
||||||
try:
|
try:
|
||||||
pk = parse_pem_private_key(privkey)
|
pk = parse_pem_private_key(privkey)
|
||||||
except UnparsableKeyError as e:
|
except UnparsableKeyError as e:
|
||||||
raise KeyFormatError(str(e))
|
raise KeyFormatError(str(e))
|
||||||
|
if self.ktype == b'rsafp':
|
||||||
|
pubkey = get_rsa_pubkey(privkey)
|
||||||
|
else:
|
||||||
|
pubkey = False
|
||||||
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")
|
||||||
|
|
||||||
@@ -653,10 +660,13 @@ class DKIM(DomainSigner):
|
|||||||
h = self.hasher()
|
h = self.hasher()
|
||||||
h.update(body)
|
h.update(body)
|
||||||
bodyhash = base64.b64encode(h.digest())
|
bodyhash = base64.b64encode(h.digest())
|
||||||
|
if self.ktype == b'rsafp':
|
||||||
|
atag = b'rsafp-sha256'
|
||||||
|
else:
|
||||||
|
atag = self.signature_algorithm
|
||||||
sigfields = [x for x in [
|
sigfields = [x for x in [
|
||||||
(b'v', b"1"),
|
(b'v', b"1"),
|
||||||
(b'a', self.signature_algorithm),
|
(b'a', atag),
|
||||||
(b'c', canon_policy.to_c_value()),
|
(b'c', canon_policy.to_c_value()),
|
||||||
(b'd', domain),
|
(b'd', domain),
|
||||||
(b'i', identity or b"@"+domain),
|
(b'i', identity or b"@"+domain),
|
||||||
@@ -665,6 +675,7 @@ class DKIM(DomainSigner):
|
|||||||
(b's', selector),
|
(b's', selector),
|
||||||
(b't', str(int(time.time())).encode('ascii')),
|
(b't', str(int(time.time())).encode('ascii')),
|
||||||
(b'h', b" : ".join(include_headers)),
|
(b'h', b" : ".join(include_headers)),
|
||||||
|
pubkey and (b'k', pubkey),
|
||||||
(b'bh', bodyhash),
|
(b'bh', bodyhash),
|
||||||
# Force b= to fold onto it's own line so that refolding after
|
# Force b= to fold onto it's own line so that refolding after
|
||||||
# adding sig doesn't change whitespace for previous tags.
|
# adding sig doesn't change whitespace for previous tags.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ __all__ = [
|
|||||||
'parse_pem_private_key',
|
'parse_pem_private_key',
|
||||||
'parse_private_key',
|
'parse_private_key',
|
||||||
'parse_public_key',
|
'parse_public_key',
|
||||||
|
'get_rsa_pubkey',
|
||||||
'RSASSA_PKCS1_v1_5_sign',
|
'RSASSA_PKCS1_v1_5_sign',
|
||||||
'RSASSA_PKCS1_v1_5_verify',
|
'RSASSA_PKCS1_v1_5_verify',
|
||||||
'UnparsableKeyError',
|
'UnparsableKeyError',
|
||||||
@@ -33,6 +34,7 @@ __all__ = [
|
|||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
import rsa
|
||||||
|
|
||||||
from dkim.asn1 import (
|
from dkim.asn1 import (
|
||||||
ASN1FormatError,
|
ASN1FormatError,
|
||||||
@@ -233,6 +235,14 @@ def rsa_decrypt(message, pk, mlen):
|
|||||||
|
|
||||||
return int2str(m2 + h * pk['prime2'], mlen)
|
return int2str(m2 + h * pk['prime2'], mlen)
|
||||||
|
|
||||||
|
def get_rsa_pubkey(privkey):
|
||||||
|
"""Extract RSA public key from PEM encoded RSA private key for use with
|
||||||
|
rsafp. Returns base64 encoded public key suitable for use in DKIM key
|
||||||
|
records. Using python-rsa instead of making a stack of custom code.
|
||||||
|
@since: 0.7"""
|
||||||
|
pkobj = rsa.PrivateKey.load_pkcs1(privkey, 'PEM')
|
||||||
|
pubobj = rsa.key.PublicKey(pkobj.n, pkobj.e)
|
||||||
|
return(base64.b64encode(rsa.PublicKey.save_pkcs1(pubobj, 'DER')))
|
||||||
|
|
||||||
def rsa_encrypt(message, pk, mlen):
|
def rsa_encrypt(message, pk, mlen):
|
||||||
"""Perform RSA encryption/verification
|
"""Perform RSA encryption/verification
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ def test_suite():
|
|||||||
test_canonicalization,
|
test_canonicalization,
|
||||||
test_crypto,
|
test_crypto,
|
||||||
test_dkim,
|
test_dkim,
|
||||||
|
test_rsafp,
|
||||||
test_util,
|
test_util,
|
||||||
test_arc,
|
test_arc,
|
||||||
test_dnsplug,
|
test_dnsplug,
|
||||||
@@ -36,6 +37,7 @@ def test_suite():
|
|||||||
test_canonicalization,
|
test_canonicalization,
|
||||||
test_crypto,
|
test_crypto,
|
||||||
test_dkim,
|
test_dkim,
|
||||||
|
test_rsafp,
|
||||||
test_util,
|
test_util,
|
||||||
test_arc,
|
test_arc,
|
||||||
test_dnsplug,
|
test_dnsplug,
|
||||||
|
|||||||
+5
-1
@@ -47,6 +47,9 @@ parser.add_argument('--bcanon', choices=['simple', 'relaxed'],
|
|||||||
parser.add_argument('--signalg', choices=['rsa-sha256', 'rsa-sha1'],
|
parser.add_argument('--signalg', choices=['rsa-sha256', 'rsa-sha1'],
|
||||||
default='rsa-sha256',
|
default='rsa-sha256',
|
||||||
help='Signature algorithm: default=rsa-sha256')
|
help='Signature algorithm: default=rsa-sha256')
|
||||||
|
parser.add_argument('--ktype', choices=['rsa', 'rsafp'],
|
||||||
|
default='rsa',
|
||||||
|
help='DKIM key type: Default is rsa')
|
||||||
parser.add_argument('--identity', help='Optional value for i= tag.')
|
parser.add_argument('--identity', help='Optional value for i= tag.')
|
||||||
args=parser.parse_args(arguments)
|
args=parser.parse_args(arguments)
|
||||||
include_headers = None
|
include_headers = None
|
||||||
@@ -61,6 +64,7 @@ if sys.version_info[0] >= 3:
|
|||||||
args.hcanon = bytes(args.hcanon, encoding='UTF-8')
|
args.hcanon = bytes(args.hcanon, encoding='UTF-8')
|
||||||
args.bcanon = bytes(args.bcanon, encoding='UTF-8')
|
args.bcanon = bytes(args.bcanon, encoding='UTF-8')
|
||||||
args.signalg = bytes(args.signalg, encoding='UTF-8')
|
args.signalg = bytes(args.signalg, encoding='UTF-8')
|
||||||
|
args.ktype = bytes(args.ktype, encoding='UTF-8')
|
||||||
# Make sys.stdin and stdout binary streams.
|
# Make sys.stdin and stdout binary streams.
|
||||||
sys.stdin = sys.stdin.detach()
|
sys.stdin = sys.stdin.detach()
|
||||||
sys.stdout = sys.stdout.detach()
|
sys.stdout = sys.stdout.detach()
|
||||||
@@ -69,7 +73,7 @@ canonicalize = (args.hcanon, args.bcanon)
|
|||||||
message = sys.stdin.read()
|
message = sys.stdin.read()
|
||||||
try:
|
try:
|
||||||
d = dkim.DKIM(message,logger=logger,
|
d = dkim.DKIM(message,logger=logger,
|
||||||
signature_algorithm=args.signalg)
|
signature_algorithm=args.signalg, ktype=args.ktype)
|
||||||
sig = d.sign(args.selector, args.domain, open(
|
sig = d.sign(args.selector, args.domain, open(
|
||||||
args.privatekeyfile, "rb").read(), identity = args.identity,
|
args.privatekeyfile, "rb").read(), identity = args.identity,
|
||||||
canonicalize=canonicalize, include_headers=include_headers,
|
canonicalize=canonicalize, include_headers=include_headers,
|
||||||
|
|||||||
Reference in New Issue
Block a user