Compare commits

..

10 Commits

Author SHA1 Message Date
Oscar Krause
5b150a7530 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-16 10:50:13 +02:00
Oscar Krause
9712a84a00 code styling 2025-04-16 09:30:04 +02:00
Oscar Krause
133b1e24e2 code styling 2025-04-16 09:29:10 +02:00
Oscar Krause
8f4d2e6086 updated versions matrix and infos 2025-04-16 09:26:52 +02:00
Oscar Krause
389b36fcb8 removed any 'instance.*.pem' reference 2025-04-16 09:19:17 +02:00
Oscar Krause
a767e73ca6 code styling 2025-04-16 09:03:27 +02:00
Oscar Krause
31957ec6d7 added '/-/config/root-ca' endpoint 2025-04-16 08:37:28 +02:00
Oscar Krause
da31c5f0a7 fixes 2025-04-16 08:10:49 +02:00
Oscar Krause
1265be5fbe fixes 2025-04-16 08:05:58 +02:00
Oscar Krause
e23912c49a code refactorings - removed INSTANCE_KEY_RSA and INSTANCE_KEY_PUB from configuration and implemented CASetup instead 2025-04-16 07:54:23 +02:00
12 changed files with 318 additions and 327 deletions

View File

@ -21,7 +21,3 @@ DATABASE=sqlite:////etc/fastapi-dls/db.sqlite
#SITE_KEY_XID="00000000-0000-0000-0000-000000000000" #SITE_KEY_XID="00000000-0000-0000-0000-000000000000"
#INSTANCE_REF="10000000-0000-0000-0000-000000000001" #INSTANCE_REF="10000000-0000-0000-0000-000000000001"
#ALLOTMENT_REF="20000000-0000-0000-0000-000000000001" #ALLOTMENT_REF="20000000-0000-0000-0000-000000000001"
# Site-wide signing keys
INSTANCE_KEY_RSA=/etc/fastapi-dls/instance.private.pem
INSTANCE_KEY_PUB=/etc/fastapi-dls/instance.public.pem

View File

@ -3,14 +3,6 @@
WORKING_DIR=/usr/share/fastapi-dls WORKING_DIR=/usr/share/fastapi-dls
CONFIG_DIR=/etc/fastapi-dls CONFIG_DIR=/etc/fastapi-dls
if [ ! -f $CONFIG_DIR/instance.private.pem ]; then
echo "> Create dls-instance keypair ..."
openssl genrsa -out $CONFIG_DIR/instance.private.pem 2048
openssl rsa -in $CONFIG_DIR/instance.private.pem -outform PEM -pubout -out $CONFIG_DIR/instance.public.pem
else
echo "> Create dls-instance keypair skipped! (exists)"
fi
while true; do while true; do
[ -f $CONFIG_DIR/webserver.key ] && default_answer="N" || default_answer="Y" [ -f $CONFIG_DIR/webserver.key ] && default_answer="N" || default_answer="Y"
[ $default_answer == "Y" ] && V="Y/n" || V="y/N" [ $default_answer == "Y" ] && V="Y/n" || V="y/N"

View File

@ -17,7 +17,7 @@ source=("git+file://${CI_PROJECT_DIR}"
"$pkgname.service" "$pkgname.service"
"$pkgname.tmpfiles") "$pkgname.tmpfiles")
sha256sums=('SKIP' sha256sums=('SKIP'
'fbd015449a30c0ae82733289a56eb98151dcfab66c91b37fe8e202e39f7a5edb' 'a4776a0ae4671751065bf3e98aa707030b8b5ffe42dde942c51050dab5028c54'
'2719338541104c537453a65261c012dda58e1dbee99154cf4f33b526ee6ca22e' '2719338541104c537453a65261c012dda58e1dbee99154cf4f33b526ee6ca22e'
'3dc60140c08122a8ec0e7fa7f0937eb8c1288058890ba09478420fc30ce9e30c') '3dc60140c08122a8ec0e7fa7f0937eb8c1288058890ba09478420fc30ce9e30c')
@ -30,8 +30,6 @@ pkgver() {
check() { check() {
cd "$srcdir/$pkgname/test" cd "$srcdir/$pkgname/test"
mkdir "$srcdir/$pkgname/app/cert" mkdir "$srcdir/$pkgname/app/cert"
openssl genrsa -out "$srcdir/$pkgname/app/cert/instance.private.pem" 2048
openssl rsa -in "$srcdir/$pkgname/app/cert/instance.private.pem" -outform PEM -pubout -out "$srcdir/$pkgname/app/cert/instance.public.pem"
python "$srcdir/$pkgname/test/main.py" python "$srcdir/$pkgname/test/main.py"
rm -rf "$srcdir/$pkgname/app/cert" rm -rf "$srcdir/$pkgname/app/cert"
} }

View File

@ -19,10 +19,6 @@ DATABASE="sqlite:////var/lib/fastapi-dls/db.sqlite"
SITE_KEY_XID="<<sitekey>>" SITE_KEY_XID="<<sitekey>>"
INSTANCE_REF="<<instanceref>>" INSTANCE_REF="<<instanceref>>"
# Site-wide signing keys
INSTANCE_KEY_RSA="/var/lib/fastapi-dls/instance.private.pem"
INSTANCE_KEY_PUB="/var/lib/fastapi-dls/instance.public.pem"
# TLS certificate # TLS certificate
INSTANCE_SSL_CERT="/var/lib/fastapi-dls/cert/webserver.crt" INSTANCE_SSL_CERT="/var/lib/fastapi-dls/cert/webserver.crt"
INSTANCE_SSL_KEY="/var/lib/fastapi-dls/cert/webserver.key" INSTANCE_SSL_KEY="/var/lib/fastapi-dls/cert/webserver.key"

View File

@ -7,8 +7,4 @@ post_install() {
echo echo
echo 'A valid HTTPS certificate needs to be installed to /var/lib/fastapi-dls/cert/webserver.{crt,key}' echo 'A valid HTTPS certificate needs to be installed to /var/lib/fastapi-dls/cert/webserver.{crt,key}'
echo 'A self-signed certificate can be generated with: openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /var/lib/fastapi-dls/cert/webserver.key -out /var/lib/fastapi-dls/cert/webserver.crt' echo 'A self-signed certificate can be generated with: openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /var/lib/fastapi-dls/cert/webserver.key -out /var/lib/fastapi-dls/cert/webserver.crt'
echo
echo 'The signing keys for your instance need to be generated as well. Generate them with these commands:'
echo 'openssl genrsa -out /var/lib/fastapi-dls/instance.private.pem 2048'
echo 'openssl rsa -in /var/lib/fastapi-dls/instance.private.pem -outform PEM -pubout -out /var/lib/fastapi-dls/instance.public.pem'
} }

View File

@ -18,9 +18,6 @@ Make sure you create these certificates before starting the container for the fi
WORKING_DIR=/mnt/user/appdata/fastapi-dls/cert&#xD; WORKING_DIR=/mnt/user/appdata/fastapi-dls/cert&#xD;
mkdir -p $WORKING_DIR&#xD; mkdir -p $WORKING_DIR&#xD;
cd $WORKING_DIR&#xD; cd $WORKING_DIR&#xD;
# create instance private and public key for singing JWT's&#xD;
openssl genrsa -out $WORKING_DIR/instance.private.pem 2048 &#xD;
openssl rsa -in $WORKING_DIR/instance.private.pem -outform PEM -pubout -out $WORKING_DIR/instance.public.pem&#xD;
# create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl&#xD; # create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl&#xD;
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt&#xD; openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt&#xD;
```&#xD; ```&#xD;

View File

@ -149,8 +149,6 @@ test:
- pip install -r $REQUIREMENTS - pip install -r $REQUIREMENTS
- pip install pytest pytest-cov pytest-custom_exit_code httpx - pip install pytest pytest-cov pytest-custom_exit_code httpx
- mkdir -p app/cert - mkdir -p app/cert
- openssl genrsa -out app/cert/instance.private.pem 2048
- openssl rsa -in app/cert/instance.private.pem -outform PEM -pubout -out app/cert/instance.public.pem
- cd test - cd test
script: script:
- python -m pytest main.py --junitxml=report.xml - python -m pytest main.py --junitxml=report.xml
@ -263,8 +261,6 @@ test_coverage:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install pytest pytest-cov pytest-custom_exit_code httpx - pip install pytest pytest-cov pytest-custom_exit_code httpx
- mkdir -p app/cert - mkdir -p app/cert
- openssl genrsa -out app/cert/instance.private.pem 2048
- openssl rsa -in app/cert/instance.private.pem -outform PEM -pubout -out app/cert/instance.public.pem
- cd test - cd test
script: script:
- coverage run -m pytest main.py --junitxml=report.xml --suppress-no-test-exit-code - coverage run -m pytest main.py --junitxml=report.xml --suppress-no-test-exit-code

View File

@ -2,13 +2,15 @@
Minimal Delegated License Service (DLS). Minimal Delegated License Service (DLS).
> [!warning] Branch support \
> Use FastAPI-DLS == 1.x until **17.x** releases. \
> Use FastAPI-DLS == 2.x since **18.x** releases in combination
> with [gridd-unlock-patcher](https://git.collinwebdesigns.de/oscar.krause/gridd-unlock-patcher).
> [!note] Compatibility > [!note] Compatibility
> Compatibility tested with official NLS 2.0.1, 2.1.0, 3.1.0, 3.3.1, 3.4.0. For Driver compatibility > Compatibility tested with official NLS 2.0.1, 2.1.0, 3.1.0, 3.3.1, 3.4.0. For Driver compatibility
> see [compatibility matrix](#vgpu-software-compatibility-matrix). > see [compatibility matrix](#vgpu-software-compatibility-matrix).
> [!warning] 18.x Drivers are not yet supported!
> Drivers are only supported until **17.x releases**.
This service can be used without internet connection. This service can be used without internet connection.
Only the clients need a connection to this service on configured port. Only the clients need a connection to this service on configured port.
@ -66,9 +68,6 @@ The images include database drivers for `postgres`, `mariadb` and `sqlite`.
WORKING_DIR=/opt/docker/fastapi-dls/cert WORKING_DIR=/opt/docker/fastapi-dls/cert
mkdir -p $WORKING_DIR mkdir -p $WORKING_DIR
cd $WORKING_DIR cd $WORKING_DIR
# create instance private and public key for singing JWT's
openssl genrsa -out $WORKING_DIR/instance.private.pem 2048
openssl rsa -in $WORKING_DIR/instance.private.pem -outform PEM -pubout -out $WORKING_DIR/instance.public.pem
# create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl # create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt
``` ```
@ -153,9 +152,6 @@ chown -R www-data:www-data $WORKING_DIR
WORKING_DIR=/opt/fastapi-dls/app/cert WORKING_DIR=/opt/fastapi-dls/app/cert
mkdir -p $WORKING_DIR mkdir -p $WORKING_DIR
cd $WORKING_DIR cd $WORKING_DIR
# create instance private and public key for singing JWT's
openssl genrsa -out $WORKING_DIR/instance.private.pem 2048
openssl rsa -in $WORKING_DIR/instance.private.pem -outform PEM -pubout -out $WORKING_DIR/instance.public.pem
# create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl # create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $WORKING_DIR/webserver.key -out $WORKING_DIR/webserver.crt
chown -R www-data:www-data $WORKING_DIR chown -R www-data:www-data $WORKING_DIR
@ -255,9 +251,6 @@ CERT_DIR=${BASE_DIR}/app/cert
SERVICE_USER=dls SERVICE_USER=dls
mkdir ${CERT_DIR} mkdir ${CERT_DIR}
cd ${CERT_DIR} cd ${CERT_DIR}
# create instance private and public key for singing JWT's
openssl genrsa -out ${CERT_DIR}/instance.private.pem 2048
openssl rsa -in ${CERT_DIR}/instance.private.pem -outform PEM -pubout -out ${CERT_DIR}/instance.public.pem
# create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl # create ssl certificate for integrated webserver (uvicorn) - because clients rely on ssl
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout ${CERT_DIR}/webserver.key -out ${CERT_DIR}/webserver.crt openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout ${CERT_DIR}/webserver.key -out ${CERT_DIR}/webserver.crt
chown -R ${SERVICE_USER} ${CERT_DIR} chown -R ${SERVICE_USER} ${CERT_DIR}
@ -435,9 +428,7 @@ After first success you have to replace `--issue` with `--renew`.
| `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) \*2 | | `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) \*2 |
| `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid | | `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid |
| `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid | | `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid |
| `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid | | `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid | |
| `INSTANCE_KEY_RSA` | `<app-dir>/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs \*3 |
| `INSTANCE_KEY_PUB` | `<app-dir>/cert/instance.public.pem` | Site-wide public key \*3 |
\*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
@ -445,8 +436,6 @@ client has 19.2 hours in which to re-establish connectivity before its license e
\*2 Always use `https`, since guest-drivers only support secure connections! \*2 Always use `https`, since guest-drivers only support secure connections!
\*3 If you recreate your instance keys you need to **recreate client-token for each guest**!
# Setup (Client) # Setup (Client)
**The token file has to be copied! It's not enough to C&P file contents, because there can be special characters.** **The token file has to be copied! It's not enough to C&P file contents, because there can be special characters.**
@ -545,6 +534,10 @@ 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`**
Returns the Root-CA Certificate which is used. This is required for patching `nvidia-gridd` on 18.x releases.
**`GET /-/readme`** **`GET /-/readme`**
HTML rendered README.md. HTML rendered README.md.
@ -617,7 +610,7 @@ Please download a new client-token. The guest have to register within an hour af
### `jose.exceptions.JWTError: Signature verification failed.` ### `jose.exceptions.JWTError: Signature verification failed.`
- Did you recreate `instance.public.pem` / `instance.private.pem`? - Did you recreate any certificate or keypair?
Then you have to download a **new** client-token on each of your guests. Then you have to download a **new** client-token on each of your guests.
@ -753,33 +746,23 @@ The error message can safely be ignored (since we have no license limitation :P)
# vGPU Software Compatibility Matrix # vGPU Software Compatibility Matrix
**18.x Drivers are not supported on FastAPI-DLS Versions < 1.6.0**
<details> <details>
<summary>Show Table</summary> <summary>Show Table</summary>
Successfully tested with this package versions. Successfully tested with this package versions.
| 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 |
|:-------------:|:-------------:|--------------------|--------------|----------------|--------------:|--------------:| |---------------------|:-------------:|:-------------:|--------------------|--------------|----------------|--------------:|--------------:|
| `17.5` | R550 | `550.144.02` | `550.144.03` | `553.62` | January 2025 | June 2025 | | `2.x` | `18.0` | **R570** | `570.124.03` | `570.124.06` | `572.60` | March 2025 | March 2026 |
| `17.4` | R550 | `550.127.06` | `550.127.05` | `553.24` | October 2024 | | | `1.x` | `17.5` | | `550.144.02` | `550.144.03` | `553.62` | January 2025 | June 2025 |
| `17.3` | R550 | `550.90.05` | `550.90.07` | `552.74` | July 2024 | | | | `17.4` | | `550.127.06` | `550.127.05` | `553.24` | October 2024 | |
| `17.2` | R550 | `550.90.05` | `550.90.07` | `552.55` | June 2024 | | | | `17.3` | | `550.90.05` | `550.90.07` | `552.74` | July 2024 | |
| `17.1` | R550 | `550.54.16` | `550.54.15` | `551.78` | March 2024 | | | | `17.2` | | `550.90.05` | `550.90.07` | `552.55` | June 2024 | |
| `17.0` | R550 | `550.54.10` | `550.54.14` | `551.61` | February 2024 | | | | `17.1` | | `550.54.16` | `550.54.15` | `551.78` | March 2024 | |
| `16.9` | R535 | `535.230.02` | `535.216.01` | `539.19` | October 2024 | July 2026 | | | `17.0` | **R550** | `550.54.10` | `550.54.14` | `551.61` | February 2024 | |
| `16.8` | R535 | `535.216.01` | `535.216.01` | `538.95` | October 2024 | | | `1.x` | `16.9` | **R535** | `535.230.02` | `535.216.01` | `539.19` | October 2024 | July 2026 |
| `16.7` | R535 | `535.183.04` | `535.183.06` | `538.78` | July 2024 | | | `1.x` | `15.4` | **R525** | `525.147.01` | `525.147.05` | `529.19` | June 2023 | December 2023 |
| `16.6` | R535 | `535.183.04` | `535.183.01` | `538.67` | June 2024 | | | `1.x` | `14.4` | **R510** | `510.108.03` | `510.108.03` | `514.08` | December 2022 | February 2023 |
| `16.5` | R535 | `535.161.05` | `535.161.08` | `538.46` | February 2024 | |
| `16.4` | R535 | `535.161.05` | `535.161.07` | `538.33` | February 2024 | |
| `16.3` | R535 | `535.154.02` | `535.154.05` | `538.15` | January 2024 | |
| `16.2` | R535 | `535.129.03` | `535.129.03` | `537.70` | October 2023 | |
| `16.1` | R535 | `535.104.06` | `535.104.05` | `537.13` | August 2023 | |
| `16.0` | R535 | `535.54.06` | `535.54.03` | `536.22` | July 2023 | |
| `15.4` | R525 | `525.147.01` | `525.147.05` | `529.19` | June 2023 | December 2023 |
| `14.4` | R510 | `510.108.03` | `510.108.03` | `514.08` | December 2022 | February 2023 |
</details> </details>

View File

@ -6,14 +6,14 @@ from datetime import datetime, timedelta, UTC
from hashlib import sha256 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, isfile from os.path import join, dirname
from uuid import uuid4 from uuid import uuid4
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from dotenv import load_dotenv from dotenv import load_dotenv
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.requests import Request from fastapi.requests import Request
from fastapi.responses import StreamingResponse, JSONResponse as JSONr, HTMLResponse as HTMLr, Response, RedirectResponse from fastapi.responses import Response, RedirectResponse, StreamingResponse
from jose import jws, jwk, jwt, JWTError from jose import jws, jwk, jwt, JWTError
from jose.constants import ALGORITHMS from jose.constants import ALGORITHMS
from sqlalchemy import create_engine from sqlalchemy import create_engine
@ -21,7 +21,7 @@ from sqlalchemy.orm import sessionmaker
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from orm import Origin, Lease, init as db_init, migrate from orm import Origin, Lease, init as db_init, migrate
from util import PrivateKey, PublicKey, load_file, Cert, ProductMapping from util import CASetup, PrivateKey, Cert, ProductMapping, load_file
# Load variables # Load variables
load_dotenv('../version.env') load_dotenv('../version.env')
@ -42,8 +42,6 @@ DLS_PORT = int(env('DLS_PORT', '443'))
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'))
INSTANCE_KEY_RSA = PrivateKey.from_file(str(env('INSTANCE_KEY_RSA', join(dirname(__file__), 'cert/instance.private.pem'))))
INSTANCE_KEY_PUB = PublicKey.from_file(str(env('INSTANCE_KEY_PUB', join(dirname(__file__), 'cert/instance.public.pem'))))
TOKEN_EXPIRE_DELTA = relativedelta(days=int(env('TOKEN_EXPIRE_DAYS', 1)), hours=int(env('TOKEN_EXPIRE_HOURS', 0))) TOKEN_EXPIRE_DELTA = relativedelta(days=int(env('TOKEN_EXPIRE_DAYS', 1)), hours=int(env('TOKEN_EXPIRE_HOURS', 0)))
LEASE_EXPIRE_DELTA = relativedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)), hours=int(env('LEASE_EXPIRE_HOURS', 0))) LEASE_EXPIRE_DELTA = relativedelta(days=int(env('LEASE_EXPIRE_DAYS', 90)), hours=int(env('LEASE_EXPIRE_HOURS', 0)))
LEASE_RENEWAL_PERIOD = float(env('LEASE_RENEWAL_PERIOD', 0.15)) LEASE_RENEWAL_PERIOD = float(env('LEASE_RENEWAL_PERIOD', 0.15))
@ -53,9 +51,16 @@ CORS_ORIGINS = str(env('CORS_ORIGINS', '')).split(',') if (env('CORS_ORIGINS'))
DT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' 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
ca_setup = CASetup(service_instance_ref=INSTANCE_REF)
my_root_certificate = Cert.from_file(ca_setup.root_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_private_key = PrivateKey.from_file(ca_setup.si_private_key_filename)
my_si_public_key = my_si_private_key.public_key()
jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.pem(), algorithm=ALGORITHMS.RS256) jwt_encode_key = jwk.construct(my_si_private_key.pem(), algorithm=ALGORITHMS.RS256)
jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.pem(), algorithm=ALGORITHMS.RS256) jwt_decode_key = jwk.construct(my_si_private_key.public_key().pem(), algorithm=ALGORITHMS.RS256)
# Logging # Logging
LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO
@ -122,12 +127,12 @@ async def _index():
@app.get('/-/health', summary='* Health') @app.get('/-/health', summary='* Health')
async def _health(): async def _health():
return JSONr({'status': 'up'}) return Response(content=json_dumps({'status': 'up'}), media_type='application/json', status_code=200)
@app.get('/-/config', summary='* Config', description='returns environment variables.') @app.get('/-/config', summary='* Config', description='returns environment variables.')
async def _config(): async def _config():
return JSONr({ response = {
'VERSION': str(VERSION), 'VERSION': str(VERSION),
'COMMIT': str(COMMIT), 'COMMIT': str(COMMIT),
'DEBUG': str(DEBUG), 'DEBUG': str(DEBUG),
@ -141,14 +146,23 @@ async def _config():
'LEASE_RENEWAL_PERIOD': str(LEASE_RENEWAL_PERIOD), 'LEASE_RENEWAL_PERIOD': str(LEASE_RENEWAL_PERIOD),
'CORS_ORIGINS': str(CORS_ORIGINS), 'CORS_ORIGINS': str(CORS_ORIGINS),
'TZ': str(TZ), 'TZ': str(TZ),
}) }
return Response(content=json_dumps(response), media_type='application/json', status_code=200)
@app.get('/-/config/root-ca', summary='* Root CA', description='returns Root-CA needed for patching nvidia-gridd')
async def _config():
return Response(content=my_root_certificate.pem().decode('utf-8'), media_type='text/plain')
@app.get('/-/readme', summary='* Readme') @app.get('/-/readme', summary='* Readme')
async def _readme(): async def _readme():
from markdown import markdown from markdown import markdown
content = load_file(join(dirname(__file__), '../README.md')).decode('utf-8') content = load_file(join(dirname(__file__), '../README.md')).decode('utf-8')
return HTMLr(markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc'])) response = markdown(text=content, extensions=['tables', 'fenced_code', 'md_in_html', 'nl2br', 'toc'])
return Response(response, media_type='text/html', status_code=200)
@app.get('/-/manage', summary='* Management UI') @app.get('/-/manage', summary='* Management UI')
@ -186,7 +200,7 @@ async def _manage(request: Request):
</body> </body>
</html> </html>
''' '''
return HTMLr(response) return Response(response, media_type='text/html', status_code=200)
@app.get('/-/origins', summary='* Origins') @app.get('/-/origins', summary='* Origins')
@ -200,7 +214,7 @@ async def _origins(request: Request, leases: bool = False):
x['leases'] = list(map(lambda _: _.serialize(**serialize), Lease.find_by_origin_ref(db, origin.origin_ref))) x['leases'] = list(map(lambda _: _.serialize(**serialize), Lease.find_by_origin_ref(db, origin.origin_ref)))
response.append(x) response.append(x)
session.close() session.close()
return JSONr(response) return Response(content=json_dumps(response), media_type='application/json', status_code=200)
@app.delete('/-/origins', summary='* Origins') @app.delete('/-/origins', summary='* Origins')
@ -222,7 +236,7 @@ async def _leases(request: Request, origin: bool = False):
x['origin'] = lease_origin.serialize() x['origin'] = lease_origin.serialize()
response.append(x) response.append(x)
session.close() session.close()
return JSONr(response) return Response(content=json_dumps(response), media_type='application/json', status_code=200)
@app.delete('/-/leases/expired', summary='* Leases') @app.delete('/-/leases/expired', summary='* Leases')
@ -235,7 +249,8 @@ async def _lease_delete_expired(request: Request):
async def _lease_delete(request: Request, lease_ref: str): async def _lease_delete(request: Request, lease_ref: str):
if Lease.delete(db, lease_ref) == 1: if Lease.delete(db, lease_ref) == 1:
return Response(status_code=201) return Response(status_code=201)
return JSONr(status_code=404, content={'status': 404, 'detail': 'lease not found'}) response = {'status': 404, 'detail': 'lease not found'}
return Response(content=json_dumps(response), media_type='application/json', status_code=404)
# venv/lib/python3.9/site-packages/nls_core_service_instance/service_instance_token_manager.py # venv/lib/python3.9/site-packages/nls_core_service_instance/service_instance_token_manager.py
@ -268,10 +283,10 @@ async def _client_token():
}, },
"service_instance_public_key_configuration": { "service_instance_public_key_configuration": {
"service_instance_public_key_me": { "service_instance_public_key_me": {
"mod": hex(INSTANCE_KEY_PUB.raw().public_numbers().n)[2:], "mod": my_si_public_key.mod(),
"exp": int(INSTANCE_KEY_PUB.raw().public_numbers().e), "exp": my_si_public_key.exp(),
}, },
"service_instance_public_key_pem": INSTANCE_KEY_PUB.pem().decode('utf-8'), "service_instance_public_key_pem": my_si_private_key.public_key().pem().decode('utf-8'),
"key_retention_mode": "LATEST_ONLY" "key_retention_mode": "LATEST_ONLY"
}, },
} }
@ -317,7 +332,7 @@ async def auth_v1_origin(request: Request):
"sync_timestamp": cur_time.strftime(DT_FORMAT) "sync_timestamp": cur_time.strftime(DT_FORMAT)
} }
return JSONr(response) return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py # venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py
@ -343,7 +358,7 @@ async def auth_v1_origin_update(request: Request):
"sync_timestamp": cur_time.strftime(DT_FORMAT) "sync_timestamp": cur_time.strftime(DT_FORMAT)
} }
return JSONr(response) return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py # venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py
@ -375,7 +390,7 @@ async def auth_v1_code(request: Request):
"sync_timestamp": cur_time.strftime(DT_FORMAT), "sync_timestamp": cur_time.strftime(DT_FORMAT),
} }
return JSONr(response) return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
# venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py # venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py
@ -387,7 +402,8 @@ async def auth_v1_token(request: Request):
try: try:
payload = jwt.decode(token=j.get('auth_code'), key=jwt_decode_key, algorithms=ALGORITHMS.RS256) payload = jwt.decode(token=j.get('auth_code'), key=jwt_decode_key, algorithms=ALGORITHMS.RS256)
except JWTError as e: except JWTError as e:
return JSONr(status_code=400, content={'status': 400, 'title': 'invalid token', 'detail': str(e)}) response = {'status': 400, 'title': 'invalid token', 'detail': str(e)}
return Response(content=json_dumps(response), media_type='application/json', status_code=400)
origin_ref = payload.get('origin_ref') origin_ref = payload.get('origin_ref')
logger.info(f'> [ auth ]: {origin_ref}: {j}') logger.info(f'> [ auth ]: {origin_ref}: {j}')
@ -395,7 +411,8 @@ async def auth_v1_token(request: Request):
# validate the code challenge # validate the code challenge
challenge = b64enc(sha256(j.get('code_verifier').encode('utf-8')).digest()).rstrip(b'=').decode('utf-8') challenge = b64enc(sha256(j.get('code_verifier').encode('utf-8')).digest()).rstrip(b'=').decode('utf-8')
if payload.get('challenge') != challenge: if payload.get('challenge') != challenge:
return JSONr(status_code=401, content={'status': 401, 'detail': 'expected challenge did not match verifier'}) response = {'status': 401, 'detail': 'expected challenge did not match verifier'}
return Response(content=json_dumps(response), media_type='application/json', status_code=401)
access_expires_on = cur_time + TOKEN_EXPIRE_DELTA access_expires_on = cur_time + TOKEN_EXPIRE_DELTA
@ -419,7 +436,7 @@ async def auth_v1_token(request: Request):
"sync_timestamp": cur_time.strftime(DT_FORMAT), "sync_timestamp": cur_time.strftime(DT_FORMAT),
} }
return JSONr(response) return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
# NLS 3.4.0 - venv/lib/python3.12/site-packages/nls_services_lease/test/test_lease_single_controller.py # NLS 3.4.0 - venv/lib/python3.12/site-packages/nls_services_lease/test/test_lease_single_controller.py
@ -427,196 +444,6 @@ async def auth_v1_token(request: Request):
async def leasing_v1_config_token(request: Request): async def leasing_v1_config_token(request: Request):
j, cur_time = json_loads((await request.body()).decode('utf-8')), datetime.now(UTC) j, cur_time = json_loads((await request.body()).decode('utf-8')), datetime.now(UTC)
logger.debug(f'CALLED /leasing/v1/config-token')
logger.debug(f'Headers: {request.headers}')
logger.debug(f'Request: {j}')
# todo: THIS IS A DEMO ONLY
###
#
# https://git.collinwebdesigns.de/nvidia/nls/-/blob/main/src/test/test_config_token.py
#
###
root_private_key_filename = join(dirname(__file__), 'cert/my_demo_root_private_key.pem')
root_certificate_filename = join(dirname(__file__), 'cert/my_demo_root_certificate.pem')
ca_private_key_filename = join(dirname(__file__), 'cert/my_demo_ca_private_key.pem')
ca_certificate_filename = join(dirname(__file__), 'cert/my_demo_ca_certificate.pem')
si_private_key_filename = join(dirname(__file__), 'cert/my_demo_si_private_key.pem')
si_certificate_filename = join(dirname(__file__), 'cert/my_demo_si_certificate.pem')
def init_config_token_demo():
from cryptography import x509
from cryptography.hazmat._oid import NameOID
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key
from cryptography.hazmat.primitives.serialization import Encoding
""" Create Root Key and Certificate """
# create root keypair
my_root_private_key = generate_private_key(public_exponent=65537, key_size=4096)
my_root_public_key = my_root_private_key.public_key()
# create root-certificate subject
my_root_subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'California'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'Nvidia'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Nvidia Licensing Service (NLS)'),
x509.NameAttribute(NameOID.COMMON_NAME, u'NLS Root CA'),
])
# create self-signed root-certificate
my_root_certificate = (
x509.CertificateBuilder()
.subject_name(my_root_subject)
.issuer_name(my_root_subject)
.public_key(my_root_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.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.SubjectKeyIdentifier.from_public_key(my_root_public_key), critical=False)
.sign(my_root_private_key, hashes.SHA256()))
my_root_private_key_as_pem = my_root_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(root_private_key_filename, 'wb') as f:
f.write(my_root_private_key_as_pem)
with open(root_certificate_filename, 'wb') as f:
f.write(my_root_certificate.public_bytes(encoding=Encoding.PEM))
""" Create CA (Intermediate) Key and Certificate """
# create ca keypair
my_ca_private_key = generate_private_key(public_exponent=65537, key_size=4096)
my_ca_public_key = my_ca_private_key.public_key()
# create ca-certificate subject
my_ca_subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'California'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'Nvidia'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Nvidia Licensing Service (NLS)'),
x509.NameAttribute(NameOID.COMMON_NAME, u'NLS Intermediate CA'),
])
# create self-signed ca-certificate
my_ca_certificate = (
x509.CertificateBuilder()
.subject_name(my_ca_subject)
.issuer_name(my_root_subject)
.public_key(my_ca_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.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.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_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(
my_root_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
), critical=False)
.sign(my_root_private_key, hashes.SHA256()))
my_ca_private_key_as_pem = my_ca_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(ca_private_key_filename, 'wb') as f:
f.write(my_ca_private_key_as_pem)
with open(ca_certificate_filename, 'wb') as f:
f.write(my_ca_certificate.public_bytes(encoding=Encoding.PEM))
""" Create Service-Instance Key and Certificate """
# create si keypair
my_si_private_key = generate_private_key(public_exponent=65537, key_size=2048)
my_si_public_key = my_si_private_key.public_key()
my_si_private_key_as_pem = my_si_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
my_si_public_key_as_pem = my_si_public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
with open(si_private_key_filename, 'wb') as f:
f.write(my_si_private_key_as_pem)
# with open('instance.public.pem', 'wb') as f:
# f.write(my_si_public_key_as_pem)
# create si-certificate subject
my_si_subject = x509.Name([
# x509.NameAttribute(NameOID.COMMON_NAME, INSTANCE_REF),
x509.NameAttribute(NameOID.COMMON_NAME, j.get('service_instance_ref')),
])
# create self-signed si-certificate
my_si_certificate = (
x509.CertificateBuilder()
.subject_name(my_si_subject)
.issuer_name(my_ca_subject)
.public_key(my_si_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.not_valid_after(datetime.now(tz=UTC) + timedelta(days=365 * 10))
.add_extension(x509.KeyUsage(digital_signature=True, key_encipherment=True, key_cert_sign=False,
key_agreement=True, content_commitment=False, data_encipherment=False,
crl_sign=False, encipher_only=False, decipher_only=False), critical=True)
.add_extension(x509.ExtendedKeyUsage([
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]
), critical=False)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(my_si_public_key), critical=False)
# .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(my_ca_public_key), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
my_ca_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
), critical=False)
.add_extension(x509.SubjectAlternativeName([
# x509.DNSName(INSTANCE_REF)
x509.DNSName(j.get('service_instance_ref'))
]), critical=False)
.sign(my_ca_private_key, hashes.SHA256()))
my_si_public_key_exp = my_si_certificate.public_key().public_numbers().e
my_si_public_key_mod = f'{my_si_certificate.public_key().public_numbers().n:x}' # hex value without "0x" prefix
with open(si_certificate_filename, 'wb') as f:
f.write(my_si_certificate.public_bytes(encoding=Encoding.PEM))
if not (isfile(root_private_key_filename)
and isfile(ca_private_key_filename)
and isfile(ca_certificate_filename)
and isfile(si_private_key_filename)
and isfile(si_certificate_filename)):
init_config_token_demo()
my_ca_certificate = Cert.from_file(ca_certificate_filename)
my_si_certificate = Cert.from_file(si_certificate_filename)
my_si_private_key = PrivateKey.from_file(si_private_key_filename)
my_si_private_key_as_pem = my_si_private_key.pem()
my_si_public_key = my_si_private_key.public_key().raw()
my_si_public_key_as_pem = my_si_private_key.public_key().pem()
""" build out payload """
cur_time = datetime.now(UTC) cur_time = datetime.now(UTC)
exp_time = cur_time + CLIENT_TOKEN_EXPIRE_DELTA exp_time = cur_time + CLIENT_TOKEN_EXPIRE_DELTA
@ -631,16 +458,16 @@ async def leasing_v1_config_token(request: Request):
"service_instance_ref": j.get('service_instance_ref'), "service_instance_ref": j.get('service_instance_ref'),
"service_instance_public_key_configuration": { "service_instance_public_key_configuration": {
"service_instance_public_key_me": { "service_instance_public_key_me": {
"mod": hex(my_si_public_key.public_numbers().n)[2:], "mod": my_si_public_key.mod(),
"exp": int(my_si_public_key.public_numbers().e), "exp": my_si_public_key.exp(),
}, },
# 64 chars per line (pem default) # 64 chars per line (pem default)
"service_instance_public_key_pem": my_si_public_key_as_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"
}, },
} }
my_jwt_encode_key = jwk.construct(my_si_private_key_as_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')
@ -660,9 +487,7 @@ async def leasing_v1_config_token(request: Request):
"configToken": config_token, "configToken": config_token,
} }
logging.debug(response) return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
return JSONr(response, status_code=200)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py
@ -673,7 +498,8 @@ async def leasing_v1_lessor(request: Request):
try: try:
token = __get_token(request) token = __get_token(request)
except JWTError: except JWTError:
return JSONr(status_code=401, content={'status': 401, 'detail': 'token is not valid'}) response = {'status': 401, 'detail': 'token is not valid'}
return Response(content=json_dumps(response), media_type='application/json', status_code=401)
origin_ref = token.get('origin_ref') origin_ref = token.get('origin_ref')
scope_ref_list = j.get('scope_ref_list') scope_ref_list = j.get('scope_ref_list')
@ -682,7 +508,8 @@ async def leasing_v1_lessor(request: Request):
for scope_ref in scope_ref_list: for scope_ref in scope_ref_list:
# if scope_ref not in [ALLOTMENT_REF]: # if scope_ref not in [ALLOTMENT_REF]:
# return JSONr(status_code=500, detail=f'no service instances found for scopes: ["{scope_ref}"]') # response = {'status': 400, 'detail': f'service instances not found for scopes: ["{scope_ref}"]')}
# return Response(content=json_dumps(response), media_type='application/json', status_code=400)
pass pass
lease_result_list = [] lease_result_list = []
@ -722,17 +549,16 @@ async def leasing_v1_lessor(request: Request):
} }
content = json_dumps(response, separators=(',', ':')) content = json_dumps(response, separators=(',', ':'))
content = f'{content}\n'.encode('utf-8') content = f'{content}\n'.encode('ascii')
signature = INSTANCE_KEY_RSA.generate_signature(content) signature = my_si_private_key.generate_signature(content)
headers = { headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'access-control-expose-headers': 'X-NLS-Signature', 'access-control-expose-headers': 'X-NLS-Signature',
'X-NLS-Signature': f'{signature.hex().encode()}' 'X-NLS-Signature': f'{signature.hex().encode()}'
} }
x = Response(content=content, media_type='text/plain')
x.raw_headers = [(k.encode("latin-1"), v.encode("latin-1")) for k, v in headers.items()] return Response(content=content, media_type='application/json', headers=headers)
return x
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py
@ -752,7 +578,7 @@ async def leasing_v1_lessor_lease(request: Request):
"sync_timestamp": cur_time.strftime(DT_FORMAT), "sync_timestamp": cur_time.strftime(DT_FORMAT),
} }
return JSONr(response) return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py
@ -766,7 +592,8 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str):
entity = Lease.find_by_origin_ref_and_lease_ref(db, origin_ref, lease_ref) entity = Lease.find_by_origin_ref_and_lease_ref(db, origin_ref, lease_ref)
if entity is None: if entity is None:
return JSONr(status_code=404, content={'status': 404, 'detail': 'requested lease not available'}) response = {'status': 404, 'detail': 'requested lease not available'}
return Response(content=json_dumps(response), media_type='application/json', status_code=404)
expires = cur_time + LEASE_EXPIRE_DELTA expires = cur_time + LEASE_EXPIRE_DELTA
response = { response = {
@ -784,17 +611,17 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str):
Lease.renew(db, entity, expires, cur_time) Lease.renew(db, entity, expires, cur_time)
content = json_dumps(response, separators=(',', ':')) content = json_dumps(response, separators=(',', ':'))
content = f'{content}\n'.encode('utf-8') content = f'{content}\n'.encode('ascii')
signature = INSTANCE_KEY_RSA.generate_signature(content) signature = my_si_private_key.generate_signature(content)
headers = { headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'access-control-expose-headers': 'X-NLS-Signature', 'access-control-expose-headers': 'X-NLS-Signature',
'X-NLS-Signature': f'{signature.hex().encode()}' 'X-NLS-Signature': f'{signature.hex().encode()}'
} }
x = Response(content=content, media_type='text/plain')
x.raw_headers = [(k.encode("latin-1"), v.encode("latin-1")) for k, v in headers.items()] return Response(content=content, media_type='application/json', headers=headers)
return x
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_single_controller.py
@ -807,12 +634,15 @@ async def leasing_v1_lease_delete(request: Request, lease_ref: str):
entity = Lease.find_by_lease_ref(db, lease_ref) entity = Lease.find_by_lease_ref(db, lease_ref)
if entity.origin_ref != origin_ref: if entity.origin_ref != origin_ref:
return JSONr(status_code=403, content={'status': 403, 'detail': 'access or operation forbidden'}) response = {'status': 403, 'detail': 'access or operation forbidden'}
return Response(content=json_dumps(response), media_type='application/json', status_code=403)
if entity is None: if entity is None:
return JSONr(status_code=404, content={'status': 404, 'detail': 'requested lease not available'}) response = {'status': 404, 'detail': 'requested lease not available'}
return Response(content=json_dumps(response), media_type='application/json', status_code=404)
if Lease.delete(db, lease_ref) == 0: if Lease.delete(db, lease_ref) == 0:
return JSONr(status_code=404, content={'status': 404, 'detail': 'lease not found'}) response = {'status': 404, 'detail': 'lease not found'}
return Response(content=json_dumps(response), media_type='application/json', status_code=404)
response = { response = {
"client_challenge": None, "client_challenge": None,
@ -821,7 +651,7 @@ async def leasing_v1_lease_delete(request: Request, lease_ref: str):
"sync_timestamp": cur_time.strftime(DT_FORMAT), "sync_timestamp": cur_time.strftime(DT_FORMAT),
} }
return JSONr(response) return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
# venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py
@ -842,7 +672,7 @@ async def leasing_v1_lessor_lease_remove(request: Request):
"sync_timestamp": cur_time.strftime(DT_FORMAT), "sync_timestamp": cur_time.strftime(DT_FORMAT),
} }
return JSONr(response) return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
@app.post('/leasing/v1/lessor/shutdown', description='shutdown all leases') @app.post('/leasing/v1/lessor/shutdown', description='shutdown all leases')
@ -864,7 +694,7 @@ async def leasing_v1_lessor_shutdown(request: Request):
"sync_timestamp": cur_time.strftime(DT_FORMAT), "sync_timestamp": cur_time.strftime(DT_FORMAT),
} }
return JSONr(response) return Response(content=json_dumps(response, separators=(',', ':')), media_type='application/json', status_code=200)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,15 +1,208 @@
import logging import logging
from datetime import datetime, UTC, timedelta
from json import loads as json_loads from json import loads as json_loads
from cryptography.hazmat.primitives import serialization from os.path import join, dirname, isfile
from cryptography import x509
from cryptography.hazmat._oid import NameOID
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey, generate_private_key from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey, generate_private_key
from cryptography.hazmat.primitives.hashes import SHA256 from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
from cryptography.x509 import load_pem_x509_certificate, Certificate from cryptography.x509 import load_pem_x509_certificate, Certificate
logging.basicConfig() logging.basicConfig()
class CASetup:
###
#
# https://git.collinwebdesigns.de/nvidia/nls/-/blob/main/src/test/test_config_token.py
#
###
ROOT_PRIVATE_KEY_FILENAME = 'root_private_key.pem'
ROOT_CERTIFICATE_FILENAME = 'root_certificate.pem'
CA_PRIVATE_KEY_FILENAME = 'ca_private_key.pem'
CA_CERTIFICATE_FILENAME = 'ca_certificate.pem'
SI_PRIVATE_KEY_FILENAME = 'si_private_key.pem'
SI_CERTIFICATE_FILENAME = 'si_certificate.pem'
def __init__(self, service_instance_ref: str):
self.service_instance_ref = service_instance_ref
self.root_private_key_filename = join(dirname(__file__), 'cert', CASetup.ROOT_PRIVATE_KEY_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_certificate_filename = join(dirname(__file__), 'cert', CASetup.CA_CERTIFICATE_FILENAME)
self.si_private_key_filename = join(dirname(__file__), 'cert', CASetup.SI_PRIVATE_KEY_FILENAME)
self.si_certificate_filename = join(dirname(__file__), 'cert', CASetup.SI_CERTIFICATE_FILENAME)
if not (isfile(self.root_private_key_filename)
and isfile(self.root_certificate_filename)
and isfile(self.ca_private_key_filename)
and isfile(self.ca_certificate_filename)
and isfile(self.si_private_key_filename)
and isfile(self.si_certificate_filename)):
self.init_config_token_demo()
def init_config_token_demo(self):
""" Create Root Key and Certificate """
# create root keypair
my_root_private_key = generate_private_key(public_exponent=65537, key_size=4096)
my_root_public_key = my_root_private_key.public_key()
# create root-certificate subject
my_root_subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'California'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'Nvidia'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Nvidia Licensing Service (NLS)'),
x509.NameAttribute(NameOID.COMMON_NAME, u'NLS Root CA'),
])
# create self-signed root-certificate
my_root_certificate = (
x509.CertificateBuilder()
.subject_name(my_root_subject)
.issuer_name(my_root_subject)
.public_key(my_root_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.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.SubjectKeyIdentifier.from_public_key(my_root_public_key), critical=False)
.sign(my_root_private_key, hashes.SHA256()))
my_root_private_key_as_pem = my_root_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(self.root_private_key_filename, 'wb') as f:
f.write(my_root_private_key_as_pem)
with open(self.root_certificate_filename, 'wb') as f:
f.write(my_root_certificate.public_bytes(encoding=Encoding.PEM))
""" Create CA (Intermediate) Key and Certificate """
# create ca keypair
my_ca_private_key = generate_private_key(public_exponent=65537, key_size=4096)
my_ca_public_key = my_ca_private_key.public_key()
# create ca-certificate subject
my_ca_subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'California'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'Nvidia'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Nvidia Licensing Service (NLS)'),
x509.NameAttribute(NameOID.COMMON_NAME, u'NLS Intermediate CA'),
])
# create self-signed ca-certificate
my_ca_certificate = (
x509.CertificateBuilder()
.subject_name(my_ca_subject)
.issuer_name(my_root_subject)
.public_key(my_ca_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.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.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_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(
my_root_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
), critical=False)
.sign(my_root_private_key, hashes.SHA256()))
my_ca_private_key_as_pem = my_ca_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(self.ca_private_key_filename, 'wb') as f:
f.write(my_ca_private_key_as_pem)
with open(self.ca_certificate_filename, 'wb') as f:
f.write(my_ca_certificate.public_bytes(encoding=Encoding.PEM))
""" Create Service-Instance Key and Certificate """
# create si keypair
my_si_private_key = generate_private_key(public_exponent=65537, key_size=2048)
my_si_public_key = my_si_private_key.public_key()
my_si_private_key_as_pem = my_si_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
my_si_public_key_as_pem = my_si_public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
with open(self.si_private_key_filename, 'wb') as f:
f.write(my_si_private_key_as_pem)
# with open(self.si_public_key_filename, 'wb') as f:
# f.write(my_si_public_key_as_pem)
# create si-certificate subject
my_si_subject = x509.Name([
# x509.NameAttribute(NameOID.COMMON_NAME, INSTANCE_REF),
x509.NameAttribute(NameOID.COMMON_NAME, self.service_instance_ref),
])
# create self-signed si-certificate
my_si_certificate = (
x509.CertificateBuilder()
.subject_name(my_si_subject)
.issuer_name(my_ca_subject)
.public_key(my_si_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(tz=UTC) - timedelta(days=1))
.not_valid_after(datetime.now(tz=UTC) + timedelta(days=365 * 10))
.add_extension(x509.KeyUsage(digital_signature=True, key_encipherment=True, key_cert_sign=False,
key_agreement=True, content_commitment=False, data_encipherment=False,
crl_sign=False, encipher_only=False, decipher_only=False), critical=True)
.add_extension(x509.ExtendedKeyUsage([
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]
), critical=False)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(my_si_public_key), critical=False)
# .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(my_ca_public_key), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
my_ca_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
), critical=False)
.add_extension(x509.SubjectAlternativeName([
# x509.DNSName(INSTANCE_REF)
x509.DNSName(self.service_instance_ref)
]), critical=False)
.sign(my_ca_private_key, hashes.SHA256()))
with open(self.si_certificate_filename, 'wb') as f:
f.write(my_si_certificate.public_bytes(encoding=Encoding.PEM))
class PrivateKey: class PrivateKey:
def __init__(self, data: bytes): def __init__(self, data: bytes):
@ -82,6 +275,12 @@ class PublicKey:
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.SubjectPublicKeyInfo
) )
def mod(self) -> str:
return hex(self.__key.public_numbers().n)[2:]
def exp(self):
return int(self.__key.public_numbers().e)
def verify_signature(self, signature: bytes, data: bytes) -> bytes: def verify_signature(self, signature: bytes, data: bytes) -> bytes:
return self.__key.verify(signature, data, padding=PKCS1v15(), algorithm=SHA256()) return self.__key.verify(signature, data, padding=PKCS1v15(), algorithm=SHA256())

View File

@ -15,7 +15,7 @@ services:
<<: *dls-variables <<: *dls-variables
volumes: volumes:
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /opt/docker/fastapi-dls/cert:/app/cert # instance.private.pem, instance.public.pem - /opt/docker/fastapi-dls/cert:/app/cert
- db:/app/database - db:/app/database
entrypoint: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app", "--proxy-headers"] entrypoint: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app", "--proxy-headers"]
healthcheck: healthcheck:

View File

@ -4,7 +4,6 @@ from base64 import b64encode as b64enc
from calendar import timegm from calendar import timegm
from datetime import datetime, UTC from datetime import datetime, UTC
from hashlib import sha256 from hashlib import sha256
from os.path import dirname, join
from uuid import uuid4, UUID from uuid import uuid4, UUID
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@ -17,21 +16,24 @@ sys.path.append('../')
sys.path.append('../app') sys.path.append('../app')
from app import main from app import main
from util import PrivateKey, PublicKey from util import CASetup, PrivateKey, PublicKey, Cert
client = TestClient(main.app) client = TestClient(main.app)
# Instance
INSTANCE_REF = '10000000-0000-0000-0000-000000000001' INSTANCE_REF = '10000000-0000-0000-0000-000000000001'
ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld' ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld'
# INSTANCE_KEY_RSA = generate_key() # CA & Signing
# INSTANCE_KEY_PUB = INSTANCE_KEY_RSA.public_key() ca_setup = CASetup(service_instance_ref=INSTANCE_REF)
my_root_certificate = Cert.from_file(ca_setup.root_certificate_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_public_key = my_si_private_key.public_key()
my_si_public_key_as_pem = my_si_private_key.public_key().pem()
INSTANCE_KEY_RSA = PrivateKey.from_file(str(join(dirname(__file__), '../app/cert/instance.private.pem'))) jwt_encode_key = jwk.construct(my_si_private_key_as_pem, algorithm=ALGORITHMS.RS256)
INSTANCE_KEY_PUB = PublicKey.from_file(str(join(dirname(__file__), '../app/cert/instance.public.pem'))) jwt_decode_key = jwk.construct(my_si_public_key_as_pem, algorithm=ALGORITHMS.RS256)
jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.pem(), algorithm=ALGORITHMS.RS256)
jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.pem(), algorithm=ALGORITHMS.RS256)
def __bearer_token(origin_ref: str) -> str: def __bearer_token(origin_ref: str) -> str:
@ -41,10 +43,10 @@ def __bearer_token(origin_ref: str) -> str:
def test_signing(): def test_signing():
signature_set_header = INSTANCE_KEY_RSA.generate_signature(b'Hello') signature_set_header = my_si_private_key.generate_signature(b'Hello')
# test plain # test plain
INSTANCE_KEY_PUB.verify_signature(signature_set_header, b'Hello') my_si_public_key.verify_signature(signature_set_header, b'Hello')
# test "X-NLS-Signature: b'....' # test "X-NLS-Signature: b'....'
x_nls_signature_header_value = f'{signature_set_header.hex().encode()}' x_nls_signature_header_value = f'{signature_set_header.hex().encode()}'
@ -54,7 +56,7 @@ def test_signing():
# test eval # test eval
signature_get_header = eval(x_nls_signature_header_value) signature_get_header = eval(x_nls_signature_header_value)
signature_get_header = bytes.fromhex(signature_get_header.decode('ascii')) signature_get_header = bytes.fromhex(signature_get_header.decode('ascii'))
INSTANCE_KEY_PUB.verify_signature(signature_get_header, b'Hello') my_si_public_key.verify_signature(signature_get_header, b'Hello')
def test_index(): def test_index():
@ -73,6 +75,12 @@ def test_config():
assert response.status_code == 200 assert response.status_code == 200
def test_config_root_ca():
response = client.get('/-/config/root-ca')
assert response.status_code == 200
assert response.content.decode('utf-8') == my_root_certificate.pem().decode('utf-8')
def test_readme(): def test_readme():
response = client.get('/-/readme') response = client.get('/-/readme')
assert response.status_code == 200 assert response.status_code == 200
@ -233,7 +241,7 @@ def test_leasing_v1_lessor():
assert len(signature) == 512 assert len(signature) == 512
signature = bytes.fromhex(signature.decode('ascii')) signature = bytes.fromhex(signature.decode('ascii'))
assert len(signature) == 256 assert len(signature) == 256
INSTANCE_KEY_PUB.verify_signature(signature, response.content) my_si_public_key.verify_signature(signature, response.content)
lease_result_list = response.json().get('lease_result_list') lease_result_list = response.json().get('lease_result_list')
assert len(lease_result_list) == 1 assert len(lease_result_list) == 1
@ -271,7 +279,7 @@ def test_leasing_v1_lease_renew():
assert len(signature) == 512 assert len(signature) == 512
signature = bytes.fromhex(signature.decode('ascii')) signature = bytes.fromhex(signature.decode('ascii'))
assert len(signature) == 256 assert len(signature) == 256
INSTANCE_KEY_PUB.verify_signature(signature, response.content) my_si_public_key.verify_signature(signature, response.content)
lease_ref = response.json().get('lease_ref') lease_ref = response.json().get('lease_ref')
assert len(lease_ref) == 36 assert len(lease_ref) == 36