@@ -0,0 +1,7 @@ | |||||
.coverage | |||||
__pycache__ | |||||
cryptography.egg-info | |||||
pycaenv | |||||
venv |
@@ -0,0 +1,16 @@ | |||||
VIRTUALENV ?= python3 -m venv | |||||
VRITUALENVARGS = | |||||
MODULES=cryptography.tests | |||||
test: venv pycaenv | |||||
find cryptography -name '*.py' | entr make test-noentr | |||||
test-noentr: | |||||
( . ./pycaenv/bin/activate && cd cryptography && python -m unittest tests) && ( . ./venv/bin/activate && pip install -e . && python -m coverage run -m unittest $(MODULES) && coverage report --omit=p/\* -m -i) | |||||
venv: | |||||
($(VIRTUALENV) $(VIRTUALENVARGS) venv && . ./venv/bin/activate && pip install -r requirements.txt) | |||||
pycaenv: | |||||
($(VIRTUALENV) $(VIRTUALENVARGS) pycaenv && . ./pycaenv/bin/activate && pip install cryptography) |
@@ -0,0 +1,33 @@ | |||||
pycryptowrap | |||||
============ | |||||
This is a translation layer to let software written for | |||||
[cryptography](https://cryptography.io/en/latest/) to used with | |||||
[pycryptodome](https://www.pycryptodome.org/). | |||||
It currently only implements a minimal interface to get | |||||
[pywebpush](https://github.com/web-push-libs/pywebpush) working. | |||||
Currently implemented, tested, and working: | |||||
* AES-GCM | |||||
* ECC SECP256R1 - DSS and ECDH | |||||
It shouldn't be too hard to add some additional ciphers or curves. | |||||
Testing | |||||
======= | |||||
The `Makefile` does the neccessary work to build an environment. To | |||||
run the tests: | |||||
``` | |||||
make test-noentr | |||||
``` | |||||
This will first run the tests against the cryptography module, and then | |||||
run the tests against this module. This makes sure that the tests are | |||||
valid, and also makes it easier to add known answers to make sure | |||||
you don't end up only self compatible. | |||||
To make developing easier, the `test` target has one dependency, | |||||
[entr](https://github.com/eradman/entr). This will watch for changes | |||||
in the `py` files, and automatically rerun the test. |
@@ -0,0 +1,5 @@ | |||||
class InvalidTag(Exception): | |||||
pass | |||||
class InvalidSignature(Exception): | |||||
pass |
@@ -0,0 +1,2 @@ | |||||
def default_backend(): | |||||
pass |
@@ -0,0 +1,87 @@ | |||||
from cryptography.exceptions import InvalidSignature | |||||
from Crypto.PublicKey import ECC | |||||
from Crypto.Protocol.DH import _compute_ecdh | |||||
from Crypto.Signature import DSS | |||||
# https://www.pycryptodome.org/src/public_key/ecc# | |||||
SECP256R1 = lambda: 'secp256r1' | |||||
class ECDSA: | |||||
def __init__(self, algorithm): | |||||
self._algo = algorithm | |||||
class ECDH: | |||||
pass | |||||
class EllipticCurvePrivateNumbers: | |||||
def __init__(self, key): | |||||
self._key = key | |||||
@property | |||||
def public_numbers(self): | |||||
return self._key._ecc.pointQ | |||||
@property | |||||
def private_value(self): | |||||
return self._key._ecc.d | |||||
class ECCWrapper: | |||||
def __init__(self, ecckey): | |||||
self._ecc = ecckey | |||||
def private_numbers(self): | |||||
return EllipticCurvePrivateNumbers(self) | |||||
def public_key(self): | |||||
return EllipticCurvePublicKey(self._ecc.public_key()) | |||||
_format_to_compress = dict(nocompress=False, compress=True) | |||||
def public_bytes(self, encoding, format): | |||||
return self.public_key()._ecc.export_key(format=encoding, compress=self._format_to_compress[format]) | |||||
def private_bytes(self, encoding, format, encryption_algorithm): | |||||
return self._ecc.export_key(format=encoding, compress=format) | |||||
# https://www.pycryptodome.org/src/protocol/dh | |||||
def exchange(self, typ, pubkey): | |||||
assert isinstance(typ, ECDH) | |||||
return _compute_ecdh(self._ecc, pubkey._ecc) | |||||
@classmethod | |||||
def generate_private_key(cls, keytype, backend=None): | |||||
if callable(keytype): | |||||
keytype = keytype() | |||||
return cls(ECC.generate(curve=keytype)) | |||||
def sign(self, data, signature_algorithm): | |||||
h = signature_algorithm._algo.new(data) | |||||
signer = DSS.new(self._ecc, 'fips-186-3', 'der') | |||||
return signer.sign(h) | |||||
def verify(self, signature, data, signature_algorithm): | |||||
h = signature_algorithm._algo.new(data) | |||||
verifier = DSS.new(self._ecc, 'fips-186-3', 'der') | |||||
try: | |||||
verifier.verify(h, signature) | |||||
except ValueError: | |||||
raise InvalidSignature | |||||
class EllipticCurvePublicKey(ECCWrapper): | |||||
@classmethod | |||||
def from_encoded_point(cls, curve, key): | |||||
return cls(ECC.import_key(key, curve_name=curve)) | |||||
generate_private_key = ECCWrapper.generate_private_key | |||||
def load_der_private_key(key, password, backend=None): | |||||
if password is not None: | |||||
raise ValueError('unsupported') | |||||
return ECCWrapper(ECC.import_key(key)) |
@@ -0,0 +1,6 @@ | |||||
from Crypto.Util.asn1 import DerSequence | |||||
def decode_dss_signature(signature): | |||||
obj = DerSequence().decode(der_encoded=signature) | |||||
return tuple(obj) |
@@ -0,0 +1,70 @@ | |||||
from Crypto.Cipher import AES as ccAES | |||||
from cryptography.exceptions import InvalidTag | |||||
# https://www.pycryptodome.org/src/cipher/modern#gcm-mode | |||||
class CipherEncryptor: | |||||
def __init__(self, encor): | |||||
self._encor = encor | |||||
self.authenticate_additional_data = encor.update | |||||
self.update = encor.encrypt | |||||
@property | |||||
def tag(self): | |||||
return self._encor.digest() | |||||
def finalize(self): | |||||
return b'' | |||||
class CipherDecryptor: | |||||
def __init__(self, decor, tag=None): | |||||
self._decor = decor | |||||
self._tag = tag | |||||
self.authenticate_additional_data = decor.update | |||||
self.update = decor.decrypt | |||||
def finalize(self): | |||||
try: | |||||
#print(repr(self._decor)) | |||||
self._decor.verify(self._tag) | |||||
except ValueError: | |||||
raise InvalidTag('tag mismatch') | |||||
return b'' | |||||
class Cipher: | |||||
def __init__(self, algo, mode, backend=None): | |||||
self._algo = algo | |||||
self._mode = mode | |||||
def _getmode(self): | |||||
if isinstance(self._mode, GCM): | |||||
return ccAES.MODE_GCM | |||||
def _nonce(self): | |||||
return self._mode._iv | |||||
def encryptor(self): | |||||
return CipherEncryptor(ccAES.new(self._algo._key, | |||||
self._getmode(), nonce=self._nonce())) | |||||
def decryptor(self): | |||||
return CipherDecryptor(ccAES.new(self._algo._key, | |||||
self._getmode(), nonce=self._nonce()), tag=self._mode._tag) | |||||
class AES: | |||||
def __init__(self, key): | |||||
self._key = key | |||||
class algorithms: | |||||
AES = AES | |||||
class GCM: | |||||
def __init__(self, iv, tag=None): | |||||
self._iv = iv | |||||
self._tag = tag | |||||
class modes: | |||||
GCM = GCM |
@@ -0,0 +1,4 @@ | |||||
def SHA256(): | |||||
from Crypto.Hash import SHA256 | |||||
return SHA256 |
@@ -0,0 +1,15 @@ | |||||
from Crypto.Protocol.KDF import HKDF as baseHKDF | |||||
# https://www.pycryptodome.org/src/protocol/kdf#hkdf | |||||
# https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#hkdf | |||||
class HKDF: | |||||
def __init__(self, algorithm, length, salt, info, backend=None): | |||||
self._algo = algorithm | |||||
self._len = length | |||||
self._salt = salt | |||||
self._info = info | |||||
def derive(self, key): | |||||
return baseHKDF(key, self._len, self._salt, self._algo, context=self._info) |
@@ -0,0 +1,15 @@ | |||||
from .asymmetric.ec import load_der_private_key | |||||
class Encoding: | |||||
X962 = 'SEC1' | |||||
Raw = 'raw' | |||||
DER = 'DER' | |||||
class PublicFormat: | |||||
UncompressedPoint = 'nocompress' | |||||
class PrivateFormat: | |||||
PKCS8 = 'pkcs8' | |||||
def NoEncryption(): | |||||
return None |
@@ -0,0 +1,2 @@ | |||||
from .ecc import TestECC | |||||
from .aes import TestAES |
@@ -0,0 +1,61 @@ | |||||
from cryptography.hazmat.backends import default_backend | |||||
from cryptography.hazmat.primitives.ciphers import ( | |||||
Cipher, algorithms, modes | |||||
) | |||||
from cryptography.exceptions import InvalidTag | |||||
import random | |||||
import unittest | |||||
TAG_LENGTH = 16 | |||||
class TestAES(unittest.TestCase): | |||||
def test_aesgcm_cav(self): | |||||
pass | |||||
def test_aesgcm(self): | |||||
buf = bytes.fromhex('00000000000000000000000000000000') | |||||
key = bytes.fromhex('00000000000000000000000000000000') | |||||
iv = bytes.fromhex('000000000000000000000000') | |||||
origdata = buf | |||||
# encryption | |||||
encryptor = Cipher(algorithms.AES(key), | |||||
modes.GCM(iv), backend=default_backend() | |||||
).encryptor() | |||||
last = True | |||||
#if version == 'aes128gcm': | |||||
data = encryptor.update(buf) | |||||
self.assertEqual(data, bytes.fromhex('0388dace60b6a392f328c2b971b2fe78')) | |||||
data += encryptor.finalize() | |||||
self.assertEqual(encryptor.tag, bytes.fromhex('ab6e47d42cec13bdf53a67b21257bddf')) | |||||
data += encryptor.tag | |||||
encdata = data | |||||
# decryption | |||||
content = encdata | |||||
decryptor = Cipher(algorithms.AES(key), | |||||
modes.GCM(iv, tag=content[-TAG_LENGTH:]), | |||||
backend=default_backend() | |||||
).decryptor() | |||||
decdata = decryptor.update(content[:-TAG_LENGTH]) + decryptor.finalize() | |||||
self.assertEqual(origdata, decdata) | |||||
# decryption | |||||
content = encdata | |||||
decryptor = Cipher(algorithms.AES(key), | |||||
modes.GCM(iv, tag=b'\x00' * TAG_LENGTH), | |||||
backend=default_backend() | |||||
).decryptor() | |||||
decdata = decryptor.update(content[:-TAG_LENGTH]) | |||||
self.assertRaises(InvalidTag, decryptor.finalize) |
@@ -0,0 +1,128 @@ | |||||
import unittest | |||||
from cryptography.exceptions import InvalidSignature | |||||
from cryptography.hazmat.backends import default_backend | |||||
from cryptography.hazmat.primitives import serialization | |||||
from cryptography.hazmat.primitives import hashes | |||||
from cryptography.hazmat.primitives.asymmetric import ec | |||||
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature | |||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF | |||||
class TestECC(unittest.TestCase): | |||||
def test_dh(self): | |||||
# slightly modified from: | |||||
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/#elliptic-curve-key-exchange-algorithm | |||||
# private keys for use in the exchange. | |||||
keya = b'0\x81\x87\x02\x01\x000\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x04m0k\x02\x01\x01\x04 \x04\xa3X\xbd\x0e\xff*\x8cw\xf8\x9f\x05BD<\nY\xb3\xf1\xd2\xc1\xb0\r\x1e\xedu\x92]4M?\x01\xa1D\x03B\x00\x04P\xd9y\x92f\t\xa7x\xf3\xcf\x17O\xad\x93\xf9\x18"\t\xd3\x13*]3\xa7#\x8bH$j\xea\xfb\x8a\xd3\xb5\xee\xd9\x0f\x9c\xdb\xcc\xf1\xd7\x10\x88\x10e\x82-\x15CR\x08\xbe\x0c\x1e\x82p\x00C\xb2.O\x17\xd4' | |||||
keyb = b'0\x81\x87\x02\x01\x000\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x04m0k\x02\x01\x01\x04 \xfb\xf8\xf7\x9f\xa3\xb7\xed\x8cT@`\xf6\x9c\xbbv\x0e?\x87\xb1(\xf6\xa8\xb3`\x91\xb4\x92W\xc6\xaa\xf5~\xa1D\x03B\x00\x04\xb8\xfe\xe4\x8dkukc\xa4^\x87\x98\x9c\xb9\xa8\xec\x86\xf8\xc2\x89\xaeF\xe8q\xb9q\x92I\x98n\xfe\xe3<{\x1c&R\x82\xb1\x94=\xa5h*)m/\x13\xfb\x05\x1d\x98u\xec\x1ew\xdfW\x84\xfe\x9eSl\x83' | |||||
#server_private_key = ec.generate_private_key(ec.SECP256R1()) | |||||
#print('spk:', repr(server_private_key.private_bytes( | |||||
# encoding=serialization.Encoding.DER, | |||||
# format=serialization.PublicFormat.UncompressedPoint, | |||||
# algorithm=None))) | |||||
server_private_key = serialization.load_der_private_key(keya, | |||||
None, backend=default_backend()) | |||||
pubpoint = server_private_key.private_numbers().public_numbers | |||||
self.assertEqual(server_private_key.private_numbers(). \ | |||||
private_value, | |||||
2097859916579721232322403601989230314767884081400167668022347085958538411777) | |||||
self.assertEqual(pubpoint.x, | |||||
36569272757924220784927781299997671003354453138026426189464987345196826688394) | |||||
self.assertEqual(pubpoint.y, | |||||
95759458837377270694950453035845273914475242769728982836658929275588228093908) | |||||
self.assertEqual(keya, server_private_key.private_bytes( | |||||
encoding=serialization.Encoding.DER, | |||||
format=serialization.PrivateFormat.PKCS8, | |||||
encryption_algorithm=serialization.NoEncryption())) | |||||
# In a real handshake the peer is a remote client. For this | |||||
# example we'll generate another local private key though. | |||||
peer_private_key = serialization.load_der_private_key(keyb, | |||||
None) | |||||
shared_key = server_private_key.exchange( | |||||
ec.ECDH(), peer_private_key.public_key()) | |||||
self.assertEqual(shared_key, | |||||
b'\x1a\x9f\x93c\xb0s\xa2\x15]{\xa3\xcc\xcf&Q\xd6g\x83\x86%\x7f\t\xfem@\xcb\xe9:U\x16\x07\x02') | |||||
# Perform key derivation. | |||||
derived_key = HKDF(algorithm=hashes.SHA256(), | |||||
length=32, salt=None, info=b'handshake data', backend=None | |||||
).derive(shared_key) | |||||
# And now we can demonstrate that the handshake performed in | |||||
# the opposite direction gives the same final value | |||||
same_shared_key = peer_private_key.exchange( | |||||
ec.ECDH(), server_private_key.public_key()) | |||||
self.assertEqual(shared_key, same_shared_key) | |||||
# Perform key derivation. | |||||
same_derived_key = HKDF(algorithm=hashes.SHA256(), | |||||
length=32, salt=None, info=b'handshake data', | |||||
).derive(same_shared_key) | |||||
self.assertEqual(derived_key, | |||||
b'\x89\r\xf7\xf0\xa6\xb9Z\xb9\xd7\xd0\x9b\x95y\xe0M\x11,\xb4\xe1Z\xe5\xa2j\xee)\xa0I\xb5Q\x18\x94\xd1') | |||||
self.assertEqual(derived_key, same_derived_key) | |||||
def test_decode_sig(self): | |||||
sig = b"0D\x02 P\x92\xaf\xffoN\xadq\r=\x92\xb5\r\xe0l3\xf2\x80*\xdd|\xfe\xd8'\xb8\\\xe8\x94\xd6\xa1\xdb\xea\x02 \x18\x89j\xa8P\x83jk*\xb8\xa2\x15r&d\xa1\x9e\xf6\xec\xd2\xf4 \xd6\x08\x91bs\x18\xb5\x11/\x04" | |||||
res = (36444202250238074078057463719437572015031876874679459625290239663827367091178, 11098302536735876471048588108325227764001515075886106999029234198831298588420) | |||||
self.assertEqual(decode_dss_signature(sig), res) | |||||
def test_sign(self): | |||||
private_key = ec.generate_private_key(ec.SECP256R1()) | |||||
data = b"this is some data I'd like to sign" | |||||
signature = private_key.sign(data, ec.ECDSA(hashes.SHA256())) | |||||
# make sure we can decode our own signatures | |||||
decode_dss_signature(signature) | |||||
public_key = private_key.public_key() | |||||
public_key.verify(signature, data, ec.ECDSA(hashes.SHA256())) | |||||
wrongsig = bytearray(signature) | |||||
wrongsig[0] ^= 1 | |||||
wrongsig[1] ^= 4 | |||||
wrongsig = bytes(wrongsig) | |||||
self.assertRaises(InvalidSignature, public_key.verify, wrongsig, data, ec.ECDSA(hashes.SHA256())) | |||||
def test_misc(self): | |||||
keya = b'0\x81\x87\x02\x01\x000\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x04m0k\x02\x01\x01\x04 \x04\xa3X\xbd\x0e\xff*\x8cw\xf8\x9f\x05BD<\nY\xb3\xf1\xd2\xc1\xb0\r\x1e\xedu\x92]4M?\x01\xa1D\x03B\x00\x04P\xd9y\x92f\t\xa7x\xf3\xcf\x17O\xad\x93\xf9\x18"\t\xd3\x13*]3\xa7#\x8bH$j\xea\xfb\x8a\xd3\xb5\xee\xd9\x0f\x9c\xdb\xcc\xf1\xd7\x10\x88\x10e\x82-\x15CR\x08\xbe\x0c\x1e\x82p\x00C\xb2.O\x17\xd4' | |||||
skey = serialization.load_der_private_key(keya, | |||||
None, backend=default_backend()) | |||||
ckey = skey.public_key().public_bytes( | |||||
encoding=serialization.Encoding.X962, | |||||
format=serialization.PublicFormat.UncompressedPoint | |||||
) | |||||
self.assertEqual(ckey, b'\x04P\xd9y\x92f\t\xa7x\xf3\xcf\x17O\xad\x93\xf9\x18"\t\xd3\x13*]3\xa7#\x8bH$j\xea\xfb\x8a\xd3\xb5\xee\xd9\x0f\x9c\xdb\xcc\xf1\xd7\x10\x88\x10e\x82-\x15CR\x08\xbe\x0c\x1e\x82p\x00C\xb2.O\x17\xd4') | |||||
pubkey = ec.EllipticCurvePublicKey.from_encoded_point( | |||||
ec.SECP256R1(), ckey) | |||||
self.assertTrue(isinstance(pubkey, ec.EllipticCurvePublicKey)) | |||||
self.assertEqual(ckey, pubkey.public_bytes( | |||||
encoding=serialization.Encoding.X962, | |||||
format=serialization.PublicFormat.UncompressedPoint | |||||
)) |
@@ -0,0 +1,3 @@ | |||||
-e . | |||||
-e .[dev] |
@@ -0,0 +1,15 @@ | |||||
from setuptools import setup, Extension | |||||
setup(name = "cryptography", version = "41.0.5", | |||||
description = "Emulate cryptography enough for some needs", | |||||
author = "John-Mark Gurney", | |||||
author_email = "jmg@funkthat.com", | |||||
url = 'about:blank', | |||||
packages = [ 'cryptography' ], | |||||
extras_require = { | |||||
'dev': [ 'coverage' ], | |||||
}, | |||||
install_requires=[ | |||||
'pycryptodome', | |||||
], | |||||
) |