Skip to content

Commit

Permalink
Add Support for Signature Validation via HTTP
Browse files Browse the repository at this point in the history
* Implement HTTP request parser in handler/base.go and related
  exception and test
* Implement request handler interface and factory
* Implement ED25519 request handler and related test
* Modify annotator factory to accomodate the new HTTPPKI annotator
* Implement httppki annotator and related tests

Fix project-alvarium#4
Signed-off-by: husseinfakharany <fakharany.hussein@gmail.com>
  • Loading branch information
husseinfakharany committed Apr 20, 2022
1 parent 9cc9f46 commit 147358c
Show file tree
Hide file tree
Showing 21 changed files with 569 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/alvarium/annotators/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
class AnnotatorException(Exception):
"""A general exception type to be used by the annotators"""
"""A general exception type to be used by the annotators"""
4 changes: 3 additions & 1 deletion src/alvarium/annotators/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .pki import PkiAnnotator
from .source import SourceAnnotator
from .tls import TlsAnnotator
from .pki_http import HttpPkiAnnotator

class AnnotatorFactory():
"""A factory that provides multiple implementations of the Annotator interface"""
Expand All @@ -23,6 +24,7 @@ def get_annotator(self, kind: AnnotationType, sdk_info: SdkInfo) -> Annotator:
return TlsAnnotator(hash=sdk_info.hash.type, signature=sdk_info.signature)
elif kind == AnnotationType.PKI:
return PkiAnnotator(hash=sdk_info.hash.type, sign_info=sdk_info.signature)
elif kind == AnnotationType.PKIHTTP:
return HttpPkiAnnotator(hash=sdk_info.hash.type, sign_info=sdk_info.signature)
else:
raise AnnotatorException("Annotator type is not supported")

Empty file.
30 changes: 30 additions & 0 deletions src/alvarium/annotators/handler/contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from dataclasses import dataclass
from enum import Enum

HTTP_REQUEST_KEY = "HttpRequestKey"
CONTENT_TYPE = "Content-Type"
CONTENT_LENGTH = "Content-Length"
class DerivedComponent(Enum):
Method = "@method"
TargetURI = "@target-uri"
Authority = "@authority"
Scheme = "@scheme"
Path = "@path"
Query = "@query"
QueryParams = "@query-params"

def __str__(self) -> str:
return f'{self.value}'
@dataclass
class parseResult:
"""A data class that holds the parsed data"""

seed: str
signature: str
keyid: str
algorithm: str

def __eq__(self, __o: object) -> bool:
if not isinstance(__o, parseResult):
return NotImplemented
return self.seed == __o.seed and self.signature == __o.signature and self.keyid == __o.keyid and self.algorithm == __o.algorithm
40 changes: 40 additions & 0 deletions src/alvarium/annotators/handler/ed25519.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import datetime
from typing import List

from requests import Request
from alvarium.annotators.handler.interfaces import RequestHandler

from alvarium.sign.contracts import SignInfo
from alvarium.sign.ed25519 import Ed25519SignProvider
from io import StringIO
from .utils import parseSignature


class Ed25519RequestHandler(RequestHandler):

def __init__(self, request: Request) -> None:
self.request = request

def AddSignatureHeaders(self, ticks: datetime, fields: List[str], keys: SignInfo) -> None:
headerValue = StringIO()

for i in range(len(fields)):
headerValue.write(f'"{str(fields[i])}"')
if i < len(fields) - 1:
headerValue.write(f' ')

headerValue.write(f';created={str(int(ticks.timestamp()))};keyid="{str(keys.public.path)}";alg="{str(keys.public.type)}";')

self.request.headers['Signature-Input'] = headerValue.getvalue()

parsed = parseSignature(self.request)
inputValue = bytes(parsed.seed, 'utf-8')
p = Ed25519SignProvider()

with open(keys.private.path, 'r') as file:
prv_hex = file.read()
prv = bytes.fromhex(prv_hex)

signature = p.sign(prv, inputValue)

self.request.headers['Signature'] = str(signature)
5 changes: 5 additions & 0 deletions src/alvarium/annotators/handler/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class ParserException(Exception):
"""A general exception type to be used by the parser"""

class RequestHandlerException(Exception):
"""A general exception type to be used by the request handler"""
12 changes: 12 additions & 0 deletions src/alvarium/annotators/handler/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import requests
from alvarium.sign.contracts import SignInfo, SignType
from .interfaces import RequestHandler
from .exceptions import RequestHandlerException

class RequestHandlerFactory():

def getRequestHandler(request: requests, keys: SignInfo) -> RequestHandler:
if keys.private.type == SignType.ED25519:
pass
else:
raise RequestHandlerException("Key type is not supported")
10 changes: 10 additions & 0 deletions src/alvarium/annotators/handler/interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from abc import ABC, abstractmethod
import datetime
from typing import List
from alvarium.sign.contracts import SignInfo

class RequestHandler(ABC):

@abstractmethod
def AddSignatureHeaders(self, ticks: datetime, fields: List[str], keys: SignInfo) -> None:
pass
93 changes: 93 additions & 0 deletions src/alvarium/annotators/handler/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from requests import Request, structures
from .exceptions import ParserException
from urllib.parse import urlparse
from .contracts import parseResult, DerivedComponent
from io import StringIO

def parseSignature(r: Request) -> parseResult:

# Making the request headers case insensitive
headers = structures.CaseInsensitiveDict(r.headers)

# Signature Inputs Extraction
signatureInput = headers.get("Signature-Input")
try:
signature = headers.get("Signature")
if signature == None:
signature = ""
except KeyError:
signature = ""

signatureInputList = signatureInput.split(";",1)
signatureInputHeader = signatureInputList[0].split(" ")
signatureInputTail = signatureInputList[1]

signatureInputParsedTail = signatureInputTail.split(";")

algorithm = ""
keyid = ""
for s in signatureInputParsedTail:
if "alg" in s:
raw = s.split("=")[1]
algorithm = raw[1:len(raw)-1]
if "keyid" in s:
raw = s.split("=")[1]
keyid = raw[1:len(raw)-1]

parsed_url = urlparse(r.url)

signatureInputFields = {}
signatureInputBody = StringIO()

for field in signatureInputHeader:
# Remove double quotes from the field to access it directly in the header map
key = field[1 : len(field)-1]
if key[0] == "@":
if DerivedComponent(key) == DerivedComponent.Method:
signatureInputFields[key] = [r.method]
elif DerivedComponent(key) == DerivedComponent.TargetURI:
signatureInputFields[key] = [r.url]
elif DerivedComponent(key) == DerivedComponent.Authority:
signatureInputFields[key] = [parsed_url.netloc]
elif DerivedComponent(key) == DerivedComponent.Scheme:
signatureInputFields[key] = [parsed_url.scheme]
elif DerivedComponent(key) == DerivedComponent.Path:
signatureInputFields[key] = [parsed_url.path]
elif DerivedComponent(key) == DerivedComponent.Query:
signatureInputFields[key] = ["?"+parsed_url.query]
elif DerivedComponent(key) == DerivedComponent.QueryParams:
queryParams = []
rawQueryParams = parsed_url.query.split("&")
for rawQueryParam in rawQueryParams:
if rawQueryParam != "":
parameter = rawQueryParam.split("=")
name = parameter[0]
value = parameter[1]
queryParam = f';name="{name}": {value}'
queryParams.append(queryParam)
signatureInputFields[key] = queryParams
else:
raise ParserException(f"Unhandled Derived Component {key}")
else:
try:
# Multi-value headers are not permitted in Python
fieldValues = headers.get(key)
# Removing leading and trailing whitespaces
signatureInputFields[key] = [fieldValues.strip()]
except KeyError:
raise ParserException(f"Header field not found {key}")

# Construct final output string
keyValues = signatureInputFields[key]
if len(keyValues) == 1:
signatureInputBody.write(f'"{key}" {keyValues[0]}\n')
else:
for value in keyValues:
signatureInputBody.write(f'"{key}"{value}\n')

parsedSignatureInput = f"{signatureInputBody.getvalue()};{signatureInputTail}"
s = parseResult(seed=parsedSignatureInput, signature=signature, keyid=keyid, algorithm=algorithm)

return s


2 changes: 1 addition & 1 deletion src/alvarium/annotators/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ class Annotator(ABC):

@abstractmethod
def execute(self, data:bytes, ctx: PropertyBag = None) -> Annotation:
pass
pass
87 changes: 87 additions & 0 deletions src/alvarium/annotators/pki_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import socket
import os
from alvarium.annotators.handler.contracts import Constants
from alvarium.annotators.handler.exceptions import ParserException

from alvarium.sign.exceptions import SignException
from alvarium.sign.factories import SignProviderFactory
from alvarium.contracts.annotation import Annotation, AnnotationType
from alvarium.hash.contracts import HashType
from alvarium.sign.contracts import KeyInfo, SignInfo, KeyInfo, SignType
from alvarium.utils import PropertyBag
from .contracts import Signable
from .utils import derive_hash, sign_annotation
from .interfaces import Annotator
from .exceptions import AnnotatorException
from alvarium.annotators.handler.utils import parseSignature

class HttpPkiAnnotator(Annotator):
def __init__(self, hash: HashType, sign_info: SignInfo) -> None:
self.hash = hash
self.sign_info = sign_info
self.kind = AnnotationType.PKIHTTP

def execute(self, data: bytes, ctx: PropertyBag = None) -> Annotation:
key = derive_hash(hash=self.hash, data=data)
host: str = socket.gethostname()

# Call parser on request
req = ctx.get_property(Constants.HttpRequestKey)

try:
parsed_data = parseSignature(req)
except ParserException as e:
raise AnnotatorException("Cannot parse the HTTP request.", e)

signable = Signable(parsed_data.seed, parsed_data.signature)

try:
signType = SignType(parsed_data.algorithm)
except Exception as e:
raise AnnotatorException("Invalid key type specified" + str(parsed_data.algorithm))

k = KeyInfo(signType, parsed_data.keyid)

try:
is_satisfied = self._verify_signature(k, signable)
except Exception as e:
raise AnnotatorException(str(e))

annotation = Annotation(key=key, host=host, hash=self.hash, kind=self.kind, is_satisfied=is_satisfied)

signature: str = sign_annotation(key_info=self.sign_info.private, annotation=annotation)
annotation.signature = signature
return annotation

def _verify_signature(self, key: KeyInfo, signable: Signable) -> bool:
""" Responsible for verifying the signature, returns true if the verification passed, false otherwise."""

if(len(signable.signature) == 0):
return False

try:
sign_provider = SignProviderFactory().get_provider(sign_type=key.type)
except SignException as e:
raise AnnotatorException("cannot get sign provider.", e)

if(not os.path.isfile(key.path)):
raise AnnotatorException("Cannot read Public Key File.")

with open(key.path, 'r') as file:
pub_key = file.read()

try:
hex_pub_key = bytes.fromhex(pub_key)
except Exception as e:
raise AnnotatorException("Cannot read Public Key File.")

try:
hex_signature = bytes.fromhex(signable.signature)
except:
raise AnnotatorException("Invalid signature syntax: It is not in hex.")



return sign_provider.verify(key=hex_pub_key,
content=bytes(signable.seed, 'utf-8'),
signed=hex_signature)
1 change: 1 addition & 0 deletions src/alvarium/contracts/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
class AnnotationType(Enum):
TPM = "tpm"
PKI = "pki"
PKIHTTP = "pki-http"
TLS = "tls"
SOURCE = "src"
MOCK = "mock"
Expand Down
2 changes: 1 addition & 1 deletion src/alvarium/sign/ed25519.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from cryptography.hazmat.primitives.asymmetric import ed25519


class Ed25519ignProvider(SignProvider):
class Ed25519SignProvider(SignProvider):
"""The implementation of the Ed25519 sign provider interface"""

def sign(self, key: bytes, content: bytes) -> str:
Expand Down
4 changes: 2 additions & 2 deletions src/alvarium/sign/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .contracts import SignType
from .exceptions import SignException
from .mock import NoneSignProvider
from .ed25519 import Ed25519ignProvider
from .ed25519 import Ed25519SignProvider


class SignProviderFactory:
Expand All @@ -15,6 +15,6 @@ def get_provider(self, sign_type: SignType) -> SignProvider:
if sign_type == SignType.NONE:
return NoneSignProvider()
if sign_type == SignType.ED25519:
return Ed25519ignProvider()
return Ed25519SignProvider()
else:
raise SignException(f'{sign_type} is not implemented yet')
Empty file.
42 changes: 42 additions & 0 deletions tests/annotators/handler/test_ed25519.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import json
import unittest
import datetime

from requests import Request

from alvarium.annotators.handler.ed25519 import Ed25519RequestHandler
from alvarium.annotators.handler.contracts import CONTENT_LENGTH, CONTENT_TYPE, DerivedComponent
from alvarium.sign.contracts import SignInfo, KeyInfo

class TestHandler(unittest.TestCase):

def test_handler_should_return_correct_signature_headers(self):
with open("./tests/mock-info.json", 'r') as file:
b = file.read()

ticks = datetime.datetime.now()
url = 'http://example.com/foo?var1=&var2=2'
headers = { "Date": str(ticks),
'Content-Type': 'application/json',
'Content-Length':'10'}

req = Request('POST', url, headers=headers)

handler = Ed25519RequestHandler(req)

info_json = json.loads(b)
keys = SignInfo(public = KeyInfo.from_json(json.dumps(info_json["signature"]["public"])),
private = KeyInfo.from_json(json.dumps(info_json["signature"]["private"])))

fields = [DerivedComponent.Method, DerivedComponent.Path, DerivedComponent.Authority, CONTENT_TYPE, CONTENT_LENGTH]

handler.AddSignatureHeaders(ticks, fields, keys)

result = handler.request.headers['Signature-Input']
expected = f'"@method" "@path" "@authority" "Content-Type" "Content-Length";created={str(int(ticks.timestamp()))};keyid="{str(keys.public.path)}";alg="{str(keys.public.type)}";'

self.assertEqual(expected, result)


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit 147358c

Please sign in to comment.