diff --git a/ChangeLog b/ChangeLog index 11e72f1..6558490 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,7 +9,8 @@ UNRELEASED Version 0.7.0 - Update dknewkey.py to use argparse. Add --ktype option to specify 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 DKIM signatures per + draft-ietf-dcrup-dkim-crypto-02 2017-05-30 Version 0.6.2 - 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 diff --git a/dkim/__init__.py b/dkim/__init__.py index 7d3b75b..89bcd88 100644 --- a/dkim/__init__.py +++ b/dkim/__init__.py @@ -49,6 +49,7 @@ from dkim.crypto import ( HASH_ALGORITHMS, parse_pem_private_key, parse_public_key, + get_rsa_pubkey, RSASSA_PKCS1_v1_5_sign, RSASSA_PKCS1_v1_5_verify, UnparsableKeyError, @@ -361,7 +362,7 @@ class DomainSigner(object): #: @param logger: a logger to which debug info will be written (default None) #: @param signature_algorithm: the signing algorithm to use when signing def __init__(self,message=None,logger=None,signature_algorithm=b'rsa-sha256', - minkey=1024): + minkey=1024,ktype=b'rsa'): self.set_message(message) if logger is None: logger = get_default_logger() @@ -382,6 +383,8 @@ class DomainSigner(object): #: Minimum public key size. Shorter keys raise KeyFormatError. The #: default is 1024 self.minkey = minkey + #: Key type to use. rsa, rsafp (DCRUP) + self.ktype = ktype #: 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 #: formed. 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: pk = parse_pem_private_key(privkey) except UnparsableKeyError as 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): raise ParameterError("identity must end with domain") @@ -653,10 +660,13 @@ class DKIM(DomainSigner): h = self.hasher() h.update(body) bodyhash = base64.b64encode(h.digest()) - + if self.ktype == b'rsafp': + atag = b'rsafp-sha256' + else: + atag = self.signature_algorithm sigfields = [x for x in [ (b'v', b"1"), - (b'a', self.signature_algorithm), + (b'a', atag), (b'c', canon_policy.to_c_value()), (b'd', domain), (b'i', identity or b"@"+domain), @@ -665,6 +675,7 @@ class DKIM(DomainSigner): (b's', selector), (b't', str(int(time.time())).encode('ascii')), (b'h', b" : ".join(include_headers)), + pubkey and (b'k', pubkey), (b'bh', bodyhash), # Force b= to fold onto it's own line so that refolding after # adding sig doesn't change whitespace for previous tags. diff --git a/dkim/crypto.py b/dkim/crypto.py index c46c97a..41e1da0 100644 --- a/dkim/crypto.py +++ b/dkim/crypto.py @@ -25,6 +25,7 @@ __all__ = [ 'parse_pem_private_key', 'parse_private_key', 'parse_public_key', + 'get_rsa_pubkey', 'RSASSA_PKCS1_v1_5_sign', 'RSASSA_PKCS1_v1_5_verify', 'UnparsableKeyError', @@ -33,6 +34,7 @@ __all__ = [ import base64 import hashlib import re +import rsa from dkim.asn1 import ( ASN1FormatError, @@ -233,6 +235,14 @@ def rsa_decrypt(message, pk, 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): """Perform RSA encryption/verification diff --git a/dkim/tests/__init__.py b/dkim/tests/__init__.py index 40cb64a..8b1b04e 100644 --- a/dkim/tests/__init__.py +++ b/dkim/tests/__init__.py @@ -28,6 +28,7 @@ def test_suite(): test_canonicalization, test_crypto, test_dkim, + test_rsafp, test_util, test_arc, test_dnsplug, @@ -36,6 +37,7 @@ def test_suite(): test_canonicalization, test_crypto, test_dkim, + test_rsafp, test_util, test_arc, test_dnsplug, diff --git a/dkimsign.py b/dkimsign.py index b73d0b6..13c85be 100644 --- a/dkimsign.py +++ b/dkimsign.py @@ -47,6 +47,9 @@ parser.add_argument('--bcanon', choices=['simple', 'relaxed'], parser.add_argument('--signalg', choices=['rsa-sha256', 'rsa-sha1'], 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.') args=parser.parse_args(arguments) include_headers = None @@ -61,6 +64,7 @@ if sys.version_info[0] >= 3: args.hcanon = bytes(args.hcanon, encoding='UTF-8') args.bcanon = bytes(args.bcanon, 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. sys.stdin = sys.stdin.detach() sys.stdout = sys.stdout.detach() @@ -69,7 +73,7 @@ canonicalize = (args.hcanon, args.bcanon) message = sys.stdin.read() try: 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( args.privatekeyfile, "rb").read(), identity = args.identity, canonicalize=canonicalize, include_headers=include_headers,