33 Commits

Author SHA1 Message Date
Oscar Krause
52e9f2cae9 ci fixes 2025-05-13 09:53:15 +02:00
Oscar Krause
09fe2a605c requirements.txt updated 2025-05-13 09:51:07 +02:00
Oscar Krause
8ec87a8859 updated compatibility 2025-05-13 09:51:02 +02:00
Oscar Krause
18f577b4f6 ci fixes 2025-05-13 09:48:30 +02:00
Oscar Krause
eab3b21d60 ci fixes 2025-04-30 14:24:54 +02:00
Oscar Krause
2157394dfa fixed debian dependency 2025-04-30 14:20:09 +02:00
Oscar Krause
b577edcf67 release registry url updated 2025-04-23 22:19:27 +02:00
Oscar Krause
8e3e49fc36 ci fixes 2025-04-23 22:02:04 +02:00
Oscar Krause
2638f67cd9 typos 2025-04-23 21:55:45 +02:00
Oscar Krause
60a6c83a3d Merge branch 'v18.x-support' into 'main'
v18.x support / NLS 3.4.x compatibility

See merge request oscar.krause/fastapi-dls!46
2025-04-23 21:54:43 +02:00
Oscar Krause
ae03867502 updated compatibility matrix 2025-04-23 10:35:07 +02:00
Oscar Krause
295e3c9482 fixes 2025-04-22 20:42:11 +02:00
Oscar Krause
3961acf231 fixes 2025-04-22 15:07:29 +02:00
Oscar Krause
04914740a4 improved tests 2025-04-22 14:38:17 +02:00
Oscar Krause
6af9cd04c9 added variable for custom cert path 2025-04-22 14:38:05 +02:00
Oscar Krause
29268b1658 code styling and tests 2025-04-22 14:16:30 +02:00
Oscar Krause
938a112b8a fixes 2025-04-22 14:00:56 +02:00
Oscar Krause
16870e9d67 code styling 2025-04-22 12:35:34 +02:00
Oscar Krause
55b7437fe7 fixes 2025-04-22 11:29:31 +02:00
Oscar Krause
e7e007a45f fixes 2025-04-22 11:22:57 +02:00
Oscar Krause
161a1430cf code styling 2025-04-22 11:06:54 +02:00
Oscar Krause
1ccb203b25 code styling 2025-04-22 11:05:03 +02:00
Oscar Krause
6c1a8d42dc fixes 2025-04-22 11:04:32 +02:00
Oscar Krause
d248496f34 fixes 2025-04-22 10:45:41 +02:00
Oscar Krause
fd1babaca5 fixes 2025-04-22 10:10:32 +02:00
Oscar Krause
cd9c655d65 fixes 2025-04-22 09:45:58 +02:00
Oscar Krause
6ed4bdfe6f fixes 2025-04-22 09:05:18 +02:00
Oscar Krause
e1ae757a50 updated tests 2025-04-22 08:53:30 +02:00
Oscar Krause
b0ca5d7ab5 fixes 2025-04-22 08:06:17 +02:00
Oscar Krause
14f8b54752 test with "76 chars per line" 2025-04-22 07:32:30 +02:00
Oscar Krause
dc783e6518 typos 2025-04-22 07:32:05 +02:00
Oscar Krause
3666e22707 fixes 2025-04-21 21:57:40 +02:00
Oscar Krause
6b54d4794b .gitlab-ci.yml bearbeiten 2025-04-16 15:07:28 +02:00
7 changed files with 148 additions and 59 deletions

View File

@@ -2,7 +2,7 @@ Package: fastapi-dls
Version: 0.0 Version: 0.0
Architecture: all Architecture: all
Maintainer: Oscar Krause oscar.krause@collinwebdesigns.de Maintainer: Oscar Krause oscar.krause@collinwebdesigns.de
Depends: python3, python3-fastapi, python3-uvicorn, python3-dotenv, python3-dateutil, python3-josepy, python3-sqlalchemy, python3-cryptography, python3-markdown, uvicorn, openssl Depends: python3, python3-fastapi, python3-uvicorn, python3-dotenv, python3-dateutil, python3-jose, python3-sqlalchemy, python3-cryptography, python3-markdown, uvicorn, openssl
Recommends: curl Recommends: curl
Installed-Size: 10240 Installed-Size: 10240
Homepage: https://git.collinwebdesigns.de/oscar.krause/fastapi-dls Homepage: https://git.collinwebdesigns.de/oscar.krause/fastapi-dls

View File

@@ -162,20 +162,22 @@ test:apt:
image: $IMAGE image: $IMAGE
stage: test stage: test
rules: rules:
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH)
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
changes: changes:
- app/**/* - app/**/*
- .DEBIAN/**/* - .DEBIAN/**/*
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' - .gitlab-ci.yml
variables:
VERSION: "0.0.1"
parallel: parallel:
matrix: matrix:
- IMAGE: - IMAGE:
- debian:trixie-slim # EOL: t.b.a. # - debian:trixie-slim # EOL: t.b.a.; "python3-jose" not available, but "python3-josepy"
- debian:bookworm-slim # EOL: June 06, 2026 - debian:bookworm-slim # EOL: June 06, 2026
- debian:bookworm-slim # EOL: June 06, 2026 - debian:bookworm-slim # EOL: June 06, 2026
- ubuntu:24.04 # EOL: April 2036 - ubuntu:24.04 # EOL: April 2036
- ubuntu:24.10 # - ubuntu:24.10 # EOL: t.b.a.; "python3-jose" not available, but "python3-josepy"
# - ubuntu:25.04 # EOL: t.b.a.; "python3-jose" not available, but "python3-josepy"
needs: needs:
- job: build:apt - job: build:apt
artifacts: true artifacts: true
@@ -210,8 +212,7 @@ test:apt:
test:pacman:archlinux: test:pacman:archlinux:
image: archlinux:base image: archlinux:base
rules: rules:
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: ($CI_PIPELINE_SOURCE == 'merge_request_event') || ($CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH)
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
changes: changes:
- app/**/* - app/**/*
- .PKGBUILD/**/* - .PKGBUILD/**/*
@@ -376,7 +377,7 @@ deploy:pacman:
release: release:
image: registry.gitlab.com/gitlab-org/release-cli:latest image: registry.gitlab.com/gitlab-org/release-cli:latest
stage: .post stage: .post
needs: [ build:docker, build:apt, build:pacman ] needs: [ deploy:docker, deploy:apt, deploy:pacman ]
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
script: script:
@@ -391,4 +392,4 @@ release:
- name: 'Package Registry' - name: 'Package Registry'
url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages' url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/packages'
- name: 'Container Registry' - name: 'Container Registry'
url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/container_registry/40' url: 'https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/container_registry/70'

View File

@@ -2,7 +2,7 @@
Minimal Delegated License Service (DLS). Minimal Delegated License Service (DLS).
> [!warning] Branch support \ > [!warning] Branch support
> FastAPI-DLS Version 1.x supports up to **`17.x`** releases. \ > FastAPI-DLS Version 1.x supports up to **`17.x`** releases. \
> FastAPI-DLS Version 2.x is backwards compatible to `17.x` and supports **`18.x`** releases in combination > FastAPI-DLS Version 2.x is backwards compatible to `17.x` and supports **`18.x`** releases in combination
> with [gridd-unlock-patcher](https://git.collinwebdesigns.de/oscar.krause/gridd-unlock-patcher). > with [gridd-unlock-patcher](https://git.collinwebdesigns.de/oscar.krause/gridd-unlock-patcher).
@@ -334,12 +334,13 @@ Successful tested with (**LTS Version**):
- *Ubuntu 23.04 (Lunar Lobster)* (EOL: January 2024) - *Ubuntu 23.04 (Lunar Lobster)* (EOL: January 2024)
- *Ubuntu 23.10 (Mantic Minotaur)* (EOL: July 2024) - *Ubuntu 23.10 (Mantic Minotaur)* (EOL: July 2024)
- **Ubuntu 24.04 (Noble Numbat)** (EOL: Apr 2029) - **Ubuntu 24.04 (Noble Numbat)** (EOL: Apr 2029)
- *Ubuntu 24.10 (Oracular Oriole)* (EOL: Jul 2025)
Not working with: Not working with:
- Debian 11 (Bullseye) and lower (missing `python-jose` dependency) - Debian 11 (Bullseye) and lower (missing `python-jose` dependency)
- Debian 13 (Trixie) (missing `python-jose` dependency)
- Ubuntu 22.04 (Jammy Jellyfish) (not supported as for 15.01.2023 due to [fastapi - uvicorn version missmatch](https://bugs.launchpad.net/ubuntu/+source/fastapi/+bug/1970557)) - Ubuntu 22.04 (Jammy Jellyfish) (not supported as for 15.01.2023 due to [fastapi - uvicorn version missmatch](https://bugs.launchpad.net/ubuntu/+source/fastapi/+bug/1970557))
- Ubuntu 24.10 (Oracular Oriole) (missing `python-jose` dependency)
**Run this on your server instance** **Run this on your server instance**
@@ -417,19 +418,20 @@ After first success you have to replace `--issue` with `--renew`.
# Configuration # Configuration
| Variable | Default | Usage | | Variable | Default | Usage |
|--------------------------|----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| |------------------------|----------------------------------------|------------------------------------------------------------------------------------------------------|
| `DEBUG` | `false` | Toggles `fastapi` debug mode | | `DEBUG` | `false` | Toggles `fastapi` debug mode |
| `DLS_URL` | `localhost` | Used in client-token to tell guest driver where dls instance is reachable | | `DLS_URL` | `localhost` | Used in client-token to tell guest driver where dls instance is reachable |
| `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable | | `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable |
| `TOKEN_EXPIRE_DAYS` | `1` | Client auth-token validity (used for authenticate client against api, **not `.tok` file!**) | | `CERT_PATH` | `None` | Path to a Directory where generated Certificates are stored. Defaults to `/<app-dir>/cert`. |
| `LEASE_EXPIRE_DAYS` | `90` | Lease time in days | | `TOKEN_EXPIRE_DAYS` | `1` | Client auth-token validity (used for authenticate client against api, **not `.tok` file!**) |
| `LEASE_RENEWAL_PERIOD` | `0.15` | The percentage of the lease period that must elapse before a licensed client can renew a license \*1 | | `LEASE_EXPIRE_DAYS` | `90` | Lease time in days |
| `DATABASE` | `sqlite:///db.sqlite` | See [official SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html) | | `LEASE_RENEWAL_PERIOD` | `0.15` | The percentage of the lease period that must elapse before a licensed client can renew a license \*1 |
| `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) \*2 | | `DATABASE` | `sqlite:///db.sqlite` | See [official SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html) |
| `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid | | `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) \*2 |
| `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid | | `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid |
| `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid | | | `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid |
| `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid |
\*1 For example, if the lease period is one day and the renewal period is 20%, the client attempts to renew its license \*1 For example, if the lease period is one day and the renewal period is 20%, the client attempts to renew its license
every 4.8 hours. If network connectivity is lost, the loss of connectivity is detected during license renewal and the every 4.8 hours. If network connectivity is lost, the loss of connectivity is detected during license renewal and the
@@ -535,9 +537,9 @@ Status endpoint, used for *healthcheck*.
Shows current runtime environment variables and their values. Shows current runtime environment variables and their values.
**`GET /-/config/root-ca`** **`GET /-/config/root-certificate`**
Returns the Root-CA Certificate which is used. This is required for patching `nvidia-gridd` on 18.x releases. Returns the Root-Certificate Certificate which is used. This is required for patching `nvidia-gridd` on 18.x releases.
**`GET /-/readme`** **`GET /-/readme`**
@@ -754,14 +756,16 @@ Successfully tested with this package versions.
| FastAPI-DLS Version | vGPU Suftware | Driver Branch | Linux vGPU Manager | Linux Driver | Windows Driver | Release Date | EOL Date | | FastAPI-DLS Version | vGPU Suftware | Driver Branch | Linux vGPU Manager | Linux Driver | Windows Driver | Release Date | EOL Date |
|---------------------|:-------------:|:-------------:|--------------------|--------------|----------------|--------------:|--------------:| |---------------------|:-------------:|:-------------:|--------------------|--------------|----------------|--------------:|--------------:|
| `2.x` | `18.0` | **R570** | `570.124.03` | `570.124.06` | `572.60` | March 2025 | March 2026 | | `2.x` | `18.1` | **R570** | `570.133.08` | `570.133.07` | `572.83` | April 2025 | March 2026 |
| `1.x` & `2.x` | `17.5` | | `550.144.02` | `550.144.03` | `553.62` | January 2025 | June 2025 | | | `18.0` | **R570** | `570.124.03` | `570.124.06` | `572.60` | March 2025 | March 2026 |
| `1.x` & `2.x` | `17.6` | **R550** | `550.163.02` | `550.63.01` | `553.74` | April 2025 | June 2025 |
| | `17.5` | | `550.144.02` | `550.144.03` | `553.62` | January 2025 | |
| | `17.4` | | `550.127.06` | `550.127.05` | `553.24` | October 2024 | | | | `17.4` | | `550.127.06` | `550.127.05` | `553.24` | October 2024 | |
| | `17.3` | | `550.90.05` | `550.90.07` | `552.74` | July 2024 | | | | `17.3` | | `550.90.05` | `550.90.07` | `552.74` | July 2024 | |
| | `17.2` | | `550.90.05` | `550.90.07` | `552.55` | June 2024 | | | | `17.2` | | `550.90.05` | `550.90.07` | `552.55` | June 2024 | |
| | `17.1` | | `550.54.16` | `550.54.15` | `551.78` | March 2024 | | | | `17.1` | | `550.54.16` | `550.54.15` | `551.78` | March 2024 | |
| | `17.0` | **R550** | `550.54.10` | `550.54.14` | `551.61` | February 2024 | | | | `17.0` | **R550** | `550.54.10` | `550.54.14` | `551.61` | February 2024 | |
| `1.x` | `16.9` | **R535** | `535.230.02` | `535.216.01` | `539.19` | October 2024 | July 2026 | | `1.x` | `16.10` | **R535** | `535.247.02` | `535.247.01` | `539.28` | April 2025 | July 2026 |
| `1.x` | `15.4` | **R525** | `525.147.01` | `525.147.05` | `529.19` | June 2023 | December 2023 | | `1.x` | `15.4` | **R525** | `525.147.01` | `525.147.05` | `529.19` | June 2023 | December 2023 |
| `1.x` | `14.4` | **R510** | `510.108.03` | `510.108.03` | `514.08` | December 2022 | February 2023 | | `1.x` | `14.4` | **R510** | `510.108.03` | `510.108.03` | `514.08` | December 2022 | February 2023 |

View File

@@ -7,6 +7,7 @@ from hashlib import sha256
from json import loads as json_loads, dumps as json_dumps from json import loads as json_loads, dumps as json_dumps
from os import getenv as env from os import getenv as env
from os.path import join, dirname from os.path import join, dirname
from textwrap import wrap
from uuid import uuid4 from uuid import uuid4
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@@ -39,6 +40,7 @@ db_init(db), migrate(db)
# Load DLS variables (all prefixed with "INSTANCE_*" is used as "SERVICE_INSTANCE_*" or "SI_*" in official dls service) # Load DLS variables (all prefixed with "INSTANCE_*" is used as "SERVICE_INSTANCE_*" or "SI_*" in official dls service)
DLS_URL = str(env('DLS_URL', 'localhost')) DLS_URL = str(env('DLS_URL', 'localhost'))
DLS_PORT = int(env('DLS_PORT', '443')) DLS_PORT = int(env('DLS_PORT', '443'))
CERT_PATH = str(env('CERT_PATH', None))
SITE_KEY_XID = str(env('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000')) SITE_KEY_XID = str(env('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000'))
INSTANCE_REF = str(env('INSTANCE_REF', '10000000-0000-0000-0000-000000000001')) INSTANCE_REF = str(env('INSTANCE_REF', '10000000-0000-0000-0000-000000000001'))
ALLOTMENT_REF = str(env('ALLOTMENT_REF', '20000000-0000-0000-0000-000000000001')) ALLOTMENT_REF = str(env('ALLOTMENT_REF', '20000000-0000-0000-0000-000000000001'))
@@ -52,7 +54,9 @@ DT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
PRODUCT_MAPPING = ProductMapping(filename=join(dirname(__file__), 'static/product_mapping.json')) PRODUCT_MAPPING = ProductMapping(filename=join(dirname(__file__), 'static/product_mapping.json'))
# Create certificate chain and signing keys # Create certificate chain and signing keys
ca_setup = CASetup(service_instance_ref=INSTANCE_REF) ca_setup = CASetup(service_instance_ref=INSTANCE_REF, cert_path=CERT_PATH)
my_root_private_key = PrivateKey.from_file(ca_setup.root_private_key_filename)
my_root_public_key = my_root_private_key.public_key()
my_root_certificate = Cert.from_file(ca_setup.root_certificate_filename) my_root_certificate = Cert.from_file(ca_setup.root_certificate_filename)
my_ca_certificate = Cert.from_file(ca_setup.ca_certificate_filename) my_ca_certificate = Cert.from_file(ca_setup.ca_certificate_filename)
my_si_certificate = Cert.from_file(ca_setup.si_certificate_filename) my_si_certificate = Cert.from_file(ca_setup.si_certificate_filename)
@@ -151,10 +155,9 @@ async def _config():
return Response(content=json_dumps(response), media_type='application/json', status_code=200) return Response(content=json_dumps(response), media_type='application/json', status_code=200)
@app.get('/-/config/root-certificate', summary='* Root Certificate', description='returns Root--Certificate needed for patching nvidia-gridd')
@app.get('/-/config/root-ca', summary='* Root CA', description='returns Root-CA needed for patching nvidia-gridd')
async def _config(): async def _config():
return Response(content=my_root_certificate.pem().decode('utf-8'), media_type='text/plain') return Response(content=my_root_certificate.pem().decode('utf-8').strip(), media_type='text/plain')
@app.get('/-/readme', summary='* Readme') @app.get('/-/readme', summary='* Readme')
@@ -287,7 +290,7 @@ async def _client_token():
"mod": my_si_public_key.mod(), "mod": my_si_public_key.mod(),
"exp": my_si_public_key.exp(), "exp": my_si_public_key.exp(),
}, },
"service_instance_public_key_pem": my_si_private_key.public_key().pem().decode('utf-8'), "service_instance_public_key_pem": my_si_public_key.pem().decode('utf-8').strip(),
"key_retention_mode": "LATEST_ONLY" "key_retention_mode": "LATEST_ONLY"
}, },
} }
@@ -462,8 +465,7 @@ async def leasing_v1_config_token(request: Request):
"mod": my_si_public_key.mod(), "mod": my_si_public_key.mod(),
"exp": my_si_public_key.exp(), "exp": my_si_public_key.exp(),
}, },
# 64 chars per line (pem default) "service_instance_public_key_pem": my_si_public_key.pem().decode('utf-8').strip(),
"service_instance_public_key_pem": my_si_private_key.public_key().pem().decode('utf-8').strip(),
"key_retention_mode": "LATEST_ONLY" "key_retention_mode": "LATEST_ONLY"
}, },
} }
@@ -471,18 +473,37 @@ async def leasing_v1_config_token(request: Request):
my_jwt_encode_key = jwk.construct(my_si_private_key.pem().decode('utf-8'), algorithm=ALGORITHMS.RS256) my_jwt_encode_key = jwk.construct(my_si_private_key.pem().decode('utf-8'), algorithm=ALGORITHMS.RS256)
config_token = jws.sign(payload, key=my_jwt_encode_key, headers=None, algorithm=ALGORITHMS.RS256) config_token = jws.sign(payload, key=my_jwt_encode_key, headers=None, algorithm=ALGORITHMS.RS256)
response_ca_chain = my_ca_certificate.pem().decode('utf-8') response_ca_chain = my_ca_certificate.pem().decode('utf-8').strip()
response_si_certificate = my_si_certificate.pem().decode('utf-8')
# 76 chars per line on original response with "\r\n"
"""
response_ca_chain = my_ca_certificate.pem().decode('utf-8').strip()
response_ca_chain = response_ca_chain.replace('-----BEGIN CERTIFICATE-----', '')
response_ca_chain = response_ca_chain.replace('-----END CERTIFICATE-----', '')
response_ca_chain = response_ca_chain.replace('\n', '')
response_ca_chain = wrap(response_ca_chain, 76)
response_ca_chain = '\r\n'.join(response_ca_chain)
response_ca_chain = f'-----BEGIN CERTIFICATE-----\r\n{response_ca_chain}\r\n-----END CERTIFICATE-----'
"""
response_si_certificate = my_si_certificate.pem().decode('utf-8').strip()
# 76 chars per line on original response with "\r\n"
"""
response_si_certificate = my_si_certificate.pem().decode('utf-8').strip()
response_si_certificate = response_si_certificate.replace('-----BEGIN CERTIFICATE-----', '')
response_si_certificate = response_si_certificate.replace('-----END CERTIFICATE-----', '')
response_si_certificate = response_si_certificate.replace('\n', '')
response_si_certificate = wrap(response_si_certificate, 76)
response_si_certificate = '\r\n'.join(response_si_certificate)
"""
response = { response = {
"certificateConfiguration": { "certificateConfiguration": {
# 76 chars per line
"caChain": [response_ca_chain], "caChain": [response_ca_chain],
# 76 chars per line
"publicCert": response_si_certificate, "publicCert": response_si_certificate,
"publicKey": { "publicKey": {
"exp": int(my_si_certificate.raw().public_key().public_numbers().e), "exp": my_si_certificate.public_key().exp(),
"mod": [hex(my_si_certificate.raw().public_key().public_numbers().n)[2:]], "mod": [my_si_certificate.public_key().mod()],
}, },
}, },
"configToken": config_token, "configToken": config_token,

View File

@@ -1,7 +1,7 @@
import logging import logging
from datetime import datetime, UTC, timedelta from datetime import datetime, UTC, timedelta
from json import loads as json_loads from json import loads as json_loads
from os.path import join, dirname, isfile from os.path import join, dirname, isfile, isdir
from cryptography import x509 from cryptography import x509
from cryptography.hazmat._oid import NameOID from cryptography.hazmat._oid import NameOID
@@ -38,9 +38,13 @@ class CASetup:
SI_PRIVATE_KEY_FILENAME = 'si_private_key.pem' SI_PRIVATE_KEY_FILENAME = 'si_private_key.pem'
SI_CERTIFICATE_FILENAME = 'si_certificate.pem' SI_CERTIFICATE_FILENAME = 'si_certificate.pem'
def __init__(self, service_instance_ref: str): def __init__(self, service_instance_ref: str, cert_path: str = None):
cert_path_prefix = join(dirname(__file__), 'cert')
if cert_path is not None and len(cert_path) > 0 and isdir(cert_path):
cert_path_prefix = cert_path
self.service_instance_ref = service_instance_ref self.service_instance_ref = service_instance_ref
self.root_private_key_filename = join(dirname(__file__), 'cert', CASetup.ROOT_PRIVATE_KEY_FILENAME) self.root_private_key_filename = join(cert_path_prefix, CASetup.ROOT_PRIVATE_KEY_FILENAME)
self.root_certificate_filename = join(dirname(__file__), 'cert', CASetup.ROOT_CERTIFICATE_FILENAME) self.root_certificate_filename = join(dirname(__file__), 'cert', CASetup.ROOT_CERTIFICATE_FILENAME)
self.ca_private_key_filename = join(dirname(__file__), 'cert', CASetup.CA_PRIVATE_KEY_FILENAME) self.ca_private_key_filename = join(dirname(__file__), 'cert', CASetup.CA_PRIVATE_KEY_FILENAME)
self.ca_certificate_filename = join(dirname(__file__), 'cert', CASetup.CA_CERTIFICATE_FILENAME) self.ca_certificate_filename = join(dirname(__file__), 'cert', CASetup.CA_CERTIFICATE_FILENAME)
@@ -81,7 +85,20 @@ class CASetup:
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1)) .not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.not_valid_after(datetime.now(tz=UTC) + timedelta(days=365 * 10)) .not_valid_after(datetime.now(tz=UTC) + timedelta(days=365 * 10))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.add_extension(x509.KeyUsage(
digital_signature=False,
key_encipherment=False,
key_cert_sign=True,
key_agreement=False,
content_commitment=False,
data_encipherment=False,
crl_sign=True,
encipher_only=False,
decipher_only=False),
critical=True
)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(my_root_public_key), critical=False) .add_extension(x509.SubjectKeyIdentifier.from_public_key(my_root_public_key), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(my_root_public_key), critical=False)
.sign(my_root_private_key, hashes.SHA256())) .sign(my_root_private_key, hashes.SHA256()))
my_root_private_key_as_pem = my_root_private_key.private_bytes( my_root_private_key_as_pem = my_root_private_key.private_bytes(
@@ -134,7 +151,6 @@ class CASetup:
critical=True critical=True
) )
.add_extension(x509.SubjectKeyIdentifier.from_public_key(my_ca_public_key), critical=False) .add_extension(x509.SubjectKeyIdentifier.from_public_key(my_ca_public_key), critical=False)
# .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(my_root_public_key), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( .add_extension(x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
my_root_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value my_root_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
), critical=False) ), critical=False)
@@ -314,16 +330,22 @@ class Cert:
def pem(self) -> bytes: def pem(self) -> bytes:
return self.__cert.public_bytes(encoding=serialization.Encoding.PEM) return self.__cert.public_bytes(encoding=serialization.Encoding.PEM)
def public_key(self) -> "PublicKey":
data = self.__cert.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return PublicKey(data=data)
def signature(self) -> bytes: def signature(self) -> bytes:
return self.__cert.signature return self.__cert.signature
def subject_key_identifier(self):
return self.__cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value.key_identifier
def authority_key_identifier(self):
return self.__cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value.key_identifier
def load_file(filename: str) -> bytes:
log = logging.getLogger(f'{__name__}')
log.debug(f'Loading contents of file "{filename}')
with open(filename, 'rb') as file:
content = file.read()
return content
class DriverMatrix: class DriverMatrix:
__DRIVER_MATRIX_FILENAME = 'static/driver_matrix.json' __DRIVER_MATRIX_FILENAME = 'static/driver_matrix.json'

View File

@@ -1,7 +1,7 @@
fastapi==0.115.12 fastapi==0.115.12
uvicorn[standard]==0.34.1 uvicorn[standard]==0.34.2
python-jose[cryptography]==3.4.0 python-jose[cryptography]==3.4.0
cryptography==44.0.2 cryptography==44.0.3
python-dateutil==2.9.0 python-dateutil==2.9.0
sqlalchemy==2.0.40 sqlalchemy==2.0.40
markdown==3.8 markdown==3.8

View File

@@ -6,6 +6,8 @@ from datetime import datetime, UTC
from hashlib import sha256 from hashlib import sha256
from uuid import uuid4, UUID from uuid import uuid4, UUID
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.hashes import SHA256
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from jose import jwt, jwk, jws from jose import jwt, jwk, jws
from jose.constants import ALGORITHMS from jose.constants import ALGORITHMS
@@ -26,11 +28,15 @@ ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-00000
# CA & Signing # CA & Signing
ca_setup = CASetup(service_instance_ref=INSTANCE_REF) ca_setup = CASetup(service_instance_ref=INSTANCE_REF)
my_root_private_key = PrivateKey.from_file(ca_setup.root_private_key_filename)
my_root_certificate = Cert.from_file(ca_setup.root_certificate_filename) my_root_certificate = Cert.from_file(ca_setup.root_certificate_filename)
my_ca_certificate = Cert.from_file(ca_setup.ca_certificate_filename)
my_ca_private_key = PrivateKey.from_file(ca_setup.ca_private_key_filename)
my_si_private_key = PrivateKey.from_file(ca_setup.si_private_key_filename) my_si_private_key = PrivateKey.from_file(ca_setup.si_private_key_filename)
my_si_private_key_as_pem = my_si_private_key.pem() my_si_private_key_as_pem = my_si_private_key.pem()
my_si_public_key = my_si_private_key.public_key() my_si_public_key = my_si_private_key.public_key()
my_si_public_key_as_pem = my_si_private_key.public_key().pem() my_si_public_key_as_pem = my_si_private_key.public_key().pem()
my_si_certificate = Cert.from_file(ca_setup.si_certificate_filename)
jwt_encode_key = jwk.construct(my_si_private_key_as_pem, algorithm=ALGORITHMS.RS256) jwt_encode_key = jwk.construct(my_si_private_key_as_pem, algorithm=ALGORITHMS.RS256)
jwt_decode_key = jwk.construct(my_si_public_key_as_pem, algorithm=ALGORITHMS.RS256) jwt_decode_key = jwk.construct(my_si_public_key_as_pem, algorithm=ALGORITHMS.RS256)
@@ -59,6 +65,31 @@ def test_signing():
my_si_public_key.verify_signature(signature_get_header, b'Hello') my_si_public_key.verify_signature(signature_get_header, b'Hello')
def test_keypair_and_certificates():
assert my_root_certificate.public_key().mod() == my_root_private_key.public_key().mod()
assert my_ca_certificate.public_key().mod() == my_ca_private_key.public_key().mod()
assert my_si_certificate.public_key().mod() == my_si_public_key.mod()
assert len(my_root_certificate.public_key().mod()) == 1024
assert len(my_ca_certificate.public_key().mod()) == 1024
assert len(my_si_certificate.public_key().mod()) == 512
#assert my_si_certificate.public_key().mod() != my_si_public_key.mod()
my_root_certificate.public_key().raw().verify(
my_ca_certificate.raw().signature,
my_ca_certificate.raw().tbs_certificate_bytes,
PKCS1v15(),
SHA256(),
)
my_ca_certificate.public_key().raw().verify(
my_si_certificate.raw().signature,
my_si_certificate.raw().tbs_certificate_bytes,
PKCS1v15(),
SHA256(),
)
def test_index(): def test_index():
response = client.get('/') response = client.get('/')
assert response.status_code == 200 assert response.status_code == 200
@@ -76,9 +107,9 @@ def test_config():
def test_config_root_ca(): def test_config_root_ca():
response = client.get('/-/config/root-ca') response = client.get('/-/config/root-certificate')
assert response.status_code == 200 assert response.status_code == 200
assert response.content.decode('utf-8') == my_root_certificate.pem().decode('utf-8') assert response.content.decode('utf-8').strip() == my_root_certificate.pem().decode('utf-8').strip()
def test_readme(): def test_readme():
@@ -103,7 +134,17 @@ def test_config_token():
assert response.status_code == 200 assert response.status_code == 200
nv_response_certificate_configuration = response.json().get('certificateConfiguration') nv_response_certificate_configuration = response.json().get('certificateConfiguration')
nv_ca_chain = nv_response_certificate_configuration.get('caChain')[0].encode('utf-8')
nv_ca_chain = Cert(nv_ca_chain)
nv_response_public_cert = nv_response_certificate_configuration.get('publicCert').encode('utf-8') nv_response_public_cert = nv_response_certificate_configuration.get('publicCert').encode('utf-8')
nv_response_public_key = nv_response_certificate_configuration.get('publicKey')
nv_si_certificate = Cert(nv_response_public_cert)
assert nv_si_certificate.public_key().mod() == nv_response_public_key.get('mod')[0]
assert nv_si_certificate.authority_key_identifier() == nv_ca_chain.subject_key_identifier()
nv_jwt_decode_key = jwk.construct(nv_response_public_cert, algorithm=ALGORITHMS.RS256) nv_jwt_decode_key = jwk.construct(nv_response_public_cert, algorithm=ALGORITHMS.RS256)
nv_response_config_token = response.json().get('configToken') nv_response_config_token = response.json().get('configToken')
@@ -116,8 +157,8 @@ def test_config_token():
nv_si_public_key_configuration = payload.get('service_instance_public_key_configuration') nv_si_public_key_configuration = payload.get('service_instance_public_key_configuration')
nv_si_public_key_me = nv_si_public_key_configuration.get('service_instance_public_key_me') nv_si_public_key_me = nv_si_public_key_configuration.get('service_instance_public_key_me')
# assert nv_si_public_key_me.get('mod') == 1 #nv_si_public_key_mod
assert len(nv_si_public_key_me.get('mod')) == 512 assert len(nv_si_public_key_me.get('mod')) == 512 # nv_si_public_key_mod
assert nv_si_public_key_me.get('exp') == 65537 # nv_si_public_key_exp assert nv_si_public_key_me.get('exp') == 65537 # nv_si_public_key_exp