diff --git a/.coveragerc b/.coveragerc index 88bd789..bb45d70 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,7 @@ [run] branch = True +parallel = True include = ./*.py -omit = ./test_*.py +omit = + ./test/* + ./integration_test/* diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..a41129f --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,2 @@ +service_name: drone +parallel: true diff --git a/.drone.jsonnet b/.drone.jsonnet index 0000161..25f7611 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -1,10 +1,34 @@ local images = { + curl: 'alpine/curl@sha256:c64976d53728ca1b4918a49257845af27e343c4a79090788f83afe9f3e800965', // https://github.com/drGrove/drone-kaniko/tree/v0.7.0 kaniko: 'drgrove/drone-kaniko@sha256:e3045421c3683e6baf5628b22ea0ee1cd7ae217f4de0e1bc53a0a1a20335b108', postgres: 'postgres:12', python: 'python:3.9-slim-buster', }; +local pr_trigger = { + event: [ + 'pull_request', + ], +}; + +local master_trigger = { + event: [ + 'push', + ], + ref: { + include: [ + 'refs/heads/master', + ], + }, +}; + +local tag_trigger = { + event: [ + 'tag', + ], +}; + local pipeline( name, kind='pipeline', @@ -79,8 +103,8 @@ local postgresql = step( detach=true, ); -local test(python_version) = step( - 'test', +local unittest(python_version) = step( + 'unit test', 'python:' + python_version + '-slim-buster', environment={ PGHOST: 'postgresql', @@ -102,16 +126,88 @@ local test(python_version) = step( ] ); +local integration_test(python_version) = step( + 'integration test', + 'python:' + python_version + '-slim-buster', + environment={ + PGHOST: 'postgresql', + COVERALLS_REPO_TOKEN: { + from_secret: 'COVERALLS_REPO_TOKEN', + }, + CLEANUP: '0', + }, + commands=[ + 'apt update && apt install -y make cmake gnupg git postgresql-client', + 'pip3 install pipenv', + 'make setup-dev', + 'cp config.ini.example config.ini', + 'make create-ca', + 'make integration-test.dev coverage coveralls', + ], + depends_on=[ + clone.name, + ] +); + local unittest_pl(pl_type, python_version, trigger={}) = pipeline( pl_type + ' Unit Test: ' + python_version, steps=[ clone, postgresql, - test(python_version), + unittest(python_version), ], trigger=trigger, ); +local integration_test_pl(pl_type, python_version, trigger={}) = pipeline( + pl_type + ' Integration Test: ' + python_version, + steps=[ + clone, + postgresql, + integration_test(python_version), + ], + trigger=trigger, +); + +local notify_coveralls_complete = step( + 'Coverage Complete', + images.curl, + environment={ + COVERALLS_REPO_TOKEN: { + from_secret: 'COVERALLS_REPO_TOKEN', + }, + }, + commands=[ + 'curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$DRONE_BUILD_NUMBER&payload[status]=done"', + ], + depends_on=[ + clone.name, + ], +); + +local coveralls_complete_pl(pl_type, trigger={}) = pipeline( + pl_type + ': Coverage', + steps=[ + clone, + notify_coveralls_complete, + ], + trigger=trigger, + depends_on=[ + unittest_pl('PR', '3.10', trigger=pr_trigger).name, + unittest_pl('Master', '3.10', trigger=master_trigger).name, + unittest_pl('Tag', '3.10', trigger=tag_trigger).name, + unittest_pl('PR', '3.9', trigger=pr_trigger).name, + unittest_pl('Master', '3.9', trigger=master_trigger).name, + unittest_pl('Tag', '3.9', trigger=tag_trigger).name, + integration_test_pl('PR', '3.10', trigger=pr_trigger).name, + integration_test_pl('Master', '3.10', trigger=master_trigger).name, + integration_test_pl('Tag', '3.10', trigger=tag_trigger).name, + integration_test_pl('PR', '3.9', trigger=pr_trigger).name, + integration_test_pl('Master', '3.9', trigger=master_trigger).name, + integration_test_pl('Tag', '3.9', trigger=tag_trigger).name, + ] +); + local get_image_tag = step( 'Get Tag', images.python, @@ -154,39 +250,22 @@ local image_build_pl(pl_type, trigger={}, push=false) = pipeline( trigger=trigger ); -local pr_trigger = { - event: [ - 'pull_request', - ], -}; - -local master_trigger = { - event: [ - 'push', - ], - ref: { - include: [ - 'refs/heads/master', - ], - }, -}; - -local tag_trigger = { - event: [ - 'tag', - ], -}; - [ + unittest_pl('PR', '3.10', trigger=pr_trigger), + unittest_pl('Master', '3.10', trigger=master_trigger), + unittest_pl('Tag', '3.10', trigger=tag_trigger), unittest_pl('PR', '3.9', trigger=pr_trigger), unittest_pl('Master', '3.9', trigger=master_trigger), unittest_pl('Tag', '3.9', trigger=tag_trigger), - unittest_pl('PR', '3.8', trigger=pr_trigger), - unittest_pl('Master', '3.8', trigger=master_trigger), - unittest_pl('Tag', '3.8', trigger=tag_trigger), - unittest_pl('PR', '3.7', trigger=pr_trigger), - unittest_pl('Master', '3.7', trigger=master_trigger), - unittest_pl('Tag', '3.7', trigger=tag_trigger), + integration_test_pl('PR', '3.10', trigger=pr_trigger), + integration_test_pl('Master', '3.10', trigger=master_trigger), + integration_test_pl('Tag', '3.10', trigger=tag_trigger), + integration_test_pl('PR', '3.9', trigger=pr_trigger), + integration_test_pl('Master', '3.9', trigger=master_trigger), + integration_test_pl('Tag', '3.9', trigger=tag_trigger), + coveralls_complete_pl('PR', trigger=pr_trigger), + coveralls_complete_pl('Master', trigger=master_trigger), + coveralls_complete_pl('Tag', trigger=tag_trigger), image_build_pl('PR', trigger=pr_trigger, push=false), image_build_pl('Master', trigger=master_trigger, push=false), image_build_pl('Tag', trigger=tag_trigger, push=true), diff --git a/.drone.yml b/.drone.yml index 11f1d16..63b422b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,529 +1,989 @@ --- -kind: pipeline -name: "PR Unit Test: 3.9" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: postgresql - image: postgres:12 - detach: true - environment: - POSTGRES_DB: mtls - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_PASSWORD: mtls - -- name: test - image: python:3.9-slim-buster - commands: - - apt update && apt install -y make cmake gnupg git postgresql-client - - pip3 install pipenv - - make setup-dev - - cp config.ini.example config.ini - - make create-ca - - make test.dev coverage coveralls - environment: - CLEANUP: 0 - COVERALLS_REPO_TOKEN: - from_secret: COVERALLS_REPO_TOKEN - PGHOST: postgresql - depends_on: - - clone - -trigger: - event: - - pull_request - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "PR Unit Test: 3.10", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.10-slim-buster", + "name": "unit test" + } + ], + "trigger": { + "event": [ + "pull_request" + ] + } +} --- -kind: pipeline -name: "Master Unit Test: 3.9" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: postgresql - image: postgres:12 - detach: true - environment: - POSTGRES_DB: mtls - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_PASSWORD: mtls - -- name: test - image: python:3.9-slim-buster - commands: - - apt update && apt install -y make cmake gnupg git postgresql-client - - pip3 install pipenv - - make setup-dev - - cp config.ini.example config.ini - - make create-ca - - make test.dev coverage coveralls - environment: - CLEANUP: 0 - COVERALLS_REPO_TOKEN: - from_secret: COVERALLS_REPO_TOKEN - PGHOST: postgresql - depends_on: - - clone - -trigger: - event: - - push - ref: - - refs/heads/master - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Master Unit Test: 3.10", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.10-slim-buster", + "name": "unit test" + } + ], + "trigger": { + "event": [ + "push" + ], + "ref": { + "include": [ + "refs/heads/master" + ] + } + } +} --- -kind: pipeline -name: "Tag Unit Test: 3.9" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: postgresql - image: postgres:12 - detach: true - environment: - POSTGRES_DB: mtls - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_PASSWORD: mtls - -- name: test - image: python:3.9-slim-buster - commands: - - apt update && apt install -y make cmake gnupg git postgresql-client - - pip3 install pipenv - - make setup-dev - - cp config.ini.example config.ini - - make create-ca - - make test.dev coverage coveralls - environment: - CLEANUP: 0 - COVERALLS_REPO_TOKEN: - from_secret: COVERALLS_REPO_TOKEN - PGHOST: postgresql - depends_on: - - clone - -trigger: - event: - - tag - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Tag Unit Test: 3.10", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.10-slim-buster", + "name": "unit test" + } + ], + "trigger": { + "event": [ + "tag" + ] + } +} --- -kind: pipeline -name: "PR Unit Test: 3.8" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: postgresql - image: postgres:12 - detach: true - environment: - POSTGRES_DB: mtls - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_PASSWORD: mtls - -- name: test - image: python:3.8-slim-buster - commands: - - apt update && apt install -y make cmake gnupg git postgresql-client - - pip3 install pipenv - - make setup-dev - - cp config.ini.example config.ini - - make create-ca - - make test.dev coverage coveralls - environment: - CLEANUP: 0 - COVERALLS_REPO_TOKEN: - from_secret: COVERALLS_REPO_TOKEN - PGHOST: postgresql - depends_on: - - clone - -trigger: - event: - - pull_request - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "PR Unit Test: 3.9", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.9-slim-buster", + "name": "unit test" + } + ], + "trigger": { + "event": [ + "pull_request" + ] + } +} --- -kind: pipeline -name: "Master Unit Test: 3.8" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: postgresql - image: postgres:12 - detach: true - environment: - POSTGRES_DB: mtls - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_PASSWORD: mtls - -- name: test - image: python:3.8-slim-buster - commands: - - apt update && apt install -y make cmake gnupg git postgresql-client - - pip3 install pipenv - - make setup-dev - - cp config.ini.example config.ini - - make create-ca - - make test.dev coverage coveralls - environment: - CLEANUP: 0 - COVERALLS_REPO_TOKEN: - from_secret: COVERALLS_REPO_TOKEN - PGHOST: postgresql - depends_on: - - clone - -trigger: - event: - - push - ref: - - refs/heads/master - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Master Unit Test: 3.9", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.9-slim-buster", + "name": "unit test" + } + ], + "trigger": { + "event": [ + "push" + ], + "ref": { + "include": [ + "refs/heads/master" + ] + } + } +} --- -kind: pipeline -name: "Tag Unit Test: 3.8" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: postgresql - image: postgres:12 - detach: true - environment: - POSTGRES_DB: mtls - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_PASSWORD: mtls - -- name: test - image: python:3.8-slim-buster - commands: - - apt update && apt install -y make cmake gnupg git postgresql-client - - pip3 install pipenv - - make setup-dev - - cp config.ini.example config.ini - - make create-ca - - make test.dev coverage coveralls - environment: - CLEANUP: 0 - COVERALLS_REPO_TOKEN: - from_secret: COVERALLS_REPO_TOKEN - PGHOST: postgresql - depends_on: - - clone - -trigger: - event: - - tag - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Tag Unit Test: 3.9", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.9-slim-buster", + "name": "unit test" + } + ], + "trigger": { + "event": [ + "tag" + ] + } +} --- -kind: pipeline -name: "PR Unit Test: 3.7" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: postgresql - image: postgres:12 - detach: true - environment: - POSTGRES_DB: mtls - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_PASSWORD: mtls - -- name: test - image: python:3.7-slim-buster - commands: - - apt update && apt install -y make cmake gnupg git postgresql-client - - pip3 install pipenv - - make setup-dev - - cp config.ini.example config.ini - - make create-ca - - make test.dev coverage coveralls - environment: - CLEANUP: 0 - COVERALLS_REPO_TOKEN: - from_secret: COVERALLS_REPO_TOKEN - PGHOST: postgresql - depends_on: - - clone - -trigger: - event: - - pull_request - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "PR Integration Test: 3.10", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make integration-test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.10-slim-buster", + "name": "integration test" + } + ], + "trigger": { + "event": [ + "pull_request" + ] + } +} --- -kind: pipeline -name: "Master Unit Test: 3.7" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: postgresql - image: postgres:12 - detach: true - environment: - POSTGRES_DB: mtls - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_PASSWORD: mtls - -- name: test - image: python:3.7-slim-buster - commands: - - apt update && apt install -y make cmake gnupg git postgresql-client - - pip3 install pipenv - - make setup-dev - - cp config.ini.example config.ini - - make create-ca - - make test.dev coverage coveralls - environment: - CLEANUP: 0 - COVERALLS_REPO_TOKEN: - from_secret: COVERALLS_REPO_TOKEN - PGHOST: postgresql - depends_on: - - clone - -trigger: - event: - - push - ref: - - refs/heads/master - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Master Integration Test: 3.10", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make integration-test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.10-slim-buster", + "name": "integration test" + } + ], + "trigger": { + "event": [ + "push" + ], + "ref": { + "include": [ + "refs/heads/master" + ] + } + } +} --- -kind: pipeline -name: "Tag Unit Test: 3.7" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: postgresql - image: postgres:12 - detach: true - environment: - POSTGRES_DB: mtls - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_PASSWORD: mtls - -- name: test - image: python:3.7-slim-buster - commands: - - apt update && apt install -y make cmake gnupg git postgresql-client - - pip3 install pipenv - - make setup-dev - - cp config.ini.example config.ini - - make create-ca - - make test.dev coverage coveralls - environment: - CLEANUP: 0 - COVERALLS_REPO_TOKEN: - from_secret: COVERALLS_REPO_TOKEN - PGHOST: postgresql - depends_on: - - clone - -trigger: - event: - - tag - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Tag Integration Test: 3.10", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make integration-test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.10-slim-buster", + "name": "integration test" + } + ], + "trigger": { + "event": [ + "tag" + ] + } +} --- -kind: pipeline -name: "PR Build: Image" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: Build - image: drgrove/drone-kaniko@sha256:e3045421c3683e6baf5628b22ea0ee1cd7ae217f4de0e1bc53a0a1a20335b108 - settings: - password: - from_secret: drgrovero - reproducible: true - username: drgrovero - depends_on: - - clone - -trigger: - event: - - pull_request - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "PR Integration Test: 3.9", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make integration-test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.9-slim-buster", + "name": "integration test" + } + ], + "trigger": { + "event": [ + "pull_request" + ] + } +} --- -kind: pipeline -name: "Master Build: Image" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: Build - image: drgrove/drone-kaniko@sha256:e3045421c3683e6baf5628b22ea0ee1cd7ae217f4de0e1bc53a0a1a20335b108 - settings: - password: - from_secret: drgrovero - reproducible: true - username: drgrovero - depends_on: - - clone - -trigger: - event: - - push - ref: - - refs/heads/master - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Master Integration Test: 3.9", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make integration-test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.9-slim-buster", + "name": "integration test" + } + ], + "trigger": { + "event": [ + "push" + ], + "ref": { + "include": [ + "refs/heads/master" + ] + } + } +} --- -kind: pipeline -name: "Tag Build: Image" - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: clone - image: plugins/git - settings: - tags: true - -- name: Get Tag - image: python:3.9-slim-buster - commands: - - apt update && apt install -y git - - git describe --tags > .tags - depends_on: - - clone - -- name: Build - image: drgrove/drone-kaniko@sha256:e3045421c3683e6baf5628b22ea0ee1cd7ae217f4de0e1bc53a0a1a20335b108 - settings: - password: - from_secret: drgrovebot - repo: drgrove/mtls-server - reproducible: true - username: drgrovebot - depends_on: - - Get Tag - -trigger: - event: - - tag - +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Tag Integration Test: 3.9", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "detach": true, + "environment": { + "POSTGRES_DB": "mtls", + "POSTGRES_HOST_AUTH_METHOD": "trust", + "POSTGRES_PASSWORD": "mtls" + }, + "image": "postgres:12", + "name": "postgresql" + }, + { + "commands": [ + "apt update && apt install -y make cmake gnupg git postgresql-client", + "pip3 install pipenv", + "make setup-dev", + "cp config.ini.example config.ini", + "make create-ca", + "make integration-test.dev coverage coveralls" + ], + "depends_on": [ + "clone" + ], + "environment": { + "CLEANUP": "0", + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + }, + "PGHOST": "postgresql" + }, + "image": "python:3.9-slim-buster", + "name": "integration test" + } + ], + "trigger": { + "event": [ + "tag" + ] + } +} +--- +{ + "clone": { + "disable": true + }, + "depends_on": [ + "PR Unit Test: 3.10", + "Master Unit Test: 3.10", + "Tag Unit Test: 3.10", + "PR Unit Test: 3.9", + "Master Unit Test: 3.9", + "Tag Unit Test: 3.9", + "PR Integration Test: 3.10", + "Master Integration Test: 3.10", + "Tag Integration Test: 3.10", + "PR Integration Test: 3.9", + "Master Integration Test: 3.9", + "Tag Integration Test: 3.9" + ], + "kind": "pipeline", + "name": "PR: Coverage", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": [ + "curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d \"payload[build_num]=$DRONE_BUILD_NUMBER&payload[status]=done\"" + ], + "depends_on": [ + "clone" + ], + "environment": { + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + } + }, + "image": "alpine/curl@sha256:c64976d53728ca1b4918a49257845af27e343c4a79090788f83afe9f3e800965", + "name": "Coverage Complete" + } + ], + "trigger": { + "event": [ + "pull_request" + ] + } +} +--- +{ + "clone": { + "disable": true + }, + "depends_on": [ + "PR Unit Test: 3.10", + "Master Unit Test: 3.10", + "Tag Unit Test: 3.10", + "PR Unit Test: 3.9", + "Master Unit Test: 3.9", + "Tag Unit Test: 3.9", + "PR Integration Test: 3.10", + "Master Integration Test: 3.10", + "Tag Integration Test: 3.10", + "PR Integration Test: 3.9", + "Master Integration Test: 3.9", + "Tag Integration Test: 3.9" + ], + "kind": "pipeline", + "name": "Master: Coverage", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": [ + "curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d \"payload[build_num]=$DRONE_BUILD_NUMBER&payload[status]=done\"" + ], + "depends_on": [ + "clone" + ], + "environment": { + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + } + }, + "image": "alpine/curl@sha256:c64976d53728ca1b4918a49257845af27e343c4a79090788f83afe9f3e800965", + "name": "Coverage Complete" + } + ], + "trigger": { + "event": [ + "push" + ], + "ref": { + "include": [ + "refs/heads/master" + ] + } + } +} +--- +{ + "clone": { + "disable": true + }, + "depends_on": [ + "PR Unit Test: 3.10", + "Master Unit Test: 3.10", + "Tag Unit Test: 3.10", + "PR Unit Test: 3.9", + "Master Unit Test: 3.9", + "Tag Unit Test: 3.9", + "PR Integration Test: 3.10", + "Master Integration Test: 3.10", + "Tag Integration Test: 3.10", + "PR Integration Test: 3.9", + "Master Integration Test: 3.9", + "Tag Integration Test: 3.9" + ], + "kind": "pipeline", + "name": "Tag: Coverage", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": [ + "curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d \"payload[build_num]=$DRONE_BUILD_NUMBER&payload[status]=done\"" + ], + "depends_on": [ + "clone" + ], + "environment": { + "COVERALLS_REPO_TOKEN": { + "from_secret": "COVERALLS_REPO_TOKEN" + } + }, + "image": "alpine/curl@sha256:c64976d53728ca1b4918a49257845af27e343c4a79090788f83afe9f3e800965", + "name": "Coverage Complete" + } + ], + "trigger": { + "event": [ + "tag" + ] + } +} +--- +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "PR Build: Image", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "depends_on": [ + "clone" + ], + "image": "drgrove/drone-kaniko@sha256:e3045421c3683e6baf5628b22ea0ee1cd7ae217f4de0e1bc53a0a1a20335b108", + "name": "Build", + "settings": { + "password": { + "from_secret": "drgrovero" + }, + "reproducible": true, + "username": "drgrovero" + } + } + ], + "trigger": { + "event": [ + "pull_request" + ] + } +} +--- +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Master Build: Image", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": null, + "depends_on": [ + "clone" + ], + "image": "drgrove/drone-kaniko@sha256:e3045421c3683e6baf5628b22ea0ee1cd7ae217f4de0e1bc53a0a1a20335b108", + "name": "Build", + "settings": { + "password": { + "from_secret": "drgrovero" + }, + "reproducible": true, + "username": "drgrovero" + } + } + ], + "trigger": { + "event": [ + "push" + ], + "ref": { + "include": [ + "refs/heads/master" + ] + } + } +} +--- +{ + "clone": { + "disable": true + }, + "kind": "pipeline", + "name": "Tag Build: Image", + "steps": [ + { + "commands": null, + "image": "plugins/git", + "name": "clone", + "settings": { + "tags": true + } + }, + { + "commands": [ + "apt update && apt install -y git", + "git describe --tags > .tags" + ], + "depends_on": [ + "clone" + ], + "image": "python:3.9-slim-buster", + "name": "Get Tag" + }, + { + "commands": null, + "depends_on": [ + "Get Tag" + ], + "image": "drgrove/drone-kaniko@sha256:e3045421c3683e6baf5628b22ea0ee1cd7ae217f4de0e1bc53a0a1a20335b108", + "name": "Build", + "settings": { + "password": { + "from_secret": "drgrovebot" + }, + "repo": "drgrove/mtls-server", + "reproducible": true, + "username": "drgrovebot" + } + } + ], + "trigger": { + "event": [ + "tag" + ] + } +} --- kind: signature -hmac: 9ee80a2ac599b4a70fa82d3b74566e67fa79244f42bea244a12e16e86956663a +hmac: 8dcb97acb28201275d74c8cc4e361e1b536921ba4fcd7a96b3fba8145d4f7069 ... diff --git a/.env b/.env new file mode 100644 index 0000000..7d5ff3e --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +PYTHONWARNINGS="ignore:DeprecationWarning" +DOCKER_CONTENT_TRUST=0 diff --git a/.gitignore b/.gitignore index 14c34e2..7c9b3ad 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__ ~* *.sw[o|p] *.db -*.coverage +*.coverage* +!/.coveragerc /.eggs /mtls_server.egg-info diff --git a/Makefile b/Makefile index c4481d6..f2c6b34 100644 --- a/Makefile +++ b/Makefile @@ -43,10 +43,12 @@ lint: .PHONY: coverage coverage: + -@pipenv run coverage combine -a @pipenv run coverage report -m .PHONY: coveralls coveralls: + -@pipenv run coverage combine -a @pipenv run coveralls .PHONY: test @@ -56,7 +58,7 @@ ifeq "${CI}" "" $(MAKE) run-postgres @until pg_isready -h localhost -p 5432; do echo waiting for database; sleep 2; done endif - coverage run -m unittest -v + coverage run -m unittest discover -v -s test ifeq "${CI}" "" $(MAKE) stop-postgres endif @@ -69,7 +71,7 @@ test.dev: test-by-name: ifeq "${CI}" "" $(MAKE) run-postgres - @until pg_isready -h localhost -p 5432; do echo waiting for database; sleep 2; done + @until pg_isready -U postgres -h localhost -p 5432; do echo waiting for database; sleep 2; done endif -@coverage run -m unittest $(NAME) -v ifeq "${CI}" "" @@ -80,6 +82,22 @@ endif test-by-name.dev: pipenv run $(MAKE) test-by-name +.PHONY: integration-test +integration-test: +ifeq "${CI}" "" + -$(MAKE) stop-postgres + $(MAKE) run-postgres + @until pg_isready -h localhost -p 5432; do echo waiting for database; sleep 2; done +endif + coverage run -m unittest discover -v -s integration_test +ifeq "${CI}" "" + $(MAKE) stop-postgres +endif + +.PHONY: integration-test.dev +integration-test.dev: + pipenv run $(MAKE) integration-test + .PHONY: build-image build-image: @docker build -t mtls-server:$(TAG) . @@ -113,6 +131,10 @@ stop-postgres: run: @pipenv run python3 server.py +.PHONY: run.debug +run.debug: + @pipenv run python3 -m pdb mtls_server/server.py + .PHONY: run-prod run-prod: build-image run-postgres @docker run \ diff --git a/Pipfile b/Pipfile index a83a319..3a289e6 100644 --- a/Pipfile +++ b/Pipfile @@ -16,3 +16,4 @@ flake8 = "==3.9.2" black = "==21.9b0" coverage = "==5.5" coveralls = "==3.2.0" +pudb = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 513496d..c6d1eb7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5e57394e37a5abc4ea6c7b8db8e5c04f0e4e19595effc7f6fa48f45b12469c5f" + "sha256": "ba5500dbfc43063dcba2c34064eae3e4922dcb55aa7992c556ba80de62aa1fa8" }, "pipfile-spec": 6, "requires": {}, @@ -16,61 +16,66 @@ "default": { "cffi": { "hashes": [ - "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", - "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", - "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", - "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", - "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", - "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", - "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", - "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", - "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", - "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", - "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", - "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", - "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", - "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", - "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", - "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", - "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", - "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", - "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", - "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", - "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", - "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", - "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", - "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", - "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", - "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", - "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", - "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", - "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", - "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", - "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", - "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", - "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", - "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", - "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", - "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", - "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", - "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", - "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", - "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", - "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", - "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", - "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", - "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", - "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" - ], - "version": "==1.14.6" + "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", + "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", + "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", + "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", + "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", + "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", + "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", + "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", + "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", + "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", + "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", + "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", + "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", + "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", + "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", + "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", + "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", + "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", + "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", + "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", + "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", + "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", + "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", + "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", + "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", + "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", + "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", + "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", + "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", + "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", + "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", + "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", + "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", + "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", + "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", + "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", + "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", + "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", + "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", + "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", + "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", + "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", + "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", + "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", + "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", + "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", + "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", + "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", + "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", + "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" + ], + "version": "==1.15.0" }, "click": { "hashes": [ - "sha256:3fab8aeb8f15f5452ae7511ad448977b3417325bceddd53df87e0bb81f3a8cf8", - "sha256:7027bc7bbafaab8b2c2816861d8eb372429ee3c02e193fc2f93d6c4ab9de49c5" + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], "markers": "python_version >= '3.6'", - "version": "==8.0.2" + "version": "==8.0.3" }, "cryptography": { "hashes": [ @@ -127,6 +132,7 @@ "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", @@ -134,6 +140,7 @@ "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", @@ -141,27 +148,36 @@ "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", + "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", + "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", @@ -169,10 +185,14 @@ "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", + "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", @@ -191,13 +211,18 @@ "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90", "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a", "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e", + "sha256:1f6ca4a9068f5c5c57e744b4baa79f40e83e3746875cac3c45467b16326bab45", "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d", + "sha256:24b0b6688b9f31a911f2361fe818492650795c9e5d3a1bc647acbd7440142a4f", "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed", "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a", "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140", "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32", + "sha256:542875f62bc56e91c6eac05a0deadeae20e1730be4c6334d8f04c944fcd99759", "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31", + "sha256:661509f51531ec125e52357a489ea3806640d0ca37d9dada461ffc69ee1e7b6e", "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a", + "sha256:736b8797b58febabb85494142c627bd182b50d2a7ec65322983e71065ad3034c", "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917", "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf", "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7", @@ -209,7 +234,9 @@ "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76", "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4", "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f", + "sha256:d92272c7c16e105788efe2cfa5d680f07e34e0c29b03c1908f8636f55d5f915a", "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34", + "sha256:ebccf1123e7ef66efc615a68295bf6fdba875a75d5bba10a05073202598085fc", "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce", "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a", "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e" @@ -219,11 +246,11 @@ }, "pycparser": { "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20" + "version": "==2.21" }, "python-gnupg": { "hashes": [ @@ -267,19 +294,19 @@ }, "charset-normalizer": { "hashes": [ - "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", - "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" + "sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0", + "sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405" ], "markers": "python_version >= '3'", - "version": "==2.0.6" + "version": "==2.0.8" }, "click": { "hashes": [ - "sha256:3fab8aeb8f15f5452ae7511ad448977b3417325bceddd53df87e0bb81f3a8cf8", - "sha256:7027bc7bbafaab8b2c2816861d8eb372429ee3c02e193fc2f93d6c4ab9de49c5" + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], "markers": "python_version >= '3.6'", - "version": "==8.0.2" + "version": "==8.0.3" }, "coverage": { "hashes": [ @@ -363,11 +390,19 @@ }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], "markers": "python_version >= '3'", - "version": "==3.2" + "version": "==3.3" + }, + "jedi": { + "hashes": [ + "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d", + "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab" + ], + "markers": "python_version >= '3.6'", + "version": "==0.18.1" }, "mccabe": { "hashes": [ @@ -383,6 +418,14 @@ ], "version": "==0.4.3" }, + "parso": { + "hashes": [ + "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398", + "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22" + ], + "markers": "python_version >= '3.6'", + "version": "==0.8.2" + }, "pathspec": { "hashes": [ "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", @@ -398,6 +441,13 @@ "markers": "python_version >= '3.6'", "version": "==2.4.0" }, + "pudb": { + "hashes": [ + "sha256:82a524ab4b89d2c701b089071ccc6afa9c8a838504e3d68eb33faa8a8abbe4cb" + ], + "index": "pypi", + "version": "==2021.2.2" + }, "pycodestyle": { "hashes": [ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", @@ -414,51 +464,92 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.3.1" }, - "regex": { + "pygments": { "hashes": [ - "sha256:09e1031e2059abd91177c302da392a7b6859ceda038be9e015b522a182c89e4f", - "sha256:176796cb7f82a7098b0c436d6daac82f57b9101bb17b8e8119c36eecf06a60a3", - "sha256:1abbd95cbe9e2467cac65c77b6abd9223df717c7ae91a628502de67c73bf6838", - "sha256:1ce02f420a7ec3b2480fe6746d756530f69769292eca363218c2291d0b116a01", - "sha256:1f51926db492440e66c89cd2be042f2396cf91e5b05383acd7372b8cb7da373f", - "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a", - "sha256:2efd47704bbb016136fe34dfb74c805b1ef5c7313aef3ce6dcb5ff844299f432", - "sha256:36c98b013273e9da5790ff6002ab326e3f81072b4616fd95f06c8fa733d2745f", - "sha256:39079ebf54156be6e6902f5c70c078f453350616cfe7bfd2dd15bdb3eac20ccc", - "sha256:3d52c5e089edbdb6083391faffbe70329b804652a53c2fdca3533e99ab0580d9", - "sha256:45cb0f7ff782ef51bc79e227a87e4e8f24bc68192f8de4f18aae60b1d60bc152", - "sha256:4786dae85c1f0624ac77cb3813ed99267c9adb72e59fdc7297e1cf4d6036d493", - "sha256:51feefd58ac38eb91a21921b047da8644155e5678e9066af7bcb30ee0dca7361", - "sha256:55ef044899706c10bc0aa052f2fc2e58551e2510694d6aae13f37c50f3f6ff61", - "sha256:5e5796d2f36d3c48875514c5cd9e4325a1ca172fc6c78b469faa8ddd3d770593", - "sha256:5f199419a81c1016e0560c39773c12f0bd924c37715bffc64b97140d2c314354", - "sha256:5f55c4804797ef7381518e683249310f7f9646da271b71cb6b3552416c7894ee", - "sha256:74e55f8d66f1b41d44bc44c891bcf2c7fad252f8f323ee86fba99d71fd1ad5e3", - "sha256:7f125fce0a0ae4fd5c3388d369d7a7d78f185f904c90dd235f7ecf8fe13fa741", - "sha256:82cfb97a36b1a53de32b642482c6c46b6ce80803854445e19bc49993655ebf3b", - "sha256:88dc3c1acd3f0ecfde5f95c32fcb9beda709dbdf5012acdcf66acbc4794468eb", - "sha256:924079d5590979c0e961681507eb1773a142553564ccae18d36f1de7324e71ca", - "sha256:973499dac63625a5ef9dfa4c791aa33a502ddb7615d992bdc89cf2cc2285daa3", - "sha256:981c786293a3115bc14c103086ae54e5ee50ca57f4c02ce7cf1b60318d1e8072", - "sha256:9c070d5895ac6aeb665bd3cd79f673775caf8d33a0b569e98ac434617ecea57d", - "sha256:9e3e2cea8f1993f476a6833ef157f5d9e8c75a59a8d8b0395a9a6887a097243b", - "sha256:9e527ab1c4c7cf2643d93406c04e1d289a9d12966529381ce8163c4d2abe4faf", - "sha256:a37305eb3199d8f0d8125ec2fb143ba94ff6d6d92554c4b8d4a8435795a6eccd", - "sha256:aa0ab3530a279a3b7f50f852f1bab41bc304f098350b03e30a3876b7dd89840e", - "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700", - "sha256:b09d3904bf312d11308d9a2867427479d277365b1617e48ad09696fa7dfcdf59", - "sha256:b8b6ee6555b6fbae578f1468b3f685cdfe7940a65675611365a7ea1f8d724991", - "sha256:b9b5c215f3870aa9b011c00daeb7be7e1ae4ecd628e9beb6d7e6107e07d81287", - "sha256:c6569ba7b948c3d61d27f04e2b08ebee24fec9ff8e9ea154d8d1e975b175bfa7", - "sha256:e4204708fa116dd03436a337e8e84261bc8051d058221ec63535c9403a1582a1", - "sha256:ea8de658d7db5987b11097445f2b1f134400e2232cb40e614e5f7b6f5428710e", - "sha256:f540f153c4f5617bc4ba6433534f8916d96366a08797cbbe4132c37b70403e92", - "sha256:fab3ab8aedfb443abb36729410403f0fe7f60ad860c19a979d47fb3eb98ef820", - "sha256:fb2baff66b7d2267e07ef71e17d01283b55b3cc51a81b54cc385e721ae172ba4", - "sha256:fe6ce4f3d3c48f9f402da1ceb571548133d3322003ce01b20d960a82251695d2", - "sha256:ff24897f6b2001c38a805d53b6ae72267025878d35ea225aa24675fbff2dba7f" + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" ], - "version": "==2021.10.8" + "markers": "python_version >= '3.5'", + "version": "==2.10.0" + }, + "regex": { + "hashes": [ + "sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05", + "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f", + "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc", + "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4", + "sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737", + "sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a", + "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4", + "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8", + "sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d", + "sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03", + "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f", + "sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264", + "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a", + "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef", + "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f", + "sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da", + "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc", + "sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063", + "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50", + "sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a", + "sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49", + "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d", + "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d", + "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733", + "sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00", + "sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b", + "sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a", + "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36", + "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345", + "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0", + "sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732", + "sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286", + "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12", + "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646", + "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667", + "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244", + "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29", + "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec", + "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf", + "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4", + "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449", + "sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0", + "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a", + "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d", + "sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129", + "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb", + "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e", + "sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b", + "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83", + "sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf", + "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e", + "sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b", + "sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942", + "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a", + "sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e", + "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94", + "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc", + "sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a", + "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e", + "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965", + "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0", + "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36", + "sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296", + "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec", + "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23", + "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7", + "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe", + "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6", + "sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8", + "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b", + "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb", + "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b", + "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30", + "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e" + ], + "version": "==2021.11.10" }, "requests": { "hashes": [ @@ -470,19 +561,19 @@ }, "tomli": { "hashes": [ - "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", - "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" + "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", + "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" ], "markers": "python_version >= '3.6'", - "version": "==1.2.1" + "version": "==1.2.2" }, "typing-extensions": { "hashes": [ - "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", - "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", - "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" + "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed", + "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9" ], - "version": "==3.10.0.2" + "markers": "python_version >= '3.6'", + "version": "==4.0.0" }, "urllib3": { "hashes": [ @@ -491,6 +582,18 @@ ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.7" + }, + "urwid": { + "hashes": [ + "sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae" + ], + "version": "==2.1.2" + }, + "urwid-readline": { + "hashes": [ + "sha256:018020cbc864bb5ed87be17dc26b069eae2755cb29f3a9c569aac3bded1efaf4" + ], + "version": "==0.13" } } } diff --git a/integration_test/__init__.py b/integration_test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/integration_test/base.py b/integration_test/base.py new file mode 100644 index 0000000..9505e8b --- /dev/null +++ b/integration_test/base.py @@ -0,0 +1,267 @@ +import logging +import os +import tempfile +from unittest import TestCase +from unittest import mock + +from configparser import ConfigParser +from flask.testing import FlaskClient +import gnupg + +from mtls_server.config import Config +from mtls_server.storage import PostgresqlStorageEngine +from mtls_server.storage import SQLiteStorageEngine +from mtls_server.utils import User +from mtls_server.utils import gen_passwd +from mtls_server.utils import generate_key + + +class BaseTests(TestCase): + app: FlaskClient + users: list + admin_users: list + invalid_users: list + new_users: list + user_gpg: gnupg.GPG + admin_gpg: gnupg.GPG + invalid_gpg: gnupg.GPG + + +class PostgresqlBaseTestCase(TestCase): + @classmethod + def setUpClass(cls): + logging.disable(logging.CRITICAL) + cls.env_patcher = mock.patch.dict(os.environ, {"SEED_ON_INIT": "0"}) + cls.env_patcher.start() + + @classmethod + def tearDownClass(cls): + logging.disable(logging.NOTSET) + cls.env_patcher.stop() + + def setUp(self): + self.USER_GNUPGHOME = tempfile.TemporaryDirectory() + self.ADMIN_GNUPGHOME = tempfile.TemporaryDirectory() + self.INVALID_GNUPGHOME = tempfile.TemporaryDirectory() + self.NEW_USER_GNUPGHOME = tempfile.TemporaryDirectory() + config = ConfigParser() + config.read_string( + f""" + [mtls] + min_lifetime=60 + max_lifetime=0 + + [ca] + key = secrets/certs/authority/RootCA.key + cert = secrets/certs/authority/RootCA.pem + issuer = My Company Name + alternate_name = *.myname.com + + [gnupg] + user={self.USER_GNUPGHOME.name} + admin={self.ADMIN_GNUPGHOME.name} + + [storage] + engine=postgres + + [storage.postgres] + database = mtls + user = postgres + password = postgres + host = {os.environ.get('PGHOST', 'localhost')} + """ + ) + Config.init_config(config=config) + self.Config = Config + self.key = generate_key(512) + self.engine = PostgresqlStorageEngine(self.Config) + with self.engine.conn.cursor() as cur: + cur.execute("DROP TABLE IF EXISTS certs") + self.engine.init_db() + self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) + self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) + self.invalid_gpg = gnupg.GPG(gnupghome=self.INVALID_GNUPGHOME.name) + self.new_user_gpg = gnupg.GPG(gnupghome=self.NEW_USER_GNUPGHOME.name) + self.users = [ + User("user@host", gen_passwd(), generate_key(512), gpg=self.user_gpg), + User( + "user2@host", gen_passwd(), generate_key(512), gpg=self.user_gpg + ), + User( + "user3@host", gen_passwd(), generate_key(512), gpg=self.user_gpg + ), + ] + self.invalid_users = [ + User( + "user4@host", + gen_passwd(), + generate_key(512), + gpg=self.invalid_gpg, + ) + ] + self.admin_users = [ + User( + "admin@host", gen_passwd(), generate_key(512), gpg=self.admin_gpg + ) + ] + self.new_users = [ + User( + "newuser@host", + gen_passwd(), + generate_key(512), + gpg=self.new_user_gpg, + ), + User( + "newuser2@host", + gen_passwd(), + generate_key(512), + gpg=self.new_user_gpg, + ), + ] + for user in self.users: + self.user_gpg.import_keys( + self.user_gpg.export_keys(user.fingerprint) + ) + for user in self.admin_users: + # Import to admin keychain + self.admin_gpg.import_keys( + self.admin_gpg.export_keys(user.fingerprint) + ) + # Import to user keychain + self.user_gpg.import_keys( + self.admin_gpg.export_keys(user.fingerprint) + ) + for user in self.invalid_users: + self.invalid_gpg.import_keys( + self.invalid_gpg.export_keys(user.fingerprint) + ) + for user in self.new_users: + self.new_user_gpg.import_keys( + self.new_user_gpg.export_keys(user.fingerprint) + ) + + def tearDown(self): + super().tearDown() + if os.environ.get('CLEANUP', '1') == '1': + self.USER_GNUPGHOME.cleanup() + self.ADMIN_GNUPGHOME.cleanup() + self.INVALID_GNUPGHOME.cleanup() + self.NEW_USER_GNUPGHOME.cleanup() + +class SQLiteBaseTestCase(TestCase): + @classmethod + def setUpClass(cls): + logging.disable(logging.CRITICAL) + cls.env_patcher = mock.patch.dict(os.environ, {"SEED_ON_INIT": "0"}) + cls.env_patcher.start() + + + @classmethod + def tearDownClass(cls): + logging.disable(logging.NOTSET) + cls.env_patcher.stop() + + def setUp(self): + self.USER_GNUPGHOME = tempfile.TemporaryDirectory() + self.ADMIN_GNUPGHOME = tempfile.TemporaryDirectory() + self.INVALID_GNUPGHOME = tempfile.TemporaryDirectory() + self.NEW_USER_GNUPGHOME = tempfile.TemporaryDirectory() + config = ConfigParser() + config.read_string( + f""" + [mtls] + min_lifetime=60 + max_lifetime=0 + + [ca] + key = secrets/certs/authority/RootCA.key + cert = secrets/certs/authority/RootCA.pem + issuer = My Company Name + alternate_name = *.myname.com + + [gnupg] + user={self.USER_GNUPGHOME.name} + admin={self.ADMIN_GNUPGHOME.name} + + [storage] + engine=sqlite3 + + [storage.sqlite3] + db_path=:memory: + """ + ) + Config.init_config(config=config) + self.Config = Config + self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) + self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) + self.invalid_gpg = gnupg.GPG(gnupghome=self.INVALID_GNUPGHOME.name) + self.new_user_gpg = gnupg.GPG(gnupghome=self.NEW_USER_GNUPGHOME.name) + self.users = [ + User("user@host", gen_passwd(), generate_key(512), gpg=self.user_gpg), + User( + "user2@host", gen_passwd(), generate_key(512), gpg=self.user_gpg + ), + User( + "user3@host", gen_passwd(), generate_key(512), gpg=self.user_gpg + ), + ] + self.invalid_users = [ + User( + "user4@host", + gen_passwd(), + generate_key(512), + gpg=self.invalid_gpg, + ) + ] + self.admin_users = [ + User( + "admin@host", gen_passwd(), generate_key(512), gpg=self.admin_gpg + ) + ] + self.new_users = [ + User( + "newuser@host", + gen_passwd(), + generate_key(512), + gpg=self.new_user_gpg, + ), + User( + "newuser2@host", + gen_passwd(), + generate_key(512), + gpg=self.new_user_gpg, + ), + ] + self.key = generate_key(512) + self.engine = SQLiteStorageEngine(self.Config) + cur = self.engine.conn.cursor() + cur.execute("DROP TABLE IF EXISTS certs") + self.engine.init_db() + for user in self.users: + self.user_gpg.import_keys( + self.user_gpg.export_keys(user.fingerprint) + ) + for user in self.admin_users: + # Import to admin keychain + self.admin_gpg.import_keys( + self.admin_gpg.export_keys(user.fingerprint) + ) + # Import to user keychain + self.user_gpg.import_keys( + self.admin_gpg.export_keys(user.fingerprint) + ) + for user in self.invalid_users: + self.invalid_gpg.import_keys( + self.invalid_gpg.export_keys(user.fingerprint) + ) + for user in self.new_users: + self.new_user_gpg.import_keys( + self.new_user_gpg.export_keys(user.fingerprint) + ) + + def tearDown(self): + if os.environ.get('CLEANUP', '1') == '1': + self.USER_GNUPGHOME.cleanup() + self.ADMIN_GNUPGHOME.cleanup() + self.INVALID_GNUPGHOME.cleanup() + self.NEW_USER_GNUPGHOME.cleanup() diff --git a/integration_test/test_certs.py b/integration_test/test_certs.py new file mode 100644 index 0000000..a9958a9 --- /dev/null +++ b/integration_test/test_certs.py @@ -0,0 +1,281 @@ +import base64 +import json +import unittest + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +from mtls_server.server import create_app + +from integration_test.base import BaseTests +from integration_test.base import PostgresqlBaseTestCase +from integration_test.base import SQLiteBaseTestCase + + +class BaseCertificateTests(BaseTests): + def get_ca_cert(self): + response = self.app.get("/ca") + self.assertEqual(response.status_code, 200) + res = json.loads(response.data) + self.assertEqual(res["issuer"], "My Company Name") + + def get_crl(self): + response = self.app.get("/crl") + self.assertEqual(response.status_code, 200) + self.assertIn(b"-----BEGIN X509 CRL-----", response.data) + self.assertIn(b"-----END X509 CRL-----", response.data) + + def user_generate_cert(self): + user = self.users[0] + csr = user.gen_csr() + payload = { + "csr": csr.public_bytes(serialization.Encoding.PEM).decode( + "utf-8" + ), + "lifetime": 60, + } + sig = self.user_gpg.sign( + json.dumps(payload), + keyid=user.fingerprint, + detach=True, + passphrase=user.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/certs", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(response.status_code, 200) + self.assertIn("-----BEGIN CERTIFICATE-----", res["cert"]) + self.assertIn("-----END CERTIFICATE-----", res["cert"]) + + def invalid_user_generate_cert(self): + user = self.invalid_users[0] + csr = user.gen_csr() + payload = { + "csr": csr.public_bytes(serialization.Encoding.PEM).decode( + "utf-8" + ), + "lifetime": 60, + } + sig = self.invalid_gpg.sign( + json.dumps(payload), + keyid=user.fingerprint, + detach=True, + passphrase=user.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/certs", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(response.status_code, 401) + self.assertEqual(res["error"], True) + + def admin_user_generate_cert(self): + user = self.users[0] + admin = self.admin_users[0] + csr = user.gen_csr() + payload = { + "csr": csr.public_bytes(serialization.Encoding.PEM).decode("utf-8"), + "lifetime": 60, + } + sig = self.admin_gpg.sign( + json.dumps(payload), + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/certs", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(response.status_code, 200) + self.assertIn("-----BEGIN CERTIFICATE-----", res["cert"]) + self.assertIn("-----END CERTIFICATE-----", res["cert"]) + + def user_cannot_revoke_other_users_cert(self): + user1 = self.users[0] + user2 = self.users[1] + csr = user1.gen_csr() + payload = { + "csr": csr.public_bytes(serialization.Encoding.PEM).decode("utf-8"), + "lifetime": 60, + } + sig = self.user_gpg.sign( + json.dumps(payload), + keyid=user1.fingerprint, + detach=True, + passphrase=user1.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/certs", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(response.status_code, 200) + cert = x509.load_pem_x509_certificate( + str(res["cert"]).encode("UTF-8"), backend=default_backend() + ) + payload = {} + sig = self.user_gpg.sign( + json.dumps(payload), + keyid=user2.fingerprint, + detach=True, + passphrase=user2.password + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.delete( + f"/certs/{cert.serial_number}", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + self.assertEqual(response.status_code, 404) + + def bad_authorization(self): + user = self.users[0] + csr = user.gen_csr() + payload = { + "csr": csr.public_bytes(serialization.Encoding.PEM).decode( + "utf-8" + ), + "lifetime": 60, + } + sig = self.user_gpg.sign( + json.dumps(payload), + keyid=user.fingerprint, + detach=True, + passphrase=user.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/certs", + json=payload, + content_type="application/json", + # Authorization header has 2 spaces + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + self.assertEqual(response.status_code, 500) + + def get_version(self): + with open("VERSION", "r") as v: + version = v.readline().strip() + response = self.app.get("/version") + res = json.loads(response.data) + self.assertEqual(response.status_code, 200) + self.assertEqual(res["version"], version) + + +class TestCertificatesSQLite(SQLiteBaseTestCase, BaseCertificateTests): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + app = create_app(self.Config) + app.testing = True + self.app = app.test_client() + + def tearDown(self): + super().tearDown() + del self.app + + def test_get_ca_cert(self): + self.get_ca_cert() + + def test_get_crl(self): + self.get_crl() + + def test_user_generate_cert(self): + self.user_generate_cert() + + def test_invalid_user_generate_cert(self): + self.invalid_user_generate_cert() + + def test_admin_user_generate_cert(self): + self.admin_user_generate_cert() + + def test_user_cannot_revoke_other_users_cert(self): + self.user_cannot_revoke_other_users_cert() + + def test_get_version(self): + self.get_version() + + def test_bad_authorization(self): + self.bad_authorization() + + +class TestCertificatesPostgresql(PostgresqlBaseTestCase, BaseCertificateTests): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + app = create_app(self.Config) + app.testing = True + self.app = app.test_client() + + def tearDown(self): + del self.app + + def test_get_ca_cert(self): + self.get_ca_cert() + + def test_get_crl(self): + self.get_crl() + + def test_user_generate_cert(self): + self.user_generate_cert() + + def test_invalid_user_generate_cert(self): + self.invalid_user_generate_cert() + + def test_admin_user_generate_cert(self): + self.admin_user_generate_cert() + + def test_user_cannot_revoke_other_users_cert(self): + self.user_cannot_revoke_other_users_cert() + + def test_get_version(self): + self.get_version() + + +if __name__ == "__main__": + unittest.main() diff --git a/integration_test/test_legacy.py b/integration_test/test_legacy.py new file mode 100644 index 0000000..1477769 --- /dev/null +++ b/integration_test/test_legacy.py @@ -0,0 +1,202 @@ +import base64 +import json +import unittest + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +from mtls_server.server import create_app + +from integration_test.base import BaseTests +from integration_test.base import PostgresqlBaseTestCase +from integration_test.base import SQLiteBaseTestCase + + +class BaseLegacyCertificateTests(BaseTests): + def user_generate_cert(self): + user = self.users[0] + csr = user.gen_csr() + payload = { + "csr": csr.public_bytes(serialization.Encoding.PEM).decode( + "utf-8" + ), + "lifetime": 60, + "type": "CERTIFICATE", + } + sig = self.user_gpg.sign( + payload["csr"], + keyid=user.fingerprint, + detach=True, + clearsign=True, + passphrase=user.password, + ) + payload["signature"] = str(sig) + response = self.app.post( + "/", + json=payload, + content_type="application/json", + ) + res = json.loads(response.data) + self.assertEqual(response.status_code, 200) + self.assertIn("-----BEGIN CERTIFICATE-----", res["cert"]) + self.assertIn("-----END CERTIFICATE-----", res["cert"]) + + def revoke_cert(self): + user = self.users[0] + csr = user.gen_csr() + payload = { + "csr": csr.public_bytes(serialization.Encoding.PEM).decode( + "utf-8" + ), + "lifetime": 60, + "type": "CERTIFICATE", + } + sig = self.user_gpg.sign( + payload["csr"], + keyid=user.fingerprint, + detach=True, + clearsign=True, + passphrase=user.password, + ) + payload["signature"] = str(sig) + response = self.app.post( + "/", + json=payload, + content_type="application/json", + ) + + res = json.loads(response.data) + + user_cert = x509.load_pem_x509_certificate( + str(res["cert"]).encode("UTF-8"), backend=default_backend() + ) + query = { + "serial_number": user_cert.serial_number + } + sig = self.user_gpg.sign( + json.dumps(query).encode('utf-8'), + keyid=user.fingerprint, + detach=True, + clearsign=True, + passphrase=user.password, + ) + payload = { + "query": query, + "signature": str(sig), + "type": "CERTIFICATE" + } + response = self.app.delete( + '/', + json=payload, + content_type='application/json' + ) + res = json.loads(response.data) + self.assertTrue(res['msg'] == "success", res) + + def user_cannot_revoke_other_users_cert(self): + user1 = self.users[0] + user2 = self.users[1] + csr = user1.gen_csr() + csr_str = csr.public_bytes(serialization.Encoding.PEM).decode("utf-8") + sig = self.user_gpg.sign( + csr_str, + keyid=user1.fingerprint, + detach=True, + clearsign=True, + passphrase=user1.password, + ) + payload = { + "csr": csr_str, + "lifetime": 60, + "type": "CERTIFICATE", + "signature": str(sig) + } + response = self.app.post( + "/", + json=payload, + content_type="application/json", + ) + res = json.loads(response.data) + self.assertEqual(response.status_code, 200) + cert = x509.load_pem_x509_certificate( + str(res["cert"]).encode("UTF-8"), backend=default_backend() + ) + query = { + "serial_number": str(cert.serial_number) + } + sig = self.user_gpg.sign( + json.dumps(query), + keyid=user2.fingerprint, + detach=True, + clearsign=True, + passphrase=user2.password + ) + payload = { + "query": query, + "type": "CERTIFICATE", + "signature": str(sig), + } + response = self.app.delete( + f"/", + json=payload, + content_type="application/json", + ) + self.assertEqual(response.status_code, 404, response.data) + + +class TestLegacyCertificatesSQLite(SQLiteBaseTestCase, BaseLegacyCertificateTests): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + app = create_app(self.Config) + app.testing = True + self.app = app.test_client() + + def tearDown(self): + super().tearDown() + del self.app + + def test_user_generate_cert(self): + self.user_generate_cert() + + def test_revoke_cert(self): + self.revoke_cert() + + def test_user_cannot_revoke_other_users_cert(self): + self.user_cannot_revoke_other_users_cert() + + +class TestLegacyCertificatesPostgresql(PostgresqlBaseTestCase, BaseLegacyCertificateTests): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + app = create_app(self.Config) + app.testing = True + self.app = app.test_client() + + def tearDown(self): + del self.app + + def test_user_generate_cert(self): + self.user_generate_cert() + + def test_revoke_cert(self): + self.revoke_cert() + + def test_user_cannot_revoke_other_users_cert(self): + self.user_cannot_revoke_other_users_cert() diff --git a/integration_test/test_other.py b/integration_test/test_other.py new file mode 100644 index 0000000..361bd2c --- /dev/null +++ b/integration_test/test_other.py @@ -0,0 +1,51 @@ +import json +import unittest + +from mtls_server.server import create_app + +from integration_test.base import BaseTests +from integration_test.base import SQLiteBaseTestCase + + +class OtherTests(BaseTests, SQLiteBaseTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + app = create_app(self.Config) + app.testing = True + self.app = app.test_client() + + def tearDown(self): + super().tearDown() + del self.app + + def test_get_ca_cert(self): + response = self.app.get("/ca") + self.assertEqual(response.status_code, 200) + res = json.loads(response.data) + self.assertEqual(res["issuer"], "My Company Name") + + def test_get_crl(self): + response = self.app.get("/crl") + self.assertEqual(response.status_code, 200) + self.assertIn(b"-----BEGIN X509 CRL-----", response.data) + self.assertIn(b"-----END X509 CRL-----", response.data) + + def test_get_version(self): + with open("VERSION", "r") as v: + version = v.readline().strip() + response = self.app.get("/version") + res = json.loads(response.data) + self.assertEqual(response.status_code, 200) + self.assertEqual(res["version"], version) + + +if __name__ == "__main__": + unittest.main() diff --git a/integration_test/test_users.py b/integration_test/test_users.py new file mode 100644 index 0000000..3e186ef --- /dev/null +++ b/integration_test/test_users.py @@ -0,0 +1,370 @@ +import base64 +import json +import os +import unittest + + +from mtls_server.server import create_app + +from integration_test.base import BaseTests +from integration_test.base import PostgresqlBaseTestCase +from integration_test.base import SQLiteBaseTestCase + + +class BaseUserTests(BaseTests): + def add_user_valid_admin(self): + admin = self.admin_users[0] + payload = { + "fingerprint": "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD", + } + sig = self.admin_gpg.sign( + json.dumps(payload, sort_keys=True), + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/users", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(response.status_code, 201) + self.assertEqual(res["msg"], "success") + + def add_user_invalid_admin(self): + user = self.invalid_users[0] + new_user = self.new_users[0] + payload = { + "fingerprint": new_user.fingerprint, + } + sig = self.user_gpg.sign( + json.dumps(payload, sort_keys=True), + keyid=user.fingerprint, + detach=True, + passphrase=user.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/users", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(response.status_code, 401) + self.assertEqual(res["error"], True) + + def add_admin_valid_admin(self): + admin = self.admin_users[0] + fingerprint = "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD" + payload = { + "fingerprint": fingerprint, + } + sig = self.admin_gpg.sign( + json.dumps(payload, sort_keys=True), + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/users", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(response.status_code, 201) + self.assertEqual(res["msg"], "success") + + def add_admin_twice_valid_admin(self): + fingerprint = "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD" + admin = self.admin_users[0] + payload = { + "fingerprint": fingerprint, + "admin": True, + } + sig = self.admin_gpg.sign( + json.dumps(payload, sort_keys=True), + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/users", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(res["msg"], "success") + + sig = self.admin_gpg.sign( + json.dumps(payload, sort_keys=True), + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/users", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(res["msg"], "success") + + def add_admin_add_key_not_on_keyserver(self): + admin = self.admin_users[0] + new_user = self.invalid_users[0] + payload = { + "fingerprint": new_user.fingerprint, + } + sig = self.admin_gpg.sign( + json.dumps(payload, sort_keys=True), + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/users", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + self.assertEqual(response.status_code, 422) + + def add_admin_invalid_admin(self): + admin = self.users[0] + new_user = self.new_users[0] + payload = { + "admin": True, + "fingerprint": new_user.fingerprint, + } + sig = self.admin_gpg.sign( + json.dumps(payload, sort_keys=True), + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.post( + "/users", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(res["error"], True) + + def remove_user_valid_admin(self): + admin = self.admin_users[0] + fingerprint = "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD" + sig = self.admin_gpg.sign( + "NOCONTENT", + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.delete( + f"/users/{fingerprint}", + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(res["msg"], "success") + + def remove_user_invalid_admin(self): + admin = self.users[0] + new_user = self.new_users[0] + sig = self.admin_gpg.sign( + "NOCONTENT", + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.delete( + f"/users/{new_user.fingerprint}", + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(res["error"], True) + + def remove_admin_valid_admin(self): + admin = self.admin_users[0] + fingerprint = "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD" + payload = { + "admin": True, + "fingerprint": fingerprint, + } + sig = self.admin_gpg.sign( + json.dumps(payload, sort_keys=True), + keyid=admin.fingerprint, + detach=True, + passphrase=admin.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.delete( + f"/users/{fingerprint}", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + self.assertEqual(response.status_code, 200) + + def remove_admin_invalid_admin(self): + user = self.users[0] + new_user = self.new_users[0] + payload = { + "admin": True, + "fingerprint": new_user.fingerprint, + } + sig = self.user_gpg.sign( + json.dumps(payload, sort_keys=True), + keyid=user.fingerprint, + detach=True, + passphrase=user.password, + ) + pgpb64 = base64.b64encode(str(sig).encode('ascii')) + response = self.app.delete( + f"/users/{new_user.fingerprint}", + json=payload, + content_type="application/json", + headers={ + 'Authorization': f'PGP-SIG {str(pgpb64.decode("utf-8"))}' + } + ) + res = json.loads(response.data) + self.assertEqual(res["error"], True) + + +class TestUserSQLite(SQLiteBaseTestCase, BaseUserTests): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + app = create_app(self.Config) + app.testing = True + self.app = app.test_client() + + def tearDown(self): + super().tearDown() + del self.app + + def test_add_user_valid_admin(self): + self.add_user_valid_admin() + + def test_add_user_invalid_admin(self): + self.add_user_invalid_admin() + + def test_add_admin_valid_admin(self): + self.add_admin_valid_admin() + + def test_add_admin_twice_valid_admin(self): + self.add_admin_twice_valid_admin() + + def test_add_admin_add_key_not_on_keyserver(self): + self.add_admin_add_key_not_on_keyserver() + + def test_add_admin_invalid_admin(self): + self.add_admin_invalid_admin() + + def test_remove_user_valid_admin(self): + self.remove_user_valid_admin() + + def test_remove_user_invalid_admin(self): + self.remove_user_invalid_admin() + + def test_remove_admin_valid_admin(self): + self.remove_admin_valid_admin() + + def test_remove_admin_invalid_admin(self): + self.remove_admin_invalid_admin() + +class TestUserPostgresql(PostgresqlBaseTestCase, BaseUserTests): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + app = create_app(self.Config) + app.testing = True + self.app = app.test_client() + + def tearDown(self): + super().tearDown() + del self.app + + def test_add_user_valid_admin(self): + self.add_user_valid_admin() + + def test_add_user_invalid_admin(self): + self.add_user_invalid_admin() + + def test_add_admin_valid_admin(self): + self.add_admin_valid_admin() + + def test_add_admin_twice_valid_admin(self): + self.add_admin_twice_valid_admin() + + def test_add_admin_add_key_not_on_keyserver(self): + self.add_admin_add_key_not_on_keyserver() + + def test_add_admin_invalid_admin(self): + self.add_admin_invalid_admin() + + def test_remove_user_valid_admin(self): + self.remove_user_valid_admin() + + def test_remove_user_invalid_admin(self): + self.remove_user_invalid_admin() + + def test_remove_admin_valid_admin(self): + self.remove_admin_valid_admin() + + def test_remove_admin_invalid_admin(self): + self.remove_admin_invalid_admin() + + +if __name__ == "__main__": + unittest.main() diff --git a/mtls_server/auth.py b/mtls_server/auth.py new file mode 100644 index 0000000..eba6e1c --- /dev/null +++ b/mtls_server/auth.py @@ -0,0 +1,140 @@ +import base64 +import os +import time +import pprint +import json +from functools import wraps + +from flask import request +from flask import g +from flask import current_app + +from .logger import logger +from .utils import error_response +from .utils import time_in_range +from .utils import write_sig_to_file + + +pp = pprint.PrettyPrinter(indent=4) + + +class SignatureTimestampOutOfBoundsException(Exception): + pass + + +class MissingTokenException(Exception): + pass + + +class BadSignatureException(Exception): + pass + + +def legacy_verify(data, sig): + sig_path = write_sig_to_file(sig) + g.is_admin = False + + verified = current_app.config['admin_gpg'].verify_data( + sig_path, + data + ) + if verified.trust_level is not None and verified.trust_level >= verified.TRUST_ULTIMATE: + logger.debug(f"authenticated user {verified.pubkey_fingerprint} is admin") + g.is_admin = True + + if not g.is_admin: + verified = current_app.config['user_gpg'].verify_data( + sig_path, + data + ) + + os.remove(sig_path) + + if verified.trust_level is None: + return error_response(f"unauthorized: {verified.trust_text} {verified.fingerprint} {verified.status}", 401) + + now = time.time() + # Time in seconds that signed messages will be accepted once signed. + sig_auth_time_range = int(os.environ.get('SIG_AUTH_TIME_RANGE', '5')) + if not time_in_range(now-sig_auth_time_range, now, int(verified.timestamp)): + return error_response("signature timestamp out of range", 401) + + g.user_fingerprint = verified.pubkey_fingerprint + return None + +def login_required(f): + @wraps(f) + def login_required_wrap(*args, **kwargs): + allowed_tokens = ['PGP-SIG'] + g.is_admin = False + + (authorization_header := request.headers['Authorization']) + + if not authorization_header: + return error_response("authentication required", 401) + + try: + token_type, token = authorization_header.split(' ') + except Exception as e: + logger.warning(e) + return error_response("could not validate authentication", 500) + + if token_type not in allowed_tokens: + return error_response("authentication required", 401) + + if token_type == "PGP-SIG": + if request.content_length: + g.data = data = request.get_data() + + try: + g.json = json.loads(g.data) + except Exception: + return error_response("Could not parse body, expected JSON", 400) + else: + g.data = data = "NOCONTENT".encode('UTF-8') + g.json = {} + + b64d_token = base64.b64decode(token) + sig_path = write_sig_to_file(b64d_token) + + verified = current_app.config['admin_gpg'].verify_data( + sig_path, + data + ) + + g.is_admin = verified.trust_level is not None and verified.trust_level >= verified.TRUST_ULTIMATE + + if g.is_admin: + logger.debug(f"authenticated user {verified.pubkey_fingerprint} is admin") + else: + verified = current_app.config['user_gpg'].verify_data( + sig_path, + data + ) + + os.remove(sig_path) + + if verified.trust_level is None: + return error_response("unauthorized", 401) + + now = time.time() + # Time in seconds that signed messages will be accepted once signed. + sig_auth_time_range = int(os.environ.get('SIG_AUTH_TIME_RANGE', '5')) + if not time_in_range(now-sig_auth_time_range, now, int(verified.timestamp)): + return error_response("signature timestamp out of range", 401) + + g.user_fingerprint = verified.pubkey_fingerprint + + return f(*args, **kwargs) + return login_required_wrap + + +def admin_required(f): + @wraps(f) + def admin_wrap(*args, **kwargs): + logger.info(f"Checking if user is admin... {g.is_admin}") + if not g.is_admin: + return error_response("insufficient permissions", 403) + + return f(*args, **kwargs) + return admin_wrap diff --git a/mtls_server/cert_processor.py b/mtls_server/cert_processor.py index 65617ac..ace2cfd 100644 --- a/mtls_server/cert_processor.py +++ b/mtls_server/cert_processor.py @@ -48,7 +48,7 @@ class CertProcessorUnsupportedCriticalExtensionError(Exception): class CertProcessor: - def __init__(self, config): + def __init__(self, config, user_gnupg, admin_gnupg): """Cerificate Processor. Args: @@ -76,8 +76,9 @@ def __init__(self, config): self.admin_gpg.encoding = "utf-8" # Start Background threads for getting revoke/expiry from Keyserver - KeyRefresh("user_key_refresh", self.user_gpg, config) - KeyRefresh("admin_key_refresh", self.admin_gpg, config) + if os.environ.get('AUTO_REFRESH_KEYS', '0') == '1': + KeyRefresh("user_key_refresh", self.user_gpg, config) + KeyRefresh("admin_key_refresh", self.admin_gpg, config) if config.get("storage", "engine", None) is None: raise StorageEngineMissing() @@ -94,70 +95,6 @@ def __init__(self, config): "mtls", "protocol", os.environ.get("PROTOCOL", "http") ) - def verify(self, data, signature): - """Verifies that the signed data is signed by a trusted key. - - Args: - data (str): The data to be verified. - signature (str): The signature file. - Raises: - CertProcessorInvalidSignatureError: Signing Key not in trust store. - CertProcessorUntrustedSignatureError: Signing Key in trust store - but does not have to correct permissions. - Returns: - str: The fingerprint of the signer. - """ - verified = self.user_gpg.verify_data(signature, data) - if verified is None: - logger.error("Invalid signature") - raise CertProcessorInvalidSignatureError - if ( - verified.trust_level is not None - and verified.trust_level < verified.TRUST_FULLY - ): - logger.error( - "User with fingerprint: {} does not have the required trust".format( - verified.pubkey_fingerprint - ) - ) - raise CertProcessorUntrustedSignatureError - if verified.valid is None or verified.valid is False: - raise CertProcessorInvalidSignatureError - return verified.pubkey_fingerprint - - def admin_verify(self, data, signature): - """Verifies that the signed data is signed by an admin key. - - Args: - data (str): The data to be verified - signature (str): The signature file - Raises: - CertProcessorInvalidSignatureError: Signing Key not in trust store. - CertProcessorUntrustedSignatureError: Signing Key in trust store - but does not have to correct permissions. - Returns: - str: The fingerprint of the signer. - """ - verified = self.admin_gpg.verify_data(signature, data) - if verified is None: - raise CertProcessorInvalidSignatureError - if verified.valid is None or verified.valid is False: - logger.error( - "Invalid signature for {}".format(verified.fingerprint) - ) - raise CertProcessorInvalidSignatureError - if ( - verified.trust_level is not None - and verified.trust_level < verified.TRUST_FULLY - ): - logger.error( - "User with fingerprint: {} does not have the required trust".format( - verified.pubkey_fingerprint - ) - ) - raise CertProcessorUntrustedSignatureError - return verified.pubkey_fingerprint - def get_csr(self, csr): """Given a CSR string, get a cryptography CSR Object. @@ -187,13 +124,12 @@ def get_ca_key(self): ca_key_path = os.path.abspath( os.path.join(os.path.dirname(__file__), ca_key_path) ) + ca_dir = "/".join(ca_key_path.split("/")[:-1]) + create_dir_if_missing(ca_dir) try: - ca_dir = "/".join(ca_key_path.split("/")[:-1]) - if not os.path.isdir(ca_dir): - os.makedirs(ca_dir) with open(ca_key_path, "rb") as key_file: if os.environ.get("CA_KEY_PASSWORD"): - pw = os.environ.get("CA_KEY_PASSWORD").encode("UTF-8") + pw = os.environ.get("CA_KEY_PASSWORD", "").encode("UTF-8") else: pw = None ca_key = serialization.load_pem_private_key( @@ -209,7 +145,7 @@ def get_ca_key(self): if os.environ.get("CA_KEY_PASSWORD"): encryption_algorithm = serialization.BestAvailableEncryption( - os.environ.get("CA_KEY_PASSWORD").encode("UTF-8") + os.environ.get("CA_KEY_PASSWORD", "").encode("UTF-8") ) else: encryption_algorithm = self.no_encyption @@ -237,7 +173,11 @@ def get_ca_cert(self, key=None): Returns: cryptography.x509.Certificate: CA Certificate """ + ca_cert_path = get_abs_path(self.config.get("ca", "cert")) + ca_dir = "/".join(ca_cert_path.split("/")[:-1]) + create_dir_if_missing(ca_dir) + # Grab the CA Certificate from filesystem if it exists and return if os.path.isfile(ca_cert_path): with open(ca_cert_path, "rb") as cert_file: @@ -246,6 +186,9 @@ def get_ca_cert(self, key=None): ) return ca_cert + # We want this to run after the attempt to get the Cert for the case where we want + # a certificate that's already been generated. e.g. the route for getting the CA + # certificate if key is None: raise CertProcessorKeyNotFoundError() diff --git a/mtls_server/handler.py b/mtls_server/handler.py deleted file mode 100644 index e22bbbd..0000000 --- a/mtls_server/handler.py +++ /dev/null @@ -1,287 +0,0 @@ -import os -import json - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization - -from .cert_processor import CertProcessor -from .cert_processor import CertProcessorInvalidSignatureError -from .cert_processor import CertProcessorKeyNotFoundError -from .cert_processor import CertProcessorMismatchedPublicKeyError -from .cert_processor import CertProcessorNoPGPKeyFoundError -from .cert_processor import CertProcessorNotAdminUserError -from .cert_processor import CertProcessorUntrustedSignatureError -from .logger import logger -from .sync import Sync -from .utils import error_response -from .utils import write_sig_to_file - - -class PGPKeyNotFoundException(Exception): - pass - - -class PGPTrustException(Exception): - pass - - -class Handler: - def __init__(self, config): - # Seed the trust stores - seed = os.environ.get("SEED_ON_INIT", "1") - if seed == "1": - Sync(config).seed() - self.cert_processor = CertProcessor(config) - self.config = config - - def create_cert(self, body): - """Create a certificate.""" - lifetime = int(body["lifetime"]) - min_lifetime = self.config.get_int("mtls", "min_lifetime", 60) - max_lifetime = self.config.get_int("mtls", "max_lifetime", 0) - if lifetime < min_lifetime: - logger.info( - f"User requested lifetime less than minimum. {lifetime} < {min_lifetime}" - ) - return error_response( - f"lifetime must be greater than {min_lifetime} seconds" - ) - if max_lifetime != 0: - if lifetime > max_lifetime: - logger.info( - f"User requested lifetime greater than maximum. {lifetime} < {max_lifetime}" - ) - return error_response( - f"lifetime must be less than {max_lifetime} seconds" - ) - csr_str = body["csr"] - csr = self.cert_processor.get_csr(csr_str) - if csr is None: - return error_response("Could not load CSR") - try: - logger.info("create_cert: get csr_public_bytes") - csr_public_bytes = csr.public_bytes(serialization.Encoding.PEM) - logger.info("create_cert: write to temp sig file") - sig_path = write_sig_to_file(body["signature"]) - logger.info("create_cert: get fingerprint") - fingerprint = self.cert_processor.verify( - csr_public_bytes, sig_path - ) - logger.info("create_cert: remove sig file") - os.remove(sig_path) - except CertProcessorUntrustedSignatureError as e: - logger.info("Unauthorized: {}".format(e)) - return error_response("Unauthorized", 403) - except CertProcessorInvalidSignatureError: - logger.info("Invalid signature in CSR.") - return error_response("Invalid signature", 401) - except Exception as e: - logger.critical("Unknown Error: {}".format(e)) - return error_response("Internal Server Error", 500) - if csr is None: - logger.info("Invalid CSR.") - return error_response("Invalid CSR") - cert = None - try: - logger.info( - f"create_cert: generating certificate for: {fingerprint}" - ) - cert = self.cert_processor.generate_cert( - csr, lifetime, fingerprint - ) - logger.info( - f"create_cert: sending certificate to client for: {fingerprint}" - ) - return json.dumps({"cert": cert.decode("UTF-8")}), 200 - except CertProcessorKeyNotFoundError: - logger.critical("Key missing. Service not properly initialized") - return error_response("Internal Error") - except CertProcessorMismatchedPublicKeyError: - logger.error("CSR Public Key does not match found certificate.") - return error_response("Internal Error") - except CertProcessorNotAdminUserError: - logger.error( - "User {} is not an admin and attempted ".format(fingerprint) - + "to generate a certificate they are not allowed to generate." - ) - return error_response("Invalid Request", 403) - except CertProcessorNoPGPKeyFoundError: - logger.info("PGP Key not found.") - return error_response("Unauthorized", 401) - except Exception as e: - logger.critical(f"Unhandled Exception: {e}") - return error_response("Internal Server Error", 500) - - def revoke_cert(self, body): - """ - A user should be able to revoke their own certificate. An admin should - be able to revoke the certificate of any user. - - Args: - body: A dictionary from the JSON input. - - Returns: - (json, int): a tuple of the json response and http status code. - """ - fingerprint = None - sig_path = write_sig_to_file(body["signature"]) - try: - fingerprint = self.cert_processor.admin_verify( - json.dumps(body["query"]).encode("UTF-8"), sig_path - ) - logger.info( - f"Admin {fingerprint} revoking certificate with query {json.dumps(body['query'])}" - ) - os.remove(sig_path) - except ( - CertProcessorInvalidSignatureError, - CertProcessorUntrustedSignatureError, - ): - try: - fingerprint = self.cert_processor.verify( - json.dumps(body["query"]).encode("UTF-8"), sig_path - ) - logger.info( - "User {userfp} revoking certificate with query {query}".format( - userfp=fingerprint, query=json.dumps(body["query"]) - ) - ) - os.remove(sig_path) - except ( - CertProcessorInvalidSignatureError, - CertProcessorUntrustedSignatureError, - ): - os.remove(sig_path) - return error_response("Unauthorized", 403) - - certs = self.cert_processor.storage.get_cert(**body["query"]) - if certs is None: - return error_response("No Cert to revoke") - for cert in certs: - cert = x509.load_pem_x509_certificate( - str(cert).encode("UTF-8"), backend=default_backend() - ) - self.cert_processor.revoke_cert(cert.serial_number) - return json.dumps({"msg": "success"}), 200 - - def add_user(self, body, is_admin=False): - """Add a user or admin.""" - fingerprint = None - sig_path = write_sig_to_file(body["signature"]) - try: - fingerprint = self.cert_processor.admin_verify( - body["fingerprint"].encode("UTF-8"), sig_path - ) - except ( - CertProcessorInvalidSignatureError, - CertProcessorUntrustedSignatureError, - ): - os.remove(sig_path) - logger.error( - "Invalid signature on adding fingerprint: {fp}".format( - fp=body["fingerprint"] - ) - ) - return error_response("Unauthorized", 403) - # Remove signature file - os.remove(sig_path) - - fingerprint = body["fingerprint"] - - try: - if is_admin: - has_user = self.has_user( - self.cert_processor.admin_gpg, fingerprint - ) - if not has_user: - logger.info( - f"Admin {fingerprint} adding admin user {body['fingerprint']}" - ) - # Add a user to the admin trust store - self.add_and_trust_user( - self.cert_processor.admin_gpg, fingerprint - ) - - has_user = self.has_user(self.cert_processor.user_gpg, fingerprint) - - if not has_user: - # Add the user to the user trust store - logger.info( - f"Admin {fingerprint} adding admin user {body['fingerprint']}" - ) - self.add_and_trust_user( - self.cert_processor.user_gpg, fingerprint - ) - return json.dumps({"msg": "success"}), 201 - except PGPKeyNotFoundException: - return ( - json.dumps( - {"msg": "Key not found on keyserver. Could not import"} - ), - 422, - ) - except PGPTrustException: - return ( - json.dumps( - {"msg": "Key could not be trusted"} - ), - 422, - ) - - def has_user(self, gpg, fingerprint): - keys = gpg.list_keys(keys=fingerprint) - if len(keys) == 0: - return False - return True - - def add_and_trust_user(self, gpg, fingerprint): - keyserver = self.config.get("gnupg", "keyserver", "keyserver.ubuntu.com") - logger.info(f"Retrieving key {fingerprint} from {keyserver}") - result = gpg.recv_keys( - keyserver, - fingerprint, - ) - if result.count is None or result.count == 0: - raise PGPKeyNotFoundException() - - logger.info(f"Trusting {fingerprint}") - try: - result = self.cert_processor.user_gpg.trust_keys( - [fingerprint], "TRUST_ULTIMATE" - ) - except ValueError: - raise PGPTrustException() - - def remove_user(self, body, is_admin=False): - """Remove a user or admin.""" - fingerprint = None - sig_path = write_sig_to_file(body["signature"]) - try: - fingerprint = self.cert_processor.admin_verify( - body["fingerprint"].encode("UTF-8"), sig_path - ) - logger.info( - "Admin {adminfp} adding user {userfp}".format( - adminfp=fingerprint, userfp=body["fingerprint"] - ) - ) - except ( - CertProcessorInvalidSignatureError, - CertProcessorUntrustedSignatureError, - ): - os.remove(sig_path) - logger.error( - f"Invalid signature on adding fingerprint: {body['fingerprint']}" - ) - return error_response("Unauthorized", 403) - # Remove signature file - os.remove(sig_path) - - if is_admin: - # Add a user to the admin trust store - self.cert_processor.admin_gpg.delete_keys(body["fingerprint"]) - - # Add the user to the user trust store - self.cert_processor.user_gpg.delete_keys(body["fingerprint"]) - return json.dumps({"msg": "success"}), 201 diff --git a/mtls_server/logger.py b/mtls_server/logger.py index 3399518..f302b00 100644 --- a/mtls_server/logger.py +++ b/mtls_server/logger.py @@ -1,5 +1,8 @@ +import os import logging +LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO') + # Log to the screen stream_handler = logging.StreamHandler() stream_handler.setFormatter( @@ -9,5 +12,5 @@ ) ) logger = logging.getLogger() -logger.setLevel(logging.INFO) +logger.setLevel(level=LOGLEVEL) logger.addHandler(stream_handler) diff --git a/mtls_server/server.py b/mtls_server/server.py index 73893d5..bf77162 100644 --- a/mtls_server/server.py +++ b/mtls_server/server.py @@ -1,18 +1,36 @@ import os import json +import gnupg +from cryptography import x509 +from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from flask import Flask from flask import request +from flask import g +from .auth import admin_required +from .auth import login_required +from .auth import legacy_verify +from .cert_processor import CertProcessor from .cert_processor import CertProcessorKeyNotFoundError +from .cert_processor import CertProcessorMismatchedPublicKeyError +from .cert_processor import CertProcessorNoPGPKeyFoundError +from .cert_processor import CertProcessorNotAdminUserError from .config import Config -from .handler import Handler +from .logger import logger +from .sync import Sync +from .utils import PGPKeyNotFoundException +from .utils import PGPTrustException +from .utils import add_and_trust_user +from .utils import create_dir_if_missing +from .utils import error_response +from .utils import get_abs_path +from .utils import has_user __author__ = "Danny Grove " app = None -handler = None CONFIG_PATH = os.environ.get( "CONFIG_PATH", os.path.join(os.getcwd(), "config.ini") ) @@ -20,6 +38,7 @@ def create_app(config=None): app = Flask(__name__) + app.config['MAX_CONTENT_LENGTH'] = os.environ.get('MAX_CONTENT_LENGTH') if config is None: Config.init_config(CONFIG_PATH) @@ -27,43 +46,167 @@ def create_app(config=None): # Set the CWD so that other areas can reference it. Config.config.set("mtls", "cwd", os.getcwd()) - handler = Handler(Config) + user_gpg_path = get_abs_path( + Config.get( + "gnupg", "user", os.path.join(os.getcwd(), "secrets/gnupg") + ) + ) + create_dir_if_missing(user_gpg_path) + user_gpg = gnupg.GPG(gnupghome=user_gpg_path) + user_gpg.encoding = 'UTF-8' + app.config.update(user_gpg=user_gpg) + admin_gpg_path = get_abs_path( + Config.get( + "gnupg", + "admin", + os.path.join(os.getcwd(), "secrets/gnupg_admin"), + ) + ) + create_dir_if_missing(admin_gpg_path) + admin_gpg = gnupg.GPG(gnupghome=admin_gpg_path) + admin_gpg.encoding = 'UTF-8' + app.config.update(admin_gpg=admin_gpg) + + # Seed the trust stores + seed = os.environ.get("SEED_ON_INIT", "1") + if seed == "1": + logger.debug("Seeding trust store") + Sync(Config).seed() + + logger.debug("Configuring certificate processor") + cert_processor = CertProcessor(Config, user_gpg, admin_gpg) with open("VERSION", "r") as f: version = str(f.readline().strip()) # This will generate a CA Certificate and Key if one does not exist try: - handler.cert_processor.get_ca_cert() + logger.debug("Getting CA Cert") + cert_processor.get_ca_cert() except CertProcessorKeyNotFoundError: # Auto-gen a new key and cert if one is not presented and this is the # first call ever made to the handler - key = handler.cert_processor.get_ca_key() - handler.cert_processor.get_ca_cert(key) + logger.debug("Getting CA Key") + key = cert_processor.get_ca_key() + cert_processor.get_ca_cert(key) @app.route("/", methods=["POST"]) - def create_handler(): - body = request.get_json() - if body["type"] == "CERTIFICATE": - return handler.create_cert(body) - if body["type"] == "USER": - return handler.add_user(body) - if body["type"] == "ADMIN": - return handler.add_user(body, is_admin=True) + def legacy_create(): + if request.content_length: + g.data = request.get_data() + + try: + g.json = json.loads(g.data) + except Exception: + return error_response("Could not parse body, expected JSON", 400) + + call_type = g.json.get('type') + if call_type != "CERTIFICATE": + return error_response(f"Legacy call for {call_type} no longer exists. Please update your client") + + csr_str = g.json.get('csr') + csr = cert_processor.get_csr(csr_str) + if csr is None: + return error_response("Could not load CSR", 400) + err = legacy_verify( + csr.public_bytes(serialization.Encoding.PEM), + g.json.get('signature').encode('utf-8') + ) + if err is not None: + return err + body = g.json + fingerprint = g.user_fingerprint + lifetime = int(body.get("lifetime")) + min_lifetime = Config.get_int("mtls", "min_lifetime", 60) + max_lifetime = Config.get_int("mtls", "max_lifetime", 0) + if lifetime < min_lifetime: + logger.info( + f"User requested lifetime less than minimum. {lifetime} < {min_lifetime}" + ) + lifetime = min_lifetime + if max_lifetime != 0: + if lifetime > max_lifetime: + logger.info( + f"User requested lifetime greater than maximum. {lifetime} < {max_lifetime}" + ) + lifetime = max_lifetime + csr_str = body["csr"] + csr = cert_processor.get_csr(csr_str) + if csr is None: + return error_response("Could not load CSR", 400) + cert = None + try: + logger.info( + f"create_cert: generating certificate for: {fingerprint}" + ) + cert = cert_processor.generate_cert( + csr, lifetime, fingerprint + ) + logger.info( + f"create_cert: sending certificate to client for: {fingerprint}" + ) + return json.dumps({"cert": cert.decode("UTF-8")}), 200 + except CertProcessorKeyNotFoundError: + logger.critical("Key missing. Service not properly initialized") + return error_response("Internal Error") + except CertProcessorMismatchedPublicKeyError: + logger.error("CSR Public Key does not match found certificate.") + return error_response("CSR Public key does not match previous user key", 400) + except CertProcessorNotAdminUserError: + logger.error( + "User {} is not an admin and attempted ".format(fingerprint) + + "to generate a certificate they are not allowed to generate." + ) + return error_response("Invalid Request", 403) + except CertProcessorNoPGPKeyFoundError: + logger.info("PGP Key not found.") + return error_response("Unauthorized", 401) + except Exception as e: + logger.critical(f"Unhandled Exception: {e}") + return error_response("Internal Server Error", 500) @app.route("/", methods=["DELETE"]) - def delete_handler(): - body = request.get_json() - if body["type"] == "CERTIFICATE": - return handler.revoke_cert(body) - if body["type"] == "USER": - return handler.remove_user(body) - if body["type"] == "ADMIN": - return handler.remove_user(body, is_admin=True) + def legacy_delete(): + if request.content_length: + g.data = request.get_data() + + try: + g.json = json.loads(g.data) + except Exception: + return error_response("Could not parse body, expected JSON", 400) + + call_type = g.json.get('type') + if call_type != "CERTIFICATE": + return error_response(f"Legacy call for {call_type} no longer exists. Please update your client") + + logger.info(f"Legacy Verification") + err = legacy_verify( + json.dumps(g.json.get('query')).encode('utf-8'), + g.json.get('signature').encode('utf-8') + ) + if err is not None: + return err + + query = g.json.get('query') + + if not g.is_admin: + query['fingerprint'] = g.user_fingerprint + + certs = cert_processor.storage.get_cert(**g.json["query"]) + if certs is None or len(certs) == 0: + return error_response("No Cert to revoke", 404) + + for cert in certs: + cert = x509.load_pem_x509_certificate( + str(cert).encode("UTF-8"), backend=default_backend() + ) + cert_processor.revoke_cert(cert.serial_number) + return json.dumps({"msg": "success"}), 200 + @app.route("/ca", methods=["GET"]) def get_ca_cert(): - cert = handler.cert_processor.get_ca_cert() + cert = cert_processor.get_ca_cert() cert = cert.public_bytes(serialization.Encoding.PEM).decode("UTF-8") return ( json.dumps({"issuer": Config.get("ca", "issuer"), "cert": cert}), @@ -72,13 +215,176 @@ def get_ca_cert(): @app.route("/crl", methods=["GET"]) def get_crl(): - crl = handler.cert_processor.get_crl() + crl = cert_processor.get_crl() return crl.public_bytes(serialization.Encoding.PEM).decode("UTF-8") @app.route("/version", methods=["GET"]) def get_version(): return json.dumps({"version": version}), 200 + @app.route("/certs", methods=["GET"]) + def get_certificates(): + return error_response("Not implemented", status_code=501) + + @app.route("/certs", methods=["POST"]) + @login_required + def create_cert(): + body = g.json + fingerprint = g.user_fingerprint + lifetime = int(body.get("lifetime")) + min_lifetime = Config.get_int("mtls", "min_lifetime", 60) + max_lifetime = Config.get_int("mtls", "max_lifetime", 0) + if lifetime < min_lifetime: + logger.info( + f"User requested lifetime less than minimum. {lifetime} < {min_lifetime}" + ) + lifetime = min_lifetime + if max_lifetime != 0: + if lifetime > max_lifetime: + logger.info( + f"User requested lifetime greater than maximum. {lifetime} < {max_lifetime}" + ) + lifetime = max_lifetime + csr_str = body["csr"] + csr = cert_processor.get_csr(csr_str) + if csr is None: + return error_response("Could not load CSR", 400) + cert = None + try: + logger.info( + f"create_cert: generating certificate for: {fingerprint}" + ) + cert = cert_processor.generate_cert( + csr, lifetime, fingerprint + ) + logger.info( + f"create_cert: sending certificate to client for: {fingerprint}" + ) + return json.dumps({"cert": cert.decode("UTF-8")}), 200 + except CertProcessorKeyNotFoundError: + logger.critical("Key missing. Service not properly initialized") + return error_response("Internal Error") + except CertProcessorMismatchedPublicKeyError: + logger.error("CSR Public Key does not match found certificate.") + return error_response("CSR Public key does not match previous user key", 400) + except CertProcessorNotAdminUserError: + logger.error( + "User {} is not an admin and attempted ".format(fingerprint) + + "to generate a certificate they are not allowed to generate." + ) + return error_response("Invalid Request", 403) + except CertProcessorNoPGPKeyFoundError: + logger.info("PGP Key not found.") + return error_response("Unauthorized", 401) + except Exception as e: + logger.critical(f"Unhandled Exception: {e}") + return error_response("Internal Server Error", 500) + + @app.route("/certs/", methods=["GET"]) + @login_required + def get_certificate_by_serial(): + return error_response("Not implemented", status_code=501) + + @app.route("/certs/", methods=["DELETE"]) + @login_required + def revoke_certificate_by_serial(serial): + if g.is_admin: + logger.info( + f"Admin {g.user_fingerprint} revoking certificate with serial {serial}" + ) + certs = cert_processor.storage.get_cert(serial) + else: + logger.info( + f"User {g.user_fingerprint} revoking certificate with serial {serial}" + ) + certs = cert_processor.storage.get_cert(serial, fingerprint=g.user_fingerprint) + + if not len(certs): + return error_response("No certificate", 404) + for cert in certs: + cert = x509.load_pem_x509_certificate( + str(cert).encode("UTF-8"), backend=default_backend() + ) + cert_processor.revoke_cert(cert.serial_number) + return json.dumps({"msg": "success"}), 200 + + @app.route("/users", methods=["GET"]) + @login_required + def get_users(): + return error_response("Not implemented", status_code=501) + + @app.route("/users", methods=["POST"]) + @login_required + @admin_required + def add_user(): + body = g.json + if not body: + return error_response("Could not parse body", 400) + keyserver = Config.get("gnupg", "keyserver", "keyserver.ubuntu.com") + fingerprint = body.get('fingerprint', None) + if not fingerprint: + return error_response("Could not parse body", 400) + admin = body.get('admin', False) + if not fingerprint: + return error_response("Fingerprint missing", 400) + try: + if admin: + admin_exists = has_user( + cert_processor.admin_gpg, + fingerprint + ) + if not admin_exists: + logger.info( + f"Admin {g.user_fingerprint} adding admin user {fingerprint}" + ) + # Add a user to the admin trust store + add_and_trust_user( + cert_processor.admin_gpg, + fingerprint, + keyserver + ) + + user_exists = has_user(cert_processor.user_gpg, fingerprint) + logger.info(f"Has User? {user_exists}") + + if not user_exists: + logger.info( + f"Admin {g.user_fingerprint} adding admin user {fingerprint}" + ) + add_and_trust_user( + cert_processor.user_gpg, + fingerprint, + keyserver, + ) + return json.dumps({"msg": "success"}), 201 + except PGPKeyNotFoundException: + return ( + json.dumps( + {"msg": "Key not found on keyserver. Could not import"} + ), + 422, + ) + except PGPTrustException: + return ( + json.dumps( + {"msg": "Key could not be trusted"} + ), + 422, + ) + + + @app.route("/users/", methods=["DELETE"]) + @login_required + @admin_required + def remove_user_by_fingerprint(fingerprint): + body = g.json + admin = body.get('admin', False) + if admin: + cert_processor.admin_gpg.delete_keys(fingerprint) + + cert_processor.user_gpg.delete_keys(fingerprint) + return json.dumps({"msg": "success"}), 200 + return app diff --git a/mtls_server/storage.py b/mtls_server/storage.py index 1a4920c..e0b9e88 100644 --- a/mtls_server/storage.py +++ b/mtls_server/storage.py @@ -107,7 +107,7 @@ def revoke_cert(self, serial_number): ) cur.execute( "UPDATE certs SET revoked=1 WHERE serial_number=?", - [str(serial_number)], + [str(serial_number),], ) self.conn.commit() @@ -147,26 +147,27 @@ def get_cert( show_revoked=False, ): cur = self.conn.cursor() - value = None query = "SELECT cert FROM certs WHERE" + query_options = [] + values = [] if serial_number is not None: - query += " serial_number=?" - value = str(serial_number) - elif fingerprint is not None: - query += " fingerprint=?" - value = str(fingerprint) - elif common_name is not None: - query += " common_name=?" - value = str(common_name) - else: - return None - + query_options.append("serial_number=?") + values.append(str(serial_number)) + if fingerprint is not None: + query_options.append("fingerprint=?") + values.append(str(fingerprint)) + if common_name is not None: + query_options.append("common_name=?") + values.append(str(common_name)) + + query_options.append("revoked=?") if show_revoked: - query += " AND revoked=1" + values.append('1') else: - query += " AND revoked=0" + values.append('0') - cur.execute(query, [str(value)]) + query = f"{query} {' AND '.join(query_options)}" + cur.execute(query, values) rows = cur.fetchall() certs = [] for row in rows: @@ -221,22 +222,26 @@ def __init__(self, config): host=config.get("storage.postgres", "host", "localhost"), port=config.get_int("storage.postgres", "port", 5432), ) + self.conn.autocommit = True + + def __del__(self): + self.conn.close() def init_db(self): - cur = self.conn.cursor() - cur.execute( - """ - CREATE TABLE IF NOT EXISTS certs ( - serial_number text, - common_name text, - not_valid_after timestamp, - cert text, - revoked boolean, - fingerprint text + with self.conn.cursor() as cur: + logger.debug("Create Table certs") + cur.execute( + """ + CREATE TABLE IF NOT EXISTS certs ( + serial_number text, + common_name text, + not_valid_after timestamp, + cert text, + revoked boolean, + fingerprint text + ) + """ ) - """ - ) - self.conn.commit() def save_cert(self, cert, fingerprint): if self.__conflicting_cert_exists(cert, fingerprint): @@ -245,29 +250,28 @@ def save_cert(self, cert, fingerprint): common_name = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) common_name = common_name[0].value - cur = self.conn.cursor() - cur.execute( - """ - INSERT INTO certs ( - serial_number, - common_name, - not_valid_after, - cert, - revoked, - fingerprint + with self.conn.cursor() as cur: + cur.execute( + """ + INSERT INTO certs ( + serial_number, + common_name, + not_valid_after, + cert, + revoked, + fingerprint + ) + VALUES (%s, %s, %s, %s, %s, %s) + """, + ( + str(cert.serial_number), + common_name, + cert.not_valid_after, + cert.public_bytes(Encoding.PEM).decode("UTF-8"), + False, + fingerprint, + ), ) - VALUES (%s, %s, %s, %s, %s, %s) - """, - ( - str(cert.serial_number), - common_name, - cert.not_valid_after, - cert.public_bytes(Encoding.PEM).decode("UTF-8"), - False, - fingerprint, - ), - ) - self.conn.commit() def get_cert( self, @@ -276,105 +280,106 @@ def get_cert( fingerprint=None, show_revoked=False, ): - cur = self.conn.cursor() - value = None - query = "SELECT cert FROM certs WHERE" - if serial_number is not None: - query += " serial_number = %s" - value = str(serial_number) - elif fingerprint is not None: - query += " fingerprint = %s" - value = fingerprint - elif common_name is not None: - query += " common_name = %s" - value = common_name - else: - return None - - query += " AND revoked = %s" - - cur.execute(query, (value, show_revoked)) - rows = cur.fetchall() - certs = [] - for row in rows: - certs.append(row[0]) - return certs + with self.conn.cursor() as cur: + query = "SELECT cert FROM certs WHERE" + query_options = [] + values = [] + if serial_number is not None: + query_options.append("serial_number=%s") + values.append(str(serial_number)) + if fingerprint is not None: + query_options.append("fingerprint=%s") + values.append(fingerprint) + if common_name is not None: + query_options.append("common_name=%s") + values.append(common_name) + + query_options.append("revoked=%s") + if show_revoked: + values.append(True) + else: + values.append(False) + + cur.execute(f"{query} {' AND '.join(query_options)}", tuple(values)) + rows = cur.fetchall() + certs = [] + for row in rows: + certs.append(row[0]) + return certs def revoke_cert(self, serial_number): - cur = self.conn.cursor() - logger.info( - "Revoking certificate {serial_number}".format( - serial_number=serial_number + with self.conn.cursor() as cur: + logger.info( + "Revoking certificate {serial_number}".format( + serial_number=serial_number + ) + ) + cur.execute( + "UPDATE certs SET revoked=true WHERE serial_number = %s", + (str(serial_number),), ) - ) - cur.execute( - "UPDATE certs SET revoked=true WHERE serial_number = %s", - (str(serial_number),), - ) - self.conn.commit() def update_cert(self, serial_number=None, cert=None): if not serial_number or not cert: logger.error("A serial number and cert are required to update.") raise UpdateCertException - cur = self.conn.cursor() - logger.info( - "Updating certificate {serial_number}".format( - serial_number=serial_number + with self.conn.cursor() as cur: + logger.info( + "Updating certificate {serial_number}".format( + serial_number=serial_number + ) + ) + cur.execute( + """ + UPDATE + certs + SET + cert = %s, + not_valid_after = %s + WHERE + serial_number = %s + """, + ( + cert.public_bytes(Encoding.PEM).decode("UTF-8"), + cert.not_valid_after, + str(serial_number), + ), ) - ) - cur.execute( - """ - UPDATE - certs - SET - cert = %s, - not_valid_after = %s - WHERE - serial_number = %s - """, - ( - cert.public_bytes(Encoding.PEM).decode("UTF-8"), - cert.not_valid_after, - str(serial_number), - ), - ) - self.conn.commit() def get_revoked_certs(self): - cur = self.conn.cursor() - now = datetime.datetime.utcnow() - not_valid_after = now.strftime("%Y-%m-%d %H:%M:%S") - cur.execute( - "SELECT cert FROM certs WHERE revoked = true AND " - + "not_valid_after > %s", - (str(not_valid_after),), - ) - rows = cur.fetchall() - certs = [] - for row in rows: - certs.append(row[0]) - return certs + with self.conn.cursor() as cur: + now = datetime.datetime.utcnow() + not_valid_after = now.strftime("%Y-%m-%d %H:%M:%S") + cur.execute( + "SELECT cert FROM certs WHERE revoked = true AND " + + "not_valid_after > %s", + (str(not_valid_after),), + ) + rows = cur.fetchall() + certs = [] + for row in rows: + certs.append(row[0]) + return certs def __conflicting_cert_exists(self, cert, fingerprint): common_name = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) common_name = common_name[0].value - cur = self.conn.cursor() - cur.execute( - """ - SELECT count(*) FROM certs - WHERE serial_number = %s - OR ( - common_name = %s - AND fingerprint = %s - AND revoked=false + with self.conn.cursor() as cur: + cur.execute( + """ + SELECT count(*) FROM certs + WHERE serial_number = %s + OR ( + common_name = %s + AND fingerprint = %s + AND revoked=false + ) + """, + (str(cert.serial_number), common_name, fingerprint), ) - """, - (str(cert.serial_number), common_name, fingerprint), - ) - conflicts = cur.fetchone()[0] - return conflicts > 0 + conflicts = cur.fetchone()[0] + return conflicts > 0 class StorageEngineNotSupportedError(Exception): diff --git a/mtls_server/utils.py b/mtls_server/utils.py index c12dad9..5c4ed6f 100644 --- a/mtls_server/utils.py +++ b/mtls_server/utils.py @@ -11,6 +11,16 @@ from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID +from .logger import logger + + +class PGPKeyNotFoundException(Exception): + pass + + +class PGPTrustException(Exception): + pass + def generate_key(key_size=4096): return rsa.generate_private_key( @@ -135,7 +145,7 @@ def error_response(msg, status_code=501): return json.dumps({"error": True, "msg": msg}), status_code -def write_sig_to_file(sig_str): +def write_sig_to_file(sig): """ Writes a signature to a file. Returns the path to the file @@ -146,7 +156,7 @@ def write_sig_to_file(sig_str): """ sig_path = "/tmp/{}.sig".format(uuid.uuid4()) with open(sig_path, "wb") as f: - f.write(sig_str.encode("utf-8")) + f.write(sig) return sig_path @@ -185,3 +195,30 @@ def import_and_trust(key_data, gpg): def create_dir_if_missing(path): if not os.path.isdir(path): os.makedirs(path) + +def time_in_range(start: float, end: float, t: float) -> bool: + """Return true if t is in the range [start,end]""" + if start <= end: + return start <= t <= end + return False + +def has_user(gpg, fingerprint): + keys = gpg.list_keys(keys=fingerprint) + return len(keys) != 0 + +def add_and_trust_user(gpg, fingerprint, keyserver="keyserver.ubuntu.com"): + logger.info(f"Retrieving key {fingerprint} from {keyserver}") + result = gpg.recv_keys( + keyserver, + fingerprint, + ) + if result.count is None or result.count == 0: + raise PGPKeyNotFoundException() + + logger.info(f"Trusting {fingerprint}") + try: + result = gpg.trust_keys( + [fingerprint], "TRUST_ULTIMATE" + ) + except ValueError: + raise PGPTrustException() diff --git a/scripts/create-ca b/scripts/create-ca index 8510a16..88d1df3 100755 --- a/scripts/create-ca +++ b/scripts/create-ca @@ -2,6 +2,8 @@ import os import sys +import gnupg + # Check if within pipenv, otherwise bail if os.getenv('PIPENV_ACTIVE') != '1' and os.getenv('CI') is None: print('Script must be run within pipenv. Use `make create-ca`') @@ -13,6 +15,8 @@ sys.path.extend([ from mtls_server.cert_processor import CertProcessor from mtls_server.config import Config +from mtls_server.utils import create_dir_if_missing +from mtls_server.utils import get_abs_path CONFIG_PATH = os.path.abspath( os.path.join( @@ -22,7 +26,27 @@ CONFIG_PATH = os.path.abspath( ) ) Config.init_config(CONFIG_PATH) -cert_processor = CertProcessor(Config) + +user_gpg_path = get_abs_path( + Config.get( + "gnupg", "user", os.path.join(os.getcwd(), "secrets/gnupg") + ) +) +create_dir_if_missing(user_gpg_path) +user_gpg = gnupg.GPG(gnupghome=user_gpg_path) +user_gpg.encoding = 'UTF-8' +admin_gpg_path = get_abs_path( + Config.get( + "gnupg", + "admin", + os.path.join(os.getcwd(), "secrets/gnupg_admin"), + ) +) +create_dir_if_missing(admin_gpg_path) +admin_gpg = gnupg.GPG(gnupghome=admin_gpg_path) +admin_gpg.encoding = 'UTF-8' + +cert_processor = CertProcessor(Config, user_gpg, admin_gpg) key = cert_processor.get_ca_key() cert_processor.get_ca_cert(key) diff --git a/test/test_cert_processor.py b/test/test_cert_processor.py index 4925c95..46a8662 100644 --- a/test/test_cert_processor.py +++ b/test/test_cert_processor.py @@ -10,6 +10,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import openssl from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa import gnupg from mtls_server import storage @@ -25,6 +26,9 @@ class TestCertProcessorBase(unittest.TestCase): + cert_processor: CertProcessor + users: list + def get_ca_cert(self): key = self.cert_processor.get_ca_key() ca_cert = self.cert_processor.get_ca_cert(key) @@ -32,54 +36,7 @@ def get_ca_cert(self): def has_ca_key(self): ca_key = self.cert_processor.get_ca_key() - self.assertIsInstance(ca_key, openssl.rsa._RSAPrivateKey) - - def verify_user(self): - for user in self.users: - csr = user.gen_csr() - signature = self.user_gpg.sign( - csr.public_bytes(serialization.Encoding.PEM), - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - signature_str = str(signature) - sig_path = "{tmpdir}/{fingerprint}.asc".format( - tmpdir=self.USER_GNUPGHOME.name, fingerprint=user.fingerprint - ) - with open(sig_path, "wb") as sig_file: - sig_file.write(bytes(signature_str, "utf-8")) - fingerprint = self.cert_processor.verify( - csr.public_bytes(serialization.Encoding.PEM), sig_path - ) - os.remove(sig_path) - self.assertEqual(fingerprint, user.pgp_key.fingerprint) - - def verify_admin(self): - for user in self.admin_users: - csr = user.gen_csr() - signature = self.admin_gpg.sign( - csr.public_bytes(serialization.Encoding.PEM), - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - signature_str = str(signature) - sig_path = "{tmpdir}/{fingerprint}.asc".format( - tmpdir=self.ADMIN_GNUPGHOME.name, fingerprint=user.fingerprint - ) - with open(sig_path, "wb") as sig_file: - sig_file.write(bytes(signature_str, "utf-8")) - fingerprint = self.cert_processor.admin_verify( - csr.public_bytes(serialization.Encoding.PEM), sig_path - ) - os.remove(sig_path) - self.assertEqual(fingerprint, user.pgp_key.fingerprint) - - def verify_unauthorized_user(self): - pass + self.assertIsInstance(ca_key, rsa.RSAPrivateKey) def generate_cert(self): for user in self.users: @@ -104,18 +61,16 @@ def generate_cert(self): def get_crl(self): rev_serial_num = None - for i, user in enumerate(self.users): - csr = user.gen_csr() - bcert = self.cert_processor.generate_cert( - csr, 60, user.fingerprint - ) - cert = x509.load_pem_x509_certificate( - bcert, backend=default_backend() - ) - if i == 1: - self.cert_processor.revoke_cert(cert.serial_number) - rev_serial_num = cert.serial_number - + user = self.users[0] + csr = user.gen_csr() + bcert = self.cert_processor.generate_cert( + csr, 60, user.fingerprint + ) + cert = x509.load_pem_x509_certificate( + bcert, backend=default_backend() + ) + self.cert_processor.revoke_cert(cert.serial_number) + rev_serial_num = cert.serial_number crl = self.cert_processor.get_crl() self.assertIsInstance(crl, x509.CertificateRevocationList) self.assertIsInstance( @@ -204,9 +159,9 @@ def setUp(self): cur.execute("DROP TABLE IF EXISTS certs") self.engine.conn.commit() self.engine.init_db() - self.cert_processor = CertProcessor(Config) self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) + self.cert_processor = CertProcessor(Config, self.user_gpg, self.admin_gpg) self.users = [ User("user@host", gen_passwd(), generate_key(), gpg=self.user_gpg), User( @@ -244,9 +199,6 @@ def test_get_ca_cert(self): def test_has_ca_key(self): self.has_ca_key() - def test_verify_user(self): - self.verify_user() - def test_generate_cert(self): self.generate_cert() @@ -303,9 +255,9 @@ def setUp(self): self.engine.conn.commit() self.engine.init_db() - self.cert_processor = CertProcessor(Config) self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) + self.cert_processor = CertProcessor(Config, self.user_gpg, self.admin_gpg) self.users = [ User("user@host", gen_passwd(), generate_key(), gpg=self.user_gpg), User( @@ -343,15 +295,6 @@ def test_get_ca_cert(self): def test_has_ca_key(self): self.has_ca_key() - def test_verify_user(self): - self.verify_user() - - def test_verify_admin(self): - self.verify_admin() - - def test_verify_unauthorized_user(self): - self.verify_unauthorized_user() - def test_generate_cert(self): self.generate_cert() @@ -398,7 +341,7 @@ def tearDown(self): def test_missing_storage(self): with self.assertRaises(storage.StorageEngineMissing): Config.init_config(config=self.config) - self.cert_processor = CertProcessor(Config) + self.cert_processor = CertProcessor(Config, None, None) class TestCertProcessorRelativeGnupgHome(TestCertProcessorBase): @@ -441,9 +384,9 @@ def setUp(self): cur.execute("DROP TABLE IF EXISTS certs") self.engine.conn.commit() self.engine.init_db() - self.cert_processor = CertProcessor(Config) self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) + self.cert_processor = CertProcessor(Config, self.user_gpg, self.admin_gpg) self.users = [ User("user@host", gen_passwd(), generate_key(), gpg=self.user_gpg), User( @@ -481,9 +424,6 @@ def test_get_ca_cert(self): def test_has_ca_key(self): self.has_ca_key() - def test_verify_user(self): - self.verify_user() - def test_generate_cert(self): self.generate_cert() @@ -537,9 +477,9 @@ def setUp(self): cur.execute("DROP TABLE IF EXISTS certs") self.engine.conn.commit() self.engine.init_db() - self.cert_processor = CertProcessor(Config) self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) + self.cert_processor = CertProcessor(Config, self.user_gpg, self.admin_gpg) self.users = [ User("user@host", gen_passwd(), generate_key(), gpg=self.user_gpg), User( @@ -631,9 +571,9 @@ def setUp(self): cur.execute("DROP TABLE IF EXISTS certs") self.engine.conn.commit() self.engine.init_db() - self.cert_processor = CertProcessor(Config) self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) + self.cert_processor = CertProcessor(Config, self.user_gpg, self.admin_gpg) self.users = [ User( "user@host.com", diff --git a/test/test_handler.py b/test/test_handler.py deleted file mode 100644 index c8a043b..0000000 --- a/test/test_handler.py +++ /dev/null @@ -1,572 +0,0 @@ -import json -import logging -import os -import tempfile -import unittest - -from configparser import ConfigParser -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.backends import openssl -from cryptography.hazmat.primitives import serialization -from cryptography.x509.oid import NameOID -import gnupg - -from mtls_server import storage -from mtls_server.cert_processor import CertProcessor -from mtls_server.config import Config -from mtls_server.handler import Handler -from mtls_server.utils import User -from mtls_server.utils import gen_passwd -from mtls_server.utils import generate_key - - -logging.disable(logging.CRITICAL) -CLEANUP = os.environ.get('CLEANUP', '1') - - -class TestHandler(unittest.TestCase): - def setUp(self): - self.USER_GNUPGHOME = tempfile.TemporaryDirectory() - self.ADMIN_GNUPGHOME = tempfile.TemporaryDirectory() - self.INVALID_GNUPGHOME = tempfile.TemporaryDirectory() - self.NEW_USER_GNUPGHOME = tempfile.TemporaryDirectory() - self.config = ConfigParser() - self.config.read_string( - """ - [mtls] - min_lifetime=60 - max_lifetime=0 - - [ca] - key = secrets/certs/authority/RootCA.key - cert = secrets/certs/authority/RootCA.pem - issuer = My Company Name - alternate_name = *.myname.com - - [gnupg] - user={user_gnupghome} - admin={admin_gnupghome} - - [storage] - engine=sqlite3 - - [storage.sqlite3] - db_path=:memory: - """.format( - user_gnupghome=self.USER_GNUPGHOME.name, - admin_gnupghome=self.ADMIN_GNUPGHOME.name, - ) - ) - Config.init_config(config=self.config) - self.common_name = "user@host" - self.key = generate_key() - self.engine = storage.SQLiteStorageEngine(Config) - cur = self.engine.conn.cursor() - cur.execute("DROP TABLE IF EXISTS certs") - self.engine.conn.commit() - self.engine.init_db() - self.cert_processor = CertProcessor(Config) - self.handler = Handler(Config) - self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) - self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) - self.invalid_gpg = gnupg.GPG(gnupghome=self.INVALID_GNUPGHOME.name) - self.new_user_gpg = gnupg.GPG(gnupghome=self.NEW_USER_GNUPGHOME.name) - self.users = [ - User("user@host", gen_passwd(), generate_key(), gpg=self.user_gpg), - User( - "user2@host", gen_passwd(), generate_key(), gpg=self.user_gpg - ), - User( - "user3@host", gen_passwd(), generate_key(), gpg=self.user_gpg - ), - ] - self.invalid_users = [ - User( - "user4@host", - gen_passwd(), - generate_key(), - gpg=self.invalid_gpg, - ) - ] - self.admin_users = [ - User( - "admin@host", gen_passwd(), generate_key(), gpg=self.admin_gpg - ) - ] - self.new_users = [ - User( - "newuser@host", - gen_passwd(), - generate_key(), - gpg=self.new_user_gpg, - ), - User( - "newuser2@host", - gen_passwd(), - generate_key(), - gpg=self.new_user_gpg, - ), - ] - for user in self.users: - self.user_gpg.import_keys( - self.user_gpg.export_keys(user.fingerprint) - ) - self.user_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - for user in self.admin_users: - # Import to admin keychain - self.admin_gpg.import_keys( - self.admin_gpg.export_keys(user.fingerprint) - ) - self.admin_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - # Import to user keychain - self.user_gpg.import_keys( - self.admin_gpg.export_keys(user.fingerprint) - ) - self.user_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - for user in self.invalid_users: - self.invalid_gpg.import_keys( - self.invalid_gpg.export_keys(user.fingerprint) - ) - self.invalid_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - for user in self.new_users: - self.new_user_gpg.import_keys( - self.new_user_gpg.export_keys(user.fingerprint) - ) - self.new_user_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - - def tearDown(self): - if CLEANUP == '1': - self.USER_GNUPGHOME.cleanup() - self.ADMIN_GNUPGHOME.cleanup() - self.INVALID_GNUPGHOME.cleanup() - self.NEW_USER_GNUPGHOME.cleanup() - - def test_user_revoke_cert_serial_number(self): - user = self.users[0] - csr = user.gen_csr() - bcert = self.cert_processor.generate_cert(csr, 60, user.fingerprint) - cert = x509.load_pem_x509_certificate(bcert, backend=default_backend()) - body = {"query": {"serial_number": str(cert.serial_number)}} - data = json.dumps(body["query"]).encode("UTF-8") - sig = self.user_gpg.sign( - data, - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - body["signature"] = str(sig) - response = json.loads(self.handler.revoke_cert(body)[0]) - self.assertTrue(response["msg"] == "success") - - def test_admin_revoke_cert_serial_number(self): - admin = self.admin_users[0] - user = self.users[0] - user_csr = user.gen_csr() - user_bcert = self.cert_processor.generate_cert( - user_csr, 60, user.fingerprint - ) - user_cert = x509.load_pem_x509_certificate( - user_bcert, backend=default_backend() - ) - body = {"query": {"serial_number": str(user_cert.serial_number)}} - data = json.dumps(body["query"]).encode("UTF-8") - sig = self.admin_gpg.sign( - data, - keyid=admin.fingerprint, - detach=True, - clearsign=True, - passphrase=admin.password, - ) - body["signature"] = str(sig) - response = json.loads(self.handler.revoke_cert(body)[0]) - self.assertTrue(response["msg"] == "success") - - def test_invalid_revoke_cert_serial_number(self): - valid_user = self.users[0] - user = self.invalid_users[0] - csr = valid_user.gen_csr() - bcert = self.cert_processor.generate_cert( - csr, 60, valid_user.fingerprint - ) - cert = x509.load_pem_x509_certificate(bcert, backend=default_backend()) - body = {"query": {"serial_number": str(cert.serial_number)}} - data = json.dumps(body["query"]).encode("UTF-8") - sig = self.invalid_gpg.sign( - data, - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - body["signature"] = str(sig) - response = json.loads(self.handler.revoke_cert(body)[0]) - self.assertEqual(response["error"], True, msg=response) - - def test_create_cert(self): - for user in self.users: - csr = user.gen_csr() - sig = self.user_gpg.sign( - csr.public_bytes(serialization.Encoding.PEM), - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - payload = { - "csr": csr.public_bytes(serialization.Encoding.PEM).decode( - "utf-8" - ), - "signature": str(sig), - "lifetime": 60, - "type": "CERTIFICATE", - } - response = json.loads(self.handler.create_cert(payload)[0]) - self.assertIn("-----BEGIN CERTIFICATE-----", response["cert"]) - cert = x509.load_pem_x509_certificate( - response["cert"].encode("UTF-8"), backend=default_backend() - ) - self.assertIsInstance(cert, x509.Certificate) - - def test_create_cert_for_other_user_as_user(self): - user = self.users[0] - csr = user.gen_csr( - "Some other random user", "someotheruser@example.com" - ) - sig = self.user_gpg.sign( - csr.public_bytes(serialization.Encoding.PEM), - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - payload = { - "csr": csr.public_bytes(serialization.Encoding.PEM).decode( - "utf-8" - ), - "signature": str(sig), - "lifetime": 60, - "type": "CERTIFICATE", - } - response = json.loads(self.handler.create_cert(payload)[0]) - self.assertEqual(response["error"], True, msg=response) - - def test_create_cert_for_other_user_as_admin(self): - user = self.admin_users[0] - csr = user.gen_csr( - "Some other random user", "someotheruser@example.com" - ) - sig = self.admin_gpg.sign( - csr.public_bytes(serialization.Encoding.PEM), - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - payload = { - "csr": csr.public_bytes(serialization.Encoding.PEM).decode( - "utf-8" - ), - "signature": str(sig), - "lifetime": 60, - "type": "CERTIFICATE", - } - response = json.loads(self.handler.create_cert(payload)[0]) - self.assertIn( - "-----BEGIN CERTIFICATE-----", - response.get("cert", ""), - msg=response, - ) - cert = x509.load_pem_x509_certificate( - response["cert"].encode("UTF-8"), backend=default_backend() - ) - email = cert.subject.get_attributes_for_oid(NameOID.EMAIL_ADDRESS)[0] - email = email.value - self.assertEqual(email, "someotheruser@example.com", msg=response) - - def test_invalid_user_create_cert(self): - user = self.invalid_users[0] - csr = user.gen_csr() - sig = self.invalid_gpg.sign( - csr.public_bytes(serialization.Encoding.PEM), - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - payload = { - "csr": csr.public_bytes(serialization.Encoding.PEM).decode( - "utf-8" - ), - "signature": str(sig), - "lifetime": 60, - "type": "CERTIFICATE", - } - response = json.loads(self.handler.create_cert(payload)[0]) - self.assertEqual(response["error"], True) - - def test_add_user_valid_admin(self): - admin = self.admin_users[0] - sig = self.admin_gpg.sign( - "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD".encode("UTF-8"), - keyid=admin.fingerprint, - clearsign=True, - detach=True, - passphrase=admin.password, - ) - payload = { - "fingerprint": "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD", - "signature": str(sig), - "type": "USER", - } - response = json.loads(self.handler.add_user(payload)[0]) - self.assertEqual(response["msg"], "success") - - def test_add_user_invalid_admin(self): - user = self.users[0] - new_user = self.new_users[0] - sig = self.user_gpg.sign( - new_user.fingerprint.encode("UTF-8"), - keyid=user.fingerprint, - clearsign=True, - detach=True, - passphrase=user.password, - ) - payload = { - "fingerprint": new_user.fingerprint, - "signature": str(sig), - "type": "USER", - } - response = json.loads(self.handler.add_user(payload)[0]) - self.assertEqual(response["error"], True) - - def test_add_admin_valid_admin(self): - admin = self.admin_users[0] - fingerprint = "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD" - sig = self.admin_gpg.sign( - fingerprint.encode("UTF-8"), - keyid=admin.fingerprint, - clearsign=True, - detach=True, - passphrase=admin.password, - ) - payload = { - "fingerprint": fingerprint, - "signature": str(sig), - "type": "USER", - } - response = self.handler.add_user(payload, is_admin=True) - response_json = json.loads(response[0]) - self.assertEqual(response_json["msg"], "success") - - def test_add_admin_twice_valid_admin(self): - fingerprint = "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD" - admin = self.admin_users[0] - sig = self.admin_gpg.sign( - fingerprint.encode("UTF-8"), - keyid=admin.fingerprint, - clearsign=True, - detach=True, - passphrase=admin.password, - ) - payload = { - "fingerprint": fingerprint, - "signature": str(sig), - "type": "USER", - } - response = self.handler.add_user(payload, is_admin=True) - response_json = json.loads(response[0]) - self.assertEqual(response_json["msg"], "success") - response = self.handler.add_user(payload, is_admin=True) - response_json = json.loads(response[0]) - self.assertEqual(response_json["msg"], "success") - - def test_add_admin_add_key_not_on_keyserver(self): - admin = self.admin_users[0] - new_user = self.invalid_users[0] - sig = self.admin_gpg.sign( - new_user.fingerprint.encode("UTF-8"), - keyid=admin.fingerprint, - clearsign=True, - detach=True, - passphrase=admin.password, - ) - payload = { - "fingerprint": new_user.fingerprint, - "signature": str(sig), - "type": "USER", - } - response = self.handler.add_user(payload, is_admin=True) - self.assertEqual(response[1], 422) - - def test_add_admin_invalid_admin(self): - admin = self.users[0] - new_user = self.new_users[0] - sig = self.admin_gpg.sign( - new_user.fingerprint.encode("UTF-8"), - keyid=admin.fingerprint, - clearsign=True, - detach=True, - passphrase=admin.password, - ) - payload = { - "fingerprint": new_user.fingerprint, - "signature": str(sig), - "type": "USER", - } - response = json.loads(self.handler.add_user(payload, is_admin=True)[0]) - self.assertEqual(response["error"], True) - - def test_remove_user_valid_admin(self): - admin = self.admin_users[0] - sig = self.admin_gpg.sign( - "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD".encode("UTF-8"), - keyid=admin.fingerprint, - clearsign=True, - detach=True, - passphrase=admin.password, - ) - payload = { - "fingerprint": "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD", - "signature": str(sig), - "type": "USER", - } - response = json.loads(self.handler.add_user(payload)[0]) - self.assertEqual(response["msg"], "success") - response = json.loads(self.handler.remove_user(payload)[0]) - self.assertEqual(response["msg"], "success") - - def test_remove_user_invalid_admin(self): - admin = self.users[0] - new_user = self.new_users[0] - sig = self.admin_gpg.sign( - new_user.fingerprint.encode("UTF-8"), - keyid=admin.fingerprint, - clearsign=True, - detach=True, - passphrase=admin.password, - ) - payload = { - "fingerprint": new_user.fingerprint, - "signature": str(sig), - "type": "USER", - } - response = json.loads(self.handler.add_user(payload, is_admin=True)[0]) - self.assertEqual(response["error"], True) - - def test_remove_admin_valid_admin(self): - admin = self.admin_users[0] - sig = self.admin_gpg.sign( - "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD".encode("UTF-8"), - keyid=admin.fingerprint, - clearsign=True, - detach=True, - passphrase=admin.password, - ) - payload = { - "fingerprint": "C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD", - "signature": str(sig), - "type": "ADMIN", - } - response = json.loads(self.handler.add_user(payload)[0]) - self.assertEqual(response["msg"], "success") - response = json.loads(self.handler.remove_user(payload)[0]) - self.assertEqual(response["msg"], "success") - - def test_remove_admin_invalid_admin(self): - user = self.users[0] - new_user = self.new_users[0] - sig = self.user_gpg.sign( - new_user.fingerprint.encode("UTF-8"), - keyid=user.fingerprint, - clearsign=True, - detach=True, - passphrase=user.password, - ) - payload = { - "fingerprint": new_user.fingerprint, - "signature": str(sig), - "type": "ADMIN", - } - response = json.loads(self.handler.add_user(payload, is_admin=True)[0]) - self.assertEqual(response["error"], True) - - -class TestHandlerSeeding(unittest.TestCase): - def setUp(self): - self.USER_GNUPGHOME = tempfile.TemporaryDirectory() - self.ADMIN_GNUPGHOME = tempfile.TemporaryDirectory() - self.NEW_USER_GNUPGHOME = tempfile.TemporaryDirectory() - self.NEW_ADMIN_GNUPGHOME = tempfile.TemporaryDirectory() - self.SEED_DIR = tempfile.TemporaryDirectory() - self.config = ConfigParser() - self.config.read_string( - """ - [mtls] - min_lifetime=60 - max_lifetime=0 - seed_dir={seed_dir} - - [ca] - key = secrets/certs/authority/RootCA.key - cert = secrets/certs/authority/RootCA.pem - issuer = My Company Name - alternate_name = *.myname.com - - [gnupg] - user={user_gnupghome} - admin={admin_gnupghome} - - [storage] - engine=sqlite3 - - [storage.sqlite3] - db_path=:memory: - """.format( - user_gnupghome=self.USER_GNUPGHOME.name, - admin_gnupghome=self.ADMIN_GNUPGHOME.name, - seed_dir=self.SEED_DIR.name, - ) - ) - Config.init_config(config=self.config) - self.common_name = "user@host" - self.key = generate_key() - self.engine = storage.SQLiteStorageEngine(Config) - cur = self.engine.conn.cursor() - cur.execute("DROP TABLE IF EXISTS certs") - self.engine.conn.commit() - self.engine.init_db() - self.cert_processor = CertProcessor(Config) - self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) - self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) - self.new_user_gpg = gnupg.GPG(gnupghome=self.NEW_USER_GNUPGHOME.name) - self.new_admin_gpg = gnupg.GPG(gnupghome=self.NEW_ADMIN_GNUPGHOME.name) - self.new_users = [ - User( - "user@host", - gen_passwd(), - generate_key(), - gpg=self.new_user_gpg, - ) - ] - self.new_admins = [ - User( - "admin@host", - gen_passwd(), - generate_key(), - gpg=self.new_admin_gpg, - ) - ] - - def tearDown(self): - if CLEANUP == '1': - self.USER_GNUPGHOME.cleanup() - self.ADMIN_GNUPGHOME.cleanup() - self.NEW_USER_GNUPGHOME.cleanup() - self.NEW_ADMIN_GNUPGHOME.cleanup() - self.SEED_DIR.cleanup() - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_server.py b/test/test_server.py deleted file mode 100644 index 767d474..0000000 --- a/test/test_server.py +++ /dev/null @@ -1,212 +0,0 @@ -import json -import logging -import os -import tempfile -import unittest - -from configparser import ConfigParser -from cryptography.hazmat.primitives import serialization -import gnupg - -from mtls_server import storage -from mtls_server.config import Config -from mtls_server.server import create_app -from mtls_server.utils import User -from mtls_server.utils import gen_passwd -from mtls_server.utils import generate_key - - -logging.disable(logging.CRITICAL) -CLEANUP = os.environ.get('CLEANUP', '1') - - -class TestServer(unittest.TestCase): - def setUp(self): - self.USER_GNUPGHOME = tempfile.TemporaryDirectory() - self.ADMIN_GNUPGHOME = tempfile.TemporaryDirectory() - self.INVALID_GNUPGHOME = tempfile.TemporaryDirectory() - self.NEW_USER_GNUPGHOME = tempfile.TemporaryDirectory() - self.config = ConfigParser() - self.config.read_string( - """ - [mtls] - min_lifetime=60 - max_lifetime=0 - - [ca] - key = secrets/certs/authority/RootCA.key - cert = secrets/certs/authority/RootCA.pem - issuer = My Company Name - alternate_name = *.myname.com - - [gnupg] - user={user_gnupghome} - admin={admin_gnupghome} - - [storage] - engine=sqlite3 - - [storage.sqlite3] - db_path=:memory: - """.format( - user_gnupghome=self.USER_GNUPGHOME.name, - admin_gnupghome=self.ADMIN_GNUPGHOME.name, - ) - ) - Config.init_config(config=self.config) - self.key = generate_key() - self.engine = storage.SQLiteStorageEngine(Config) - cur = self.engine.conn.cursor() - cur.execute("DROP TABLE IF EXISTS certs") - self.engine.conn.commit() - self.engine.init_db() - self.user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) - self.admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) - self.invalid_gpg = gnupg.GPG(gnupghome=self.INVALID_GNUPGHOME.name) - self.new_user_gpg = gnupg.GPG(gnupghome=self.NEW_USER_GNUPGHOME.name) - app = create_app(Config) - self.app = app.test_client() - self.users = [ - User("user@host", gen_passwd(), generate_key(), gpg=self.user_gpg), - User( - "user2@host", gen_passwd(), generate_key(), gpg=self.user_gpg - ), - User( - "user3@host", gen_passwd(), generate_key(), gpg=self.user_gpg - ), - ] - self.invalid_users = [ - User( - "user4@host", - gen_passwd(), - generate_key(), - gpg=self.invalid_gpg, - ) - ] - self.admin_users = [ - User( - "admin@host", gen_passwd(), generate_key(), gpg=self.admin_gpg - ) - ] - self.new_users = [ - User( - "newuser@host", - gen_passwd(), - generate_key(), - gpg=self.new_user_gpg, - ), - User( - "newuser2@host", - gen_passwd(), - generate_key(), - gpg=self.new_user_gpg, - ), - ] - for user in self.users: - self.user_gpg.import_keys( - self.user_gpg.export_keys(user.fingerprint) - ) - self.user_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - for user in self.admin_users: - # Import to admin keychain - self.admin_gpg.import_keys( - self.admin_gpg.export_keys(user.fingerprint) - ) - self.admin_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - # Import to user keychain - self.user_gpg.import_keys( - self.admin_gpg.export_keys(user.fingerprint) - ) - self.user_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - for user in self.invalid_users: - self.invalid_gpg.import_keys( - self.invalid_gpg.export_keys(user.fingerprint) - ) - self.invalid_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - for user in self.new_users: - self.new_user_gpg.import_keys( - self.new_user_gpg.export_keys(user.fingerprint) - ) - self.new_user_gpg.trust_keys([user.fingerprint], "TRUST_ULTIMATE") - - def tearDown(self): - if CLEANUP == '1': - self.USER_GNUPGHOME.cleanup() - self.ADMIN_GNUPGHOME.cleanup() - self.INVALID_GNUPGHOME.cleanup() - self.NEW_USER_GNUPGHOME.cleanup() - - def test_get_ca_cert(self): - response = self.app.get("/ca") - self.assertEqual(response.status_code, 200) - res = json.loads(response.data) - self.assertEqual(res["issuer"], "My Company Name") - - def test_get_crl(self): - response = self.app.get("/crl") - self.assertEqual(response.status_code, 200) - self.assertIn(b"-----BEGIN X509 CRL-----", response.data) - self.assertIn(b"-----END X509 CRL-----", response.data) - - def test_user_generate_cert(self): - user = self.users[0] - csr = user.gen_csr() - sig = self.user_gpg.sign( - csr.public_bytes(serialization.Encoding.PEM), - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - payload = { - "csr": csr.public_bytes(serialization.Encoding.PEM).decode( - "utf-8" - ), - "signature": str(sig), - "lifetime": 60, - "type": "CERTIFICATE", - } - response = self.app.post( - "/", data=json.dumps(payload), content_type="application/json" - ) - res = json.loads(response.data) - self.assertEqual(response.status_code, 200) - self.assertIn("-----BEGIN CERTIFICATE-----", res["cert"]) - self.assertIn("-----END CERTIFICATE-----", res["cert"]) - - def test_invalid_user_generate_cert(self): - user = self.invalid_users[0] - csr = user.gen_csr() - sig = self.invalid_gpg.sign( - csr.public_bytes(serialization.Encoding.PEM), - keyid=user.fingerprint, - detach=True, - clearsign=True, - passphrase=user.password, - ) - payload = { - "csr": csr.public_bytes(serialization.Encoding.PEM).decode( - "utf-8" - ), - "signature": str(sig), - "lifetime": 60, - "type": "CERTIFICATE", - } - response = self.app.post( - "/", data=json.dumps(payload), content_type="application/json" - ) - res = json.loads(response.data) - self.assertEqual(response.status_code, 401) - self.assertEqual(res["error"], True) - - def test_get_version(self): - with open("VERSION", "r") as v: - version = v.readline().strip() - response = self.app.get("/version") - res = json.loads(response.data) - self.assertEqual(response.status_code, 200) - self.assertEqual(res["version"], version) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_sync.py b/test/test_sync.py index eeb84b8..93cfc05 100644 --- a/test/test_sync.py +++ b/test/test_sync.py @@ -6,8 +6,8 @@ from configparser import ConfigParser import gnupg -from mtls_server.handler import Handler from mtls_server.config import Config +from mtls_server.sync import Sync from mtls_server.utils import User from mtls_server.utils import gen_passwd from mtls_server.utils import generate_key @@ -83,6 +83,7 @@ def tearDown(self): def test_seed_users(self): seed_subpath = "user" os.makedirs("{}/{}".format(self.SEED_DIR.name, seed_subpath)) + user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) for user in self.new_users: fingerprint = user.fingerprint pgp_armored_key = self.new_user_gpg.export_keys(fingerprint) @@ -93,8 +94,7 @@ def test_seed_users(self): ) with open(fingerprint_file, "w") as fpf: fpf.write(pgp_armored_key) - handler = Handler(Config) - user_gpg = handler.cert_processor.user_gpg + Sync(Config).seed() stored_fingerprints = [] for key in user_gpg.list_keys(): stored_fingerprints.append(key["fingerprint"]) @@ -104,6 +104,7 @@ def test_seed_users(self): def test_seed_admins(self): seed_subpath = "admin" os.makedirs("{}/{}".format(self.SEED_DIR.name, seed_subpath)) + admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) for admin in self.new_admins: fingerprint = admin.fingerprint pgp_armored_key = self.new_admin_gpg.export_keys(fingerprint) @@ -114,8 +115,7 @@ def test_seed_admins(self): ) with open(fingerprint_file, "w") as fpf: fpf.write(pgp_armored_key) - handler = Handler(Config) - admin_gpg = handler.cert_processor.admin_gpg + Sync(Config).seed() stored_fingerprints = [] for key in admin_gpg.list_keys(): stored_fingerprints.append(key["fingerprint"]) @@ -125,6 +125,8 @@ def test_seed_admins(self): def test_seed_separate_admin_and_user(self): for seed_subpath in ["user", "admin"]: os.makedirs("{}/{}".format(self.SEED_DIR.name, seed_subpath)) + user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) + admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) for user in self.new_users: fingerprint = user.fingerprint pgp_armored_key = self.new_user_gpg.export_keys(fingerprint) @@ -145,9 +147,48 @@ def test_seed_separate_admin_and_user(self): ) with open(fingerprint_file, "w") as fpf: fpf.write(pgp_armored_key) - handler = Handler(Config) - user_gpg = handler.cert_processor.user_gpg - admin_gpg = handler.cert_processor.admin_gpg + + Sync(Config).seed() + user_stored_fingerprints = [] + admin_stored_fingerprints = [] + for key in user_gpg.list_keys(): + user_stored_fingerprints.append(key["fingerprint"]) + for key in admin_gpg.list_keys(): + admin_stored_fingerprints.append(key["fingerprint"]) + for admin in self.new_admins: + self.assertIn(admin.fingerprint, admin_stored_fingerprints) + self.assertIn(admin.fingerprint, user_stored_fingerprints) + for user in self.new_users: + self.assertIn(user.fingerprint, user_stored_fingerprints) + self.assertNotIn(user.fingerprint, admin_stored_fingerprints) + + def test_seed_binary_file(self): + for seed_subpath in ["user", "admin"]: + os.makedirs("{}/{}".format(self.SEED_DIR.name, seed_subpath)) + user_gpg = gnupg.GPG(gnupghome=self.USER_GNUPGHOME.name) + admin_gpg = gnupg.GPG(gnupghome=self.ADMIN_GNUPGHOME.name) + for user in self.new_users: + fingerprint = user.fingerprint + pgp_armored_key = self.new_user_gpg.export_keys(fingerprint, armor=False) + fingerprint_file = "{base}/{subpath}/{fingerprint}.gpg".format( + base=self.SEED_DIR.name, + subpath="user", + fingerprint=fingerprint, + ) + with open(fingerprint_file, "wb") as fpf: + fpf.write(pgp_armored_key) + for admin in self.new_admins: + fingerprint = admin.fingerprint + pgp_armored_key = self.new_admin_gpg.export_keys(fingerprint, armor=False) + fingerprint_file = "{base}/{subpath}/{fingerprint}.gpg".format( + base=self.SEED_DIR.name, + subpath="admin", + fingerprint=fingerprint, + ) + with open(fingerprint_file, "wb") as fpf: + fpf.write(pgp_armored_key) + + Sync(Config).seed() user_stored_fingerprints = [] admin_stored_fingerprints = [] for key in user_gpg.list_keys(): diff --git a/test/test_utils.py b/test/test_utils.py index 747b365..c02d009 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -2,10 +2,12 @@ import logging import os import tempfile +import time import unittest from mtls_server.utils import create_dir_if_missing from mtls_server.utils import get_abs_path +from mtls_server.utils import time_in_range logging.disable(logging.CRITICAL) CLEANUP = os.environ.get('CLEANUP', '1') @@ -29,3 +31,8 @@ def test_create_dir_if_missing(self): self.assertTrue(os.path.isdir(new_dir)) if CLEANUP == '1': self.TEMPDIR.cleanup() + + def test_time_in_range(self): + self.assertTrue(time_in_range(1,5,2)) + self.assertTrue(time_in_range(time.time()-5, time.time()+5, time.time())) + self.assertFalse(time_in_range(time.time()+5, time.time()-5, time.time()))