diff options
| author | StevenWdV <stevenwdv@gmail.com> | 2021-11-25 15:02:33 +0100 |
|---|---|---|
| committer | StevenWdV <stevenwdv@gmail.com> | 2021-11-25 15:02:33 +0100 |
| commit | 5addc3fa00be1ac2017bd1a5ab1ecba4f73ce1da (patch) | |
| tree | 2f293ca68330927278310cf84eb78ed376f75f58 /wrappers | |
| parent | 638edbb2ffc69fd44d547f93b43bd741ab110096 (diff) | |
Add Python wrapper
Diffstat (limited to 'wrappers')
| -rw-r--r-- | wrappers/python/Makefile | 7 | ||||
| -rw-r--r-- | wrappers/python/discovery.py | 90 | ||||
| -rw-r--r-- | wrappers/python/test_discovery.py | 78 |
3 files changed, 175 insertions, 0 deletions
diff --git a/wrappers/python/Makefile b/wrappers/python/Makefile new file mode 100644 index 0000000..53d1375 --- /dev/null +++ b/wrappers/python/Makefile @@ -0,0 +1,7 @@ +.PHONY: compile test + +compile: + ./discovery.py + +test: + ./test_discovery.py diff --git a/wrappers/python/discovery.py b/wrappers/python/discovery.py new file mode 100644 index 0000000..2a9dcbe --- /dev/null +++ b/wrappers/python/discovery.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import platform +from ctypes import * +from enum import Enum + +_arch = platform.machine().lower() +_arch = \ + { + "aarch64_be": "arm64", + "aarch64": "arm64", + "armv8b": "arm64", + "armv8l": "arm64", + "x86": "386", + "x86pc": "386", + "i86pc": "386", + "i386": "386", + "i686": "386", + "x86_64": "amd64", + "i686-64": "amd64", + }.get(_arch, _arch) + +_os = platform.system().lower() + +_lib = cdll.LoadLibrary(f"../../exports/{_os}/{_arch}/eduvpn_verify") + + +class GoSlice(Structure): + _fields_ = [("data", POINTER(c_char)), ("len", c_int64), ("cap", c_int64)] + + @staticmethod + def make(bs: bytes) -> "GoSlice": + return GoSlice((c_char * len(bs))(*bs), len(bs), len(bs)) + + +_lib.Verify.argtypes, _lib.Verify.restype = [GoSlice, GoSlice, GoSlice, c_uint64], c_int64 +_lib.InsecureTestingSetExtraKey.argtypes, _lib.InsecureTestingSetExtraKey.restype = [GoSlice], None + + +class VerifyErrorCode(Enum): + ErrUnknownExpectedFileName = 1 # Expected file name is not one of the recognized values. + ErrInvalidSignature = 2 # Signature is invalid (for the expected file type). + ErrInvalidSignatureUnknownKey = 3 # Signature was created with an unknown key and has not been verified. + ErrTooOld = 4 # Signature has a timestamp lower than the specified minimum signing time. + Unknown = -1 # Other unknown error + + +class VerifyError(Exception): + code: VerifyErrorCode + code_int: int # Original error code also for VerifyErrorCode.Unknown + + def __init__(self, err: int): + try: + self.code = VerifyErrorCode(err) + except ValueError: + self.code = VerifyErrorCode.Unknown + self.code_int = err + + def __str__(self): + return \ + { + VerifyErrorCode.ErrUnknownExpectedFileName: "unknown expected file name", + VerifyErrorCode.ErrInvalidSignature: "invalid signature", + VerifyErrorCode.ErrInvalidSignatureUnknownKey: "invalid signature (unknown key)", + VerifyErrorCode.ErrTooOld: "replay of previous signature (rollback)" + }[self.code] if self.code != VerifyErrorCode.Unknown else f"unknown verify error ({self.code_int})" + + +def verify(signature: bytes, signed_json: bytes, expected_file_name: str, min_sign_time: int) -> None: + """ + Verifies the signature on the JSON server_list.json/organization_list.json file. + If the function returns the signature is valid for the given file type. + :param signature: .minisig signature file contents. + :param signed_json: Signed .json file contents. + :param expected_file_name: The file type to be verified, one of "server_list.json" or "organization_list.json". + :param min_sign_time: Minimum time for signature. Should be set to at least the time in a previously retrieved file. + + :raises VerifyException: If signature verification fails or expectedFileName is not one of the allowed values. + """ + + err = _lib.Verify(GoSlice.make(signature), GoSlice.make(signed_json), + GoSlice.make(expected_file_name.encode()), min_sign_time) + if err: + raise VerifyError(err) + + +def _insecure_testing_set_extra_key(key_string: str) -> None: + """Use for testing only, see Go documentation.""" + + _lib.InsecureTestingSetExtraKey(GoSlice.make(key_string.encode())) diff --git a/wrappers/python/test_discovery.py b/wrappers/python/test_discovery.py new file mode 100644 index 0000000..369fdcb --- /dev/null +++ b/wrappers/python/test_discovery.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import unittest +import discovery + +test_data_dir = "../../test_data" + + +def read_bytes(path: str) -> bytes: + with open(path, "rb") as f: + return f.read() + + +class VerifyTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + with open(f"{test_data_dir}/dummy/public.key") as f: + discovery._insecure_testing_set_extra_key(f.readlines()[-1][:-1]) + + def testValid(self): + discovery.verify( + read_bytes(f"{test_data_dir}/dummy/server_list.json.minisig"), + read_bytes(f"{test_data_dir}/dummy/server_list.json"), + "server_list.json", + 0 + ) + + def testValidMemoryView(self): + discovery.verify( + memoryview(b"abc" + read_bytes(f"{test_data_dir}/dummy/server_list.json.minisig") + b"abc")[3:-3], + read_bytes(f"{test_data_dir}/dummy/server_list.json"), + "server_list.json", + 0 + ) + + def testInvalidSignature(self): + with self.assertRaises(discovery.VerifyError) as ctx: + discovery.verify( + read_bytes(f"{test_data_dir}/dummy/random.txt"), + read_bytes(f"{test_data_dir}/dummy/server_list.json"), + "server_list.json", + 0 + ) + self.assertEqual(ctx.exception.code, discovery.VerifyErrorCode.ErrInvalidSignature) + + def testWrongKey(self): + with self.assertRaises(discovery.VerifyError) as ctx: + discovery.verify( + read_bytes(f"{test_data_dir}/dummy/server_list.json.wrong_key.minisig"), + read_bytes(f"{test_data_dir}/dummy/server_list.json"), + "server_list.json", + 0 + ) + self.assertEqual(ctx.exception.code, discovery.VerifyErrorCode.ErrInvalidSignatureUnknownKey) + + def testOldSignature(self): + with self.assertRaises(discovery.VerifyError) as ctx: + discovery.verify( + read_bytes(f"{test_data_dir}/dummy/server_list.json.minisig"), + read_bytes(f"{test_data_dir}/dummy/server_list.json"), + "server_list.json", + 1 << 31 + ) + self.assertEqual(ctx.exception.code, discovery.VerifyErrorCode.ErrTooOld) + + def TestUnknownExpectedFile(self): + with self.assertRaises(discovery.VerifyError) as ctx: + discovery.verify( + read_bytes(f"{test_data_dir}/dummy/other_list.json.minisig"), + read_bytes(f"{test_data_dir}/dummy/other_list.json"), + "other_list.json", + 0 + ) + self.assertEqual(ctx.exception.code, discovery.VerifyErrorCode.ErrUnknownExpectedFileName) + + +if __name__ == "__main__": + unittest.main() |
