def sign(self, selector, domain, privkey, auth_results, chain_validation_status, include_headers=None, timestamp=None, standardize=False): try: pk = parse_pem_private_key(privkey) except UnparsableKeyError as e: raise KeyFormatError(str(e)) # Setup headers if include_headers is None: include_headers = self.default_sign_headers() if b'arc-authentication-results' not in include_headers: include_headers.append(b'arc-authentication-results') include_headers = tuple([x.lower() for x in include_headers]) # record what verify should extract self.include_headers = include_headers # rfc4871 says FROM is required if b'from' not in include_headers: raise ParameterError("The From header field MUST be signed") # raise exception for any SHOULD_NOT headers, call can modify # SHOULD_NOT if really needed. for x in set(include_headers).intersection(self.should_not_sign): raise ParameterError("The %s header field SHOULD NOT be signed" % x) max_instance, arc_headers_w_instance = self.sorted_arc_headers() instance = 1 if len(arc_headers_w_instance) != 0: instance = max_instance + 1 if instance == 1 and chain_validation_status != CV_None: raise ParameterError( "No existing chain found on message, cv should be none") elif instance != 1 and chain_validation_status == CV_None: raise ParameterError("cv=none not allowed on instance %d" % instance) new_arc_set = [] arc_headers = [y for x, y in arc_headers_w_instance] # Compute ARC-Authentication-Results aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results if aar_value[-1] != b'\n': aar_value += b'\r\n' new_arc_set.append(b"ARC-Authentication-Results: " + aar_value) self.headers.insert(0, (b"arc-authentication-results", aar_value)) arc_headers.insert(0, (b"ARC-Authentication-Results", aar_value)) # Compute bh= canon_policy = CanonicalizationPolicy.from_c_value(b'relaxed/relaxed') self.hasher = HASH_ALGORITHMS[self.signature_algorithm] h = HashThrough(self.hasher()) h.update(canon_policy.canonicalize_body(self.body)) self.logger.debug("sign ams body hashed: %r" % h.hashed()) bodyhash = base64.b64encode(h.digest()) # Compute ARC-Message-Signature timestamp = str(timestamp or int(time.time())).encode('ascii') ams_fields = [ x for x in [ (b'i', str(instance).encode('ascii')), (b'a', self.signature_algorithm), (b'c', b'relaxed/relaxed'), (b'd', domain), (b's', selector), (b't', timestamp), (b'h', b" : ".join(include_headers)), (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. (b'b', b'0' * 60), ] if x ] res = self.gen_header(ams_fields, include_headers, canon_policy, b"ARC-Message-Signature", pk, standardize) new_arc_set.append(b"ARC-Message-Signature: " + res) self.headers.insert(0, (b"ARC-Message-Signature", res)) arc_headers.insert(0, (b"ARC-Message-Signature", res)) # Compute ARC-Seal as_fields = [ x for x in [ (b'i', str(instance).encode('ascii')), (b'cv', chain_validation_status), (b'a', self.signature_algorithm), (b'd', domain), (b's', selector), (b't', timestamp), # Force b= to fold onto it's own line so that refolding after # adding sig doesn't change whitespace for previous tags. (b'b', b'0' * 60), ] if x ] as_include_headers = [x[0].lower() for x in arc_headers] as_include_headers.reverse() res = self.gen_header(as_fields, as_include_headers, canon_policy, b"ARC-Seal", pk, standardize) new_arc_set.append(b"ARC-Seal: " + res) self.headers.insert(0, (b"ARC-Seal", res)) arc_headers.insert(0, (b"ARC-Seal", res)) new_arc_set.reverse() return new_arc_set
def setUp(self): self.key = parse_pem_private_key(read_test_data("test.private")) self.hash = hashlib.sha1(self.test_digest)
def sign(self, selector, domain, privkey, identity=None, 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 identity is not None and not identity.endswith(domain): raise ParameterError("identity must end with domain") canon_policy = CanonicalizationPolicy.from_c_value( b'/'.join(canonicalize)) if include_headers is None: include_headers = self.default_sign_headers() include_headers = tuple([x.lower() for x in include_headers]) # record what verify should extract self.include_headers = include_headers # rfc4871 says FROM is required if b'from' not in include_headers: raise ParameterError("The From header field MUST be signed") # raise exception for any SHOULD_NOT headers, call can modify # SHOULD_NOT if really needed. for x in set(include_headers).intersection(self.should_not_sign): raise ParameterError("The %s header field SHOULD NOT be signed" % x) body = canon_policy.canonicalize_body(self.body) self.hasher = HASH_ALGORITHMS[self.signature_algorithm] h = self.hasher() h.update(body) bodyhash = base64.b64encode(h.digest()) sigfields = [ x for x in [ (b'v', b"1"), (b'a', self.signature_algorithm), (b'c', canon_policy.to_c_value()), (b'd', domain), (b'i', identity or b"@" + domain), length and (b'l', str(len(body)).encode('ascii')), (b'q', b"dns/txt"), (b's', selector), (b't', str(int(time.time())).encode('ascii')), (b'h', b" : ".join(include_headers)), (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. (b'b', b'0' * 60), ] if x ] res = self.gen_header(sigfields, include_headers, canon_policy, b"DKIM-Signature", pk) self.domain = domain self.selector = selector self.signature_fields = dict(sigfields) return b'DKIM-Signature: ' + res
def test_parse_pem_private_key(self): key = parse_pem_private_key(read_test_data("test.private")) self.assertEqual(key, TEST_PK)
def sign(self, selector, domain, privkey, identity=None, 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 identity is not None and not identity.endswith(domain): raise ParameterError("identity must end with domain") canon_policy = CanonicalizationPolicy.from_c_value( b'/'.join(canonicalize)) headers = canon_policy.canonicalize_headers(self.headers) if include_headers is None: include_headers = self.default_sign_headers() # rfc4871 says FROM is required if b'from' not in ( x.lower() for x in include_headers ): raise ParameterError("The From header field MUST be signed") # raise exception for any SHOULD_NOT headers, call can modify # SHOULD_NOT if really needed. for x in include_headers: if x.lower() in self.should_not_sign: raise ParameterError("The %s header field SHOULD NOT be signed"%x) body = canon_policy.canonicalize_body(self.body) hasher = HASH_ALGORITHMS[self.signature_algorithm] h = hasher() h.update(body) bodyhash = base64.b64encode(h.digest()) sigfields = [x for x in [ (b'v', b"1"), (b'a', self.signature_algorithm), (b'c', canon_policy.to_c_value()), (b'd', domain), (b'i', identity or b"@"+domain), length and (b'l', len(body)), (b'q', b"dns/txt"), (b's', selector), (b't', str(int(time.time())).encode('ascii')), (b'h', b" : ".join(include_headers)), (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. (b'b', b'0'*60), ] if x] include_headers = [x.lower() for x in include_headers] # record what verify should extract self.include_headers = tuple(include_headers) sig_value = fold(b"; ".join(b"=".join(x) for x in sigfields)) sig_value = RE_BTAG.sub(b'\\1',sig_value) dkim_header = (b'DKIM-Signature', b' ' + sig_value) h = hasher() sig = dict(sigfields) self.signed_headers = hash_headers( h, canon_policy, headers, include_headers, dkim_header,sig) self.logger.debug("sign headers: %r" % self.signed_headers) try: sig2 = RSASSA_PKCS1_v1_5_sign(h, pk) except DigestTooLargeError: raise ParameterError("digest too large for modulus") # Folding b= is explicity allowed, but yahoo and live.com are broken #sig_value += base64.b64encode(bytes(sig2)) # Instead of leaving unfolded (which lets an MTA fold it later and still # breaks yahoo and live.com), we change the default signing mode to # relaxed/simple (for broken receivers), and fold now. sig_value = fold(sig_value + base64.b64encode(bytes(sig2))) self.domain = domain self.selector = selector self.signature_fields = sig return b'DKIM-Signature: ' + sig_value + b"\r\n"
def test_parse_pem_private_key(self): key = parse_pem_private_key(read_test_data('test.private')) self.assertEqual(key, TEST_PK)
def setUp(self): self.key = parse_pem_private_key(read_test_data('test.private')) self.hash = hashlib.sha1(self.test_digest)
def sig_gen(public, private, body, amsh, arsh, fold=False, verbose=False, as_tmp = None, ams_tmp = None): # body hasher = HASH_ALGORITHMS[b'rsa-sha256'] h = hasher() h.update(body) bh = base64.b64encode(h.digest()) print("ams bh= ") print(bh) #amsh hasher = HASH_ALGORITHMS[b'rsa-sha256'] h = hasher() h = HashThrough(hasher()) h.update(b"\r\n".join([x + b":" + y for (x,y) in amsh(bh)])) if verbose: print("\nsign ams hashed: %r" % h.hashed()) pk = parse_pem_private_key(private) sig2 = RSASSA_PKCS1_v1_5_sign(h, pk) msb = base64.b64encode(bytes(sig2)) if fold: msb = msb[:70] + b" " + msb[70:142] + b" " + msb[142:214]# + b" " + msb[214:286] + b" " + msb[286:] print("ams b= ") print(msb) #pk = parse_public_key(base64.b64decode(public)) #signature = base64.b64decode(b) #ams_valid = RSASSA_PKCS1_v1_5_verify(h, signature, pk) #print("ams sig valid: %r" % ams_valid) hasher = HASH_ALGORITHMS[b'rsa-sha256'] h = hasher() h = HashThrough(hasher()) h.update(b"\r\n".join([x + b":" + y for (x,y) in arsh(msb, bh)])) if verbose: print("\nsign ars hashed: %r" % h.hashed()) pk = parse_pem_private_key(private) sig2 = RSASSA_PKCS1_v1_5_sign(h, pk) sb = base64.b64encode(bytes(sig2)) print("arsh b=") print(sb) #signature = base64.b64decode(b) #ams_valid = RSASSA_PKCS1_v1_5_verify(h, signature, pk) #print("arsh sig valid: %r" % ams_valid) spc = fold and b"" or b" " accum = '' if as_tmp: sb = sb[:70] + b"\n " + spc + sb[70:142] + b"\n " + spc + sb[142:214]# + b"\n " + sb[214:286] + b"\n " + msb[286:] res = as_tmp.replace(b'%b', sb) accum = res print(res.decode('utf-8')) if ams_tmp: msb = msb.replace(b' ', b'') msb = msb[:70] + b"\n " + spc + msb[70:142] + b"\n " + spc + msb[142:214]# + b"\n " + msb[214:286] + b"\n " + msb[286:] res = ams_tmp.replace(b'%bh', bh) res = res.replace(b'%b', msb) accum += b"\n" + res print(res.decode('utf-8')) os.system(b'echo "' + accum + b'" | pbcopy')