Skip to content

Commit

Permalink
PR fix keystore method signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
Mitchell Yuwono committed Feb 13, 2017
1 parent f245abc commit b3e8731
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 109 deletions.
35 changes: 25 additions & 10 deletions docs/api.rst
Expand Up @@ -12,36 +12,51 @@
See the License for the specific language governing permissions and
limitations under the License.
API Client
==========
API
====

The `covata.delta.api` module provides a set of tools for executing REST
calls to Delta API.

DeltaApiClient
--------------

The Delta API Client is an abstraction over the Delta API for execution of
requests and responses.

.. currentmodule:: covata.delta

.. autoclass:: ApiClient
.. autoclass:: DeltaApiClient
:members:

RequestsApiClient
-----------------

.. currentmodule:: covata.delta.api

An implementation of ``ApiClient`` abstract base class using ``Requests``.
Requests ApiClient
~~~~~~~~~~~~~~~~~~

An implementation of ``DeltaApiClient`` abstract base class using ``Requests``.

.. autoclass:: RequestsApiClient
:show-inheritance:
:members:

RequestsSigner
--------------
CVTSigner
---------

The Delta CVT Signer is utility class for signing outbound requests using
the CVT1 signing scheme.

.. autoclass:: CVTSigner
:members:

RequestsCVTSigner
~~~~~~~~~~~~~~~~~

An authentication interceptor for ``Requests`` library.
This interceptor generates and inserts an Authorization header into the
request based on the CVT1 signing scheme. A date header will also be added
to the request.

.. autoclass:: RequestsSigner
.. autoclass:: RequestsCVTSigner
:show-inheritance:
:members:
13 changes: 10 additions & 3 deletions docs/crypto.rst
Expand Up @@ -21,17 +21,24 @@ The Delta Crypto package provides functionality for client side cryptography.
.. automodule:: covata.delta.crypto
:members:

KeyStore
--------
DeltaKeyStore
-------------

The ``DeltaKeyStore`` provides the interface for a key-storage
backend of choice.

.. currentmodule:: covata.delta

.. autoclass:: KeyStore
.. autoclass:: DeltaKeyStore
:members:

FileSystemKeyStore
~~~~~~~~~~~~~~~~~~

Implementation of the ``DeltaKeyStore`` abstract base class using the file
system. Private keys are saved in the file system as encrypted PEM formats
and are only decrypted in memory on read.

.. currentmodule:: covata.delta.crypto

.. autoclass:: FileSystemKeyStore
Expand Down
4 changes: 2 additions & 2 deletions src/main/python/covata/delta/__init__.py
Expand Up @@ -14,7 +14,7 @@

from __future__ import absolute_import

from .interfaces import ApiClient, KeyStore
from .interfaces import DeltaApiClient, DeltaKeyStore
from .utils import LogMixin

__all__ = ["ApiClient", "KeyStore", "LogMixin"]
__all__ = ["DeltaApiClient", "DeltaKeyStore", "LogMixin"]
15 changes: 7 additions & 8 deletions src/main/python/covata/delta/api/requestsclient.py
Expand Up @@ -16,17 +16,16 @@

import requests

from covata.delta import ApiClient, LogMixin, crypto
from covata.delta import DeltaApiClient, LogMixin, crypto
from covata.delta.api.signer import CVTSigner

from requests.auth import AuthBase


class RequestsApiClient(ApiClient, LogMixin):
class RequestsApiClient(DeltaApiClient, LogMixin):
def register_identity(self, external_id=None, metadata=None):
keystore = self.keystore
signing_private_key = crypto.generate_key()
crypto_private_key = crypto.generate_key()
signing_private_key = crypto.generate_private_key()
crypto_private_key = crypto.generate_private_key()

signing_public_key = signing_private_key.public_key()
crypto_public_key = crypto_private_key.public_key()
Expand All @@ -43,9 +42,9 @@ def register_identity(self, external_id=None, metadata=None):

identity_id = response.json()['identityId']

keystore.save(signing_private_key, identity_id + ".signing.pem")
keystore.save(crypto_private_key, identity_id + ".crypto.pem")

self.keystore.save(signing_private_key=signing_private_key,
crypto_private_key=crypto_private_key,
identity_id=identity_id)
return identity_id

def get_identity(self, requestor_id, identity_id):
Expand Down
47 changes: 26 additions & 21 deletions src/main/python/covata/delta/api/signer.py
Expand Up @@ -25,9 +25,15 @@
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

from ..crypto import sha256hex
from ..crypto import calculate_sha256hex
from ..utils import LogMixin

__all__ = ["CVTSigner"]

UNDESIRED_HEADERS = ["Connection", "Content-Length"]
SIGNING_ALGORITHM = "CVT1-RSA4096-SHA256"
CVT_DATE_FORMAT = "%Y%m%dT%H%M%SZ"


class SignatureMaterial(namedtuple('SignatureMaterial', [
'method',
Expand All @@ -53,10 +59,6 @@ def canonical_request(self):


class CVTSigner(LogMixin):
UNDESIRED_HEADERS = ["Connection", "Content-Length"]
SIGNING_ALGORITHM = "CVT1-RSA4096-SHA256"
CVT_DATE_FORMAT = "%Y%m%dT%H%M%SZ"

def __init__(self, keystore):
"""
Creates a Request Signer object to sign a request
Expand All @@ -69,45 +71,48 @@ def __init__(self, keystore):

def get_signed_headers(self, identity_id, method, url, headers, payload):
"""
Gets the signed headers
Gets an updated header dictionary with an authorization header
signed using the CVT1 request signing scheme.
:param str identity_id: the authorizing identity id
:param str method: the HTTP request method
:param str url: the delta url
:param dict headers: the request headers
:param payload: the request payload
:return: the original headers with a signed Authorization header.
:return:
the original headers with additional Cvt-Date, Host, and
Authorization headers.
:rtype: dict
"""
_url = urllib.parse.urlparse(url)
cvt_date = datetime.utcnow().strftime(self.CVT_DATE_FORMAT)
_headers = dict(headers)
_headers["Cvt-Date"] = cvt_date
_headers['Host'] = _url.hostname
url_parsed = urllib.parse.urlparse(url)
cvt_date = datetime.utcnow().strftime(CVT_DATE_FORMAT)
headers_ = dict(headers)
headers_["Cvt-Date"] = cvt_date
headers_['Host'] = url_parsed.hostname
signature_materials = self.__get_materials(
method, _url.path, _url.query, _headers, payload)
method, url_parsed.path, url_parsed.query, headers_, payload)
canonical_request = signature_materials.canonical_request
self.logger.debug(canonical_request)
string_to_sign = "\n".join([
self.SIGNING_ALGORITHM,
SIGNING_ALGORITHM,
cvt_date,
sha256hex(canonical_request).decode('utf-8')])
calculate_sha256hex(canonical_request).decode('utf-8')])

self.logger.debug(string_to_sign)
signature = \
b64encode(self.__sign(string_to_sign, identity_id)).decode('utf-8')

_headers["Authorization"] = \
headers_["Authorization"] = \
"{algorithm} Identity={identity_id}, " \
"SignedHeaders={signed_headers}, Signature={signature}" \
.format(algorithm=self.SIGNING_ALGORITHM,
.format(algorithm=SIGNING_ALGORITHM,
identity_id=identity_id,
signed_headers=signature_materials.signed_headers,
signature=signature)
return _headers
return headers_

def __sign(self, string_to_sign, identity_id):
private_key = self.__keystore.load(identity_id + ".signing.pem")
private_key = self.__keystore.load_signing_private_key(identity_id)
return private_key.sign(string_to_sign.encode("utf-8"),
padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
salt_length=32),
Expand All @@ -122,7 +127,7 @@ def __get_materials(self, method, path, query, headers, payload):
sorted_header = OrderedDict(sorted(
(k.lower(), re.sub("\s+", ' ', v).strip())
for k, v in headers.items()
if k not in self.UNDESIRED_HEADERS))
if k not in UNDESIRED_HEADERS))

canonical_headers = "\n ".join(
"{}:{}".format(k, v) for (k, v) in sorted_header.items())
Expand All @@ -144,7 +149,7 @@ def __get_hashed_payload(payload):
json.loads(payload.decode('utf-8')),
separators=(',', ':'),
sort_keys=True)
return sha256hex(sorted_payload).decode('utf-8')
return calculate_sha256hex(sorted_payload).decode('utf-8')

@staticmethod
def __encode_uri(resource_path):
Expand Down
6 changes: 4 additions & 2 deletions src/main/python/covata/delta/crypto/__init__.py
Expand Up @@ -14,8 +14,10 @@

from __future__ import absolute_import

from .cryptoservice import serialize_public_key, sha256hex, generate_key
from .cryptoservice import serialize_public_key, calculate_sha256hex, \
generate_private_key
from .keystore import FileSystemKeyStore

__all__ = ["FileSystemKeyStore",
"serialize_public_key", "sha256hex", "generate_key"]
"serialize_public_key", "calculate_sha256hex",
"generate_private_key"]
9 changes: 5 additions & 4 deletions src/main/python/covata/delta/crypto/cryptoservice.py
Expand Up @@ -21,16 +21,17 @@
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa

__all__ = ["generate_key", "serialize_public_key", "sha256hex"]
__all__ = ["generate_private_key", "serialize_public_key",
"calculate_sha256hex"]


def generate_key():
def generate_private_key():
# type: () -> rsa.RSAPrivateKey
"""
Generates an RSA private key object. The public key object can be
extracted by calling public_key() method on the generated key object.
>>> private_key = FileSystemKeyStore.generate_key() # generate a private key
>>> private_key = generate_private_key() # generate a private key
>>> public_key = private_key.public_key() # get associated public key
:return: the generated private key object
Expand All @@ -57,7 +58,7 @@ def serialize_public_key(public_key):
return base64.b64encode(der).decode(encoding='utf-8')


def sha256hex(payload):
def calculate_sha256hex(payload):
"""
Calculates the SHA256 hex digest of the given payload.
Expand Down
31 changes: 21 additions & 10 deletions src/main/python/covata/delta/crypto/keystore.py
Expand Up @@ -20,11 +20,11 @@
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey

from ..interfaces import KeyStore
from ..utils import LogMixin
from covata.delta import DeltaKeyStore
from covata.delta import LogMixin


class FileSystemKeyStore(KeyStore, LogMixin):
class FileSystemKeyStore(DeltaKeyStore, LogMixin):
def __init__(self,
key_store_path,
key_store_passphrase):
Expand All @@ -39,7 +39,18 @@ def __init__(self,
self.key_store_path = os.path.expanduser(key_store_path)
self.__key_store_passphrase = key_store_passphrase

def save(self, private_key, name):
def save(self, signing_private_key, crypto_private_key, identity_id):
# type: (RSAPrivateKey, RSAPrivateKey, str) -> None
self.__save(signing_private_key, "{}.signing.pem".format(identity_id))
self.__save(crypto_private_key, "{}.crypto.pem".format(identity_id))

def load_signing_private_key(self, identity_id):
return self.__load("{}.signing.pem".format(identity_id))

def load_crypto_private_key(self, identity_id):
return self.__load("{}.crypto.pem".format(identity_id))

def __save(self, private_key, file_name):
# type: (RSAPrivateKey, str) -> None
if not isinstance(private_key, RSAPrivateKey):
raise TypeError("private_key must be an instance of RSAPrivateKey, "
Expand All @@ -50,26 +61,26 @@ def save(self, private_key, name):
serialization.PrivateFormat.PKCS8,
serialization.BestAvailableEncryption(self.__key_store_passphrase))

file_path = os.path.join(self.key_store_path, name)
file_path = os.path.join(self.key_store_path, file_name)
if not os.path.isdir(self.key_store_path):
self.logger.debug("creating directory %s", self.key_store_path)
os.makedirs(self.key_store_path)

if os.path.isfile(file_path):
msg = "Save failed: A key with name [{}] exists in keystore".format(
name)
file_name)
self.logger.error(msg)
raise IOError(msg)

with open(file_path, 'w') as f:
self.logger.debug("Saving %s", name)
self.logger.debug("Saving %s", file_name)
f.write(pem.decode(encoding='utf8'))

def load(self, name):
def __load(self, file_name):
# type: (str) -> RSAPrivateKey
file_path = os.path.join(self.key_store_path, name)
file_path = os.path.join(self.key_store_path, file_name)
with(open(file_path, 'r')) as f:
self.logger.debug("Loading %s", name)
self.logger.debug("Loading %s", file_name)
return serialization.load_pem_private_key(
f.read().encode('utf-8'),
password=self.__key_store_passphrase,
Expand Down

0 comments on commit b3e8731

Please sign in to comment.