From 26729d68cde1bc2cc3e1a0fbb46206c267830c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Wed, 23 Apr 2025 11:39:38 +0200 Subject: [PATCH 01/14] feat: change crypto to use secp256k1 --- nilai-api/src/nilai_api/crypto.py | 72 +++++++++++++++----- nilai-api/src/nilai_api/routers/private.py | 2 +- nilai-api/src/nilai_api/routers/public.py | 8 +++ nilai-api/src/nilai_api/state.py | 2 +- tests/unit/nilai_api/routers/test_private.py | 2 +- tests/unit/nilai_api/test_cryptography.py | 32 +++++---- tests/unit/nilai_api/test_state.py | 2 +- 7 files changed, 85 insertions(+), 35 deletions(-) diff --git a/nilai-api/src/nilai_api/crypto.py b/nilai-api/src/nilai_api/crypto.py index a58d172a..767da3d4 100644 --- a/nilai-api/src/nilai_api/crypto.py +++ b/nilai-api/src/nilai_api/crypto.py @@ -1,25 +1,63 @@ from base64 import b64encode +import os -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec +from secp256k1 import PrivateKey, PublicKey +PRIVATE_KEY_PATH = "private_key.key" -def generate_key_pair(): - private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) - public_key = private_key.public_key() - verifying_key = b64encode( - public_key.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - ).decode() - return private_key, public_key, verifying_key +def generate_key_pair() -> tuple[PrivateKey, PublicKey, str]: + """ + Generate a new key pair and return the private key, public key, and base64 encoded public key. -def sign_message(private_key, message): - return private_key.sign(message.encode(), ec.ECDSA(hashes.SHA256())) + Returns: + tuple[PrivateKey, PublicKey, str]: A tuple containing the private key, public key, and base64 encoded public key. + """ + private_key: PrivateKey + if os.path.exists(PRIVATE_KEY_PATH): + with open(PRIVATE_KEY_PATH, "rb") as f: + private_key = PrivateKey(f.read()) + else: + private_key = PrivateKey() + with open(PRIVATE_KEY_PATH, "wb") as f: + private_key_bytes: bytes = private_key.private_key # type: ignore + f.write(private_key_bytes) + public_key = private_key.pubkey + if public_key is None: + raise ValueError("Keypair generation failed:Public key is None") + b64_public_key: str = b64encode(public_key.serialize()).decode() -def verify_signature(public_key, message, signature): - public_key.verify(signature, message.encode(), ec.ECDSA(hashes.SHA256())) + return private_key, public_key, b64_public_key + + +def sign_message(private_key: PrivateKey, message: str) -> bytes: + """ + Sign a message using the private key. + + Args: + private_key (PrivateKey): The private key to sign the message with. + message (str): The message to sign. + + Returns: + bytes: The signature of the message. + """ + signature = private_key.ecdsa_sign(message.encode()) + serialized_signature: bytes = private_key.ecdsa_serialize(signature) + return serialized_signature + + +def verify_signature(public_key: PublicKey, message: str, signature: bytes) -> bool: + """ + Verify a signature using the public key. + + Args: + public_key (PublicKey): The public key to verify the signature with. + message (str): The message to verify the signature with. + signature (bytes): The signature to verify. + + Returns: + bool: True if the signature is valid, False otherwise. + """ + sig = public_key.ecdsa_deserialize(signature) + return public_key.ecdsa_verify(message.encode(), sig) diff --git a/nilai-api/src/nilai_api/routers/private.py b/nilai-api/src/nilai_api/routers/private.py index 13134818..0e604618 100644 --- a/nilai-api/src/nilai_api/routers/private.py +++ b/nilai-api/src/nilai_api/routers/private.py @@ -77,7 +77,7 @@ async def get_attestation( """ attestation_report = await get_attestation_report(nonce) - attestation_report.verifying_key = state.verifying_key + attestation_report.verifying_key = state.b64_public_key return attestation_report diff --git a/nilai-api/src/nilai_api/routers/public.py b/nilai-api/src/nilai_api/routers/public.py index 3066fbfa..2a6b09bc 100644 --- a/nilai-api/src/nilai_api/routers/public.py +++ b/nilai-api/src/nilai_api/routers/public.py @@ -9,6 +9,14 @@ router = APIRouter() +@router.get("/v1/public_key", tags=["Public"]) +async def get_public_key() -> str: + """ + Get the public key of the API. + """ + return state.b64_public_key + + # Health Check Endpoint @router.get("/v1/health", tags=["Health"]) async def health_check() -> HealthCheckResponse: diff --git a/nilai-api/src/nilai_api/state.py b/nilai-api/src/nilai_api/state.py index f3f8bd1e..21349fb0 100644 --- a/nilai-api/src/nilai_api/state.py +++ b/nilai-api/src/nilai_api/state.py @@ -13,7 +13,7 @@ class AppState: def __init__(self): - self.private_key, self.public_key, self.verifying_key = generate_key_pair() + self.private_key, self.public_key, self.b64_public_key = generate_key_pair() self.sem = Semaphore(2) self.discovery_service = ModelServiceDiscovery( diff --git a/tests/unit/nilai_api/routers/test_private.py b/tests/unit/nilai_api/routers/test_private.py index 910d6d72..375a547c 100644 --- a/tests/unit/nilai_api/routers/test_private.py +++ b/tests/unit/nilai_api/routers/test_private.py @@ -95,7 +95,7 @@ def mock_state(mocker, event_loop): mocker.patch.object(state, "discovery_service", mock_discovery_service) # Patch other attributes - mocker.patch.object(state, "verifying_key", "test-verifying-key") + mocker.patch.object(state, "b64_public_key", "test-verifying-key") # Patch get_model method mocker.patch.object(state, "get_model", return_value=model_endpoint) diff --git a/tests/unit/nilai_api/test_cryptography.py b/tests/unit/nilai_api/test_cryptography.py index 3719feb5..3811db11 100644 --- a/tests/unit/nilai_api/test_cryptography.py +++ b/tests/unit/nilai_api/test_cryptography.py @@ -1,23 +1,23 @@ from base64 import b64decode import pytest -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives.asymmetric import ec +from secp256k1 import PrivateKey, PublicKey from nilai_api.crypto import generate_key_pair, sign_message, verify_signature def test_generate_key_pair(): # Generate keys - private_key, public_key, verifying_key = generate_key_pair() + private_key, public_key, b64_public_key = generate_key_pair() # Check private_key and public_key are instances of the expected types - assert isinstance(private_key, ec.EllipticCurvePrivateKey) - assert isinstance(public_key, ec.EllipticCurvePublicKey) + assert isinstance(private_key, PrivateKey) + assert isinstance(public_key, PublicKey) # Ensure the verifying_key is a valid PEM-encoded public key - decoded_key = b64decode(verifying_key) - assert b"BEGIN PUBLIC KEY" in decoded_key - assert b"END PUBLIC KEY" in decoded_key + decoded_key = b64decode(b64_public_key) + assert decoded_key == public_key.serialize(), ( + "Public key should be valid and match the generated key" + ) def test_sign_message(): @@ -31,8 +31,10 @@ def test_sign_message(): signature = sign_message(private_key, message) # Ensure the signature is a byte string - assert isinstance(signature, bytes) - assert len(signature) > 0 + assert isinstance(signature, bytes), ( + f"Signature should be a byte string but is: {type(signature)}" + ) + assert len(signature) > 0, f"Signature should not be empty but is: {signature}" def test_verify_signature_valid(): @@ -64,8 +66,9 @@ def test_verify_signature_invalid_message(): signature = sign_message(private_key, message) # Verify the tampered message (should raise InvalidSignature) - with pytest.raises(InvalidSignature): - verify_signature(public_key, tampered_message, signature) + assert not verify_signature(public_key, tampered_message, signature), ( + "Signature should be invalid" + ) def test_verify_signature_invalid_signature(): @@ -82,5 +85,6 @@ def test_verify_signature_invalid_signature(): tampered_signature = signature[:-1] + b"\x00" # Verify the tampered signature (should raise InvalidSignature) - with pytest.raises(InvalidSignature): - verify_signature(public_key, message, tampered_signature) + assert not verify_signature(public_key, message, tampered_signature), ( + "Signature should be invalid" + ) diff --git a/tests/unit/nilai_api/test_state.py b/tests/unit/nilai_api/test_state.py index 7430d5e1..bd5ba299 100644 --- a/tests/unit/nilai_api/test_state.py +++ b/tests/unit/nilai_api/test_state.py @@ -12,7 +12,7 @@ def app_state(mocker): def test_generate_key_pair(app_state): assert app_state.private_key is not None assert app_state.public_key is not None - assert app_state.verifying_key is not None + assert app_state.b64_public_key is not None def test_semaphore_initialization(app_state): From b8873d01dd27abb18d95ec97de77492fd5dbcde4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Fri, 25 Apr 2025 11:15:21 +0200 Subject: [PATCH 02/14] feat: added nilchain and nilauth to docker compose dev and unified with macos --- docker-compose.dev.macos.yml | 20 --------------- docker-compose.dev.yml | 49 ++++++++++++++++++++++++++++++++++++ docker/api.Dockerfile | 2 +- docker/nilauth/config.yaml | 23 +++++++++++++++++ 4 files changed, 73 insertions(+), 21 deletions(-) delete mode 100644 docker-compose.dev.macos.yml create mode 100644 docker/nilauth/config.yaml diff --git a/docker-compose.dev.macos.yml b/docker-compose.dev.macos.yml deleted file mode 100644 index 63dabcfe..00000000 --- a/docker-compose.dev.macos.yml +++ /dev/null @@ -1,20 +0,0 @@ -services: - api: - platform: linux/amd64 - ports: - - "8080:8080" - volumes: - - ./nilai-api/:/app/nilai-api/ - - ./packages/:/app/packages/ - attestation: - ports: - - "8081:8080" - volumes: - - ./nilai-attestation/:/app/nilai-attestation/ - - ./packages/:/app/packages/ - redis: - ports: - - "6379:6379" - postgres: - ports: - - "5432:5432" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index da88e4f5..efd40057 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,10 +1,13 @@ services: api: + platform: linux/amd64 # for macOS to force running on Rosetta 2 ports: - "8080:8080" volumes: - ./nilai-api/:/app/nilai-api/ - ./packages/:/app/packages/ + networks: + - nilauth attestation: ports: - "8081:8080" @@ -20,3 +23,49 @@ services: grafana: ports: - "3000:3000" + + nilauth-postgres: + image: postgres:17 + environment: + POSTGRES_PASSWORD: postgres + ports: + - "30432:5432" + networks: + - nilauth + + nilchain: + image: ghcr.io/nillionnetwork/nilchain-devnet:v0.1.0 + restart: unless-stopped + shm_size: 128mb + ports: + - "26648:26648" # JSON RPC + - "26649:26649" # gRPC + - "26650:26650" # REST + networks: + - nilauth + + nilauth: + image: public.ecr.aws/k5d9x2g2/nilauth:latest + depends_on: + - nilauth-postgres + - nilchain + volumes: + - ./docker/nilauth/config.yaml:/opt/config.yaml + command: ["--config-file", "/opt/config.yaml"] + ports: + - "30921:30921" # main server + - "30922:30022" # metrics server + networks: + - nilauth + + token-price-api: + image: caddy:2 + ports: + - "30923:80" + command: | + caddy respond --listen :80 --body '{"nillion":{"usd":1}}' --header "Content-Type: application/json" + networks: + - nilauth + +networks: + nilauth: diff --git a/docker/api.Dockerfile b/docker/api.Dockerfile index 1e39e595..69df609c 100644 --- a/docker/api.Dockerfile +++ b/docker/api.Dockerfile @@ -5,7 +5,7 @@ COPY --link . /app/ WORKDIR /app/nilai-api/ RUN apt-get update && \ -apt-get install build-essential curl -y && \ +apt-get install build-essential curl git pkg-config automake file -y && \ apt-get clean && \ apt-get autoremove && \ rm -rf /var/lib/apt/lists/* && \ diff --git a/docker/nilauth/config.yaml b/docker/nilauth/config.yaml new file mode 100644 index 00000000..fe42c669 --- /dev/null +++ b/docker/nilauth/config.yaml @@ -0,0 +1,23 @@ +server: + bind_endpoint: 0.0.0.0:30921 + +private_key: + hex: 7c5c8d3114ac2410249ca2baae7dec86ac2950a389ac44a5fdca8941b92b6c86 + +metrics: + bind_endpoint: 0.0.0.0:30022 + +payments: + nilchain_url: http://nilchain:26648 + + subscriptions: + renewal_threshold_seconds: 1000 + length_seconds: 120 + dollar_cost: 1 + + token_price: + base_url: http://token-price-api/api/v3/simple/price + api_key: abc + +postgres: + url: postgres://postgres:postgres@nilauth-postgres:5432/postgres From 7635deb2664d893b8c25ae4cf28e545fd58b83ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Fri, 25 Apr 2025 14:22:08 +0200 Subject: [PATCH 03/14] feat: support for NUC as authentication mechanism for nilAI --- .env.ci | 4 +- .env.sample | 5 ++ ...ix_remove_mail_and_adjust_field_lengths.py | 82 +++++++++++++++++++ nilai-api/gunicorn.conf.py | 2 +- nilai-api/private_key.key | 1 + nilai-api/pyproject.toml | 2 + .../src/nilai_api/attestation/__init__.py | 6 +- nilai-api/src/nilai_api/auth/__init__.py | 4 + nilai-api/src/nilai_api/auth/nuc.py | 74 +++++++++++++++++ nilai-api/src/nilai_api/auth/strategies.py | 30 ++++++- nilai-api/src/nilai_api/commands/add_user.py | 4 - nilai-api/src/nilai_api/config/__init__.py | 4 +- nilai-api/src/nilai_api/config/mainnet.py | 2 - nilai-api/src/nilai_api/config/testnet.py | 2 - nilai-api/src/nilai_api/db/logs.py | 2 +- nilai-api/src/nilai_api/db/users.py | 31 +++++-- nilai-api/src/nilai_api/rate_limiting.py | 8 +- nilai-attestation/gunicorn.conf.py | 4 +- nilai-models/src/nilai_models/daemon.py | 8 +- .../nilai-common/src/nilai_common/config.py | 40 +++++---- tests/unit/nilai_api/routers/test_private.py | 1 + 21 files changed, 266 insertions(+), 50 deletions(-) create mode 100644 nilai-api/alembic/versions/ca76e3ebe6ee_fix_remove_mail_and_adjust_field_lengths.py create mode 100644 nilai-api/private_key.key create mode 100644 nilai-api/src/nilai_api/auth/nuc.py diff --git a/.env.ci b/.env.ci index fe6e10f8..a9c428fd 100644 --- a/.env.ci +++ b/.env.ci @@ -8,6 +8,7 @@ HF_TOKEN="Hugging Face API Token" ENVIRONMENT = "mainnet" NILAI_GUNICORN_WORKERS = 10 +AUTH_STRATEGY = "api_key" # The domain name of the server # - It must be written as "localhost" or "test.nilai.nillion" @@ -18,7 +19,8 @@ NILAI_SERVER_DOMAIN = "localhost" ATTESTATION_HOST = "attestation" ATTESTATION_PORT = 8080 - +# nilAuth Trusted URLs +NILAUTH_TRUSTED_ROOT_ISSUERS = "http://localhost:30921" # Postgres Docker Compose Config POSTGRES_HOST = "postgres" diff --git a/.env.sample b/.env.sample index 5c523ef4..a9c428fd 100644 --- a/.env.sample +++ b/.env.sample @@ -8,6 +8,8 @@ HF_TOKEN="Hugging Face API Token" ENVIRONMENT = "mainnet" NILAI_GUNICORN_WORKERS = 10 +AUTH_STRATEGY = "api_key" + # The domain name of the server # - It must be written as "localhost" or "test.nilai.nillion" # - Do not put "https://" or "http://" in the domain name or / at the end @@ -17,6 +19,9 @@ NILAI_SERVER_DOMAIN = "localhost" ATTESTATION_HOST = "attestation" ATTESTATION_PORT = 8080 +# nilAuth Trusted URLs +NILAUTH_TRUSTED_ROOT_ISSUERS = "http://localhost:30921" + # Postgres Docker Compose Config POSTGRES_HOST = "postgres" POSTGRES_USER = "user" diff --git a/nilai-api/alembic/versions/ca76e3ebe6ee_fix_remove_mail_and_adjust_field_lengths.py b/nilai-api/alembic/versions/ca76e3ebe6ee_fix_remove_mail_and_adjust_field_lengths.py new file mode 100644 index 00000000..4d176609 --- /dev/null +++ b/nilai-api/alembic/versions/ca76e3ebe6ee_fix_remove_mail_and_adjust_field_lengths.py @@ -0,0 +1,82 @@ +"""fix: remove mail and adjust field lengths + +Revision ID: ca76e3ebe6ee +Revises: da89d3230653 +Create Date: 2025-04-25 09:08:37.775973 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "ca76e3ebe6ee" +down_revision: Union[str, None] = "da89d3230653" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "query_logs", + "userid", + existing_type=sa.VARCHAR(length=50), + type_=sa.String(length=75), + existing_nullable=False, + ) + op.alter_column( + "users", + "userid", + existing_type=sa.VARCHAR(length=50), + type_=sa.String(length=75), + existing_nullable=False, + ) + op.alter_column( + "users", + "apikey", + existing_type=sa.VARCHAR(length=50), + type_=sa.String(length=75), + existing_nullable=False, + ) + op.drop_index("ix_users_email", table_name="users") + op.drop_index("ix_users_apikey", table_name="users") + op.create_index(op.f("ix_users_apikey"), "users", ["apikey"], unique=False) + op.drop_column("users", "email") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "users", + sa.Column("email", sa.VARCHAR(length=255), autoincrement=False, nullable=False), + ) + op.drop_index(op.f("ix_users_apikey"), table_name="users") + op.create_index("ix_users_apikey", "users", ["apikey"], unique=True) + op.create_index("ix_users_email", "users", ["email"], unique=True) + op.alter_column( + "users", + "apikey", + existing_type=sa.String(length=75), + type_=sa.VARCHAR(length=50), + existing_nullable=False, + ) + op.alter_column( + "users", + "userid", + existing_type=sa.String(length=75), + type_=sa.VARCHAR(length=50), + existing_nullable=False, + ) + op.alter_column( + "query_logs", + "userid", + existing_type=sa.String(length=75), + type_=sa.VARCHAR(length=50), + existing_nullable=False, + ) + # ### end Alembic commands ### diff --git a/nilai-api/gunicorn.conf.py b/nilai-api/gunicorn.conf.py index 3179284a..9cab55ee 100644 --- a/nilai-api/gunicorn.conf.py +++ b/nilai-api/gunicorn.conf.py @@ -5,7 +5,7 @@ bind = ["0.0.0.0:8080"] # Set the number of workers (2) -workers = SETTINGS["gunicorn_workers"] +workers = SETTINGS.gunicorn_workers # Set the number of threads per worker (16) threads = 1 diff --git a/nilai-api/private_key.key b/nilai-api/private_key.key new file mode 100644 index 00000000..4c56f3de --- /dev/null +++ b/nilai-api/private_key.key @@ -0,0 +1 @@ +.rqq7^XʏHœ)i_v diff --git a/nilai-api/pyproject.toml b/nilai-api/pyproject.toml index bc44d7be..7dbae40e 100644 --- a/nilai-api/pyproject.toml +++ b/nilai-api/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "verifier", "web3>=7.8.0", "click>=8.1.8", + "nuc", ] @@ -40,3 +41,4 @@ build-backend = "hatchling.build" [tool.uv.sources] nilai-common = { workspace = true } +nuc = { git = "https://github.com/NillionNetwork/nuc-py.git" } diff --git a/nilai-api/src/nilai_api/attestation/__init__.py b/nilai-api/src/nilai_api/attestation/__init__.py index 17857c07..1eb4434e 100644 --- a/nilai-api/src/nilai_api/attestation/__init__.py +++ b/nilai-api/src/nilai_api/attestation/__init__.py @@ -1,6 +1,6 @@ from fastapi import HTTPException import httpx -from nilai_common import SETTINGS, Nonce, AttestationReport +from nilai_common import Nonce, AttestationReport, SETTINGS from nilai_common.logger import setup_logger logger = setup_logger(__name__) @@ -12,7 +12,7 @@ async def get_attestation_report( """Get the attestation report for the given nonce""" try: - attestation_url = f"http://{SETTINGS['attestation_host']}:{SETTINGS['attestation_port']}/attestation/report" + attestation_url = f"http://{SETTINGS.attestation_host}:{SETTINGS.attestation_port}/attestation/report" async with httpx.AsyncClient() as client: response: httpx.Response = await client.get(attestation_url, params=nonce) report = AttestationReport(**response.json()) @@ -24,7 +24,7 @@ async def get_attestation_report( async def verify_attestation_report(attestation_report: AttestationReport) -> bool: """Verify the attestation report""" try: - attestation_url = f"http://{SETTINGS['attestation_host']}:{SETTINGS['attestation_port']}/attestation/verify" + attestation_url = f"http://{SETTINGS.attestation_host}:{SETTINGS.attestation_port}/attestation/verify" async with httpx.AsyncClient() as client: response: httpx.Response = await client.post( attestation_url, json=attestation_report.model_dump() diff --git a/nilai-api/src/nilai_api/auth/__init__.py b/nilai-api/src/nilai_api/auth/__init__.py index 2287feb1..17fa9146 100644 --- a/nilai-api/src/nilai_api/auth/__init__.py +++ b/nilai-api/src/nilai_api/auth/__init__.py @@ -8,6 +8,8 @@ from nilai_api.db.users import UserManager, UserModel from nilai_api.auth.strategies import STRATEGIES +from nuc.validate import ValidationException + logger = getLogger(__name__) bearer_scheme = HTTPBearer() @@ -38,6 +40,8 @@ async def get_user( raise e except ValueError as e: raise AuthenticationError(detail="Authentication failed: " + str(e)) + except ValidationException as e: + raise AuthenticationError(detail="NUC validation failed: " + str(e)) except Exception as e: raise AuthenticationError(detail="Unexpected authentication error: " + str(e)) diff --git a/nilai-api/src/nilai_api/auth/nuc.py b/nilai-api/src/nilai_api/auth/nuc.py new file mode 100644 index 00000000..0b9ab92a --- /dev/null +++ b/nilai-api/src/nilai_api/auth/nuc.py @@ -0,0 +1,74 @@ +from typing import Tuple +from nuc.validate import NucTokenValidator, ValidationParameters, InvocationRequirement +from nuc.envelope import NucTokenEnvelope +from nuc.nilauth import NilauthClient +from nuc.token import Did, NucToken +from functools import lru_cache +from nilai_api.config import NILAUTH_TRUSTED_ROOT_ISSUERS +from nilai_api.state import state + +from nilai_common.logger import setup_logger + +logger = setup_logger(__name__) + + +@lru_cache(maxsize=1) +def get_validator() -> NucTokenValidator: + """ + Get the public key of the Nilauth service + + Returns: + A NucTokenValidator that can be used to validate NUC tokens + The validator is cached to avoid re-initializing the validator for each request + """ + try: + nilauth_public_keys = [ + Did(NilauthClient(key).about().public_key.serialize()) + for key in NILAUTH_TRUSTED_ROOT_ISSUERS + ] + except Exception as e: + logger.error(f"Error getting validator: {e}") + raise e + + return NucTokenValidator(nilauth_public_keys) + + +@lru_cache(maxsize=1) +def get_validation_parameters() -> ValidationParameters: + """ + Get the validation parameters for the NUC token + + Returns: + The validation parameters for the NUC token + """ + default_parameters = ValidationParameters.default() + default_parameters.token_requirements = InvocationRequirement( + audience=Did(state.public_key.serialize()) + ) + return default_parameters + + +def validate_nuc(nuc_token: str) -> Tuple[str, str]: + """ + Validate a NUC token + + Args: + nuc_token: The NUC token to validate + + Returns: + The subscription holder and the user that the NUC token is for in hex format (str, str) + """ + nuc_token_envelope = NucTokenEnvelope.parse(nuc_token) + logger.info(f"Validating NUC token: {nuc_token_envelope.token.token}") + logger.info(f"Validation parameters: {get_validation_parameters()}") + logger.info(f"Public key: {state.public_key.serialize()}") + get_validator().validate(nuc_token_envelope, get_validation_parameters()) + token: NucToken = nuc_token_envelope.token.token + + # Validate the + # Return the subject of the token, the subscription holder + subscription_holder = token.subject.public_key.hex() + user = token.issuer.public_key.hex() + logger.info(f"Subscription holder: {subscription_holder}") + logger.info(f"User: {user}") + return subscription_holder, user diff --git a/nilai-api/src/nilai_api/auth/strategies.py b/nilai-api/src/nilai_api/auth/strategies.py index f343b212..871ad2ce 100644 --- a/nilai-api/src/nilai_api/auth/strategies.py +++ b/nilai-api/src/nilai_api/auth/strategies.py @@ -1,12 +1,17 @@ from nilai_api.db.users import UserManager, UserModel from nilai_api.auth.jwt import validate_jwt +from nilai_api.auth.nuc import validate_nuc +# All strategies must return a UserModel +# The strategies can raise any exception, which will be caught and converted to an AuthenticationError +# The exception detail will be passed to the client -async def api_key_strategy(api_key): + +async def api_key_strategy(api_key) -> UserModel: return await UserManager.check_api_key(api_key) -async def jwt_strategy(jwt_creds): +async def jwt_strategy(jwt_creds) -> UserModel: result = validate_jwt(jwt_creds) user = await UserManager.check_api_key(result.user_address) if user: @@ -14,16 +19,35 @@ async def jwt_strategy(jwt_creds): user = UserModel( userid=result.user_address, name=result.pub_key, - email=result.pub_key, apikey=result.user_address, ) await UserManager.insert_user_model(user) return user +async def nuc_strategy(nuc_token) -> UserModel: + """ + Validate a NUC token and return the user model + """ + subscription_holder, user = validate_nuc(nuc_token) + + user_model = await UserManager.check_user(user) + if user_model: + return user_model + + user_model = UserModel( + userid=user, + name=user, + apikey=subscription_holder, + ) + await UserManager.insert_user_model(user_model) + return user_model + + STRATEGIES = { "api_key": api_key_strategy, "jwt": jwt_strategy, + "nuc": nuc_strategy, } __all__ = ["STRATEGIES"] diff --git a/nilai-api/src/nilai_api/commands/add_user.py b/nilai-api/src/nilai_api/commands/add_user.py index 172ae961..521c546d 100644 --- a/nilai-api/src/nilai_api/commands/add_user.py +++ b/nilai-api/src/nilai_api/commands/add_user.py @@ -7,7 +7,6 @@ @click.command() @click.option("--name", type=str, required=True, help="User Name") -@click.option("--email", type=str, required=True, help="User Email") @click.option("--apikey", type=str, help="API Key") @click.option("--userid", type=str, help="User Id") @click.option("--ratelimit-day", type=int, help="number of request per day") @@ -15,7 +14,6 @@ @click.option("--ratelimit-minute", type=int, help="number of request per minute") def main( name, - email, apikey: str | None, userid: str | None, ratelimit_day: int | None, @@ -25,7 +23,6 @@ def main( async def add_user(): user = await UserManager.insert_user( name, - email, apikey, userid, ratelimit_day, @@ -36,7 +33,6 @@ async def add_user(): { "userid": user.userid, "name": user.name, - "email": user.email, "apikey": user.apikey, "ratelimit_day": user.ratelimit_day, "ratelimit_hour": user.ratelimit_hour, diff --git a/nilai-api/src/nilai_api/config/__init__.py b/nilai-api/src/nilai_api/config/__init__.py index 7b1ec498..781dbb3e 100644 --- a/nilai-api/src/nilai_api/config/__init__.py +++ b/nilai-api/src/nilai_api/config/__init__.py @@ -21,7 +21,9 @@ DB_NAME = os.getenv("POSTGRES_DB", "nilai_users") -AUTH_STRATEGY = "api_key" +NILAUTH_TRUSTED_ROOT_ISSUERS = os.getenv("NILAUTH_TRUSTED_ROOT_ISSUERS", "").split(",") + +AUTH_STRATEGY = os.getenv("AUTH_STRATEGY", "api_key") if ENVIRONMENT == "mainnet": diff --git a/nilai-api/src/nilai_api/config/mainnet.py b/nilai-api/src/nilai_api/config/mainnet.py index 2a27af1a..3c1552f6 100644 --- a/nilai-api/src/nilai_api/config/mainnet.py +++ b/nilai-api/src/nilai_api/config/mainnet.py @@ -1,5 +1,3 @@ -AUTH_STRATEGY = "api_key" - # It defines the number of concurrent requests allowed for each model. # At a same point of time, if all models are available, # there can be 45 + 30 + 15 + 5 = 85 concurrent requests in the system diff --git a/nilai-api/src/nilai_api/config/testnet.py b/nilai-api/src/nilai_api/config/testnet.py index 8efcb7df..7e6bdf1b 100644 --- a/nilai-api/src/nilai_api/config/testnet.py +++ b/nilai-api/src/nilai_api/config/testnet.py @@ -1,5 +1,3 @@ -AUTH_STRATEGY = "jwt" - # It defines the number of concurrent requests allowed for each model. # At a same point of time, if all models are available, # there can be 10 + 10 + 5 + 5 = 30 concurrent requests in the system diff --git a/nilai-api/src/nilai_api/db/logs.py b/nilai-api/src/nilai_api/db/logs.py index c4e4b854..36ab6e7c 100644 --- a/nilai-api/src/nilai_api/db/logs.py +++ b/nilai-api/src/nilai_api/db/logs.py @@ -17,7 +17,7 @@ class QueryLog(Base): id: int = Column(Integer, primary_key=True, autoincrement=True) # type: ignore userid: str = Column( - String(36), ForeignKey(UserModel.userid), nullable=False, index=True + String(75), ForeignKey(UserModel.userid), nullable=False, index=True ) # type: ignore query_timestamp: datetime = Column( DateTime, server_default=sqlalchemy.func.now(), nullable=False diff --git a/nilai-api/src/nilai_api/db/users.py b/nilai-api/src/nilai_api/db/users.py index 048ac97e..2f9aae14 100644 --- a/nilai-api/src/nilai_api/db/users.py +++ b/nilai-api/src/nilai_api/db/users.py @@ -23,10 +23,9 @@ class UserModel(Base): __tablename__ = "users" - userid: str = Column(String(50), primary_key=True, index=True) # type: ignore + userid: str = Column(String(75), primary_key=True, index=True) # type: ignore name: str = Column(String(100), nullable=False) # type: ignore - email: str = Column(String(255), unique=True, nullable=False, index=True) # type: ignore - apikey: str = Column(String(50), unique=True, nullable=False, index=True) # type: ignore + apikey: str = Column(String(75), unique=False, nullable=False, index=True) # type: ignore prompt_tokens: int = Column(Integer, default=0, nullable=False) # type: ignore completion_tokens: int = Column(Integer, default=0, nullable=False) # type: ignore queries: int = Column(Integer, default=0, nullable=False) # type: ignore @@ -39,7 +38,7 @@ class UserModel(Base): ratelimit_minute: int = Column(Integer, default=10, nullable=True) # type: ignore def __repr__(self): - return f"" + return f"" @dataclass @@ -86,7 +85,6 @@ async def update_last_activity(userid: str): @staticmethod async def insert_user( name: str, - email: str, apikey: str | None = None, userid: str | None = None, ratelimit_day: int | None = USER_RATE_LIMIT_DAY, @@ -98,7 +96,6 @@ async def insert_user( Args: name (str): Name of the user - email (str): Email of the user apikey (str): API key for the user userid (str): Unique ID for the user ratelimit_day (int): Daily rate limit @@ -118,7 +115,6 @@ async def insert_user( user = UserModel( userid=userid, name=name, - email=email, apikey=apikey, ratelimit_day=ratelimit_day, ratelimit_hour=ratelimit_hour, @@ -144,6 +140,27 @@ async def insert_user_model(user: UserModel) -> UserModel: logger.error(f"Error inserting user: {e}") raise + @staticmethod + async def check_user(userid: str) -> Optional[UserModel]: + """ + Validate a user. + + Args: + userid (str): User ID to validate + + Returns: + User's name if user is valid, None otherwise + """ + try: + async with get_db_session() as session: + query = sqlalchemy.select(UserModel).filter(UserModel.userid == userid) # type: ignore + user = await session.execute(query) + user = user.scalar_one_or_none() + return user + except SQLAlchemyError as e: + logger.error(f"Error checking API key: {e}") + return None + @staticmethod async def check_api_key(api_key: str) -> Optional[UserModel]: """ diff --git a/nilai-api/src/nilai_api/rate_limiting.py b/nilai-api/src/nilai_api/rate_limiting.py index 47d31eec..e888ba4c 100644 --- a/nilai-api/src/nilai_api/rate_limiting.py +++ b/nilai-api/src/nilai_api/rate_limiting.py @@ -48,8 +48,14 @@ class UserRateLimits(BaseModel): def get_user_limits(user: Annotated[UserModel, Depends(get_user)]) -> UserRateLimits: + # TODO: When the only allowed strategy is NUC, we can change the apikey name to subscription_holder + # In apikey mode, the apikey is unique as the userid. + # In nuc mode, the apikey is associated with a subscription holder and the userid is the user + # For NUCs we want the rate limit to be per subscription holder, not per user + # In JWT mode, the apikey is the userid too + # So we use the apikey as the id return UserRateLimits( - id=user.userid, + id=user.apikey, day_limit=user.ratelimit_day, hour_limit=user.ratelimit_hour, minute_limit=user.ratelimit_minute, diff --git a/nilai-attestation/gunicorn.conf.py b/nilai-attestation/gunicorn.conf.py index 2fc4fe4d..e19b2758 100644 --- a/nilai-attestation/gunicorn.conf.py +++ b/nilai-attestation/gunicorn.conf.py @@ -2,10 +2,10 @@ from nilai_common.config import SETTINGS # Bind to address and port -bind = [f"0.0.0.0:{SETTINGS['attestation_port']}"] +bind = [f"0.0.0.0:{SETTINGS.attestation_port}"] # Set the number of workers (2) -workers = SETTINGS["gunicorn_workers"] +workers = 1 # Set the number of threads per worker (16) threads = 1 diff --git a/nilai-models/src/nilai_models/daemon.py b/nilai-models/src/nilai_models/daemon.py index aef5ee10..7402f3f3 100644 --- a/nilai-models/src/nilai_models/daemon.py +++ b/nilai-models/src/nilai_models/daemon.py @@ -21,7 +21,7 @@ async def get_metadata(num_retries=30): while True: url = None try: - url = f"http://{SETTINGS['host']}:{SETTINGS['port']}/v1/models" + url = f"http://{SETTINGS.host}:{SETTINGS.port}/v1/models" # Request model metadata from localhost:8000/v1/models async with httpx.AsyncClient() as client: response = await client.get(url) @@ -37,7 +37,7 @@ async def get_metadata(num_retries=30): license="Apache 2.0", # Usage license source=f"https://huggingface.co/{model_name}", # Model source supported_features=["chat_completion"], # Capabilities - tool_support=SETTINGS["tool_support"], # Tool support + tool_support=SETTINGS.tool_support, # Tool support ) except Exception as e: @@ -81,12 +81,12 @@ async def main(): try: # Initialize discovery service discovery_service = ModelServiceDiscovery( - host=SETTINGS["etcd_host"], port=SETTINGS["etcd_port"] + host=SETTINGS.etcd_host, port=SETTINGS.etcd_port ) metadata = await get_metadata() model_endpoint = ModelEndpoint( - url=f"http://{SETTINGS['host']}:{SETTINGS['port']}", metadata=metadata + url=f"http://{SETTINGS.host}:{SETTINGS.port}", metadata=metadata ) # Setup signal handlers diff --git a/packages/nilai-common/src/nilai_common/config.py b/packages/nilai-common/src/nilai_common/config.py index 30ae951f..e597b695 100644 --- a/packages/nilai-common/src/nilai_common/config.py +++ b/packages/nilai-common/src/nilai_common/config.py @@ -1,24 +1,28 @@ import os - +from pydantic import BaseModel # from dotenv import load_dotenv # load_dotenv() # Only needed locally if using a .env file -SETTINGS = { - "host": os.getenv("SVC_HOST", "localhost"), - "port": os.getenv("SVC_PORT", 8000), - "etcd_host": os.getenv("ETCD_HOST", "localhost"), - "etcd_port": os.getenv("ETCD_PORT", 2379), - "tool_support": os.getenv("TOOL_SUPPORT", False), - "gunicorn_workers": os.getenv("NILAI_GUNICORN_WORKERS", 10), - "attestation_host": os.getenv("ATTESTATION_HOST", "localhost"), - "attestation_port": os.getenv("ATTESTATION_PORT", 8080), -} -# if environment == "docker": -# config = "docker_settings.py" -# else: -# config = "local_settings.py" -# # Import the appropriate config dynamically -# from importlib import import_module -# settings = import_module(config) +class HostSettings(BaseModel): + host: str = "localhost" + port: int = 8000 + etcd_host: str = "localhost" + etcd_port: int = 2379 + tool_support: bool = False + gunicorn_workers: int = 10 + attestation_host: str = "localhost" + attestation_port: int = 8081 + + +SETTINGS: HostSettings = HostSettings( + host=str(os.getenv("SVC_HOST", "localhost")), + port=int(os.getenv("SVC_PORT", 8000)), + etcd_host=str(os.getenv("ETCD_HOST", "localhost")), + etcd_port=int(os.getenv("ETCD_PORT", 2379)), + tool_support=bool(os.getenv("TOOL_SUPPORT", False)), + gunicorn_workers=int(os.getenv("NILAI_GUNICORN_WORKERS", 10)), + attestation_host=str(os.getenv("ATTESTATION_HOST", "localhost")), + attestation_port=int(os.getenv("ATTESTATION_PORT", 8081)), +) diff --git a/tests/unit/nilai_api/routers/test_private.py b/tests/unit/nilai_api/routers/test_private.py index 375a547c..f3138302 100644 --- a/tests/unit/nilai_api/routers/test_private.py +++ b/tests/unit/nilai_api/routers/test_private.py @@ -21,6 +21,7 @@ def mock_user(): mock = MagicMock(spec=UserModel) mock.userid = "test-user-id" mock.name = "Test User" + mock.apikey = "test-api-key" mock.prompt_tokens = 100 mock.completion_tokens = 50 mock.total_tokens = 150 From 42bca8c8c8943917e459f7a11851a55797d895bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Fri, 25 Apr 2025 17:15:56 +0200 Subject: [PATCH 04/14] feat: added example nilai-auth services --- README.md | 2 + nilai-auth/README.md | 13 + nilai-auth/nilai-auth-client/README.md | 43 ++ .../nilai-auth-client/examples/test.ipynb | 346 ++++++++++++++ nilai-auth/nilai-auth-client/pyproject.toml | 23 + .../src/nilai_auth_client/__init__.py | 2 + .../src/nilai_auth_client/main.py | 145 ++++++ .../src/nilai_auth_client/py.typed | 0 nilai-auth/nilai-auth-server/README.md | 57 +++ nilai-auth/nilai-auth-server/gunicorn.conf.py | 16 + nilai-auth/nilai-auth-server/pyproject.toml | 23 + .../src/nilai_auth_server/__init__.py | 2 + .../src/nilai_auth_server/app.py | 162 +++++++ .../src/nilai_auth_server/config.py | 9 + .../src/nilai_auth_server/py.typed | 0 .../src/nilai_auth_server/test copy.ipynb | 441 ++++++++++++++++++ pyproject.toml | 10 +- uv.lock | 288 ++++++++++-- 18 files changed, 1551 insertions(+), 31 deletions(-) create mode 100644 nilai-auth/README.md create mode 100644 nilai-auth/nilai-auth-client/README.md create mode 100644 nilai-auth/nilai-auth-client/examples/test.ipynb create mode 100644 nilai-auth/nilai-auth-client/pyproject.toml create mode 100644 nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py create mode 100644 nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py create mode 100644 nilai-auth/nilai-auth-client/src/nilai_auth_client/py.typed create mode 100644 nilai-auth/nilai-auth-server/README.md create mode 100644 nilai-auth/nilai-auth-server/gunicorn.conf.py create mode 100644 nilai-auth/nilai-auth-server/pyproject.toml create mode 100644 nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py create mode 100644 nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py create mode 100644 nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py create mode 100644 nilai-auth/nilai-auth-server/src/nilai_auth_server/py.typed create mode 100644 nilai-auth/nilai-auth-server/src/nilai_auth_server/test copy.ipynb diff --git a/README.md b/README.md index 99eae5c5..d3230a17 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,8 @@ git checkout v0.7.3 # We use v0.7.3 # Build vLLM OpenAI (vllm folder) cd vllm docker build -f Dockerfile.arm -t vllm/vllm-openai . --shm-size=4g +# Build nilai attestation container +docker build -t nillion/nilai-attestation:latest -f docker/attestation.Dockerfile . # Build vLLM docker container (root folder) docker build -t nillion/nilai-vllm:latest -f docker/vllm.Dockerfile . # Build nilai_api container diff --git a/nilai-auth/README.md b/nilai-auth/README.md new file mode 100644 index 00000000..296f3b76 --- /dev/null +++ b/nilai-auth/README.md @@ -0,0 +1,13 @@ +# Example: nilAuth services. + +# nilAuth Services + +This repository contains two main services: + +## nilai-auth-server + +This server acts as a delegation authority for Nillion User Compute (NUC) tokens, specifically for interacting with the Nilai API. It handles obtaining a root NUC token from a configured NilAuth instance, managing subscriptions on the Nillion Chain, and delegating compute capabilities to end-user public keys. See `nilai-auth/nilai-auth-server/README.md` for more details. + +## nilai-auth-client + +This client demonstrates the end-to-end process of authenticating with the Nilai API using Nillion User Compute (NUC) tokens obtained via the Nilai Auth Server. See `nilai-auth/nilai-auth-client/README.md` for more details. diff --git a/nilai-auth/nilai-auth-client/README.md b/nilai-auth/nilai-auth-client/README.md new file mode 100644 index 00000000..157ba8aa --- /dev/null +++ b/nilai-auth/nilai-auth-client/README.md @@ -0,0 +1,43 @@ +# Nilai Auth Client + +This client demonstrates the end-to-end process of authenticating with the Nilai API using Nillion User Compute (NUC) tokens obtained via the Nilai Auth Server. + +## Functionality + +1. **Key Generation:** Generates a new secp256k1 private/public key pair for the user. +2. **Request Delegation:** Sends the user's public key (base64 encoded) to the Nilai Auth Server (`/v1/delegate/` endpoint) to request a delegated NUC token. +3. **Token Validation:** Validates the received delegated token against the public key of the NilAuth instance (acting as the root issuer). +4. **Nilai Public Key Retrieval:** Fetches the public key of the target Nilai API instance (`/v1/public_key` endpoint). +5. **Invocation Token Creation:** Creates an invocation NUC token by: + * Extending the previously obtained delegated token. + * Setting the audience to the Nilai API's public key. + * Signing the invocation token with the user's private key. +6. **Invocation Token Validation:** Validates the created invocation token, ensuring it's correctly targeted at the Nilai API. +7. **API Call:** Uses the `openai` library (configured with the Nilai API base URL) to make a chat completion request. + * The invocation token is passed as the `api_key` in the request header. + * The Nilai API verifies this token before processing the request. +8. **Prints Response:** Outputs the response received from the Nilai API. + +## Prerequisites + +* A running **Nilai Auth Server** (default: `localhost:8100`). +* A running **Nilai API** instance (default: `localhost:8080`). +* A running **NilAuth** node (default: `localhost:30921`). + +## Running the Client + +```bash +cd nilai-auth/nilai-auth-client +# Make sure dependencies are installed (e.g., using uv or pip) +python src/nilai_auth_client/main.py +``` + +## Configuration + +Endpoints for the dependent services are currently hardcoded in `main.py`: + +* `SERVICE_ENDPOINT`: Nilai Auth Server (`localhost:8100`) +* `NILAI_ENDPOINT`: Nilai API (`localhost:8080`) +* `NILAUTH_ENDPOINT`: NilAuth Node (`localhost:30921`) + +These could be made configurable via environment variables or command-line arguments if needed. diff --git a/nilai-auth/nilai-auth-client/examples/test.ipynb b/nilai-auth/nilai-auth-client/examples/test.ipynb new file mode 100644 index 00000000..3a83ecf4 --- /dev/null +++ b/nilai-auth/nilai-auth-client/examples/test.ipynb @@ -0,0 +1,346 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\n", + "\n", + "Paying for wallet: nillion1mqukqr7d4s3eqhcxwctu7yypm560etp2dghpy6\n", + "Wallet balance: 999999992000000 unil\n", + "[>] Creating nilauth client\n", + "[>] Creating payer\n", + "IS SUBSCRIBED: True\n", + "[>] Subscription is already paid for\n", + "EXPIRES IN: 0:01:10.293477\n", + "CAN BE RENEWED IN: -1 day, 23:44:30.293450\n" + ] + } + ], + "source": [ + "from nuc.payer import Payer\n", + "from nuc.builder import NucTokenBuilder\n", + "from nuc.nilauth import NilauthClient\n", + "from nuc.envelope import NucTokenEnvelope\n", + "from nuc.token import Command, Did, InvocationBody, DelegationBody\n", + "from nuc.validate import NucTokenValidator, ValidationParameters, InvocationRequirement\n", + "from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey\n", + "from cosmpy.aerial.wallet import LocalWallet\n", + "from cosmpy.aerial.client import LedgerClient, NetworkConfig\n", + "from secp256k1 import PrivateKey as NilAuthPrivateKey\n", + "import base64\n", + "import datetime\n", + "\n", + "\n", + "def get_wallet():\n", + " keypair = NilchainPrivateKey(\"l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\")\n", + " print(keypair.private_key)\n", + " print(keypair.public_key)\n", + " wallet = LocalWallet(keypair, prefix=\"nillion\")\n", + " return wallet, keypair\n", + "\n", + "\n", + "def get_private_key():\n", + " private_key = NilAuthPrivateKey(\n", + " base64.b64decode(\"l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\")\n", + " )\n", + " return private_key\n", + "\n", + "\n", + "builder_private_key = get_private_key()\n", + "\n", + "wallet, keypair = get_wallet()\n", + "address = wallet.address()\n", + "print(\"Paying for wallet:\", wallet.address())\n", + "\n", + "cfg = NetworkConfig(\n", + " chain_id=\"nillion-chain-devnet\",\n", + " url=\"grpc+http://localhost:26649\",\n", + " fee_minimum_gas_price=1,\n", + " fee_denomination=\"unil\",\n", + " staking_denomination=\"unil\",\n", + ")\n", + "\n", + "ledger_client = LedgerClient(cfg)\n", + "\n", + "balances = ledger_client.query_bank_balance(address, \"unil\")\n", + "\n", + "print(f\"Wallet balance: {balances} unil\")\n", + "print(\"[>] Creating nilauth client\")\n", + "nilauth_client = NilauthClient(\"http://localhost:30921\")\n", + "\n", + "print(\"[>] Creating payer\")\n", + "payer = Payer(\n", + " wallet_private_key=keypair,\n", + " chain_id=\"nillion-chain-devnet\",\n", + " grpc_endpoint=\"http://localhost:26649\",\n", + " gas_limit=1000000000000,\n", + ")\n", + "# Pretty print the subscription details\n", + "subscription_details = nilauth_client.subscription_status(builder_private_key)\n", + "print(f\"IS SUBSCRIBED: {subscription_details.subscribed}\")\n", + "\n", + "\n", + "if not subscription_details.subscribed:\n", + " print(\"[>] Paying for subscription\")\n", + " nilauth_client.pay_subscription(\n", + " key=builder_private_key,\n", + " payer=payer,\n", + " )\n", + "else:\n", + " print(\"[>] Subscription is already paid for\")\n", + "\n", + " print(\n", + " f\"EXPIRES IN: {subscription_details.details.expires_at - datetime.datetime.now(datetime.timezone.utc)}\"\n", + " )\n", + " print(\n", + " f\"CAN BE RENEWED IN: {subscription_details.details.renewable_at - datetime.datetime.now(datetime.timezone.utc)}\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ='" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "keypair.private_key" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'97f49889fceed88a9cdddb16a161d13f6a12307c2b39163f3c3c397c3c2d2434'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "builder_private_key.serialize()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Root Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDc3NzEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjA1NWU2M2YxODhhOTQ3YTNiZmE2YTg1OWQyNzVjOGZmIiwicHJmIjpbXX0.LRGEAaWbxlODtqHG0KJx-iXZ_uZg3F8i2e-rk5b77ewwSLuESX6bTX53roZdqWxhC-0aPFnxmPtfeBRwMkbEig\n", + "Builder Private Key: 97f49889fceed88a9cdddb16a161d13f6a12307c2b39163f3c3c397c3c2d2434\n", + "Builder Public Key: 030923f2e7120c50e42905b857ddd2947f6ecced6bb02aab64e63b28e9e2e06d10\n", + "Delegated Private Key: 802712144df168d6e6d003b13dcfbb5275d49d886cff1bf27aebfd944aec4e36\n", + "Delegated Public Key: 030a7d5a9a3a4229fe82809d78bf3b9974da443b55e87fb36cfb527b15bb10b20f\n", + "Root Token Envelope: \n" + ] + } + ], + "source": [ + "root_token = nilauth_client.request_token(key=builder_private_key)\n", + "print(f\"Root Token: {root_token}\")\n", + "\n", + "print(\"Builder Private Key: \", builder_private_key.serialize())\n", + "print(\"Builder Public Key: \", builder_private_key.pubkey.serialize().hex())\n", + "delegated_key = NilAuthPrivateKey()\n", + "print(\"Delegated Private Key: \", delegated_key.serialize())\n", + "print(\"Delegated Public Key: \", delegated_key.pubkey.serialize().hex())\n", + "root_token_envelope = NucTokenEnvelope.parse(root_token)\n", + "print(f\"Root Token Envelope: {root_token_envelope}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Delegation Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDMwYTdkNWE5YTNhNDIyOWZlODI4MDlkNzhiZjNiOTk3NGRhNDQzYjU1ZTg3ZmIzNmNmYjUyN2IxNWJiMTBiMjBmIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogIjhjMzI0YWQ3ZjNkZDAxMmViY2Q4MmU4ZGVjOTJkMzVkIiwgInByZiI6IFsiNDRjYmU4OGE2ZTY5NjM4ZDUwYmUzZTQzYmE3YjRiMzEzNzQ4NGQ2ZTNhOWE3MWQ3ZWUxM2U3ZjFiM2NhMTRlZCJdfQ.cy797SekPbaItK9FQsDkDze0fNnrCF9R6yoKac7MWClyDNxjv2b8_SMlu-RgCUuRZhHFM2nOvXNAQ_b4l1OlOQ/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDc3NzEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjA1NWU2M2YxODhhOTQ3YTNiZmE2YTg1OWQyNzVjOGZmIiwicHJmIjpbXX0.LRGEAaWbxlODtqHG0KJx-iXZ_uZg3F8i2e-rk5b77ewwSLuESX6bTX53roZdqWxhC-0aPFnxmPtfeBRwMkbEig\n", + "Delegated Token Envelope: \n" + ] + } + ], + "source": [ + "delegated_token = (\n", + " NucTokenBuilder.extending(root_token_envelope)\n", + " .body(DelegationBody(policies=[]))\n", + " .audience(Did(delegated_key.pubkey.serialize()))\n", + " .command(Command([\"nil\", \"ai\", \"generate\"]))\n", + " .build(builder_private_key)\n", + ")\n", + "\n", + "print(f\"Delegation Token: {delegated_token}\")\n", + "\n", + "delegated_token_envelope = NucTokenEnvelope.parse(delegated_token)\n", + "\n", + "print(\"Delegated Token Envelope: \", delegated_token_envelope)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New Private Key: 848666abdf75e2d9359fc6b9927c218130a68f14689843ee114864da9be599a1\n", + "Delegated Key: 0242a4f768b88b552905d4e13dc2b363719295c87d096ffaef19a6fc02bff1ab03\n", + "Delegated Token Envelope: \n", + "Invocation: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzBhN2Q1YTlhM2E0MjI5ZmU4MjgwOWQ3OGJmM2I5OTc0ZGE0NDNiNTVlODdmYjM2Y2ZiNTI3YjE1YmIxMGIyMGYiLCAiYXVkIjogImRpZDpuaWw6MDI0MmE0Zjc2OGI4OGI1NTI5MDVkNGUxM2RjMmIzNjM3MTkyOTVjODdkMDk2ZmZhZWYxOWE2ZmMwMmJmZjFhYjAzIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJhcmdzIjoge30sICJub25jZSI6ICJkODk1OTQ4ZDZiNGY3NmU0ZWUyZTFjM2U3MDYxMGIyNyIsICJwcmYiOiBbImZlZGYxYzM0Njg4ZGRlYjgyOGIzN2U1MzZiMjFmNjA5MmE4MmVmZDRjYThiNzc5Y2UyYWRmZjBkODUxZTMyYTAiXX0.fCDUDPakJPezN0ppw2FfzkK4nGkDj2epgHkgLGyY8Utaz1uiWZCMhnr71HkCf25FtYkcZ_iGhDbOE8MlzkSeow/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDMwYTdkNWE5YTNhNDIyOWZlODI4MDlkNzhiZjNiOTk3NGRhNDQzYjU1ZTg3ZmIzNmNmYjUyN2IxNWJiMTBiMjBmIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogIjhjMzI0YWQ3ZjNkZDAxMmViY2Q4MmU4ZGVjOTJkMzVkIiwgInByZiI6IFsiNDRjYmU4OGE2ZTY5NjM4ZDUwYmUzZTQzYmE3YjRiMzEzNzQ4NGQ2ZTNhOWE3MWQ3ZWUxM2U3ZjFiM2NhMTRlZCJdfQ.cy797SekPbaItK9FQsDkDze0fNnrCF9R6yoKac7MWClyDNxjv2b8_SMlu-RgCUuRZhHFM2nOvXNAQ_b4l1OlOQ/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDc3NzEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjA1NWU2M2YxODhhOTQ3YTNiZmE2YTg1OWQyNzVjOGZmIiwicHJmIjpbXX0.LRGEAaWbxlODtqHG0KJx-iXZ_uZg3F8i2e-rk5b77ewwSLuESX6bTX53roZdqWxhC-0aPFnxmPtfeBRwMkbEig\n", + "--------------------------------\n" + ] + } + ], + "source": [ + "nilai_public_key = NilAuthPrivateKey()\n", + "\n", + "print(\"New Private Key: \", nilai_public_key.serialize())\n", + "print(\"Delegated Key: \", nilai_public_key.pubkey.serialize().hex())\n", + "print(\"Delegated Token Envelope: \", delegated_token_envelope)\n", + "\n", + "invocation = (\n", + " NucTokenBuilder.extending(delegated_token_envelope)\n", + " .body(InvocationBody(args={}))\n", + " .audience(Did(nilai_public_key.pubkey.serialize()))\n", + " .build(delegated_key)\n", + ")\n", + "\n", + "print(\"Invocation: \", invocation)\n", + "print(\"--------------------------------\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzBhN2Q1YTlhM2E0MjI5ZmU4MjgwOWQ3OGJmM2I5OTc0ZGE0NDNiNTVlODdmYjM2Y2ZiNTI3YjE1YmIxMGIyMGYiLCAiYXVkIjogImRpZDpuaWw6MDI0MmE0Zjc2OGI4OGI1NTI5MDVkNGUxM2RjMmIzNjM3MTkyOTVjODdkMDk2ZmZhZWYxOWE2ZmMwMmJmZjFhYjAzIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJhcmdzIjoge30sICJub25jZSI6ICJkODk1OTQ4ZDZiNGY3NmU0ZWUyZTFjM2U3MDYxMGIyNyIsICJwcmYiOiBbImZlZGYxYzM0Njg4ZGRlYjgyOGIzN2U1MzZiMjFmNjA5MmE4MmVmZDRjYThiNzc5Y2UyYWRmZjBkODUxZTMyYTAiXX0.fCDUDPakJPezN0ppw2FfzkK4nGkDj2epgHkgLGyY8Utaz1uiWZCMhnr71HkCf25FtYkcZ_iGhDbOE8MlzkSeow',\n", + " 'eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDMwYTdkNWE5YTNhNDIyOWZlODI4MDlkNzhiZjNiOTk3NGRhNDQzYjU1ZTg3ZmIzNmNmYjUyN2IxNWJiMTBiMjBmIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogIjhjMzI0YWQ3ZjNkZDAxMmViY2Q4MmU4ZGVjOTJkMzVkIiwgInByZiI6IFsiNDRjYmU4OGE2ZTY5NjM4ZDUwYmUzZTQzYmE3YjRiMzEzNzQ4NGQ2ZTNhOWE3MWQ3ZWUxM2U3ZjFiM2NhMTRlZCJdfQ.cy797SekPbaItK9FQsDkDze0fNnrCF9R6yoKac7MWClyDNxjv2b8_SMlu-RgCUuRZhHFM2nOvXNAQ_b4l1OlOQ',\n", + " 'eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDc3NzEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjA1NWU2M2YxODhhOTQ3YTNiZmE2YTg1OWQyNzVjOGZmIiwicHJmIjpbXX0.LRGEAaWbxlODtqHG0KJx-iXZ_uZg3F8i2e-rk5b77ewwSLuESX6bTX53roZdqWxhC-0aPFnxmPtfeBRwMkbEig']" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "invocation.split(\"/\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nilauth Public Key: did:nil:03520e70bd97a5fa6d70c614d50ee47bf445ae0b0941a1d61ddd5afa022b97ab14\n", + "Invocation Envelope: \n", + "Invocation Envelope Token Proofs: 2\n", + "Validating Root Token Envelope\n", + "Validating Delegated Token Envelope\n", + "None\n", + "Validating Invocation Envelope\n", + "InvocationRequirement(audience=Did(public_key=b'\\x02B\\xa4\\xf7h\\xb8\\x8bU)\\x05\\xd4\\xe1=\\xc2\\xb3cq\\x92\\x95\\xc8}\\to\\xfa\\xef\\x19\\xa6\\xfc\\x02\\xbf\\xf1\\xab\\x03'))\n", + "did:nil:0242a4f768b88b552905d4e13dc2b363719295c87d096ffaef19a6fc02bff1ab03 did:nil:0242a4f768b88b552905d4e13dc2b363719295c87d096ffaef19a6fc02bff1ab03\n" + ] + } + ], + "source": [ + "nilauth_public_key = Did(nilauth_client.about().public_key.serialize())\n", + "\n", + "print(f\"Nilauth Public Key: {nilauth_public_key}\")\n", + "\n", + "invocation_envelope = NucTokenEnvelope.parse(invocation)\n", + "\n", + "print(f\"Invocation Envelope: {invocation_envelope}\")\n", + "\n", + "print(f\"Invocation Envelope Token Proofs: {len(invocation_envelope.proofs)}\")\n", + "\n", + "\n", + "print(\"Validating Root Token Envelope\")\n", + "# NucTokenValidator([nilauth_public_key]).validate(root_token_envelope)\n", + "print(\"Validating Delegated Token Envelope\")\n", + "NucTokenValidator([nilauth_public_key]).validate(delegated_token_envelope)\n", + "print(\"Validating Invocation Envelope\")\n", + "default_parameters = ValidationParameters.default()\n", + "default_parameters.token_requirements = InvocationRequirement(\n", + " audience=Did(nilai_public_key.pubkey.serialize())\n", + ")\n", + "validation_parameters = default_parameters\n", + "NucTokenValidator([nilauth_public_key]).validate(\n", + " invocation_envelope, validation_parameters\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/nilai-auth/nilai-auth-client/pyproject.toml b/nilai-auth/nilai-auth-client/pyproject.toml new file mode 100644 index 00000000..3952fe71 --- /dev/null +++ b/nilai-auth/nilai-auth-client/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "nilai-auth-client" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "José Cabrero-Holgueras", email = "jose.cabrero@nillion.com" } +] +requires-python = ">=3.12" +dependencies = [ + "httpx>=0.28.1", + "nuc", + "openai>=1.70.0", + "pydantic>=2.11.2", + "secp256k1>=0.14.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv.sources] +nuc = { git = "https://github.com/NillionNetwork/nuc-py.git" } diff --git a/nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py b/nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py new file mode 100644 index 00000000..c08aca32 --- /dev/null +++ b/nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py @@ -0,0 +1,2 @@ +def hello() -> str: + return "Hello from nilai-auth-client!" diff --git a/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py b/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py new file mode 100644 index 00000000..7263218d --- /dev/null +++ b/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py @@ -0,0 +1,145 @@ +# Do an HTTP request to the nilai-auth-server +import httpx +from secp256k1 import PrivateKey as NilAuthPrivateKey, PublicKey as NilAuthPublicKey +from nuc.validate import NucTokenValidator, ValidationParameters, InvocationRequirement + +import base64 + +from pydantic import BaseModel +from nuc.envelope import NucTokenEnvelope +from nuc.builder import NucTokenBuilder +from nuc.token import InvocationBody, Did +from nuc.nilauth import NilauthClient + +import openai + +SERVICE_ENDPOINT = "localhost:8100" +NILAI_ENDPOINT = "localhost:8080" +NILAUTH_ENDPOINT = "localhost:30921" + + +class DelegationToken(BaseModel): + token: str + + +def get_delegation_token(b64_public_key: str): + """ + Get a delegation token for the given public key + + Args: + b64_public_key: The base64 encoded public key + + Returns: + delegation_token: The delegation token + """ + response = httpx.post( + f"http://{SERVICE_ENDPOINT}/v1/delegate/", + json={"user_public_key": b64_public_key}, + ) + return DelegationToken(**response.json()).token + + +def get_nilai_public_key(): + """ + Get the nilai public key + """ + response = httpx.get(f"http://{NILAI_ENDPOINT}/v1/public_key") + public_key = NilAuthPublicKey(base64.b64decode(response.text), raw=True) + print(f"Nilai public key: {public_key.serialize().hex()}") + return public_key + + +def make_invocation_token( + delegated_token: str, + nilai_public_key: NilAuthPublicKey, + delegated_key: NilAuthPrivateKey, +): + """ + Make an invocation token for the given delegated token and nilai public key + + Args: + delegated_token: The delegated token + nilai_public_key: The nilai public key + delegated_key: The private key + """ + delegated_token_envelope = NucTokenEnvelope.parse(delegated_token) + + invocation = ( + NucTokenBuilder.extending(delegated_token_envelope) + .body(InvocationBody(args={})) + .audience(Did(nilai_public_key.serialize())) + .build(delegated_key) + ) + return invocation + + +def get_nilauth_public_key(): + """ + Get the nilauth public key + """ + nilauth_client = NilauthClient(f"http://{NILAUTH_ENDPOINT}") + nilauth_public_key = Did(nilauth_client.about().public_key.serialize()) + return nilauth_public_key + + +def validate_token(token: str, validation_parameters: ValidationParameters): + validator = NucTokenValidator([get_nilauth_public_key()]) + + validator.validate(NucTokenEnvelope.parse(token), validation_parameters) + + print("[>] Token validated") + + +def main(): + """ + Main function + """ + private_key = NilAuthPrivateKey() + public_key = private_key.pubkey + + if public_key is None: + raise Exception("Failed to get public key") + + b64_public_key = base64.b64encode(public_key.serialize()).decode("utf-8") + + delegated_token = get_delegation_token(b64_public_key) + + validate_token(delegated_token, ValidationParameters.default()) + nilai_public_key = get_nilai_public_key() + if nilai_public_key is None: + raise Exception("Failed to get nilai public key") + + invocation_token = make_invocation_token( + delegated_token, nilai_public_key, private_key + ) + + print(f"Invocation token: {invocation_token}") + + default_parameters = ValidationParameters.default() + default_parameters.token_requirements = InvocationRequirement( + audience=Did(nilai_public_key.serialize()) + ) + validation_parameters = default_parameters + + validate_token(invocation_token, validation_parameters) + client = openai.OpenAI( + base_url=f"http://{NILAI_ENDPOINT}/v1", api_key=invocation_token + ) + + response = client.chat.completions.create( + model="meta-llama/Llama-3.2-1B-Instruct", + messages=[ + { + "role": "system", + "content": "You are a helpful assistant that provides accurate and concise information.", + }, + {"role": "user", "content": "What is the capital of France?"}, + ], + temperature=0.2, + max_tokens=100, + ) + print(response) + + +if __name__ == "__main__": + main() diff --git a/nilai-auth/nilai-auth-client/src/nilai_auth_client/py.typed b/nilai-auth/nilai-auth-client/src/nilai_auth_client/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/nilai-auth/nilai-auth-server/README.md b/nilai-auth/nilai-auth-server/README.md new file mode 100644 index 00000000..3ef6dddc --- /dev/null +++ b/nilai-auth/nilai-auth-server/README.md @@ -0,0 +1,57 @@ +# Nilai Auth Server + +This server acts as a delegation authority for Nillion User Compute (NUC) tokens, specifically for interacting with the Nilai API. It handles obtaining a root NUC token from a configured NilAuth instance, managing subscriptions on the Nillion Chain, and delegating compute capabilities to end-user public keys. + +## Functionality + +1. **Wallet Initialization:** On startup (or first request), it initializes a Nilchain wallet using a hardcoded private key (for development purposes). +2. **NilAuth Client:** Connects to a NilAuth instance specified in `NILAUTH_TRUSTED_ROOT_ISSUERS`. +3. **Subscription Management:** Checks the Nilchain subscription status associated with its wallet. If not subscribed, it pays for the subscription using its wallet. +4. **Root Token Retrieval:** Obtains a root NUC token from the NilAuth instance. +5. **Delegation Endpoint (`/v1/delegate/`):** + * Accepts a POST request containing the end-user's public key (`user_public_key`). + * Validates the subscription and root token. + * Creates a new NUC token, extending the root token's capabilities. + * Sets the audience of the new token to the provided user public key. + * Authorizes the `nil ai generate` command. + * Signs the new token with its private key. + * Returns the delegated NUC token to the user. + +## Prerequisites + +* A running NilAuth instance accessible at the URL(s) defined in the `NILAUTH_TRUSTED_ROOT_ISSUERS` environment variable (or configured within `nilai_auth_server/config.py`). +* A running Nillion Chain node accessible via gRPC (currently hardcoded to `http://localhost:26649`). +* The server's wallet must have sufficient `unil` tokens to pay for NilAuth subscriptions if needed. + +## Running the Server + +Use a ASGI server like Uvicorn: + +```bash +cd nilai-auth/nilai-auth-server +# Make sure dependencies are installed (e.g., using uv or pip) +uvicorn src.nilai_auth_server.app:app --host 0.0.0.0 --port 8100 --reload +``` + +## Configuration + +* **Private Key:** Currently hardcoded within `app.py`. **This should be replaced with a secure key management solution for production.** +* **Nilchain gRPC Endpoint:** Hardcoded to `http://localhost:26649` in `app.py`. Consider making this configurable. +* **NilAuth Trusted Issuers:** Configured via `NILAUTH_TRUSTED_ROOT_ISSUERS` in `config.py`. + +## API + +### POST `/v1/delegate/` + +* **Request Body:** + ```json + { + "user_public_key": "string (base64 encoded secp256k1 public key)" + } + ``` +* **Response Body:** + ```json + { + "token": "string (NUC token envelope)" + } + ``` diff --git a/nilai-auth/nilai-auth-server/gunicorn.conf.py b/nilai-auth/nilai-auth-server/gunicorn.conf.py new file mode 100644 index 00000000..494c3c5d --- /dev/null +++ b/nilai-auth/nilai-auth-server/gunicorn.conf.py @@ -0,0 +1,16 @@ +# gunicorn.config.py + +# Bind to address and port +bind = ["0.0.0.0:8080"] + +# Set the number of workers (2) +workers = 1 + +# Set the number of threads per worker (16) +threads = 1 + +# Set the timeout (120 seconds) +timeout = 120 + +# Set the worker class to UvicornWorker for async handling +worker_class = "uvicorn.workers.UvicornWorker" diff --git a/nilai-auth/nilai-auth-server/pyproject.toml b/nilai-auth/nilai-auth-server/pyproject.toml new file mode 100644 index 00000000..b3b7041d --- /dev/null +++ b/nilai-auth/nilai-auth-server/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "nilai-auth-server" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "José Cabrero-Holgueras", email = "jose.cabrero@nillion.com" } +] +requires-python = ">=3.12" +dependencies = [ + "cosmpy==0.9.2", + "fastapi[standard]>=0.115.5", + "gunicorn>=23.0.0", + "nuc", + "uvicorn>=0.34.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv.sources] +nuc = { git = "https://github.com/NillionNetwork/nuc-py.git" } diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py new file mode 100644 index 00000000..c8d9f1d6 --- /dev/null +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py @@ -0,0 +1,2 @@ +def hello() -> str: + return "Hello from nilai-auth-server!" diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py new file mode 100644 index 00000000..2c671a98 --- /dev/null +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py @@ -0,0 +1,162 @@ +from fastapi import FastAPI +from nuc.payer import Payer +from nuc.builder import NucTokenBuilder +from nuc.nilauth import NilauthClient +from nuc.envelope import NucTokenEnvelope +from nuc.token import Command, Did +from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey +from cosmpy.aerial.wallet import LocalWallet +from cosmpy.aerial.client import LedgerClient, NetworkConfig +from pydantic import BaseModel +from secp256k1 import PrivateKey as NilAuthPrivateKey, PublicKey as NilAuthPublicKey +import base64 +import datetime +from functools import lru_cache +from nilai_auth_server.config import NILAUTH_TRUSTED_ROOT_ISSUERS + +app = FastAPI() + +PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" +NILCHAIN_GRPC = "http://localhost:26649" + + +@lru_cache(maxsize=1) +def get_wallet_and_private_key(): + """ + Get the wallet and private key + + Returns: + wallet: The wallet + keypair: The keypair + private_key: The private key + """ + # FIXME: Use a real wallet, and don't hardcode the private key + keypair = NilchainPrivateKey(PRIVATE_KEY) + wallet = LocalWallet(keypair, prefix="nillion") + private_key = NilAuthPrivateKey(base64.b64decode(PRIVATE_KEY)) + return wallet, keypair, private_key + + +def get_root_token(nilauth_client, private_key) -> NucTokenEnvelope: + """ + Get the root token from nilauth + + Args: + nilauth_client: The nilauth client + private_key: The private key of the user + + Returns: + The root token + """ + + ## Getting the root token from nilauth + root_token: str = nilauth_client.request_token(key=private_key) + + root_token_envelope = NucTokenEnvelope.parse(root_token) + + return root_token_envelope + + +def get_unil_balance(address) -> int: + """ + Get the UNIL balance of the user + + Args: + address: The address of the user + """ + + cfg = NetworkConfig( + chain_id="nillion-chain-devnet", + url="grpc+" + NILCHAIN_GRPC, + fee_minimum_gas_price=1, + fee_denomination="unil", + staking_denomination="unil", + ) + ledger_client = LedgerClient(cfg) + balance = ledger_client.query_bank_balance(address, "unil") + return balance + + +def pay_for_subscription(nilauth_client, keypair, private_key): + """ + Pay for the subscription using the Nilchain keypair if the user is not subscribed + + Args: + nilauth_client: The nilauth client + keypair: The Nilchain keypair + private_key: The NilAuth private key of the user + """ + + payer = Payer( + wallet_private_key=keypair, + chain_id="nillion-chain-devnet", + grpc_endpoint=NILCHAIN_GRPC, + gas_limit=1000000000000, + ) + + # Pretty print the subscription details + subscription_details = nilauth_client.subscription_status(private_key) + print(f"IS SUBSCRIBED: {subscription_details.subscribed}") + + if not subscription_details.subscribed: + print("[>] Paying for subscription") + nilauth_client.pay_subscription( + key=private_key, + payer=payer, + ) + else: + print("[>] Subscription is already paid for") + + print( + f"EXPIRES IN: {subscription_details.details.expires_at - datetime.datetime.now(datetime.timezone.utc)}" + ) + print( + f"CAN BE RENEWED IN: {subscription_details.details.renewable_at - datetime.datetime.now(datetime.timezone.utc)}" + ) + + +class DelegateRequest(BaseModel): + user_public_key: str + + +class DelegationToken(BaseModel): + token: str + + +@app.post("/v1/delegate/") +def delegate(request: DelegateRequest) -> DelegationToken: + """ + Delegate the root token to the delegated key + + Args: + request: The request body + """ + + wallet, keypair, private_key = get_wallet_and_private_key() + balance = get_unil_balance(wallet.address()) + + print(f"Wallet balance: {balance} unil") + print("[>] Creating nilauth client") + nilauth_clients = [NilauthClient(url) for url in NILAUTH_TRUSTED_ROOT_ISSUERS] + nilauth_client = nilauth_clients[0] + pay_for_subscription(nilauth_client, keypair, private_key) + + root_token = get_root_token(nilauth_client, private_key) + + user_public_key = NilAuthPublicKey( + base64.b64decode(request.user_public_key), raw=True + ) + + delegated_token = ( + NucTokenBuilder.extending(root_token) + .audience(Did(user_public_key.serialize())) + .command(Command(["nil", "ai", "generate"])) + .build(private_key) + ) + return DelegationToken(token=delegated_token) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8100) diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py new file mode 100644 index 00000000..aeb89583 --- /dev/null +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py @@ -0,0 +1,9 @@ +import os + +from dotenv import load_dotenv + +load_dotenv() + +NILAUTH_TRUSTED_ROOT_ISSUERS = os.getenv("NILAUTH_TRUSTED_ROOT_ISSUERS", "").split(",") + +print("With trusted root issuers: ", NILAUTH_TRUSTED_ROOT_ISSUERS) diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/py.typed b/nilai-auth/nilai-auth-server/src/nilai_auth_server/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/test copy.ipynb b/nilai-auth/nilai-auth-server/src/nilai_auth_server/test copy.ipynb new file mode 100644 index 00000000..529e9dea --- /dev/null +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/test copy.ipynb @@ -0,0 +1,441 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Paying for wallet: nillion1xllevsf8c76za7p84hc98ytzu2zskd8s6dqay5\n", + "Wallet balance: 999967000000 unil\n", + "[>] Creating nilauth client\n", + "[>] Creating payer\n", + "IS SUBSCRIBED: False\n", + "[>] Paying for subscription\n", + "Wallet funds: 999967000000\n", + "Nilauth Public Key: did:nil:03520e70bd97a5fa6d70c614d50ee47bf445ae0b0941a1d61ddd5afa022b97ab14\n" + ] + } + ], + "source": [ + "from nuc.payer import Payer\n", + "from nuc.builder import NucTokenBuilder\n", + "from nuc.nilauth import NilauthClient\n", + "from nuc.envelope import NucTokenEnvelope\n", + "from nuc.token import Command, Did, InvocationBody, DelegationBody\n", + "from nuc.validate import NucTokenValidator\n", + "from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey\n", + "from cosmpy.aerial.wallet import LocalWallet\n", + "from cosmpy.aerial.client import LedgerClient, NetworkConfig\n", + "from secp256k1 import PrivateKey as NilAuthPrivateKey\n", + "import base64\n", + "import datetime\n", + "\n", + "\n", + "def get_wallet():\n", + " keypair = NilchainPrivateKey(\"U8LrusERG5Z30Dagnjm3/aNe53l/M6MBenW+mA3xwO4=\")\n", + " wallet = LocalWallet(keypair, prefix=\"nillion\")\n", + " return wallet, keypair\n", + "\n", + "\n", + "def get_private_key():\n", + " private_key = NilAuthPrivateKey(\n", + " base64.b64decode(\"U8LrusERG5Z30Dagnjm3/aNe53l/M6MBenW+mA3xwO4=\")\n", + " )\n", + " return private_key\n", + "\n", + "\n", + "builder_private_key = get_private_key()\n", + "\n", + "wallet, keypair = get_wallet()\n", + "address = wallet.address()\n", + "print(\"Paying for wallet:\", wallet.address())\n", + "\n", + "cfg = NetworkConfig(\n", + " chain_id=\"nillion-chain-devnet\",\n", + " url=\"grpc+http://localhost:30649\",\n", + " fee_minimum_gas_price=1,\n", + " fee_denomination=\"unil\",\n", + " staking_denomination=\"unil\",\n", + ")\n", + "\n", + "ledger_client = LedgerClient(cfg)\n", + "\n", + "balances = ledger_client.query_bank_balance(address, \"unil\")\n", + "\n", + "print(f\"Wallet balance: {balances} unil\")\n", + "print(\"[>] Creating nilauth client\")\n", + "nilauth_client = NilauthClient(\"http://localhost:30921\")\n", + "\n", + "print(\"[>] Creating payer\")\n", + "payer = Payer(\n", + " wallet_private_key=keypair,\n", + " chain_id=\"nillion-chain-devnet\",\n", + " grpc_endpoint=\"http://localhost:30649\",\n", + " gas_limit=1000000000000,\n", + ")\n", + "# Pretty print the subscription details\n", + "subscription_details = nilauth_client.subscription_status(builder_private_key)\n", + "print(f\"IS SUBSCRIBED: {subscription_details.subscribed}\")\n", + "\n", + "\n", + "if not subscription_details.subscribed:\n", + " print(\"[>] Paying for subscription\")\n", + " nilauth_client.pay_subscription(\n", + " key=builder_private_key,\n", + " payer=payer,\n", + " )\n", + "else:\n", + " print(\"[>] Subscription is already paid for\")\n", + "\n", + " print(\n", + " f\"EXPIRES IN: {subscription_details.details.expires_at - datetime.datetime.now(datetime.timezone.utc)}\"\n", + " )\n", + " print(\n", + " f\"CAN BE RENEWED IN: {subscription_details.details.renewable_at - datetime.datetime.now(datetime.timezone.utc)}\"\n", + " )\n", + "nilauth_public_key = Did(nilauth_client.about().public_key.serialize())\n", + "\n", + "print(f\"Nilauth Public Key: {nilauth_public_key}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New Private Key: eb540d7477890364fe0c88159d550cf79d5d7944b826d5f31f1767ddecbb89bc\n", + "Delegated Key: 0230b806939879096581cba908d411cb13ae6568a1b39cedd88765ed4a66f08e68\n", + "New Private Key: aa9c6d8808d1fec4bfd85821821b675ebdbc9a756ed9998ca7e666d2a7caabc1\n", + "Delegated Key: 03cf80bed14919fdafce672893733b0af7bf4d7b5c99ab1c1f7d537060a6ffe95f\n" + ] + } + ], + "source": [ + "delegated_key = NilAuthPrivateKey()\n", + "print(\"New Private Key: \", delegated_key.serialize())\n", + "print(\"Delegated Key: \", delegated_key.pubkey.serialize().hex())\n", + "\n", + "nilai_public_key = NilAuthPrivateKey()\n", + "\n", + "print(\"New Private Key: \", nilai_public_key.serialize())\n", + "print(\"Delegated Key: \", nilai_public_key.pubkey.serialize().hex())" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Root Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDNhZWZmNmY0YjQ0Yzk1ZDA5NmE5ZjEyNDZjZWFmNDViNGE5MGU4NzY4MDVmZjBhNGZkNmJjNDA5YmViNzNmNTcxIiwic3ViIjoiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCJleHAiOjE3NDUzMzc3OTEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjY0MjAyYzlmZTFlZTJlZWExNzJjMDQ1YWI0ODc0MjlmIiwicHJmIjpbXX0.BAAHSDM0RP8Xci0ifOKaPPbEdUS3pQG-e6vv6oCIN2RIuoBdlCABtFCpXi5mEGUsU_pcZ4zRVNxSbV-pqkSA4Q\n", + "Root Token Envelope: \n" + ] + } + ], + "source": [ + "root_token = nilauth_client.request_token(key=builder_private_key)\n", + "print(f\"Root Token: {root_token}\")\n", + "root_token_envelope = NucTokenEnvelope.parse(root_token)\n", + "print(f\"Root Token Envelope: {root_token_envelope}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Delegation Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCAiYXVkIjogImRpZDpuaWw6MDIzMGI4MDY5Mzk4NzkwOTY1ODFjYmE5MDhkNDExY2IxM2FlNjU2OGExYjM5Y2VkZDg4NzY1ZWQ0YTY2ZjA4ZTY4IiwgInN1YiI6ICJkaWQ6bmlsOjAzYWVmZjZmNGI0NGM5NWQwOTZhOWYxMjQ2Y2VhZjQ1YjRhOTBlODc2ODA1ZmYwYTRmZDZiYzQwOWJlYjczZjU3MSIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogIjNjZWU0OTBkMjM3YWQ4NTg1NThmZDMzZGViNGU0ZjkyIiwgInByZiI6IFsiMjczOTBjZmVkZmUyZGIxODJmYWM0ZjEyYTI0ZDc1NjczMTljNzU3NTg0MDMyNWM3YWI0NmQwOGZkZjQxZjUyNSJdfQ.UB6061VreW9a2hy03M6IWq9TjbpZgPuW4pj3HO3ziXEQ_XKasp--Q-rksEZ6VGQ72I7XS3Q3XV75b1kO7DxRYw/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDNhZWZmNmY0YjQ0Yzk1ZDA5NmE5ZjEyNDZjZWFmNDViNGE5MGU4NzY4MDVmZjBhNGZkNmJjNDA5YmViNzNmNTcxIiwic3ViIjoiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCJleHAiOjE3NDUzMzc3OTEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjY0MjAyYzlmZTFlZTJlZWExNzJjMDQ1YWI0ODc0MjlmIiwicHJmIjpbXX0.BAAHSDM0RP8Xci0ifOKaPPbEdUS3pQG-e6vv6oCIN2RIuoBdlCABtFCpXi5mEGUsU_pcZ4zRVNxSbV-pqkSA4Q\n", + "Delegated Token Envelope: \n", + "Validating Delegated Token Envelope\n", + "None\n" + ] + } + ], + "source": [ + "delegated_token = (\n", + " NucTokenBuilder.extending(root_token_envelope)\n", + " .body(DelegationBody(policies=[]))\n", + " .audience(Did(delegated_key.pubkey.serialize()))\n", + " .command(Command([\"nil\", \"ai\", \"generate\"]))\n", + " .build(builder_private_key)\n", + ")\n", + "\n", + "print(f\"Delegation Token: {delegated_token}\")\n", + "\n", + "delegated_token_envelope = NucTokenEnvelope.parse(delegated_token)\n", + "\n", + "print(\"Delegated Token Envelope: \", delegated_token_envelope)\n", + "\n", + "print(\"Validating Delegated Token Envelope\")\n", + "NucTokenValidator([nilauth_public_key]).validate(delegated_token_envelope)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Invocation: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCAiYXVkIjogImRpZDpuaWw6MDNjZjgwYmVkMTQ5MTlmZGFmY2U2NzI4OTM3MzNiMGFmN2JmNGQ3YjVjOTlhYjFjMWY3ZDUzNzA2MGE2ZmZlOTVmIiwgInN1YiI6ICJkaWQ6bmlsOjAzYWVmZjZmNGI0NGM5NWQwOTZhOWYxMjQ2Y2VhZjQ1YjRhOTBlODc2ODA1ZmYwYTRmZDZiYzQwOWJlYjczZjU3MSIsICJjbWQiOiAiL25pbCIsICJhcmdzIjoge30sICJub25jZSI6ICI1MDcwMjM4MWVjYzRmNTk0YTg4NmEwZjQ2OWRiMjRmNyIsICJwcmYiOiBbIjI3MzkwY2ZlZGZlMmRiMTgyZmFjNGYxMmEyNGQ3NTY3MzE5Yzc1NzU4NDAzMjVjN2FiNDZkMDhmZGY0MWY1MjUiXX0.qV-Urb8exWFwLOzbMNH7N0Fq-Cj7BFQcMWsm2ahdnbJncDz5BTzVsHMBPEJ96sOd2AFcL2i7SqbRm4CL0Jam3w/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDNhZWZmNmY0YjQ0Yzk1ZDA5NmE5ZjEyNDZjZWFmNDViNGE5MGU4NzY4MDVmZjBhNGZkNmJjNDA5YmViNzNmNTcxIiwic3ViIjoiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCJleHAiOjE3NDUzMzc3OTEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjY0MjAyYzlmZTFlZTJlZWExNzJjMDQ1YWI0ODc0MjlmIiwicHJmIjpbXX0.BAAHSDM0RP8Xci0ifOKaPPbEdUS3pQG-e6vv6oCIN2RIuoBdlCABtFCpXi5mEGUsU_pcZ4zRVNxSbV-pqkSA4Q\n", + "--------------------------------\n", + "Invocation Envelope: \n", + "Invocation Envelope Token Proofs: 1\n", + "Validating Invocation Envelope\n", + "None\n" + ] + } + ], + "source": [ + "invocation = (\n", + " NucTokenBuilder.extending(root_token_envelope)\n", + " .body(InvocationBody(args={}))\n", + " .audience(Did(nilai_public_key.pubkey.serialize()))\n", + " .build(builder_private_key)\n", + ")\n", + "\n", + "print(\"Invocation: \", invocation)\n", + "print(\"--------------------------------\")\n", + "\n", + "invocation_envelope = NucTokenEnvelope.parse(invocation)\n", + "\n", + "print(f\"Invocation Envelope: {invocation_envelope}\")\n", + "\n", + "print(f\"Invocation Envelope Token Proofs: {len(invocation_envelope.proofs)}\")\n", + "\n", + "\n", + "print(\"Validating Invocation Envelope\")\n", + "NucTokenValidator([nilauth_public_key]).validate(invocation_envelope)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Did(public_key=b'\\x03\\xae\\xffoKD\\xc9]\\tj\\x9f\\x12F\\xce\\xafE\\xb4\\xa9\\x0e\\x87h\\x05\\xff\\nO\\xd6\\xbc@\\x9b\\xebs\\xf5q')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "invocation_envelope.token.token.subject" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Public Key (base64): A/1ubGp+RMoFrhD5ahy0zf5PszhzmoZbbNjV2SrZ+xtg\n", + "Signature (base64): MEQCIGco8+RDttKxejy0eBrbaCgQTRJpERO3X1LamVfL0hv8AiArle4uJ8SErO0v1q5deaEHSI35/yOesM3lah/wFpdBHg==\n", + "Is the signature valid? True\n" + ] + } + ], + "source": [ + "from base64 import b64encode, b64decode\n", + "from secp256k1 import PrivateKey, PublicKey\n", + "\n", + "# Generate a new private key\n", + "private_key = PrivateKey()\n", + "public_key = private_key.pubkey\n", + "\n", + "# Serialize the public key to base64 (compressed form)\n", + "verifying_key = b64encode(public_key.serialize()).decode()\n", + "print(\"Public Key (base64):\", verifying_key)\n", + "\n", + "# Message to sign\n", + "message = b\"Hello secp256k1\"\n", + "\n", + "# Sign the message\n", + "signature = private_key.ecdsa_sign(message)\n", + "serialized_sig = private_key.ecdsa_serialize(signature)\n", + "\n", + "# Encode the signature to base64 for transmission/storage\n", + "sig_base64 = b64encode(serialized_sig).decode()\n", + "print(\"Signature (base64):\", sig_base64)\n", + "\n", + "\n", + "# Verify the signature\n", + "is_valid = public_key.ecdsa_verify(message, signature)\n", + "print(\"Is the signature valid?\", is_valid)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Is the signature valid? True\n" + ] + } + ], + "source": [ + "# ----------- Verification -----------\n", + "\n", + "# Decode public key and signature from base64\n", + "pubkey_bytes = b64decode(verifying_key)\n", + "sig_bytes = b64decode(sig_base64)\n", + "\n", + "# Reconstruct the public key object\n", + "pubkey = PublicKey(pubkey_bytes, raw=True)\n", + "\n", + "# Deserialize the signature\n", + "sig = pubkey.ecdsa_deserialize(sig_bytes)\n", + "\n", + "# Verify the signature\n", + "is_valid = pubkey.ecdsa_verify(message, sig)\n", + "print(\"Is the signature valid?\", is_valid)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "b'\\xac\\xf7\\xa4\\x98\\x1d\\xd5\\xa7\\xf7\\xef\\x1e\\xfd0\\x07\\xf3\\x8d\\xc41\\xd1r\\xbd\\x04lx\\x13\\x0c\\x83\\xb5,\\x16\\xe1\\xc3\\x1d'\n", + "acf7a4981dd5a7f7ef1efd3007f38dc431d172bd046c78130c83b52c16e1c31d\n" + ] + }, + { + "ename": "AttributeError", + "evalue": "'str' object has no attribute 'bytes'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[42]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28mprint\u001b[39m(private_key.private_key)\n\u001b[32m 2\u001b[39m \u001b[38;5;28mprint\u001b[39m(private_key.serialize())\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[43mprivate_key\u001b[49m\u001b[43m.\u001b[49m\u001b[43mserialize\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbytes\u001b[49m()\n", + "\u001b[31mAttributeError\u001b[39m: 'str' object has no attribute 'bytes'" + ] + } + ], + "source": [ + "print(private_key.private_key)\n", + "print(private_key.serialize())\n", + "private_key.serialize().bytes()" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "from base64 import b64encode\n", + "import os\n", + "\n", + "from secp256k1 import PrivateKey, PublicKey\n", + "\n", + "\n", + "def generate_key_pair() -> tuple[PrivateKey, PublicKey, str]:\n", + " \"\"\"\n", + " Generate a new key pair and return the private key, public key, and base64 encoded public key.\n", + "\n", + " Returns:\n", + " tuple[PrivateKey, PublicKey, str]: A tuple containing the private key, public key, and base64 encoded public key.\n", + " \"\"\"\n", + " if os.path.exists(\"private_key.pem\"):\n", + " with open(\"private_key.pem\", \"rb\") as f:\n", + " private_key = PrivateKey(f.read())\n", + " else:\n", + " private_key = PrivateKey()\n", + " with open(\"private_key.pem\", \"wb\") as f:\n", + " f.write(private_key.private_key)\n", + "\n", + " public_key = private_key.pubkey\n", + " b64_public_key = b64encode(public_key.serialize()).decode()\n", + "\n", + " return private_key, public_key, b64_public_key" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(,\n", + " ,\n", + " 'AquGL1Q0qjyEcT4K3sjOXGDEZSzyWvO4/5essOhvQjV5')" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "generate_key_pair()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml index 8e7ba0df..a3166922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,11 +21,11 @@ dev = [ "isort>=5.13.2", "pytest-mock>=3.14.0", "pytest>=8.3.3", - "ruff>=0.11.4", + "ruff>=0.11.7", "uvicorn>=0.32.1", "pytest-asyncio>=0.25.0", "testcontainers>=4.9.1", - "pyright>=1.1.399", + "pyright>=1.1.400", "pre-commit>=4.1.0", "httpx>=0.28.1", ] @@ -38,14 +38,14 @@ build-backend = "setuptools.build_meta" find = { include = ["nilai"] } [tool.uv.workspace] -members = ["nilai-models", "nilai-api", "packages/nilai-common", "packages/verifier"] +members = ["nilai-models", "nilai-api", "packages/nilai-common", "nilai-auth/nilai-auth-server", "nilai-auth/nilai-auth-client"] [tool.uv.sources] nilai-common = { workspace = true } nilai-api = { workspace = true } nilai-models = { workspace = true } [tool.pyright] -exclude = [".venv", "packages/verifier/"] +exclude = [".venv"] [tool.ruff] -exclude = [".venv", "packages/verifier"] +exclude = [".venv"] diff --git a/uv.lock b/uv.lock index 7d80dbb1..1e21038a 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,8 @@ requires-python = ">=3.12" members = [ "nilai", "nilai-api", + "nilai-auth-client", + "nilai-auth-server", "nilai-common", "nilai-models", ] @@ -217,6 +219,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/45/302d6712a8ff733a259446a7d24ff3c868715103032f50eef0d93ba70221/bcl-2.3.1-cp39-abi3-win_amd64.whl", hash = "sha256:52cf26c4ecd76e806c6576c4848633ff44ebfff528fca63ad0e52085b6ba5aa9", size = 96394 }, ] +[[package]] +name = "bech32" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/fe/b67ac9b123e25a3c1b8fc3f3c92648804516ab44215adb165284e024c43f/bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899", size = 3695 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/41/7022a226e5a6ac7091a95ba36bad057012ab7330b9894ad4e14e31d0b858/bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981", size = 4587 }, +] + [[package]] name = "bitarray" version = "3.3.1" @@ -410,6 +421,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "cosmpy" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bech32" }, + { name = "ecdsa" }, + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "jsonschema" }, + { name = "protobuf" }, + { name = "pycryptodome" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/5f/39167cf97a03813911e518d1b615c4ef5fc3e4eb26454b8cb3b557a03fba/cosmpy-0.9.2.tar.gz", hash = "sha256:0f0eb80152f28ef5ee4d846d581d2e34ba2d952900f0e3570cacb84bb376f664", size = 205720 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/bf/2b5e594858b0d41e372c9e4f975b3e5b2b655af1670f3a600d684d5c68d4/cosmpy-0.9.2-py3-none-any.whl", hash = "sha256:3591311198b08a0aa75340851ca166669974f17ffaa207a8d2cb26504fb0fa19", size = 413103 }, +] + [[package]] name = "cryptography" version = "43.0.1" @@ -820,6 +851,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/39/49b4cbcecd17290974b38244f983a0f3e7cfde6b3f710f30958ca7312b27/futurist-3.1.0-py3-none-any.whl", hash = "sha256:a0733cd6f8ba8e173913336527a82843118980c19959645252eed1d8682e9dc1", size = 37119 }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530 }, +] + [[package]] name = "greenlet" version = "3.1.1" @@ -853,6 +896,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, ] +[[package]] +name = "grpcio" +version = "1.71.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/95/aa11fc09a85d91fbc7dd405dcb2a1e0256989d67bf89fa65ae24b3ba105a/grpcio-1.71.0.tar.gz", hash = "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c", size = 12549828 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/83/bd4b6a9ba07825bd19c711d8b25874cd5de72c2a3fbf635c3c344ae65bd2/grpcio-1.71.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537", size = 5184101 }, + { url = "https://files.pythonhosted.org/packages/31/ea/2e0d90c0853568bf714693447f5c73272ea95ee8dad107807fde740e595d/grpcio-1.71.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7", size = 11310927 }, + { url = "https://files.pythonhosted.org/packages/ac/bc/07a3fd8af80467390af491d7dc66882db43884128cdb3cc8524915e0023c/grpcio-1.71.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec", size = 5654280 }, + { url = "https://files.pythonhosted.org/packages/16/af/21f22ea3eed3d0538b6ef7889fce1878a8ba4164497f9e07385733391e2b/grpcio-1.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594", size = 6312051 }, + { url = "https://files.pythonhosted.org/packages/49/9d/e12ddc726dc8bd1aa6cba67c85ce42a12ba5b9dd75d5042214a59ccf28ce/grpcio-1.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c", size = 5910666 }, + { url = "https://files.pythonhosted.org/packages/d9/e9/38713d6d67aedef738b815763c25f092e0454dc58e77b1d2a51c9d5b3325/grpcio-1.71.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67", size = 6012019 }, + { url = "https://files.pythonhosted.org/packages/80/da/4813cd7adbae6467724fa46c952d7aeac5e82e550b1c62ed2aeb78d444ae/grpcio-1.71.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db", size = 6637043 }, + { url = "https://files.pythonhosted.org/packages/52/ca/c0d767082e39dccb7985c73ab4cf1d23ce8613387149e9978c70c3bf3b07/grpcio-1.71.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79", size = 6186143 }, + { url = "https://files.pythonhosted.org/packages/00/61/7b2c8ec13303f8fe36832c13d91ad4d4ba57204b1c723ada709c346b2271/grpcio-1.71.0-cp312-cp312-win32.whl", hash = "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a", size = 3604083 }, + { url = "https://files.pythonhosted.org/packages/fd/7c/1e429c5fb26122055d10ff9a1d754790fb067d83c633ff69eddcf8e3614b/grpcio-1.71.0-cp312-cp312-win_amd64.whl", hash = "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8", size = 4272191 }, + { url = "https://files.pythonhosted.org/packages/04/dd/b00cbb45400d06b26126dcfdbdb34bb6c4f28c3ebbd7aea8228679103ef6/grpcio-1.71.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379", size = 5184138 }, + { url = "https://files.pythonhosted.org/packages/ed/0a/4651215983d590ef53aac40ba0e29dda941a02b097892c44fa3357e706e5/grpcio-1.71.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3", size = 11310747 }, + { url = "https://files.pythonhosted.org/packages/57/a3/149615b247f321e13f60aa512d3509d4215173bdb982c9098d78484de216/grpcio-1.71.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db", size = 5653991 }, + { url = "https://files.pythonhosted.org/packages/ca/56/29432a3e8d951b5e4e520a40cd93bebaa824a14033ea8e65b0ece1da6167/grpcio-1.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29", size = 6312781 }, + { url = "https://files.pythonhosted.org/packages/a3/f8/286e81a62964ceb6ac10b10925261d4871a762d2a763fbf354115f9afc98/grpcio-1.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4", size = 5910479 }, + { url = "https://files.pythonhosted.org/packages/35/67/d1febb49ec0f599b9e6d4d0d44c2d4afdbed9c3e80deb7587ec788fcf252/grpcio-1.71.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3", size = 6013262 }, + { url = "https://files.pythonhosted.org/packages/a1/04/f9ceda11755f0104a075ad7163fc0d96e2e3a9fe25ef38adfc74c5790daf/grpcio-1.71.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b", size = 6643356 }, + { url = "https://files.pythonhosted.org/packages/fb/ce/236dbc3dc77cf9a9242adcf1f62538734ad64727fabf39e1346ad4bd5c75/grpcio-1.71.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637", size = 6186564 }, + { url = "https://files.pythonhosted.org/packages/10/fd/b3348fce9dd4280e221f513dd54024e765b21c348bc475516672da4218e9/grpcio-1.71.0-cp313-cp313-win32.whl", hash = "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb", size = 3601890 }, + { url = "https://files.pythonhosted.org/packages/be/f8/db5d5f3fc7e296166286c2a397836b8b042f7ad1e11028d82b061701f0f7/grpcio-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366", size = 4273308 }, +] + [[package]] name = "gunicorn" version = "23.0.0" @@ -1043,6 +1114,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, ] +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, +] + [[package]] name = "mako" version = "1.3.9" @@ -1233,11 +1331,11 @@ dev = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "isort", specifier = ">=5.13.2" }, { name = "pre-commit", specifier = ">=4.1.0" }, - { name = "pyright", specifier = ">=1.1.399" }, + { name = "pyright", specifier = ">=1.1.400" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-asyncio", specifier = ">=0.25.0" }, { name = "pytest-mock", specifier = ">=3.14.0" }, - { name = "ruff", specifier = ">=0.11.4" }, + { name = "ruff", specifier = ">=0.11.7" }, { name = "testcontainers", specifier = ">=4.9.1" }, { name = "uvicorn", specifier = ">=0.32.1" }, ] @@ -1259,6 +1357,7 @@ dependencies = [ { name = "httpx" }, { name = "nilai-common" }, { name = "nilrag" }, + { name = "nuc" }, { name = "openai" }, { name = "pg8000" }, { name = "prometheus-fastapi-instrumentator" }, @@ -1284,6 +1383,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.27.2" }, { name = "nilai-common", editable = "packages/nilai-common" }, { name = "nilrag", specifier = ">=0.1.11" }, + { name = "nuc", git = "https://github.com/NillionNetwork/nuc-py.git" }, { name = "openai", specifier = ">=1.59.9" }, { name = "pg8000", specifier = ">=1.31.2" }, { name = "prometheus-fastapi-instrumentator", specifier = ">=7.0.2" }, @@ -1295,6 +1395,48 @@ requires-dist = [ { name = "web3", specifier = ">=7.8.0" }, ] +[[package]] +name = "nilai-auth-client" +version = "0.1.0" +source = { editable = "nilai-auth/nilai-auth-client" } +dependencies = [ + { name = "httpx" }, + { name = "nuc" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "secp256k1" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "nuc", git = "https://github.com/NillionNetwork/nuc-py.git" }, + { name = "openai", specifier = ">=1.70.0" }, + { name = "pydantic", specifier = ">=2.11.2" }, + { name = "secp256k1", specifier = ">=0.14.0" }, +] + +[[package]] +name = "nilai-auth-server" +version = "0.1.0" +source = { editable = "nilai-auth/nilai-auth-server" } +dependencies = [ + { name = "cosmpy" }, + { name = "fastapi", extra = ["standard"] }, + { name = "gunicorn" }, + { name = "nuc" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "cosmpy", specifier = "==0.9.2" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.115.5" }, + { name = "gunicorn", specifier = ">=23.0.0" }, + { name = "nuc", git = "https://github.com/NillionNetwork/nuc-py.git" }, + { name = "uvicorn", specifier = ">=0.34.0" }, +] + [[package]] name = "nilai-common" version = "0.1.0" @@ -1369,6 +1511,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] +[[package]] +name = "nuc" +version = "0.0.0a0" +source = { git = "https://github.com/NillionNetwork/nuc-py.git#ff950979d3ebe0c0808453e578fc76c927575687" } +dependencies = [ + { name = "cosmpy" }, + { name = "requests" }, + { name = "secp256k1" }, +] + [[package]] name = "numpy" version = "1.26.4" @@ -1741,6 +1893,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, ] +[[package]] +name = "protobuf" +version = "4.25.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/d5/cccc7e82bbda9909ced3e7a441a24205ea07fea4ce23a772743c0c7611fa/protobuf-4.25.6.tar.gz", hash = "sha256:f8cfbae7c5afd0d0eaccbe73267339bff605a2315860bb1ba08eb66670a9a91f", size = 380631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/41/0ff3559d9a0fbdb37c9452f2b84e61f7784d8d7b9850182c7ef493f523ee/protobuf-4.25.6-cp310-abi3-win32.whl", hash = "sha256:61df6b5786e2b49fc0055f636c1e8f0aff263808bb724b95b164685ac1bcc13a", size = 392454 }, + { url = "https://files.pythonhosted.org/packages/79/84/c700d6c3f3be770495b08a1c035e330497a31420e4a39a24c22c02cefc6c/protobuf-4.25.6-cp310-abi3-win_amd64.whl", hash = "sha256:b8f837bfb77513fe0e2f263250f423217a173b6d85135be4d81e96a4653bcd3c", size = 413443 }, + { url = "https://files.pythonhosted.org/packages/b7/03/361e87cc824452376c2abcef0eabd18da78a7439479ec6541cf29076a4dc/protobuf-4.25.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:6d4381f2417606d7e01750e2729fe6fbcda3f9883aa0c32b51d23012bded6c91", size = 394246 }, + { url = "https://files.pythonhosted.org/packages/64/d5/7dbeb69b74fa88f297c6d8f11b7c9cef0c2e2fb1fdf155c2ca5775cfa998/protobuf-4.25.6-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:5dd800da412ba7f6f26d2c08868a5023ce624e1fdb28bccca2dc957191e81fb5", size = 293714 }, + { url = "https://files.pythonhosted.org/packages/d4/f0/6d5c100f6b18d973e86646aa5fc09bc12ee88a28684a56fd95511bceee68/protobuf-4.25.6-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:4434ff8bb5576f9e0c78f47c41cdf3a152c0b44de475784cd3fd170aef16205a", size = 294634 }, + { url = "https://files.pythonhosted.org/packages/71/eb/be11a1244d0e58ee04c17a1f939b100199063e26ecca8262c04827fe0bf5/protobuf-4.25.6-py3-none-any.whl", hash = "sha256:07972021c8e30b870cfc0863409d033af940213e0e7f64e27fe017b929d2c9f7", size = 156466 }, +] + [[package]] name = "psutil" version = "7.0.0" @@ -1865,15 +2031,15 @@ crypto = [ [[package]] name = "pyright" -version = "1.1.399" +version = "1.1.400" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584 }, + { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460 }, ] [[package]] @@ -2011,6 +2177,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, ] +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + [[package]] name = "regex" version = "2024.11.6" @@ -2103,29 +2283,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/fb/e4c0ced9893b84ac95b7181d69a9786ce5879aeb3bbbcbba80a164f85d6a/rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f", size = 19973 }, ] +[[package]] +name = "rpds-py" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/b3/52b213298a0ba7097c7ea96bee95e1947aa84cc816d48cebb539770cdf41/rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e", size = 26863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e0/1c55f4a3be5f1ca1a4fd1f3ff1504a1478c1ed48d84de24574c4fa87e921/rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205", size = 366945 }, + { url = "https://files.pythonhosted.org/packages/39/1b/a3501574fbf29118164314dbc800d568b8c1c7b3258b505360e8abb3902c/rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7", size = 351935 }, + { url = "https://files.pythonhosted.org/packages/dc/47/77d3d71c55f6a374edde29f1aca0b2e547325ed00a9da820cabbc9497d2b/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9", size = 390817 }, + { url = "https://files.pythonhosted.org/packages/4e/ec/1e336ee27484379e19c7f9cc170f4217c608aee406d3ae3a2e45336bff36/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e", size = 401983 }, + { url = "https://files.pythonhosted.org/packages/07/f8/39b65cbc272c635eaea6d393c2ad1ccc81c39eca2db6723a0ca4b2108fce/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda", size = 451719 }, + { url = "https://files.pythonhosted.org/packages/32/05/05c2b27dd9c30432f31738afed0300659cb9415db0ff7429b05dfb09bbde/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e", size = 442546 }, + { url = "https://files.pythonhosted.org/packages/7d/e0/19383c8b5d509bd741532a47821c3e96acf4543d0832beba41b4434bcc49/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029", size = 393695 }, + { url = "https://files.pythonhosted.org/packages/9d/15/39f14e96d94981d0275715ae8ea564772237f3fa89bc3c21e24de934f2c7/rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9", size = 427218 }, + { url = "https://files.pythonhosted.org/packages/22/b9/12da7124905a680f690da7a9de6f11de770b5e359f5649972f7181c8bf51/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7", size = 568062 }, + { url = "https://files.pythonhosted.org/packages/88/17/75229017a2143d915f6f803721a6d721eca24f2659c5718a538afa276b4f/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91", size = 596262 }, + { url = "https://files.pythonhosted.org/packages/aa/64/8e8a1d8bd1b6b638d6acb6d41ab2cec7f2067a5b8b4c9175703875159a7c/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56", size = 564306 }, + { url = "https://files.pythonhosted.org/packages/68/1c/a7eac8d8ed8cb234a9b1064647824c387753343c3fab6ed7c83481ed0be7/rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30", size = 224281 }, + { url = "https://files.pythonhosted.org/packages/bb/46/b8b5424d1d21f2f2f3f2d468660085318d4f74a8df8289e3dd6ad224d488/rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034", size = 239719 }, + { url = "https://files.pythonhosted.org/packages/9d/c3/3607abc770395bc6d5a00cb66385a5479fb8cd7416ddef90393b17ef4340/rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c", size = 367072 }, + { url = "https://files.pythonhosted.org/packages/d8/35/8c7ee0fe465793e3af3298dc5a9f3013bd63e7a69df04ccfded8293a4982/rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c", size = 351919 }, + { url = "https://files.pythonhosted.org/packages/91/d3/7e1b972501eb5466b9aca46a9c31bcbbdc3ea5a076e9ab33f4438c1d069d/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240", size = 390360 }, + { url = "https://files.pythonhosted.org/packages/a2/a8/ccabb50d3c91c26ad01f9b09a6a3b03e4502ce51a33867c38446df9f896b/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8", size = 400704 }, + { url = "https://files.pythonhosted.org/packages/53/ae/5fa5bf0f3bc6ce21b5ea88fc0ecd3a439e7cb09dd5f9ffb3dbe1b6894fc5/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8", size = 450839 }, + { url = "https://files.pythonhosted.org/packages/e3/ac/c4e18b36d9938247e2b54f6a03746f3183ca20e1edd7d3654796867f5100/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b", size = 441494 }, + { url = "https://files.pythonhosted.org/packages/bf/08/b543969c12a8f44db6c0f08ced009abf8f519191ca6985509e7c44102e3c/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d", size = 393185 }, + { url = "https://files.pythonhosted.org/packages/da/7e/f6eb6a7042ce708f9dfc781832a86063cea8a125bbe451d663697b51944f/rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7", size = 426168 }, + { url = "https://files.pythonhosted.org/packages/38/b0/6cd2bb0509ac0b51af4bb138e145b7c4c902bb4b724d6fd143689d6e0383/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad", size = 567622 }, + { url = "https://files.pythonhosted.org/packages/64/b0/c401f4f077547d98e8b4c2ec6526a80e7cb04f519d416430ec1421ee9e0b/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120", size = 595435 }, + { url = "https://files.pythonhosted.org/packages/9f/ec/7993b6e803294c87b61c85bd63e11142ccfb2373cf88a61ec602abcbf9d6/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9", size = 563762 }, + { url = "https://files.pythonhosted.org/packages/1f/29/4508003204cb2f461dc2b83dd85f8aa2b915bc98fe6046b9d50d4aa05401/rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143", size = 223510 }, + { url = "https://files.pythonhosted.org/packages/f9/12/09e048d1814195e01f354155fb772fb0854bd3450b5f5a82224b3a319f0e/rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a", size = 239075 }, + { url = "https://files.pythonhosted.org/packages/d2/03/5027cde39bb2408d61e4dd0cf81f815949bb629932a6c8df1701d0257fc4/rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114", size = 362974 }, + { url = "https://files.pythonhosted.org/packages/bf/10/24d374a2131b1ffafb783e436e770e42dfdb74b69a2cd25eba8c8b29d861/rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405", size = 348730 }, + { url = "https://files.pythonhosted.org/packages/7a/d1/1ef88d0516d46cd8df12e5916966dbf716d5ec79b265eda56ba1b173398c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47", size = 387627 }, + { url = "https://files.pythonhosted.org/packages/4e/35/07339051b8b901ecefd449ebf8e5522e92bcb95e1078818cbfd9db8e573c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272", size = 394094 }, + { url = "https://files.pythonhosted.org/packages/dc/62/ee89ece19e0ba322b08734e95441952062391065c157bbd4f8802316b4f1/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd", size = 449639 }, + { url = "https://files.pythonhosted.org/packages/15/24/b30e9f9e71baa0b9dada3a4ab43d567c6b04a36d1cb531045f7a8a0a7439/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a", size = 438584 }, + { url = "https://files.pythonhosted.org/packages/28/d9/49f7b8f3b4147db13961e19d5e30077cd0854ccc08487026d2cb2142aa4a/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d", size = 391047 }, + { url = "https://files.pythonhosted.org/packages/49/b0/e66918d0972c33a259ba3cd7b7ff10ed8bd91dbcfcbec6367b21f026db75/rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7", size = 418085 }, + { url = "https://files.pythonhosted.org/packages/e1/6b/99ed7ea0a94c7ae5520a21be77a82306aac9e4e715d4435076ead07d05c6/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d", size = 564498 }, + { url = "https://files.pythonhosted.org/packages/28/26/1cacfee6b800e6fb5f91acecc2e52f17dbf8b0796a7c984b4568b6d70e38/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797", size = 590202 }, + { url = "https://files.pythonhosted.org/packages/a9/9e/57bd2f9fba04a37cef673f9a66b11ca8c43ccdd50d386c455cd4380fe461/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c", size = 561771 }, + { url = "https://files.pythonhosted.org/packages/9f/cf/b719120f375ab970d1c297dbf8de1e3c9edd26fe92c0ed7178dd94b45992/rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba", size = 221195 }, + { url = "https://files.pythonhosted.org/packages/2d/e5/22865285789f3412ad0c3d7ec4dc0a3e86483b794be8a5d9ed5a19390900/rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350", size = 237354 }, +] + [[package]] name = "ruff" -version = "0.11.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 }, - { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 }, - { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 }, - { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 }, - { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 }, - { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 }, - { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 }, - { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 }, - { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 }, - { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 }, - { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 }, - { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 }, - { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 }, - { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 }, - { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 }, - { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 }, - { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 }, +version = "0.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/89/6f9c9674818ac2e9cc2f2b35b704b7768656e6b7c139064fc7ba8fbc99f1/ruff-0.11.7.tar.gz", hash = "sha256:655089ad3224070736dc32844fde783454f8558e71f501cb207485fe4eee23d4", size = 4054861 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ec/21927cb906c5614b786d1621dba405e3d44f6e473872e6df5d1a6bca0455/ruff-0.11.7-py3-none-linux_armv6l.whl", hash = "sha256:d29e909d9a8d02f928d72ab7837b5cbc450a5bdf578ab9ebee3263d0a525091c", size = 10245403 }, + { url = "https://files.pythonhosted.org/packages/e2/af/fec85b6c2c725bcb062a354dd7cbc1eed53c33ff3aa665165871c9c16ddf/ruff-0.11.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dd1fb86b168ae349fb01dd497d83537b2c5541fe0626e70c786427dd8363aaee", size = 11007166 }, + { url = "https://files.pythonhosted.org/packages/31/9a/2d0d260a58e81f388800343a45898fd8df73c608b8261c370058b675319a/ruff-0.11.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d3d7d2e140a6fbbc09033bce65bd7ea29d6a0adeb90b8430262fbacd58c38ada", size = 10378076 }, + { url = "https://files.pythonhosted.org/packages/c2/c4/9b09b45051404d2e7dd6d9dbcbabaa5ab0093f9febcae664876a77b9ad53/ruff-0.11.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4809df77de390a1c2077d9b7945d82f44b95d19ceccf0c287c56e4dc9b91ca64", size = 10557138 }, + { url = "https://files.pythonhosted.org/packages/5e/5e/f62a1b6669870a591ed7db771c332fabb30f83c967f376b05e7c91bccd14/ruff-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3a0c2e169e6b545f8e2dba185eabbd9db4f08880032e75aa0e285a6d3f48201", size = 10095726 }, + { url = "https://files.pythonhosted.org/packages/45/59/a7aa8e716f4cbe07c3500a391e58c52caf665bb242bf8be42c62adef649c/ruff-0.11.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49b888200a320dd96a68e86736cf531d6afba03e4f6cf098401406a257fcf3d6", size = 11672265 }, + { url = "https://files.pythonhosted.org/packages/dd/e3/101a8b707481f37aca5f0fcc3e42932fa38b51add87bfbd8e41ab14adb24/ruff-0.11.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2b19cdb9cf7dae00d5ee2e7c013540cdc3b31c4f281f1dacb5a799d610e90db4", size = 12331418 }, + { url = "https://files.pythonhosted.org/packages/dd/71/037f76cbe712f5cbc7b852e4916cd3cf32301a30351818d32ab71580d1c0/ruff-0.11.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64e0ee994c9e326b43539d133a36a455dbaab477bc84fe7bfbd528abe2f05c1e", size = 11794506 }, + { url = "https://files.pythonhosted.org/packages/ca/de/e450b6bab1fc60ef263ef8fcda077fb4977601184877dce1c59109356084/ruff-0.11.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bad82052311479a5865f52c76ecee5d468a58ba44fb23ee15079f17dd4c8fd63", size = 13939084 }, + { url = "https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7940665e74e7b65d427b82bffc1e46710ec7f30d58b4b2d5016e3f0321436502", size = 11450441 }, + { url = "https://files.pythonhosted.org/packages/9d/7d/1b048eb460517ff9accd78bca0fa6ae61df2b276010538e586f834f5e402/ruff-0.11.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:169027e31c52c0e36c44ae9a9c7db35e505fee0b39f8d9fca7274a6305295a92", size = 10441060 }, + { url = "https://files.pythonhosted.org/packages/3a/57/8dc6ccfd8380e5ca3d13ff7591e8ba46a3b330323515a4996b991b10bd5d/ruff-0.11.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:305b93f9798aee582e91e34437810439acb28b5fc1fee6b8205c78c806845a94", size = 10058689 }, + { url = "https://files.pythonhosted.org/packages/23/bf/20487561ed72654147817885559ba2aa705272d8b5dee7654d3ef2dbf912/ruff-0.11.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a681db041ef55550c371f9cd52a3cf17a0da4c75d6bd691092dfc38170ebc4b6", size = 11073703 }, + { url = "https://files.pythonhosted.org/packages/9d/27/04f2db95f4ef73dccedd0c21daf9991cc3b7f29901a4362057b132075aa4/ruff-0.11.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:07f1496ad00a4a139f4de220b0c97da6d4c85e0e4aa9b2624167b7d4d44fd6b6", size = 11532822 }, + { url = "https://files.pythonhosted.org/packages/e1/72/43b123e4db52144c8add336581de52185097545981ff6e9e58a21861c250/ruff-0.11.7-py3-none-win32.whl", hash = "sha256:f25dfb853ad217e6e5f1924ae8a5b3f6709051a13e9dad18690de6c8ff299e26", size = 10362436 }, + { url = "https://files.pythonhosted.org/packages/c5/a0/3e58cd76fdee53d5c8ce7a56d84540833f924ccdf2c7d657cb009e604d82/ruff-0.11.7-py3-none-win_amd64.whl", hash = "sha256:0a931d85959ceb77e92aea4bbedfded0a31534ce191252721128f77e5ae1f98a", size = 11566676 }, + { url = "https://files.pythonhosted.org/packages/68/ca/69d7c7752bce162d1516e5592b1cc6b6668e9328c0d270609ddbeeadd7cf/ruff-0.11.7-py3-none-win_arm64.whl", hash = "sha256:778c1e5d6f9e91034142dfd06110534ca13220bfaad5c3735f6cb844654f6177", size = 10677936 }, ] [[package]] @@ -2228,6 +2455,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/9f/8b2f2749ccfbe4fcef08650896ac47ed919ff25b7ac57b7a1ae7da16c8c3/scramp-1.4.5-py3-none-any.whl", hash = "sha256:50e37c464fc67f37994e35bee4151e3d8f9320e9c204fca83a5d313c121bbbe7", size = 12781 }, ] +[[package]] +name = "secp256k1" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/41/bb668a6e4192303542d2d90c3b38d564af3c17c61bd7d4039af4f29405fe/secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397", size = 2420607 } + [[package]] name = "sentence-transformers" version = "4.0.2" From 5b98e773edb95c8415aac4eefa710ba9176be949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Fri, 25 Apr 2025 17:36:26 +0200 Subject: [PATCH 05/14] fix: added pkg config to ci --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 815d0bc0..c020c27d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,9 +92,10 @@ jobs: with: enable-cache: true cache-dependency-glob: "**/pyproject.toml" - - name: Install dependencies - run: uv sync + run: | + apt-get update && apt-get install curl git pkg-config automake file -y + uv sync - name: Build vllm run: docker build -t nillion/nilai-vllm:latest -f docker/vllm.Dockerfile . @@ -126,7 +127,7 @@ jobs: - name: Run E2E tests run: | set -e - export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --email test1@test.com --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 | jq ".apikey" -r) + export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 | jq ".apikey" -r) export ENVIRONMENT=ci uv run pytest -v tests/e2e From 8777a68705c6c9d13a5bde5c435b94ca7a65ea90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Thu, 8 May 2025 11:33:04 +0200 Subject: [PATCH 06/14] feat: improved readme and tutorial --- README.md | 2 +- .../nilai-auth-client/examples/tutorial.ipynb | 473 ++++++++++++++++++ 2 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 nilai-auth/nilai-auth-client/examples/tutorial.ipynb diff --git a/README.md b/README.md index d3230a17..271f8213 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ docker build -t nillion/nilai-attestation:latest -f docker/attestation.Dockerfil # Build vLLM docker container docker build -t nillion/nilai-vllm:latest -f docker/vllm.Dockerfile . # Build nilai_api container -docker build -t nillion/nilai-api:latest -f docker/api.Dockerfile --target nilai . +docker build -t nillion/nilai-api:latest -f docker/api.Dockerfile --target nilai --platform linux/amd64 . ``` To deploy: ```shell diff --git a/nilai-auth/nilai-auth-client/examples/tutorial.ipynb b/nilai-auth/nilai-auth-client/examples/tutorial.ipynb new file mode 100644 index 00000000..05a24818 --- /dev/null +++ b/nilai-auth/nilai-auth-client/examples/tutorial.ipynb @@ -0,0 +1,473 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## NUC Token Delegation and Subscription Management Tutorial\n", + "\n", + "This notebook demonstrates the process of interacting with the NilAuth service to manage subscriptions and prepare for NUC token operations.\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Import Libraries:** Import necessary classes and functions from `nuc`, `cosmpy`, `secp256k1`, and standard Python libraries.\n", + "2. **Load Keys:** Define functions to load or generate the Nilchain wallet (`cosmpy`) private key and the NilAuth (`secp256k1`) private key. These keys are currently hardcoded for demonstration purposes. **Note:** Hardcoding keys is insecure for production environments.\n", + "3. **Initialize Keys and Wallet:** Call the functions to get the `NilAuthPrivateKey` (`builder_private_key`) and the `cosmpy` wallet and keypair. Print the wallet address.\n", + "4. **Configure Nilchain Connection:** Set up the `NetworkConfig` for connecting to the Nillion devnet.\n", + "5. **Connect to Ledger:** Create a `LedgerClient` instance using the network configuration.\n", + "6. **Query Balance:** Check and print the `unil` balance of the initialized wallet on the Nillion Chain.\n", + "7. **Initialize NilAuth Client:** Create a `NilauthClient` instance, connecting to the local NilAuth service endpoint.\n", + "8. **Initialize Payer:** Create a `Payer` object, configuring it with the Nilchain wallet, chain details, and gRPC endpoint. This object will be used to pay for transactions like subscriptions.\n", + "9. **Check Subscription Status:** Use the `NilauthClient` and the builder's private key to check the current subscription status associated with the key.\n", + "10. **Pay for Subscription (if necessary):**\n", + " * If the key is not currently subscribed, use the `nilauth_client.pay_subscription` method along with the `Payer` object to pay for a new subscription on the Nillion Chain.\n", + " * If already subscribed, print the time remaining until expiration and renewal availability." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\n", + "\n", + "Paying for wallet: nillion1mqukqr7d4s3eqhcxwctu7yypm560etp2dghpy6\n", + "Wallet balance: 999999989000000 unil\n", + "[>] Creating nilauth client\n", + "[>] Creating payer\n", + "IS SUBSCRIBED: True\n", + "[>] Subscription is already paid for\n", + "EXPIRES IN: 0:01:01.197447\n", + "CAN BE RENEWED IN: -1 day, 23:44:21.197434\n" + ] + } + ], + "source": [ + "# %% Import necessary libraries\n", + "from nuc.payer import Payer\n", + "from nuc.builder import NucTokenBuilder\n", + "from nuc.nilauth import NilauthClient\n", + "from nuc.envelope import NucTokenEnvelope\n", + "from nuc.token import Command, Did, InvocationBody, DelegationBody\n", + "from nuc.validate import (\n", + " NucTokenValidator,\n", + " ValidationParameters,\n", + " InvocationRequirement,\n", + " ValidationException,\n", + ")\n", + "from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey\n", + "from cosmpy.aerial.wallet import LocalWallet\n", + "from cosmpy.aerial.client import LedgerClient, NetworkConfig\n", + "from secp256k1 import PrivateKey as NilAuthPrivateKey\n", + "import base64\n", + "import datetime\n", + "\n", + "\n", + "# %% Define functions to load keys (replace with secure loading in production)\n", + "def get_wallet():\n", + " \"\"\"Loads the Nilchain wallet private key and creates a wallet object.\"\"\"\n", + " # IMPORTANT: Hardcoding private keys is insecure. Use environment variables or a secrets manager.\n", + " keypair = NilchainPrivateKey(\"l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\")\n", + " print(f\"Nilchain Private Key (bytes): {keypair.private_key}\")\n", + " print(f\"Nilchain Public Key (bytes): {keypair.public_key}\")\n", + " wallet = LocalWallet(\n", + " keypair, prefix=\"nillion\"\n", + " ) # Nillion uses the 'nillion' address prefix\n", + " return wallet, keypair\n", + "\n", + "\n", + "def get_private_key():\n", + " \"\"\"Loads the NilAuth private key used for signing NUC tokens.\"\"\"\n", + " # IMPORTANT: Hardcoding private keys is insecure. Use environment variables or a secrets manager.\n", + " private_key = NilAuthPrivateKey(\n", + " base64.b64decode(\"l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\")\n", + " )\n", + " return private_key\n", + "\n", + "\n", + "# %% Initialize keys and wallet\n", + "# This key will be used to sign NUC tokens later (e.g., the root token from NilAuth or delegated tokens)\n", + "builder_private_key = get_private_key()\n", + "\n", + "# This wallet is used for interacting with the Nillion Chain (e.g., paying subscriptions)\n", + "wallet, keypair = get_wallet()\n", + "address = wallet.address()\n", + "print(f\"Paying for wallet: {address}\")\n", + "\n", + "# %% Configure and connect to Nillion Chain\n", + "cfg = NetworkConfig(\n", + " chain_id=\"nillion-chain-devnet\",\n", + " url=\"grpc+http://localhost:26649\", # Nillion Chain gRPC endpoint\n", + " fee_minimum_gas_price=1,\n", + " fee_denomination=\"unil\", # The currency used for fees\n", + " staking_denomination=\"unil\", # The currency used for staking\n", + ")\n", + "ledger_client = LedgerClient(cfg)\n", + "\n", + "# %% Query wallet balance\n", + "balances = ledger_client.query_bank_balance(address, \"unil\")\n", + "print(f\"Wallet balance: {balances} unil\")\n", + "\n", + "# %% Initialize NilAuth Client and Payer\n", + "print(\"[>] Creating nilauth client\")\n", + "# Connect to the NilAuth service which issues root NUC tokens\n", + "nilauth_client = NilauthClient(\"http://localhost:30921\") # NilAuth service endpoint\n", + "\n", + "print(\"[>] Creating payer\")\n", + "# The Payer object bundles wallet details needed to pay for chain transactions\n", + "payer = Payer(\n", + " wallet_private_key=keypair,\n", + " chain_id=\"nillion-chain-devnet\",\n", + " grpc_endpoint=\"http://localhost:26649\", # Nillion Chain gRPC endpoint for the payer\n", + " gas_limit=1000000000000, # Gas limit for transactions\n", + ")\n", + "\n", + "# %% Check and manage NilAuth subscription\n", + "# Check if the builder_private_key is associated with an active subscription\n", + "subscription_details = nilauth_client.subscription_status(builder_private_key)\n", + "print(f\"IS SUBSCRIBED: {subscription_details.subscribed}\")\n", + "\n", + "# If not subscribed, pay for one\n", + "if not subscription_details.subscribed:\n", + " print(\"[>] Paying for subscription\")\n", + " nilauth_client.pay_subscription(\n", + " key=builder_private_key, # The key to associate the subscription with\n", + " payer=payer, # The payer object to execute the transaction\n", + " )\n", + "else:\n", + " # If already subscribed, print details\n", + " print(\"[>] Subscription is already paid for\")\n", + " now = datetime.datetime.now(datetime.timezone.utc)\n", + " print(f\"EXPIRES IN: {subscription_details.details.expires_at - now}\")\n", + " # Note: Renewal might only be possible within a certain window before expiry\n", + " print(f\"CAN BE RENEWED IN: {subscription_details.details.renewable_at - now}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Requesting and Preparing NUC Tokens\n", + "\n", + "This section focuses on obtaining the initial NUC token (the \"root\" token) from NilAuth and preparing for delegation by generating a new key pair.\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Request Root Token:** Use the `nilauth_client` (initialized earlier) and the `builder_private_key` (which has an active subscription) to request a root NUC token from the NilAuth service. This token grants the initial set of permissions associated with the `builder_private_key`.\n", + "2. **Print Root Token:** Display the raw, encoded string representation of the obtained root token.\n", + "3. **Display Builder Keys:** Print the raw private key bytes and the hex-encoded public key of the `builder_private_key` for reference. This is the key that *owns* the root token.\n", + "4. **Generate Delegated Key Pair:** Create a completely *new* `NilAuthPrivateKey` instance. This key pair (`delegated_key` and its corresponding public key) will represent the entity *receiving* delegated permissions from the root token.\n", + "5. **Display Delegated Keys:** Print the raw private key bytes and the hex-encoded public key of the newly generated `delegated_key`.\n", + "6. **Parse Root Token:** Convert the raw `root_token` string into a structured `NucTokenEnvelope` object using `NucTokenEnvelope.parse()`. This allows programmatic access to the token's claims and structure.\n", + "7. **Print Parsed Envelope:** Display the `NucTokenEnvelope` object, showing its parsed structure." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Root Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDg2OTcsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6ImMxNDE0MTIwYjg5ODk3ZTdlM2YyN2ZiZGE0OWEyZDAwIiwicHJmIjpbXX0.3csz4DdLahcYmc6vbwE-HdweGF6TCFUiatoO6AAssSJNUR0VGiRHmCSU3rlmdYAHPHEPcLWNBBLo-e9bMOQXUg\n", + "Builder Private Key: 97f49889fceed88a9cdddb16a161d13f6a12307c2b39163f3c3c397c3c2d2434\n", + "Builder Public Key: 030923f2e7120c50e42905b857ddd2947f6ecced6bb02aab64e63b28e9e2e06d10\n", + "Delegated Private Key: 5586c1abd910c869517a5c1a733c19b3513fef5f700e2c8624e867b620e981ca\n", + "Delegated Public Key: 03f65bb3be4bd7752e9d680ac315025c5252e9e36836217fe9bdb4b8514f04e8d9\n", + "Root Token Envelope: \n" + ] + } + ], + "source": [ + "# %% Request Root Token from NilAuth\n", + "# Use the key associated with the subscription to request the base NUC token\n", + "root_token = nilauth_client.request_token(key=builder_private_key)\n", + "print(f\"Root Token (raw string): {root_token}\")\n", + "\n", + "# %% Display Builder Key Details (Owner of Root Token)\n", + "print(f\"Builder Private Key (bytes): {builder_private_key.serialize()}\")\n", + "print(f\"Builder Public Key (hex): {builder_private_key.pubkey.serialize().hex()}\")\n", + "\n", + "# %% Generate a New Key Pair for Delegation Target\n", + "# This key pair will be the recipient of the delegated permissions\n", + "delegated_key = NilAuthPrivateKey()\n", + "print(f\"Delegated Private Key (bytes): {delegated_key.serialize()}\")\n", + "print(f\"Delegated Public Key (hex): {delegated_key.pubkey.serialize().hex()}\")\n", + "\n", + "# %% Parse the Root Token String into an Object\n", + "# Parsing allows easier access to token attributes and structure\n", + "root_token_envelope = NucTokenEnvelope.parse(root_token)\n", + "print(f\"Root Token Envelope (parsed object): {root_token_envelope}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating a Delegated NUC Token\n", + "\n", + "Now that we have the root token and a key pair for the intended recipient, we create a new NUC token that delegates specific permissions from the root token holder (`builder_private_key`) to the recipient (`delegated_key`).\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Initialize Builder:** Start building a new token using `NucTokenBuilder.extending()`, passing the previously parsed `root_token_envelope`. This signifies that the new token derives its authority from the root token.\n", + "2. **Set Body (Delegation):** Specify the token's body using `.body()`. Here, `DelegationBody(policies=[])` indicates this is a delegation token. Policies could further restrict the delegation, but none are added in this example.\n", + "3. **Set Audience:** Define the recipient of this delegated token using `.audience()`. We pass a `Did` (Decentralized Identifier) object created from the *public key* of the `delegated_key` generated in the previous step. This means only the holder of `delegated_key`'s private key can use this token effectively for further actions (like creating an invocation).\n", + "4. **Specify Command:** Grant permission to execute a specific command using `.command()`. Here, `Command([\"nil\", \"ai\", \"generate\"])` authorizes the audience (the holder of `delegated_key`) to perform the `nil ai generate` action. Multiple commands could be listed.\n", + "5. **Build and Sign:** Finalize the token creation and sign it using `.build()`. Crucially, the signing key here is `builder_private_key` – the private key corresponding to the issuer of the *root* token, proving its authority to delegate.\n", + "6. **Print Delegated Token:** Display the raw, encoded string representation of the newly created delegated token.\n", + "7. **Parse Delegated Token:** Convert the raw `delegated_token` string into a structured `NucTokenEnvelope` object.\n", + "8. **Print Parsed Envelope:** Display the `delegated_token_envelope` object to show its structure, including the specified audience and command." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Delegation Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDNmNjViYjNiZTRiZDc3NTJlOWQ2ODBhYzMxNTAyNWM1MjUyZTllMzY4MzYyMTdmZTliZGI0Yjg1MTRmMDRlOGQ5IiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogImZlNTE1ZDA0MGY3MWEyYmY0NzdlYjc2MzBjOWE5YjViIiwgInByZiI6IFsiYWU3ZmY3NTkxYjcxMjQ1MjY3Nzg0NTBmMDZlZDlkYjlkYTU2YmIwZjRmOTIzZmI1YTUzMzMyNWViYWU5ZmQ5MSJdfQ.CguMqBWX0YX2rErcpiHX4PvExo6kiEmnE3QOJMPZ1KU_iiQD1p6kzjY5YRHHT_mWVjgQVNsVR2B9swr7Zk63mw/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDg2OTcsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6ImMxNDE0MTIwYjg5ODk3ZTdlM2YyN2ZiZGE0OWEyZDAwIiwicHJmIjpbXX0.3csz4DdLahcYmc6vbwE-HdweGF6TCFUiatoO6AAssSJNUR0VGiRHmCSU3rlmdYAHPHEPcLWNBBLo-e9bMOQXUg\n", + "Delegated Token Envelope: \n" + ] + } + ], + "source": [ + "# %% Create the Delegated Token\n", + "# Use the NucTokenBuilder to create a new token based on the root token\n", + "delegated_token = (\n", + " NucTokenBuilder.extending(\n", + " root_token_envelope\n", + " ) # Start from the root token's authority\n", + " .body(\n", + " DelegationBody(policies=[])\n", + " ) # Mark as a delegation token (no specific policies here)\n", + " .audience(\n", + " Did(delegated_key.pubkey.serialize())\n", + " ) # Set the recipient to the delegated public key\n", + " .command(\n", + " Command([\"nil\", \"ai\", \"generate\"])\n", + " ) # Authorize the 'nil ai generate' command\n", + " .build(\n", + " builder_private_key\n", + " ) # Sign the delegation using the *root* token's private key\n", + ")\n", + "\n", + "# Print the resulting delegated token string\n", + "print(f\"Delegation Token (raw string): {delegated_token}\")\n", + "\n", + "# %% Parse the Delegated Token String into an Object\n", + "delegated_token_envelope = NucTokenEnvelope.parse(delegated_token)\n", + "\n", + "# Print the parsed object to see its structure\n", + "print(f\"Delegated Token Envelope (parsed object): {delegated_token_envelope}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating an Invocation NUC Token\n", + "\n", + "This final step creates the token that will actually be sent to the target service (e.g., Nilai API) to authorize a specific action. This is called an \"invocation\" token. It uses the permissions granted by the `delegated_token` and targets a specific service endpoint.\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Generate Placeholder Target Key:** Create a *new* `NilAuthPrivateKey` instance (`nilai_public_key`). **WARNING:** In a real application, you would **fetch the actual public key of the service you want to call** (e.g., from a discovery endpoint like `/v1/public_key` on the Nilai API) instead of generating a new one here. This generated key acts as a placeholder for the target service's identity in this example.\n", + "2. **Display Placeholder Keys:** Print the details of this placeholder key pair.\n", + "3. **Display Delegated Token:** Re-print the parsed `delegated_token_envelope` for context.\n", + "4. **Initialize Invocation Builder:** Start building the invocation token using `NucTokenBuilder.extending()`, passing the `delegated_token_envelope`. This signifies the invocation derives its authority from the permissions granted in the delegation token.\n", + "5. **Set Body (Invocation):** Specify the token's body using `.body()`. `InvocationBody(args={})` marks this as an invocation token. The `args` dictionary could contain specific parameters for the command being invoked, but it's empty here.\n", + "6. **Set Audience (Target Service):** Define the intended recipient service using `.audience()`. We pass a `Did` created from the public key of the **placeholder** `nilai_public_key`. **Critically, in a real scenario, this MUST be the actual public key of the target service.** This ensures the token is only valid for that specific service instance.\n", + "7. **Build and Sign:** Finalize the token creation and sign it using `.build()`. The signing key is `delegated_key` – the private key that *received* the permissions in the previous delegation step. This proves the caller is authorized by the delegation.\n", + "8. **Print Invocation Token:** Display the raw, encoded string representation of the invocation token. This is the token you would typically send as an API key or Bearer token to the target service." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New Private Key: 6288b98e03a595f54788137b0a9353d334b363af30c71d469ae8368a2257ab0e\n", + "Delegated Key: 02e24769ba1fc9aa1dd819ca12cc8179b5417e3bebf9c9c9e5880342d48246f420\n", + "Delegated Token Envelope: \n", + "Invocation: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowM2Y2NWJiM2JlNGJkNzc1MmU5ZDY4MGFjMzE1MDI1YzUyNTJlOWUzNjgzNjIxN2ZlOWJkYjRiODUxNGYwNGU4ZDkiLCAiYXVkIjogImRpZDpuaWw6MDJlMjQ3NjliYTFmYzlhYTFkZDgxOWNhMTJjYzgxNzliNTQxN2UzYmViZjljOWM5ZTU4ODAzNDJkNDgyNDZmNDIwIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJhcmdzIjoge30sICJub25jZSI6ICI1ZmRkODk1NjE4MWM1YTZiZjRlNDVhMzBlYzc1YmE0NSIsICJwcmYiOiBbIjBjYWY3MWU3ZGVjMWEwNjc3Y2M0ZWNhYjJhYzY5Nzk1MWRjZDBjZDc4MTM4MDA4MzVkZjY2MmE2ODQzNTNjYzAiXX0.2D5Z7JcodjWyPcRqPx2OIn2Marnk-46XCJAlYIBzdTZCPQSs99gfWslQdoaC84pWYqiaoTvYuogqTDTdrtXaeA/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDNmNjViYjNiZTRiZDc3NTJlOWQ2ODBhYzMxNTAyNWM1MjUyZTllMzY4MzYyMTdmZTliZGI0Yjg1MTRmMDRlOGQ5IiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogImZlNTE1ZDA0MGY3MWEyYmY0NzdlYjc2MzBjOWE5YjViIiwgInByZiI6IFsiYWU3ZmY3NTkxYjcxMjQ1MjY3Nzg0NTBmMDZlZDlkYjlkYTU2YmIwZjRmOTIzZmI1YTUzMzMyNWViYWU5ZmQ5MSJdfQ.CguMqBWX0YX2rErcpiHX4PvExo6kiEmnE3QOJMPZ1KU_iiQD1p6kzjY5YRHHT_mWVjgQVNsVR2B9swr7Zk63mw/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDg2OTcsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6ImMxNDE0MTIwYjg5ODk3ZTdlM2YyN2ZiZGE0OWEyZDAwIiwicHJmIjpbXX0.3csz4DdLahcYmc6vbwE-HdweGF6TCFUiatoO6AAssSJNUR0VGiRHmCSU3rlmdYAHPHEPcLWNBBLo-e9bMOQXUg\n", + "--------------------------------\n" + ] + } + ], + "source": [ + "# %% Generate Placeholder Target Key (Replace with actual service key retrieval in practice)\n", + "# WARNING: This creates a random key. In a real scenario, fetch the target service's public key.\n", + "nilai_public_key = NilAuthPrivateKey() # Placeholder for the target service's key\n", + "\n", + "# Display the placeholder key details\n", + "print(f\"Placeholder Target Private Key (bytes): {nilai_public_key.serialize()}\")\n", + "print(\n", + " f\"Placeholder Target Public Key (hex): {nilai_public_key.pubkey.serialize().hex()}\"\n", + ")\n", + "\n", + "# Display the delegation token again for context\n", + "print(f\"Delegated Token Envelope (used for invocation): {delegated_token_envelope}\")\n", + "\n", + "# %% Create the Invocation Token\n", + "# Use the NucTokenBuilder to create the token that calls the service\n", + "invocation = (\n", + " NucTokenBuilder.extending(\n", + " delegated_token_envelope\n", + " ) # Start from the delegated token's authority\n", + " .body(\n", + " InvocationBody(args={})\n", + " ) # Mark as an invocation token (no specific args here)\n", + " .audience(\n", + " Did(nilai_public_key.pubkey.serialize())\n", + " ) # Set the target service (using placeholder key here)\n", + " .build(delegated_key) # Sign with the *delegated* private key\n", + ")\n", + "\n", + "# Print the resulting invocation token string (this would be sent to the service)\n", + "print(f\"Invocation Token (raw string): {invocation}\")\n", + "print(\"--------------------------------\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Validating the NUC Token Chain\n", + "\n", + "After creating the root, delegated, and invocation tokens, it's crucial to validate them to ensure they are correctly formed, properly signed, and that the chain of delegation is intact. Validation typically checks signatures, expiration times (if set), audience restrictions, and command permissions against a trusted root issuer (in this case, the NilAuth service).\n", + "\n", + "**Steps:**\n", + "\n", + "1. **Get NilAuth Public Key:** Retrieve the public key of the NilAuth service itself using `nilauth_client.about().public_key.serialize()`. This key acts as the ultimate trust anchor for validating the token chain, as NilAuth issued the root token. Wrap it in a `Did` object.\n", + "2. **Print NilAuth Public Key:** Display the retrieved NilAuth public key `Did`.\n", + "3. **Parse Invocation Token:** Convert the raw `invocation` token string into a structured `NucTokenEnvelope` object.\n", + "4. **Print Parsed Invocation Envelope:** Display the parsed invocation token object.\n", + "5. **Print Proof Count:** Show the number of proofs (signatures) attached to the invocation envelope. An invocation token derived from a delegated token, which itself derived from a root token, should have multiple proofs forming a chain back to the root issuer.\n", + "6. **Initialize Validator:** Create instances of `NucTokenValidator`. The validator needs a list of trusted root public keys. Here, we only trust the `nilauth_public_key`.\n", + "7. **Validate Delegated Token:** Call `validator.validate()` on the `delegated_token_envelope`. This checks if it's correctly signed by the `builder_private_key` (whose authority ultimately comes from NilAuth) and if its structure is valid. (Note: The root token validation is commented out but would follow the same principle).\n", + "8. **Prepare Invocation Validation Parameters:** Create `ValidationParameters` specifically for the invocation token.\n", + " * Set `token_requirements` to an `InvocationRequirement`.\n", + " * Crucially, set the `audience` within the `InvocationRequirement` to the **expected audience** (the placeholder `nilai_public_key.pubkey`'s `Did` in this example, but should be the actual target service's `Did` in practice). This tells the validator to specifically check if the token was intended for this recipient.\n", + "9. **Validate Invocation Token:** Call `validator.validate()` on the `invocation_envelope` using the specific `validation_parameters`. This checks:\n", + " * The signature (must be signed by `delegated_key`).\n", + " * The chain of proofs back to the trusted `nilauth_public_key`.\n", + " * The audience matches the one specified in `validation_parameters`.\n", + " * Expiration, command permissions (implicitly checked based on delegation chain)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nilauth Public Key: did:nil:03520e70bd97a5fa6d70c614d50ee47bf445ae0b0941a1d61ddd5afa022b97ab14\n", + "Invocation Envelope: \n", + "Invocation Envelope Token Proofs: 2\n", + "Validating Root Token Envelope\n", + "Validating Delegated Token Envelope\n", + "None\n", + "Validating Invocation Envelope\n", + "InvocationRequirement(audience=Did(public_key=b'\\x02\\xe2Gi\\xba\\x1f\\xc9\\xaa\\x1d\\xd8\\x19\\xca\\x12\\xcc\\x81y\\xb5A~;\\xeb\\xf9\\xc9\\xc9\\xe5\\x88\\x03B\\xd4\\x82F\\xf4 '))\n", + "{\"iss\": \"did:nil:03f65bb3be4bd7752e9d680ac315025c5252e9e36836217fe9bdb4b8514f04e8d9\", \"aud\": \"did:nil:02e24769ba1fc9aa1dd819ca12cc8179b5417e3bebf9c9c9e5880342d48246f420\", \"sub\": \"did:nil:030923f2e7120c50e42905b857ddd2947f6ecced6bb02aab64e63b28e9e2e06d10\", \"cmd\": \"/nil/ai/generate\", \"args\": {}, \"nonce\": \"5fdd8956181c5a6bf4e45a30ec75ba45\", \"prf\": [\"0caf71e7dec1a0677cc4ecab2ac697951dcd0cd7813800835df662a684353cc0\"]}\n", + "Expected audience: did:nil:02e24769ba1fc9aa1dd819ca12cc8179b5417e3bebf9c9c9e5880342d48246f420\n", + "Token audience: did:nil:02e24769ba1fc9aa1dd819ca12cc8179b5417e3bebf9c9c9e5880342d48246f420\n" + ] + } + ], + "source": [ + "# %% Get the Public Key of the Root Issuer (NilAuth)\n", + "# The NilAuth service's public key is the ultimate trust anchor\n", + "nilauth_public_key = Did(nilauth_client.about().public_key.serialize())\n", + "print(f\"Nilauth Public Key (Trust Anchor): {nilauth_public_key}\")\n", + "\n", + "# %% Parse the Invocation Token String into an Object\n", + "invocation_envelope = NucTokenEnvelope.parse(invocation)\n", + "print(f\"Invocation Envelope (parsed object): {invocation_envelope}\")\n", + "\n", + "# An invocation token derived from a delegated token should have multiple proofs (signatures)\n", + "print(f\"Invocation Envelope Token Proofs Count: {len(invocation_envelope.proofs)}\")\n", + "\n", + "# %% Validate the Tokens\n", + "# Initialize the validator with the trusted root public key(s)\n", + "validator = NucTokenValidator([nilauth_public_key])\n", + "\n", + "# --- Root Token Validation (Optional - Commented Out) ---\n", + "# print(\"Validating Root Token Envelope...\")\n", + "# try:\n", + "# validator.validate(root_token_envelope)\n", + "# print(\"Root Token is Valid.\")\n", + "# except ValidationException as e:\n", + "# print(f\"Root Token Validation Failed: {e}\")\n", + "\n", + "# --- Delegated Token Validation ---\n", + "print(\"Validating Delegated Token Envelope...\")\n", + "try:\n", + " # Basic validation checks structure and signature relative to the root\n", + " validator.validate(delegated_token_envelope)\n", + " print(\"Delegated Token is Valid.\")\n", + "except ValidationException as e:\n", + " print(f\"Delegated Token Validation Failed: {e}\")\n", + "\n", + "# --- Invocation Token Validation ---\n", + "print(\"Validating Invocation Envelope...\")\n", + "try:\n", + " # Prepare specific parameters for invocation validation\n", + " default_parameters = ValidationParameters.default()\n", + " # Tell the validator to check if the audience matches our (placeholder) target service key\n", + " default_parameters.token_requirements = InvocationRequirement(\n", + " audience=Did(\n", + " nilai_public_key.pubkey.serialize()\n", + " ) # Use actual service key DID here\n", + " )\n", + " validation_parameters = default_parameters\n", + "\n", + " # Validate the invocation token against the root and check specific requirements\n", + " validator.validate(invocation_envelope, validation_parameters)\n", + " print(\"Invocation Token is Valid (including audience check).\")\n", + "except ValidationException as e:\n", + " print(f\"Invocation Token Validation Failed: {e}\")\n", + "except Exception as e:\n", + " print(f\"An unexpected error occurred during invocation validation: {e}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 2b5264df643748e10de08b686d4a10ac416eb5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Thu, 8 May 2025 11:36:23 +0200 Subject: [PATCH 07/14] fix: concurrent access to private key file --- .github/workflows/ci.yml | 2 +- .gitignore | 3 +++ nilai-api/private_key.key | 1 - nilai-api/src/nilai_api/crypto.py | 43 +++++++++++++++++++++---------- 4 files changed, 34 insertions(+), 15 deletions(-) delete mode 100644 nilai-api/private_key.key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c020c27d..2d430fcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,7 +104,7 @@ jobs: run: docker build -t nillion/nilai-attestation:latest -f docker/attestation.Dockerfile . - name: Build nilal API - run: docker build -t nillion/nilai-api:latest -f docker/api.Dockerfile --target nilai . + run: docker build -t nillion/nilai-api:latest -f docker/api.Dockerfile --target nilai --platform linux/amd64 . - name: Create .env run: | diff --git a/.gitignore b/.gitignore index e53cf46b..f1592719 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,6 @@ grafana/runtime-data/* prometheus/data/* !prometheus/data/.gitkeep + +private_key.key +private_key.key.lock diff --git a/nilai-api/private_key.key b/nilai-api/private_key.key deleted file mode 100644 index 4c56f3de..00000000 --- a/nilai-api/private_key.key +++ /dev/null @@ -1 +0,0 @@ -.rqq7^XʏHœ)i_v diff --git a/nilai-api/src/nilai_api/crypto.py b/nilai-api/src/nilai_api/crypto.py index 767da3d4..cab6ce1b 100644 --- a/nilai-api/src/nilai_api/crypto.py +++ b/nilai-api/src/nilai_api/crypto.py @@ -1,5 +1,6 @@ -from base64 import b64encode import os +import fcntl +from base64 import b64encode from secp256k1 import PrivateKey, PublicKey @@ -8,26 +9,42 @@ def generate_key_pair() -> tuple[PrivateKey, PublicKey, str]: """ - Generate a new key pair and return the private key, public key, and base64 encoded public key. + Generate or load a key pair safely, preventing concurrent access using fcntl. Returns: - tuple[PrivateKey, PublicKey, str]: A tuple containing the private key, public key, and base64 encoded public key. + tuple[PrivateKey, PublicKey, str]: Private key, public key, and base64-encoded public key. """ private_key: PrivateKey - if os.path.exists(PRIVATE_KEY_PATH): - with open(PRIVATE_KEY_PATH, "rb") as f: - private_key = PrivateKey(f.read()) - else: - private_key = PrivateKey() - with open(PRIVATE_KEY_PATH, "wb") as f: - private_key_bytes: bytes = private_key.private_key # type: ignore - f.write(private_key_bytes) + + # Use a separate lock file to avoid corrupting the key file itself + lock_path = PRIVATE_KEY_PATH + ".lock" + + # Ensure the lock file exists + open(lock_path, "a").close() + + with open(lock_path, "r+") as lock_file: + fcntl.flock(lock_file, fcntl.LOCK_EX) + + if os.path.exists(PRIVATE_KEY_PATH): + with open(PRIVATE_KEY_PATH, "rb") as f: + private_key_bytes: bytes = f.read() + if not private_key_bytes: + raise ValueError("Private key file is empty or corrupted.") + private_key = PrivateKey(private_key_bytes) + else: + private_key = PrivateKey() + with open(PRIVATE_KEY_PATH, "wb") as f: + private_key_bytes: bytes = private_key.private_key # type: ignore + f.write(private_key_bytes) + + # Release the lock + fcntl.flock(lock_file, fcntl.LOCK_UN) public_key = private_key.pubkey if public_key is None: - raise ValueError("Keypair generation failed:Public key is None") - b64_public_key: str = b64encode(public_key.serialize()).decode() + raise ValueError("Keypair generation failed: Public key is None") + b64_public_key: str = b64encode(public_key.serialize()).decode() return private_key, public_key, b64_public_key From 6393483c94d2b1cca4c3f08c4af1e805b507735d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Thu, 8 May 2025 12:29:54 +0200 Subject: [PATCH 08/14] fix: tests httpx timeout and improvement to logging --- nilai-api/src/nilai_api/routers/private.py | 38 ++++++++++++++-------- tests/e2e/test_http.py | 1 + 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/nilai-api/src/nilai_api/routers/private.py b/nilai-api/src/nilai_api/routers/private.py index 0e604618..b579e0b6 100644 --- a/nilai-api/src/nilai_api/routers/private.py +++ b/nilai-api/src/nilai_api/routers/private.py @@ -215,24 +215,36 @@ async def chat_completion_stream_generator() -> AsyncGenerator[str, None]: }, ) # type: ignore + prompt_token_usage: int = 0 + completion_token_usage: int = 0 async for chunk in response: - if chunk.usage is not None: - await UserManager.update_token_usage( - user.userid, - prompt_tokens=chunk.usage.prompt_tokens, - completion_tokens=chunk.usage.completion_tokens, - ) - await QueryLogManager.log_query( - user.userid, - model=req.model, - prompt_tokens=chunk.usage.prompt_tokens, - completion_tokens=chunk.usage.completion_tokens, - ) - + if ( + chunk.usage is not None + and chunk.usage.prompt_tokens is not None + and chunk.usage.completion_tokens is not None + ): + prompt_token_usage = chunk.usage.prompt_tokens + completion_token_usage += chunk.usage.completion_tokens + + logger.info( + f"Prompt token usage: {chunk.usage.prompt_tokens}/{prompt_token_usage}, Completion token usage: {chunk.usage.completion_tokens}/{completion_token_usage}" + ) data = chunk.model_dump_json(exclude_unset=True) yield f"data: {data}\n\n" await asyncio.sleep(0) + await UserManager.update_token_usage( + user.userid, + prompt_tokens=prompt_token_usage, + completion_tokens=completion_token_usage, + ) + await QueryLogManager.log_query( + user.userid, + model=req.model, + prompt_tokens=prompt_token_usage, + completion_tokens=completion_token_usage, + ) + except Exception as e: logger.error(f"Error streaming response: {e}") return diff --git a/tests/e2e/test_http.py b/tests/e2e/test_http.py index d311cc63..a020ae1b 100644 --- a/tests/e2e/test_http.py +++ b/tests/e2e/test_http.py @@ -25,6 +25,7 @@ def client(): "Content-Type": "application/json", "Authorization": f"Bearer {AUTH_TOKEN}", }, + timeout=None, ) From 1d1f72d3e339506f5a102ab7008f13968a9a2c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Thu, 8 May 2025 18:29:32 +0200 Subject: [PATCH 09/14] feat: added nuc-helpers for nilai --- nilai-auth/nilai-auth-client/pyproject.toml | 7 +- .../src/nilai_auth_client/main.py | 117 +++----- nilai-auth/nilai-auth-server/README.md | 3 +- nilai-auth/nilai-auth-server/pyproject.toml | 5 +- .../src/nilai_auth_server/app.py | 158 +++-------- .../src/nilai_auth_server/config.py | 6 +- nilai-auth/nuc-helpers/README.md | 0 nilai-auth/nuc-helpers/pyproject.toml | 23 ++ .../nuc-helpers/src/nuc_helpers/__init__.py | 34 +++ .../nuc-helpers/src/nuc_helpers/helpers.py | 260 ++++++++++++++++++ .../nuc-helpers/src/nuc_helpers/main.py | 138 ++++++++++ .../nuc-helpers/src/nuc_helpers/py.typed | 0 pyproject.toml | 4 +- tests/e2e/nuc.py | 5 + uv.lock | 40 ++- 15 files changed, 567 insertions(+), 233 deletions(-) create mode 100644 nilai-auth/nuc-helpers/README.md create mode 100644 nilai-auth/nuc-helpers/pyproject.toml create mode 100644 nilai-auth/nuc-helpers/src/nuc_helpers/__init__.py create mode 100644 nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py create mode 100644 nilai-auth/nuc-helpers/src/nuc_helpers/main.py create mode 100644 nilai-auth/nuc-helpers/src/nuc_helpers/py.typed create mode 100644 tests/e2e/nuc.py diff --git a/nilai-auth/nilai-auth-client/pyproject.toml b/nilai-auth/nilai-auth-client/pyproject.toml index 3952fe71..e22369ba 100644 --- a/nilai-auth/nilai-auth-client/pyproject.toml +++ b/nilai-auth/nilai-auth-client/pyproject.toml @@ -8,11 +8,8 @@ authors = [ ] requires-python = ">=3.12" dependencies = [ - "httpx>=0.28.1", - "nuc", + "nuc-helpers", "openai>=1.70.0", - "pydantic>=2.11.2", - "secp256k1>=0.14.0", ] [build-system] @@ -20,4 +17,4 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.uv.sources] -nuc = { git = "https://github.com/NillionNetwork/nuc-py.git" } +nuc-helpers = { workspace = true } diff --git a/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py b/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py index 7263218d..705a493a 100644 --- a/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py +++ b/nilai-auth/nilai-auth-client/src/nilai_auth_client/main.py @@ -1,28 +1,28 @@ # Do an HTTP request to the nilai-auth-server import httpx -from secp256k1 import PrivateKey as NilAuthPrivateKey, PublicKey as NilAuthPublicKey -from nuc.validate import NucTokenValidator, ValidationParameters, InvocationRequirement +from secp256k1 import PrivateKey as NilAuthPrivateKey +from nuc.validate import ValidationParameters, InvocationRequirement import base64 -from pydantic import BaseModel -from nuc.envelope import NucTokenEnvelope -from nuc.builder import NucTokenBuilder -from nuc.token import InvocationBody, Did -from nuc.nilauth import NilauthClient +from nuc.token import Did import openai +from nuc_helpers import ( + DelegationToken, + InvocationToken, + get_nilai_public_key, + get_invocation_token, + validate_token, +) + SERVICE_ENDPOINT = "localhost:8100" NILAI_ENDPOINT = "localhost:8080" NILAUTH_ENDPOINT = "localhost:30921" -class DelegationToken(BaseModel): - token: str - - -def get_delegation_token(b64_public_key: str): +def retrieve_delegation_token(b64_public_key: str) -> DelegationToken: """ Get a delegation token for the given public key @@ -36,94 +36,51 @@ def get_delegation_token(b64_public_key: str): f"http://{SERVICE_ENDPOINT}/v1/delegate/", json={"user_public_key": b64_public_key}, ) - return DelegationToken(**response.json()).token - - -def get_nilai_public_key(): - """ - Get the nilai public key - """ - response = httpx.get(f"http://{NILAI_ENDPOINT}/v1/public_key") - public_key = NilAuthPublicKey(base64.b64decode(response.text), raw=True) - print(f"Nilai public key: {public_key.serialize().hex()}") - return public_key - - -def make_invocation_token( - delegated_token: str, - nilai_public_key: NilAuthPublicKey, - delegated_key: NilAuthPrivateKey, -): - """ - Make an invocation token for the given delegated token and nilai public key - - Args: - delegated_token: The delegated token - nilai_public_key: The nilai public key - delegated_key: The private key - """ - delegated_token_envelope = NucTokenEnvelope.parse(delegated_token) - - invocation = ( - NucTokenBuilder.extending(delegated_token_envelope) - .body(InvocationBody(args={})) - .audience(Did(nilai_public_key.serialize())) - .build(delegated_key) - ) - return invocation - - -def get_nilauth_public_key(): - """ - Get the nilauth public key - """ - nilauth_client = NilauthClient(f"http://{NILAUTH_ENDPOINT}") - nilauth_public_key = Did(nilauth_client.about().public_key.serialize()) - return nilauth_public_key - - -def validate_token(token: str, validation_parameters: ValidationParameters): - validator = NucTokenValidator([get_nilauth_public_key()]) - - validator.validate(NucTokenEnvelope.parse(token), validation_parameters) - - print("[>] Token validated") + return DelegationToken(**response.json()) def main(): """ Main function """ - private_key = NilAuthPrivateKey() - public_key = private_key.pubkey + # Create a user private key and public key + user_private_key = NilAuthPrivateKey() + user_public_key = user_private_key.pubkey - if public_key is None: + if user_public_key is None: raise Exception("Failed to get public key") - b64_public_key = base64.b64encode(public_key.serialize()).decode("utf-8") + b64_public_key = base64.b64encode(user_public_key.serialize()).decode("utf-8") - delegated_token = get_delegation_token(b64_public_key) + delegation_token = retrieve_delegation_token(b64_public_key) - validate_token(delegated_token, ValidationParameters.default()) - nilai_public_key = get_nilai_public_key() + validate_token( + f"http://{NILAUTH_ENDPOINT}", + delegation_token.token, + ValidationParameters.default(), + ) + nilai_public_key = get_nilai_public_key(f"http://{NILAI_ENDPOINT}") if nilai_public_key is None: raise Exception("Failed to get nilai public key") - invocation_token = make_invocation_token( - delegated_token, nilai_public_key, private_key + invocation_token: InvocationToken = get_invocation_token( + delegation_token, + nilai_public_key, + user_private_key, ) - print(f"Invocation token: {invocation_token}") - - default_parameters = ValidationParameters.default() - default_parameters.token_requirements = InvocationRequirement( + default_validation_parameters = ValidationParameters.default() + default_validation_parameters.token_requirements = InvocationRequirement( audience=Did(nilai_public_key.serialize()) ) - validation_parameters = default_parameters - validate_token(invocation_token, validation_parameters) + validate_token( + f"http://{NILAUTH_ENDPOINT}", + invocation_token.token, + default_validation_parameters, + ) client = openai.OpenAI( - base_url=f"http://{NILAI_ENDPOINT}/v1", api_key=invocation_token + base_url=f"http://{NILAI_ENDPOINT}/v1", api_key=invocation_token.token ) response = client.chat.completions.create( diff --git a/nilai-auth/nilai-auth-server/README.md b/nilai-auth/nilai-auth-server/README.md index 3ef6dddc..4ef0bd72 100644 --- a/nilai-auth/nilai-auth-server/README.md +++ b/nilai-auth/nilai-auth-server/README.md @@ -29,8 +29,7 @@ Use a ASGI server like Uvicorn: ```bash cd nilai-auth/nilai-auth-server -# Make sure dependencies are installed (e.g., using uv or pip) -uvicorn src.nilai_auth_server.app:app --host 0.0.0.0 --port 8100 --reload +uv run python3 src/nilai_auth_server/app.py ``` ## Configuration diff --git a/nilai-auth/nilai-auth-server/pyproject.toml b/nilai-auth/nilai-auth-server/pyproject.toml index b3b7041d..95a622cd 100644 --- a/nilai-auth/nilai-auth-server/pyproject.toml +++ b/nilai-auth/nilai-auth-server/pyproject.toml @@ -8,10 +8,9 @@ authors = [ ] requires-python = ">=3.12" dependencies = [ - "cosmpy==0.9.2", "fastapi[standard]>=0.115.5", "gunicorn>=23.0.0", - "nuc", + "nuc-helpers", "uvicorn>=0.34.0", ] @@ -20,4 +19,4 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.uv.sources] -nuc = { git = "https://github.com/NillionNetwork/nuc-py.git" } +nuc-helpers = { workspace = true } diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py index 2c671a98..1f0a571e 100644 --- a/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py @@ -1,128 +1,29 @@ from fastapi import FastAPI -from nuc.payer import Payer -from nuc.builder import NucTokenBuilder from nuc.nilauth import NilauthClient -from nuc.envelope import NucTokenEnvelope -from nuc.token import Command, Did -from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey -from cosmpy.aerial.wallet import LocalWallet -from cosmpy.aerial.client import LedgerClient, NetworkConfig from pydantic import BaseModel -from secp256k1 import PrivateKey as NilAuthPrivateKey, PublicKey as NilAuthPublicKey +from secp256k1 import PublicKey as NilAuthPublicKey import base64 -import datetime -from functools import lru_cache from nilai_auth_server.config import NILAUTH_TRUSTED_ROOT_ISSUERS -app = FastAPI() - -PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" -NILCHAIN_GRPC = "http://localhost:26649" - - -@lru_cache(maxsize=1) -def get_wallet_and_private_key(): - """ - Get the wallet and private key - - Returns: - wallet: The wallet - keypair: The keypair - private_key: The private key - """ - # FIXME: Use a real wallet, and don't hardcode the private key - keypair = NilchainPrivateKey(PRIVATE_KEY) - wallet = LocalWallet(keypair, prefix="nillion") - private_key = NilAuthPrivateKey(base64.b64decode(PRIVATE_KEY)) - return wallet, keypair, private_key - - -def get_root_token(nilauth_client, private_key) -> NucTokenEnvelope: - """ - Get the root token from nilauth - - Args: - nilauth_client: The nilauth client - private_key: The private key of the user - - Returns: - The root token - """ - - ## Getting the root token from nilauth - root_token: str = nilauth_client.request_token(key=private_key) - - root_token_envelope = NucTokenEnvelope.parse(root_token) - - return root_token_envelope - - -def get_unil_balance(address) -> int: - """ - Get the UNIL balance of the user +from nuc_helpers import ( + RootToken, + DelegationToken, + pay_for_subscription, + get_wallet_and_private_key, + get_root_token, + get_delegation_token, +) - Args: - address: The address of the user - """ - - cfg = NetworkConfig( - chain_id="nillion-chain-devnet", - url="grpc+" + NILCHAIN_GRPC, - fee_minimum_gas_price=1, - fee_denomination="unil", - staking_denomination="unil", - ) - ledger_client = LedgerClient(cfg) - balance = ledger_client.query_bank_balance(address, "unil") - return balance - - -def pay_for_subscription(nilauth_client, keypair, private_key): - """ - Pay for the subscription using the Nilchain keypair if the user is not subscribed - - Args: - nilauth_client: The nilauth client - keypair: The Nilchain keypair - private_key: The NilAuth private key of the user - """ - - payer = Payer( - wallet_private_key=keypair, - chain_id="nillion-chain-devnet", - grpc_endpoint=NILCHAIN_GRPC, - gas_limit=1000000000000, - ) - - # Pretty print the subscription details - subscription_details = nilauth_client.subscription_status(private_key) - print(f"IS SUBSCRIBED: {subscription_details.subscribed}") - - if not subscription_details.subscribed: - print("[>] Paying for subscription") - nilauth_client.pay_subscription( - key=private_key, - payer=payer, - ) - else: - print("[>] Subscription is already paid for") +app = FastAPI() - print( - f"EXPIRES IN: {subscription_details.details.expires_at - datetime.datetime.now(datetime.timezone.utc)}" - ) - print( - f"CAN BE RENEWED IN: {subscription_details.details.renewable_at - datetime.datetime.now(datetime.timezone.utc)}" - ) +PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" # This is an example private key with funds for testing devnet, and should not be used in production +NILCHAIN_GRPC = "localhost:26649" class DelegateRequest(BaseModel): user_public_key: str -class DelegationToken(BaseModel): - token: str - - @app.post("/v1/delegate/") def delegate(request: DelegateRequest) -> DelegationToken: """ @@ -132,28 +33,33 @@ def delegate(request: DelegateRequest) -> DelegationToken: request: The request body """ - wallet, keypair, private_key = get_wallet_and_private_key() - balance = get_unil_balance(wallet.address()) - - print(f"Wallet balance: {balance} unil") - print("[>] Creating nilauth client") - nilauth_clients = [NilauthClient(url) for url in NILAUTH_TRUSTED_ROOT_ISSUERS] - nilauth_client = nilauth_clients[0] - pay_for_subscription(nilauth_client, keypair, private_key) + server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( + PRIVATE_KEY + ) + nilauth_client = NilauthClient(f"http://{NILAUTH_TRUSTED_ROOT_ISSUERS}") + + # Pay for the subscription + pay_for_subscription( + nilauth_client, + server_wallet, + server_keypair, + server_private_key, + f"http://{NILCHAIN_GRPC}", + ) - root_token = get_root_token(nilauth_client, private_key) + # Create a root token + root_token: RootToken = get_root_token(nilauth_client, server_private_key) user_public_key = NilAuthPublicKey( base64.b64decode(request.user_public_key), raw=True ) - delegated_token = ( - NucTokenBuilder.extending(root_token) - .audience(Did(user_public_key.serialize())) - .command(Command(["nil", "ai", "generate"])) - .build(private_key) + delegation_token: DelegationToken = get_delegation_token( + root_token, + server_private_key, + user_public_key, ) - return DelegationToken(token=delegated_token) + return delegation_token if __name__ == "__main__": diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py index aeb89583..3325e80d 100644 --- a/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py @@ -1,9 +1,7 @@ -import os - from dotenv import load_dotenv load_dotenv() - -NILAUTH_TRUSTED_ROOT_ISSUERS = os.getenv("NILAUTH_TRUSTED_ROOT_ISSUERS", "").split(",") +NILAUTH_TRUSTED_ROOT_ISSUERS = "localhost:30921" +# NILAUTH_TRUSTED_ROOT_ISSUERS = os.getenv("NILAUTH_TRUSTED_ROOT_ISSUERS", "").split(",") print("With trusted root issuers: ", NILAUTH_TRUSTED_ROOT_ISSUERS) diff --git a/nilai-auth/nuc-helpers/README.md b/nilai-auth/nuc-helpers/README.md new file mode 100644 index 00000000..e69de29b diff --git a/nilai-auth/nuc-helpers/pyproject.toml b/nilai-auth/nuc-helpers/pyproject.toml new file mode 100644 index 00000000..347e9272 --- /dev/null +++ b/nilai-auth/nuc-helpers/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "nuc-helpers" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "José Cabrero-Holgueras", email = "jose.cabrero@nillion.com" } +] +requires-python = ">=3.12" +dependencies = [ + "cosmpy==0.9.2", + "nuc", + "pydantic>=2.11.2", + "secp256k1>=0.14.0", + "httpx>=0.28.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv.sources] +nuc = { git = "https://github.com/NillionNetwork/nuc-py.git" } diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/__init__.py b/nilai-auth/nuc-helpers/src/nuc_helpers/__init__.py new file mode 100644 index 00000000..88c75cea --- /dev/null +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/__init__.py @@ -0,0 +1,34 @@ +from nuc_helpers.helpers import ( + RootToken, + DelegationToken, + InvocationToken, + get_wallet_and_private_key, + pay_for_subscription, + get_root_token, + get_delegation_token, + get_invocation_token, + get_nilai_public_key, + get_nilauth_public_key, + validate_token, + NilAuthPublicKey, + NilAuthPrivateKey, + NilchainPrivateKey, +) + + +__all__ = [ + "RootToken", + "DelegationToken", + "InvocationToken", + "get_wallet_and_private_key", + "pay_for_subscription", + "get_root_token", + "get_delegation_token", + "get_invocation_token", + "get_nilai_public_key", + "get_nilauth_public_key", + "validate_token", + "NilAuthPublicKey", + "NilAuthPrivateKey", + "NilchainPrivateKey", +] diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py new file mode 100644 index 00000000..1f8afbf8 --- /dev/null +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py @@ -0,0 +1,260 @@ +import base64 +import datetime +import logging +from functools import lru_cache +from typing import Tuple +import httpx + +# Importing the pydantic library dependencies +from pydantic import BaseModel + +# Importing the secp256k1 library dependencies +from secp256k1 import PrivateKey as NilAuthPrivateKey, PublicKey as NilAuthPublicKey + +# Importing the nuc library dependencies +from nuc.payer import Payer +from nuc.builder import NucTokenBuilder +from nuc.nilauth import NilauthClient +from nuc.envelope import NucTokenEnvelope +from nuc.token import Command, Did, InvocationBody +from nuc.validate import NucTokenValidator, ValidationParameters + +# Importing the cosmpy library dependencies +from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey +from cosmpy.aerial.wallet import LocalWallet, Address +from cosmpy.aerial.client import LedgerClient, NetworkConfig + +logger = logging.getLogger(__name__) + +## Pydantic models + + +class RootToken(BaseModel): + token: str + + +class DelegationToken(BaseModel): + token: str + + +class InvocationToken(BaseModel): + token: str + + +## Helpers +@lru_cache(maxsize=1) +def get_wallet_and_private_key( + private_key_bytes: str | bytes | None = None, +) -> Tuple[LocalWallet, NilchainPrivateKey, NilAuthPrivateKey]: + """ + Get the wallet and private key + + Args: + private_key_bytes: The private key bytes to use for the wallet + + Returns: + wallet: The wallet + keypair: The keypair + private_key: The private key + """ + keypair = NilchainPrivateKey(private_key_bytes) + wallet = LocalWallet(keypair, prefix="nillion") + private_key = NilAuthPrivateKey(base64.b64decode(keypair.private_key)) + return wallet, keypair, private_key + + +def get_root_token( + nilauth_client: NilauthClient, private_key: NilAuthPrivateKey +) -> RootToken: + """ + Get the root token from nilauth + + Args: + nilauth_client: The nilauth client + private_key: The private key of the user + + Returns: + The root token + """ + ## Getting the root token from nilauth + root_token: str = nilauth_client.request_token(key=private_key) + + return RootToken(token=root_token) + + +def get_unil_balance(address: Address, grpc_endpoint: str) -> int: + """ + Get the UNIL balance of the user + + Args: + address: The address of the user + grpc_endpoint: The endpoint of the grpc server + + Returns: + The balance of the user in UNIL + """ + print("grpc_endpoint", grpc_endpoint) + + cfg = NetworkConfig( + chain_id="nillion-chain-devnet", + url="grpc+" + grpc_endpoint, + fee_minimum_gas_price=1, + fee_denomination="unil", + staking_denomination="unil", + ) + ledger_client = LedgerClient(cfg) + balance = ledger_client.query_bank_balance(address, "unil") # type: ignore + return balance + + +def pay_for_subscription( + nilauth_client: NilauthClient, + wallet: LocalWallet, + keypair: NilchainPrivateKey, + private_key: NilAuthPrivateKey, + grpc_endpoint: str, +) -> None: + """ + Pay for the subscription using the Nilchain keypair if the user is not subscribed + + Args: + nilauth_client: The nilauth client + keypair: The Nilchain keypair + private_key: The NilAuth private key of the user + grpc_endpoint: The endpoint of the grpc server + """ + + if get_unil_balance(wallet.address(), grpc_endpoint=grpc_endpoint) < 0: + raise RuntimeError("User does not have enough UNIL to pay for the subscription") + + payer = Payer( + wallet_private_key=keypair, + chain_id="nillion-chain-devnet", + grpc_endpoint=grpc_endpoint, + gas_limit=1000000000000, + ) + + # Pretty print the subscription details + subscription_details = nilauth_client.subscription_status(private_key) + logger.info(f"IS SUBSCRIBED: {subscription_details.subscribed}") + if ( + not subscription_details + or not subscription_details.subscribed + or not subscription_details.details + ): + raise RuntimeError("User subscription details could not be retrieved") + + if not subscription_details.subscribed: + logger.info("[>] Paying for subscription") + nilauth_client.pay_subscription( + key=private_key, + payer=payer, + ) + else: + logger.info("[>] Subscription is already paid for") + + logger.info( + f"EXPIRES IN: {subscription_details.details.expires_at - datetime.datetime.now(datetime.timezone.utc)}" + ) + logger.info( + f"CAN BE RENEWED IN: {subscription_details.details.renewable_at - datetime.datetime.now(datetime.timezone.utc)}" + ) + + +def get_delegation_token( + root_token: RootToken, + private_key: NilAuthPrivateKey, + user_public_key: NilAuthPublicKey, +) -> DelegationToken: + """ + Delegate the root token to the delegated key + + Args: + user_public_key_b64: The base64 encoded public key of the user + nilauth_url: The URL of the nilauth server + grpc_endpoint: The endpoint of the grpc server + Returns: + The delegation token + """ + + root_token_envelope = NucTokenEnvelope.parse(root_token.token) + delegated_token = ( + NucTokenBuilder.extending(root_token_envelope) + .audience(Did(user_public_key.serialize())) + .command(Command(["nil", "ai", "generate"])) + .build(private_key) + ) + return DelegationToken(token=delegated_token) + + +def get_nilai_public_key(nilai_url: str) -> NilAuthPublicKey: + """ + Get the nilai public key from the nilai server + + Args: + nilai_url: The URL of the nilai server + + Returns: + The nilai public key + """ + response = httpx.get(f"{nilai_url}/v1/public_key") + public_key = NilAuthPublicKey(base64.b64decode(response.text), raw=True) + logger.info(f"Nilai public key: {public_key.serialize().hex()}") + return public_key + + +def get_invocation_token( + delegation_token: RootToken | DelegationToken, + nilai_public_key: NilAuthPublicKey, + delegated_key: NilAuthPrivateKey, +) -> InvocationToken: + """ + Make an invocation token for the given delegated token and nilai public key + + Args: + delegated_token: The delegated token + nilai_public_key: The nilai public key + delegated_key: The private key + """ + print("Delegation token: ", delegation_token) + delegated_token_envelope = NucTokenEnvelope.parse(delegation_token.token) + + invocation = ( + NucTokenBuilder.extending(delegated_token_envelope) + .body(InvocationBody(args={})) + .audience(Did(nilai_public_key.serialize())) + .build(delegated_key) + ) + return InvocationToken(token=invocation) + + +def get_nilauth_public_key(nilauth_url: str) -> Did: + """ + Get the nilauth public key from the nilauth server + + Args: + nilauth_url: The URL of the nilauth server + + Returns: + The nilauth public key as a Did + """ + nilauth_client = NilauthClient(nilauth_url) + nilauth_public_key = Did(nilauth_client.about().public_key.serialize()) + return nilauth_public_key + + +def validate_token( + nilauth_url: str, token: str, validation_parameters: ValidationParameters +): + """ + Validate a token + + Args: + token: The token to validate + validation_parameters: The validation parameters + """ + validator = NucTokenValidator([get_nilauth_public_key(nilauth_url)]) + + validator.validate(NucTokenEnvelope.parse(token), validation_parameters) + + print("[>] Token validated") diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/main.py b/nilai-auth/nuc-helpers/src/nuc_helpers/main.py new file mode 100644 index 00000000..a0c25f74 --- /dev/null +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/main.py @@ -0,0 +1,138 @@ +from nuc_helpers import ( + get_wallet_and_private_key, + pay_for_subscription, + get_root_token, + get_delegation_token, + get_nilai_public_key, + get_invocation_token, + validate_token, + InvocationToken, + RootToken, + DelegationToken, + NilAuthPublicKey, + NilAuthPrivateKey, +) +from nuc.nilauth import NilauthClient +from nuc.token import Did +from nuc.validate import ValidationParameters, InvocationRequirement + + +def b2b2c_test(): + # Services must be running for this to work + PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" + NILAI_ENDPOINT = "localhost:8080" + NILAUTH_ENDPOINT = "localhost:30921" + NILCHAIN_GRPC = "localhost:26649" + + # Server private key + server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( + PRIVATE_KEY + ) + nilauth_client = NilauthClient(f"http://{NILAUTH_ENDPOINT}") + + # Pay for the subscription + pay_for_subscription( + nilauth_client, + server_wallet, + server_keypair, + server_private_key, + f"http://{NILCHAIN_GRPC}", + ) + + # Create a root token + root_token: RootToken = get_root_token(nilauth_client, server_private_key) + + # Create a user private key and public key + user_private_key = NilAuthPrivateKey() + user_public_key = user_private_key.pubkey + + if user_public_key is None: + raise Exception("Failed to get public key") + # b64_public_key = base64.b64encode(public_key.serialize()).decode("utf-8") + + delegation_token: DelegationToken = get_delegation_token( + root_token, + server_private_key, + user_public_key, + ) + + print("Delegation token: ", delegation_token, type(delegation_token)) + nilai_public_key: NilAuthPublicKey = get_nilai_public_key( + f"http://{NILAI_ENDPOINT}" + ) + invocation_token: InvocationToken = get_invocation_token( + delegation_token, + nilai_public_key, + user_private_key, + ) + + print("Root token type: ", type(root_token)) + default_validation_parameters = ValidationParameters.default() + default_validation_parameters.token_requirements = InvocationRequirement( + audience=Did(nilai_public_key.serialize()) + ) + + validate_token( + f"http://{NILAUTH_ENDPOINT}", + invocation_token.token, + default_validation_parameters, + ) + + +def b2c_test(): + # Services must be running for this to work + PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" + NILAI_ENDPOINT = "localhost:8080" + NILAUTH_ENDPOINT = "localhost:30921" + NILCHAIN_GRPC = "localhost:26649" + + # Server private key + server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( + PRIVATE_KEY + ) + nilauth_client = NilauthClient(f"http://{NILAUTH_ENDPOINT}") + + # Pay for the subscription + pay_for_subscription( + nilauth_client, + server_wallet, + server_keypair, + server_private_key, + f"http://{NILCHAIN_GRPC}", + ) + + # Create a root token + root_token: RootToken = get_root_token(nilauth_client, server_private_key) + + nilai_public_key: NilAuthPublicKey = get_nilai_public_key( + f"http://{NILAI_ENDPOINT}" + ) + invocation_token: InvocationToken = get_invocation_token( + root_token, + nilai_public_key, + server_private_key, + ) + + print("Root token type: ", type(root_token)) + default_validation_parameters = ValidationParameters.default() + default_validation_parameters.token_requirements = InvocationRequirement( + audience=Did(nilai_public_key.serialize()) + ) + + validate_token( + f"http://{NILAUTH_ENDPOINT}", + invocation_token.token, + default_validation_parameters, + ) + + +def main(): + """ + Main function to test the helpers + """ + b2b2c_test() + b2c_test() + + +if __name__ == "__main__": + main() diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/py.typed b/nilai-auth/nuc-helpers/src/nuc_helpers/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/pyproject.toml b/pyproject.toml index a3166922..8a7aa14e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "nilai-api", "nilai-common", "nilai-models", + "nuc-helpers", ] [dependency-groups] @@ -38,12 +39,13 @@ build-backend = "setuptools.build_meta" find = { include = ["nilai"] } [tool.uv.workspace] -members = ["nilai-models", "nilai-api", "packages/nilai-common", "nilai-auth/nilai-auth-server", "nilai-auth/nilai-auth-client"] +members = ["nilai-models", "nilai-api", "packages/nilai-common", "nilai-auth/nilai-auth-server", "nilai-auth/nilai-auth-client", "nilai-auth/nuc-helpers"] [tool.uv.sources] nilai-common = { workspace = true } nilai-api = { workspace = true } nilai-models = { workspace = true } +nuc-helpers = { workspace = true } [tool.pyright] exclude = [".venv"] diff --git a/tests/e2e/nuc.py b/tests/e2e/nuc.py new file mode 100644 index 00000000..19d983f4 --- /dev/null +++ b/tests/e2e/nuc.py @@ -0,0 +1,5 @@ +def get_nuc_token(): + """ + Get a NUC token for the given nilai public key + """ + pass diff --git a/uv.lock b/uv.lock index 1e21038a..8f0e1b75 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,7 @@ members = [ "nilai-auth-server", "nilai-common", "nilai-models", + "nuc-helpers", ] [[package]] @@ -1301,6 +1302,7 @@ dependencies = [ { name = "nilai-api" }, { name = "nilai-common" }, { name = "nilai-models" }, + { name = "nuc-helpers" }, ] [package.dev-dependencies] @@ -1323,6 +1325,7 @@ requires-dist = [ { name = "nilai-api", editable = "nilai-api" }, { name = "nilai-common", editable = "packages/nilai-common" }, { name = "nilai-models", editable = "nilai-models" }, + { name = "nuc-helpers", editable = "nilai-auth/nuc-helpers" }, ] [package.metadata.requires-dev] @@ -1400,20 +1403,14 @@ name = "nilai-auth-client" version = "0.1.0" source = { editable = "nilai-auth/nilai-auth-client" } dependencies = [ - { name = "httpx" }, - { name = "nuc" }, + { name = "nuc-helpers" }, { name = "openai" }, - { name = "pydantic" }, - { name = "secp256k1" }, ] [package.metadata] requires-dist = [ - { name = "httpx", specifier = ">=0.28.1" }, - { name = "nuc", git = "https://github.com/NillionNetwork/nuc-py.git" }, + { name = "nuc-helpers", editable = "nilai-auth/nuc-helpers" }, { name = "openai", specifier = ">=1.70.0" }, - { name = "pydantic", specifier = ">=2.11.2" }, - { name = "secp256k1", specifier = ">=0.14.0" }, ] [[package]] @@ -1421,19 +1418,17 @@ name = "nilai-auth-server" version = "0.1.0" source = { editable = "nilai-auth/nilai-auth-server" } dependencies = [ - { name = "cosmpy" }, { name = "fastapi", extra = ["standard"] }, { name = "gunicorn" }, - { name = "nuc" }, + { name = "nuc-helpers" }, { name = "uvicorn" }, ] [package.metadata] requires-dist = [ - { name = "cosmpy", specifier = "==0.9.2" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.5" }, { name = "gunicorn", specifier = ">=23.0.0" }, - { name = "nuc", git = "https://github.com/NillionNetwork/nuc-py.git" }, + { name = "nuc-helpers", editable = "nilai-auth/nuc-helpers" }, { name = "uvicorn", specifier = ">=0.34.0" }, ] @@ -1521,6 +1516,27 @@ dependencies = [ { name = "secp256k1" }, ] +[[package]] +name = "nuc-helpers" +version = "0.1.0" +source = { editable = "nilai-auth/nuc-helpers" } +dependencies = [ + { name = "cosmpy" }, + { name = "httpx" }, + { name = "nuc" }, + { name = "pydantic" }, + { name = "secp256k1" }, +] + +[package.metadata] +requires-dist = [ + { name = "cosmpy", specifier = "==0.9.2" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "nuc", git = "https://github.com/NillionNetwork/nuc-py.git" }, + { name = "pydantic", specifier = ">=2.11.2" }, + { name = "secp256k1", specifier = ">=0.14.0" }, +] + [[package]] name = "numpy" version = "1.26.4" From 4530e50870acad8bdc3ba528e4daef1d587c969c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Fri, 9 May 2025 10:36:19 +0200 Subject: [PATCH 10/14] feat: modified ci to use nucs --- .env.ci | 4 +- .github/workflows/ci.yml | 2 +- .../nuc-helpers/src/nuc_helpers/helpers.py | 8 ++- .../nuc-helpers/src/nuc_helpers/main.py | 4 +- tests/e2e/nuc.py | 67 +++++++++++++++++-- tests/e2e/test_http.py | 6 +- tests/e2e/test_openai.py | 4 +- 7 files changed, 79 insertions(+), 16 deletions(-) diff --git a/.env.ci b/.env.ci index a9c428fd..f448185b 100644 --- a/.env.ci +++ b/.env.ci @@ -8,7 +8,7 @@ HF_TOKEN="Hugging Face API Token" ENVIRONMENT = "mainnet" NILAI_GUNICORN_WORKERS = 10 -AUTH_STRATEGY = "api_key" +AUTH_STRATEGY = "nuc" # The domain name of the server # - It must be written as "localhost" or "test.nilai.nillion" @@ -20,7 +20,7 @@ ATTESTATION_HOST = "attestation" ATTESTATION_PORT = 8080 # nilAuth Trusted URLs -NILAUTH_TRUSTED_ROOT_ISSUERS = "http://localhost:30921" +NILAUTH_TRUSTED_ROOT_ISSUERS = "http://nilauth:30921" # Postgres Docker Compose Config POSTGRES_HOST = "postgres" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d430fcc..96493381 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: - name: Run E2E tests run: | set -e - export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 | jq ".apikey" -r) + # export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 | jq ".apikey" -r) export ENVIRONMENT=ci uv run pytest -v tests/e2e diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py index 1f8afbf8..635143c5 100644 --- a/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py @@ -139,10 +139,12 @@ def pay_for_subscription( logger.info(f"IS SUBSCRIBED: {subscription_details.subscribed}") if ( not subscription_details - or not subscription_details.subscribed - or not subscription_details.details + or subscription_details.subscribed is None + or subscription_details.details is None ): - raise RuntimeError("User subscription details could not be retrieved") + raise RuntimeError( + f"User subscription details could not be retrieved: {subscription_details}, {subscription_details.subscribed}, {subscription_details.details}" + ) if not subscription_details.subscribed: logger.info("[>] Paying for subscription") diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/main.py b/nilai-auth/nuc-helpers/src/nuc_helpers/main.py index a0c25f74..c106f5d0 100644 --- a/nilai-auth/nuc-helpers/src/nuc_helpers/main.py +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/main.py @@ -19,7 +19,7 @@ def b2b2c_test(): # Services must be running for this to work - PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" + PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" # This is an example private key with funds for testing devnet, and should not be used in production NILAI_ENDPOINT = "localhost:8080" NILAUTH_ENDPOINT = "localhost:30921" NILCHAIN_GRPC = "localhost:26649" @@ -81,7 +81,7 @@ def b2b2c_test(): def b2c_test(): # Services must be running for this to work - PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" + PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" # This is an example private key with funds for testing devnet, and should not be used in production NILAI_ENDPOINT = "localhost:8080" NILAUTH_ENDPOINT = "localhost:30921" NILCHAIN_GRPC = "localhost:26649" diff --git a/tests/e2e/nuc.py b/tests/e2e/nuc.py index 19d983f4..c7c27adb 100644 --- a/tests/e2e/nuc.py +++ b/tests/e2e/nuc.py @@ -1,5 +1,62 @@ -def get_nuc_token(): - """ - Get a NUC token for the given nilai public key - """ - pass +from nuc_helpers import ( + get_wallet_and_private_key, + pay_for_subscription, + get_root_token, + get_nilai_public_key, + get_invocation_token, + validate_token, + InvocationToken, + RootToken, + NilAuthPublicKey, +) +from nuc.nilauth import NilauthClient +from nuc.token import Did +from nuc.validate import ValidationParameters, InvocationRequirement + + +def get_nuc_token() -> InvocationToken: + # Services must be running for this to work + PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" # This is an example private key with funds for testing devnet, and should not be used in production + NILAI_ENDPOINT = "localhost:8080" + NILAUTH_ENDPOINT = "localhost:30921" + NILCHAIN_GRPC = "localhost:26649" + + # Server private key + server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( + PRIVATE_KEY + ) + nilauth_client = NilauthClient(f"http://{NILAUTH_ENDPOINT}") + + # Pay for the subscription + pay_for_subscription( + nilauth_client, + server_wallet, + server_keypair, + server_private_key, + f"http://{NILCHAIN_GRPC}", + ) + + # Create a root token + root_token: RootToken = get_root_token(nilauth_client, server_private_key) + + nilai_public_key: NilAuthPublicKey = get_nilai_public_key( + f"http://{NILAI_ENDPOINT}" + ) + invocation_token: InvocationToken = get_invocation_token( + root_token, + nilai_public_key, + server_private_key, + ) + + default_validation_parameters = ValidationParameters.default() + default_validation_parameters.token_requirements = InvocationRequirement( + audience=Did(nilai_public_key.serialize()) + ) + + validate_token( + f"http://{NILAUTH_ENDPOINT}", + invocation_token.token, + default_validation_parameters, + ) + + return invocation_token diff --git a/tests/e2e/test_http.py b/tests/e2e/test_http.py index a020ae1b..704b3169 100644 --- a/tests/e2e/test_http.py +++ b/tests/e2e/test_http.py @@ -10,7 +10,8 @@ import json -from .config import BASE_URL, AUTH_TOKEN, test_models +from .config import BASE_URL, test_models +from .nuc import get_nuc_token import httpx import pytest @@ -18,12 +19,13 @@ @pytest.fixture def client(): """Create an HTTPX client with default headers""" + invocation_token = get_nuc_token() return httpx.Client( base_url=BASE_URL, headers={ "accept": "application/json", "Content-Type": "application/json", - "Authorization": f"Bearer {AUTH_TOKEN}", + "Authorization": f"Bearer {invocation_token.token}", }, timeout=None, ) diff --git a/tests/e2e/test_openai.py b/tests/e2e/test_openai.py index d5540b47..f4b944a9 100644 --- a/tests/e2e/test_openai.py +++ b/tests/e2e/test_openai.py @@ -14,12 +14,14 @@ from openai import OpenAI from openai.types.chat import ChatCompletion from .config import BASE_URL, AUTH_TOKEN, test_models +from .nuc import get_nuc_token @pytest.fixture def client(): """Create an OpenAI client configured to use the Nilai API""" - return OpenAI(base_url=BASE_URL, api_key=AUTH_TOKEN) + invocation_token = get_nuc_token() + return OpenAI(base_url=BASE_URL, api_key=invocation_token.token) @pytest.mark.parametrize( From c918b4bf882a9b468e5a23f9c97fce791e794abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Fri, 9 May 2025 11:49:34 +0200 Subject: [PATCH 11/14] fix: small fixes --- nilai-api/src/nilai_api/config/__init__.py | 4 +- nilai-api/src/nilai_api/db/users.py | 8 +- .../nilai-auth-client/examples/test.ipynb | 346 -------------- .../src/nilai_auth_client/__init__.py | 2 - .../src/nilai_auth_server/__init__.py | 2 - .../src/nilai_auth_server/test copy.ipynb | 441 ------------------ .../nuc-helpers/src/nuc_helpers/helpers.py | 11 +- tests/e2e/config.py | 3 +- tests/e2e/test_openai.py | 9 +- 9 files changed, 21 insertions(+), 805 deletions(-) delete mode 100644 nilai-auth/nilai-auth-client/examples/test.ipynb delete mode 100644 nilai-auth/nilai-auth-server/src/nilai_auth_server/test copy.ipynb diff --git a/nilai-api/src/nilai_api/config/__init__.py b/nilai-api/src/nilai_api/config/__init__.py index 781dbb3e..bcd7cd79 100644 --- a/nilai-api/src/nilai_api/config/__init__.py +++ b/nilai-api/src/nilai_api/config/__init__.py @@ -31,5 +31,5 @@ elif ENVIRONMENT == "testnet": from .testnet import * # noqa else: - # default to testnet - from .testnet import * # noqa + # default to mainnet with no limits + from .mainnet import * # noqa diff --git a/nilai-api/src/nilai_api/db/users.py b/nilai-api/src/nilai_api/db/users.py index 2f9aae14..b94e796d 100644 --- a/nilai-api/src/nilai_api/db/users.py +++ b/nilai-api/src/nilai_api/db/users.py @@ -33,9 +33,11 @@ class UserModel(Base): DateTime, server_default=sqlalchemy.func.now(), nullable=False ) # type: ignore last_activity: datetime = Column(DateTime, nullable=True) # type: ignore - ratelimit_day: int = Column(Integer, default=1000, nullable=True) # type: ignore - ratelimit_hour: int = Column(Integer, default=100, nullable=True) # type: ignore - ratelimit_minute: int = Column(Integer, default=10, nullable=True) # type: ignore + ratelimit_day: int = Column(Integer, default=USER_RATE_LIMIT_DAY, nullable=True) # type: ignore + ratelimit_hour: int = Column(Integer, default=USER_RATE_LIMIT_HOUR, nullable=True) # type: ignore + ratelimit_minute: int = Column( + Integer, default=USER_RATE_LIMIT_MINUTE, nullable=True + ) # type: ignore def __repr__(self): return f"" diff --git a/nilai-auth/nilai-auth-client/examples/test.ipynb b/nilai-auth/nilai-auth-client/examples/test.ipynb deleted file mode 100644 index 3a83ecf4..00000000 --- a/nilai-auth/nilai-auth-client/examples/test.ipynb +++ /dev/null @@ -1,346 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\n", - "\n", - "Paying for wallet: nillion1mqukqr7d4s3eqhcxwctu7yypm560etp2dghpy6\n", - "Wallet balance: 999999992000000 unil\n", - "[>] Creating nilauth client\n", - "[>] Creating payer\n", - "IS SUBSCRIBED: True\n", - "[>] Subscription is already paid for\n", - "EXPIRES IN: 0:01:10.293477\n", - "CAN BE RENEWED IN: -1 day, 23:44:30.293450\n" - ] - } - ], - "source": [ - "from nuc.payer import Payer\n", - "from nuc.builder import NucTokenBuilder\n", - "from nuc.nilauth import NilauthClient\n", - "from nuc.envelope import NucTokenEnvelope\n", - "from nuc.token import Command, Did, InvocationBody, DelegationBody\n", - "from nuc.validate import NucTokenValidator, ValidationParameters, InvocationRequirement\n", - "from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey\n", - "from cosmpy.aerial.wallet import LocalWallet\n", - "from cosmpy.aerial.client import LedgerClient, NetworkConfig\n", - "from secp256k1 import PrivateKey as NilAuthPrivateKey\n", - "import base64\n", - "import datetime\n", - "\n", - "\n", - "def get_wallet():\n", - " keypair = NilchainPrivateKey(\"l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\")\n", - " print(keypair.private_key)\n", - " print(keypair.public_key)\n", - " wallet = LocalWallet(keypair, prefix=\"nillion\")\n", - " return wallet, keypair\n", - "\n", - "\n", - "def get_private_key():\n", - " private_key = NilAuthPrivateKey(\n", - " base64.b64decode(\"l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=\")\n", - " )\n", - " return private_key\n", - "\n", - "\n", - "builder_private_key = get_private_key()\n", - "\n", - "wallet, keypair = get_wallet()\n", - "address = wallet.address()\n", - "print(\"Paying for wallet:\", wallet.address())\n", - "\n", - "cfg = NetworkConfig(\n", - " chain_id=\"nillion-chain-devnet\",\n", - " url=\"grpc+http://localhost:26649\",\n", - " fee_minimum_gas_price=1,\n", - " fee_denomination=\"unil\",\n", - " staking_denomination=\"unil\",\n", - ")\n", - "\n", - "ledger_client = LedgerClient(cfg)\n", - "\n", - "balances = ledger_client.query_bank_balance(address, \"unil\")\n", - "\n", - "print(f\"Wallet balance: {balances} unil\")\n", - "print(\"[>] Creating nilauth client\")\n", - "nilauth_client = NilauthClient(\"http://localhost:30921\")\n", - "\n", - "print(\"[>] Creating payer\")\n", - "payer = Payer(\n", - " wallet_private_key=keypair,\n", - " chain_id=\"nillion-chain-devnet\",\n", - " grpc_endpoint=\"http://localhost:26649\",\n", - " gas_limit=1000000000000,\n", - ")\n", - "# Pretty print the subscription details\n", - "subscription_details = nilauth_client.subscription_status(builder_private_key)\n", - "print(f\"IS SUBSCRIBED: {subscription_details.subscribed}\")\n", - "\n", - "\n", - "if not subscription_details.subscribed:\n", - " print(\"[>] Paying for subscription\")\n", - " nilauth_client.pay_subscription(\n", - " key=builder_private_key,\n", - " payer=payer,\n", - " )\n", - "else:\n", - " print(\"[>] Subscription is already paid for\")\n", - "\n", - " print(\n", - " f\"EXPIRES IN: {subscription_details.details.expires_at - datetime.datetime.now(datetime.timezone.utc)}\"\n", - " )\n", - " print(\n", - " f\"CAN BE RENEWED IN: {subscription_details.details.renewable_at - datetime.datetime.now(datetime.timezone.utc)}\"\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ='" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "keypair.private_key" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'97f49889fceed88a9cdddb16a161d13f6a12307c2b39163f3c3c397c3c2d2434'" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "builder_private_key.serialize()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Root Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDc3NzEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjA1NWU2M2YxODhhOTQ3YTNiZmE2YTg1OWQyNzVjOGZmIiwicHJmIjpbXX0.LRGEAaWbxlODtqHG0KJx-iXZ_uZg3F8i2e-rk5b77ewwSLuESX6bTX53roZdqWxhC-0aPFnxmPtfeBRwMkbEig\n", - "Builder Private Key: 97f49889fceed88a9cdddb16a161d13f6a12307c2b39163f3c3c397c3c2d2434\n", - "Builder Public Key: 030923f2e7120c50e42905b857ddd2947f6ecced6bb02aab64e63b28e9e2e06d10\n", - "Delegated Private Key: 802712144df168d6e6d003b13dcfbb5275d49d886cff1bf27aebfd944aec4e36\n", - "Delegated Public Key: 030a7d5a9a3a4229fe82809d78bf3b9974da443b55e87fb36cfb527b15bb10b20f\n", - "Root Token Envelope: \n" - ] - } - ], - "source": [ - "root_token = nilauth_client.request_token(key=builder_private_key)\n", - "print(f\"Root Token: {root_token}\")\n", - "\n", - "print(\"Builder Private Key: \", builder_private_key.serialize())\n", - "print(\"Builder Public Key: \", builder_private_key.pubkey.serialize().hex())\n", - "delegated_key = NilAuthPrivateKey()\n", - "print(\"Delegated Private Key: \", delegated_key.serialize())\n", - "print(\"Delegated Public Key: \", delegated_key.pubkey.serialize().hex())\n", - "root_token_envelope = NucTokenEnvelope.parse(root_token)\n", - "print(f\"Root Token Envelope: {root_token_envelope}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Delegation Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDMwYTdkNWE5YTNhNDIyOWZlODI4MDlkNzhiZjNiOTk3NGRhNDQzYjU1ZTg3ZmIzNmNmYjUyN2IxNWJiMTBiMjBmIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogIjhjMzI0YWQ3ZjNkZDAxMmViY2Q4MmU4ZGVjOTJkMzVkIiwgInByZiI6IFsiNDRjYmU4OGE2ZTY5NjM4ZDUwYmUzZTQzYmE3YjRiMzEzNzQ4NGQ2ZTNhOWE3MWQ3ZWUxM2U3ZjFiM2NhMTRlZCJdfQ.cy797SekPbaItK9FQsDkDze0fNnrCF9R6yoKac7MWClyDNxjv2b8_SMlu-RgCUuRZhHFM2nOvXNAQ_b4l1OlOQ/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDc3NzEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjA1NWU2M2YxODhhOTQ3YTNiZmE2YTg1OWQyNzVjOGZmIiwicHJmIjpbXX0.LRGEAaWbxlODtqHG0KJx-iXZ_uZg3F8i2e-rk5b77ewwSLuESX6bTX53roZdqWxhC-0aPFnxmPtfeBRwMkbEig\n", - "Delegated Token Envelope: \n" - ] - } - ], - "source": [ - "delegated_token = (\n", - " NucTokenBuilder.extending(root_token_envelope)\n", - " .body(DelegationBody(policies=[]))\n", - " .audience(Did(delegated_key.pubkey.serialize()))\n", - " .command(Command([\"nil\", \"ai\", \"generate\"]))\n", - " .build(builder_private_key)\n", - ")\n", - "\n", - "print(f\"Delegation Token: {delegated_token}\")\n", - "\n", - "delegated_token_envelope = NucTokenEnvelope.parse(delegated_token)\n", - "\n", - "print(\"Delegated Token Envelope: \", delegated_token_envelope)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "New Private Key: 848666abdf75e2d9359fc6b9927c218130a68f14689843ee114864da9be599a1\n", - "Delegated Key: 0242a4f768b88b552905d4e13dc2b363719295c87d096ffaef19a6fc02bff1ab03\n", - "Delegated Token Envelope: \n", - "Invocation: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzBhN2Q1YTlhM2E0MjI5ZmU4MjgwOWQ3OGJmM2I5OTc0ZGE0NDNiNTVlODdmYjM2Y2ZiNTI3YjE1YmIxMGIyMGYiLCAiYXVkIjogImRpZDpuaWw6MDI0MmE0Zjc2OGI4OGI1NTI5MDVkNGUxM2RjMmIzNjM3MTkyOTVjODdkMDk2ZmZhZWYxOWE2ZmMwMmJmZjFhYjAzIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJhcmdzIjoge30sICJub25jZSI6ICJkODk1OTQ4ZDZiNGY3NmU0ZWUyZTFjM2U3MDYxMGIyNyIsICJwcmYiOiBbImZlZGYxYzM0Njg4ZGRlYjgyOGIzN2U1MzZiMjFmNjA5MmE4MmVmZDRjYThiNzc5Y2UyYWRmZjBkODUxZTMyYTAiXX0.fCDUDPakJPezN0ppw2FfzkK4nGkDj2epgHkgLGyY8Utaz1uiWZCMhnr71HkCf25FtYkcZ_iGhDbOE8MlzkSeow/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDMwYTdkNWE5YTNhNDIyOWZlODI4MDlkNzhiZjNiOTk3NGRhNDQzYjU1ZTg3ZmIzNmNmYjUyN2IxNWJiMTBiMjBmIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogIjhjMzI0YWQ3ZjNkZDAxMmViY2Q4MmU4ZGVjOTJkMzVkIiwgInByZiI6IFsiNDRjYmU4OGE2ZTY5NjM4ZDUwYmUzZTQzYmE3YjRiMzEzNzQ4NGQ2ZTNhOWE3MWQ3ZWUxM2U3ZjFiM2NhMTRlZCJdfQ.cy797SekPbaItK9FQsDkDze0fNnrCF9R6yoKac7MWClyDNxjv2b8_SMlu-RgCUuRZhHFM2nOvXNAQ_b4l1OlOQ/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDc3NzEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjA1NWU2M2YxODhhOTQ3YTNiZmE2YTg1OWQyNzVjOGZmIiwicHJmIjpbXX0.LRGEAaWbxlODtqHG0KJx-iXZ_uZg3F8i2e-rk5b77ewwSLuESX6bTX53roZdqWxhC-0aPFnxmPtfeBRwMkbEig\n", - "--------------------------------\n" - ] - } - ], - "source": [ - "nilai_public_key = NilAuthPrivateKey()\n", - "\n", - "print(\"New Private Key: \", nilai_public_key.serialize())\n", - "print(\"Delegated Key: \", nilai_public_key.pubkey.serialize().hex())\n", - "print(\"Delegated Token Envelope: \", delegated_token_envelope)\n", - "\n", - "invocation = (\n", - " NucTokenBuilder.extending(delegated_token_envelope)\n", - " .body(InvocationBody(args={}))\n", - " .audience(Did(nilai_public_key.pubkey.serialize()))\n", - " .build(delegated_key)\n", - ")\n", - "\n", - "print(\"Invocation: \", invocation)\n", - "print(\"--------------------------------\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzBhN2Q1YTlhM2E0MjI5ZmU4MjgwOWQ3OGJmM2I5OTc0ZGE0NDNiNTVlODdmYjM2Y2ZiNTI3YjE1YmIxMGIyMGYiLCAiYXVkIjogImRpZDpuaWw6MDI0MmE0Zjc2OGI4OGI1NTI5MDVkNGUxM2RjMmIzNjM3MTkyOTVjODdkMDk2ZmZhZWYxOWE2ZmMwMmJmZjFhYjAzIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJhcmdzIjoge30sICJub25jZSI6ICJkODk1OTQ4ZDZiNGY3NmU0ZWUyZTFjM2U3MDYxMGIyNyIsICJwcmYiOiBbImZlZGYxYzM0Njg4ZGRlYjgyOGIzN2U1MzZiMjFmNjA5MmE4MmVmZDRjYThiNzc5Y2UyYWRmZjBkODUxZTMyYTAiXX0.fCDUDPakJPezN0ppw2FfzkK4nGkDj2epgHkgLGyY8Utaz1uiWZCMhnr71HkCf25FtYkcZ_iGhDbOE8MlzkSeow',\n", - " 'eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCAiYXVkIjogImRpZDpuaWw6MDMwYTdkNWE5YTNhNDIyOWZlODI4MDlkNzhiZjNiOTk3NGRhNDQzYjU1ZTg3ZmIzNmNmYjUyN2IxNWJiMTBiMjBmIiwgInN1YiI6ICJkaWQ6bmlsOjAzMDkyM2YyZTcxMjBjNTBlNDI5MDViODU3ZGRkMjk0N2Y2ZWNjZWQ2YmIwMmFhYjY0ZTYzYjI4ZTllMmUwNmQxMCIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogIjhjMzI0YWQ3ZjNkZDAxMmViY2Q4MmU4ZGVjOTJkMzVkIiwgInByZiI6IFsiNDRjYmU4OGE2ZTY5NjM4ZDUwYmUzZTQzYmE3YjRiMzEzNzQ4NGQ2ZTNhOWE3MWQ3ZWUxM2U3ZjFiM2NhMTRlZCJdfQ.cy797SekPbaItK9FQsDkDze0fNnrCF9R6yoKac7MWClyDNxjv2b8_SMlu-RgCUuRZhHFM2nOvXNAQ_b4l1OlOQ',\n", - " 'eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDMwOTIzZjJlNzEyMGM1MGU0MjkwNWI4NTdkZGQyOTQ3ZjZlY2NlZDZiYjAyYWFiNjRlNjNiMjhlOWUyZTA2ZDEwIiwic3ViIjoiZGlkOm5pbDowMzA5MjNmMmU3MTIwYzUwZTQyOTA1Yjg1N2RkZDI5NDdmNmVjY2VkNmJiMDJhYWI2NGU2M2IyOGU5ZTJlMDZkMTAiLCJleHAiOjE3NDU1MDc3NzEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjA1NWU2M2YxODhhOTQ3YTNiZmE2YTg1OWQyNzVjOGZmIiwicHJmIjpbXX0.LRGEAaWbxlODtqHG0KJx-iXZ_uZg3F8i2e-rk5b77ewwSLuESX6bTX53roZdqWxhC-0aPFnxmPtfeBRwMkbEig']" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "invocation.split(\"/\")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Nilauth Public Key: did:nil:03520e70bd97a5fa6d70c614d50ee47bf445ae0b0941a1d61ddd5afa022b97ab14\n", - "Invocation Envelope: \n", - "Invocation Envelope Token Proofs: 2\n", - "Validating Root Token Envelope\n", - "Validating Delegated Token Envelope\n", - "None\n", - "Validating Invocation Envelope\n", - "InvocationRequirement(audience=Did(public_key=b'\\x02B\\xa4\\xf7h\\xb8\\x8bU)\\x05\\xd4\\xe1=\\xc2\\xb3cq\\x92\\x95\\xc8}\\to\\xfa\\xef\\x19\\xa6\\xfc\\x02\\xbf\\xf1\\xab\\x03'))\n", - "did:nil:0242a4f768b88b552905d4e13dc2b363719295c87d096ffaef19a6fc02bff1ab03 did:nil:0242a4f768b88b552905d4e13dc2b363719295c87d096ffaef19a6fc02bff1ab03\n" - ] - } - ], - "source": [ - "nilauth_public_key = Did(nilauth_client.about().public_key.serialize())\n", - "\n", - "print(f\"Nilauth Public Key: {nilauth_public_key}\")\n", - "\n", - "invocation_envelope = NucTokenEnvelope.parse(invocation)\n", - "\n", - "print(f\"Invocation Envelope: {invocation_envelope}\")\n", - "\n", - "print(f\"Invocation Envelope Token Proofs: {len(invocation_envelope.proofs)}\")\n", - "\n", - "\n", - "print(\"Validating Root Token Envelope\")\n", - "# NucTokenValidator([nilauth_public_key]).validate(root_token_envelope)\n", - "print(\"Validating Delegated Token Envelope\")\n", - "NucTokenValidator([nilauth_public_key]).validate(delegated_token_envelope)\n", - "print(\"Validating Invocation Envelope\")\n", - "default_parameters = ValidationParameters.default()\n", - "default_parameters.token_requirements = InvocationRequirement(\n", - " audience=Did(nilai_public_key.pubkey.serialize())\n", - ")\n", - "validation_parameters = default_parameters\n", - "NucTokenValidator([nilauth_public_key]).validate(\n", - " invocation_envelope, validation_parameters\n", - ")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.10" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py b/nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py index c08aca32..e69de29b 100644 --- a/nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py +++ b/nilai-auth/nilai-auth-client/src/nilai_auth_client/__init__.py @@ -1,2 +0,0 @@ -def hello() -> str: - return "Hello from nilai-auth-client!" diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py index c8d9f1d6..e69de29b 100644 --- a/nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/__init__.py @@ -1,2 +0,0 @@ -def hello() -> str: - return "Hello from nilai-auth-server!" diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/test copy.ipynb b/nilai-auth/nilai-auth-server/src/nilai_auth_server/test copy.ipynb deleted file mode 100644 index 529e9dea..00000000 --- a/nilai-auth/nilai-auth-server/src/nilai_auth_server/test copy.ipynb +++ /dev/null @@ -1,441 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Paying for wallet: nillion1xllevsf8c76za7p84hc98ytzu2zskd8s6dqay5\n", - "Wallet balance: 999967000000 unil\n", - "[>] Creating nilauth client\n", - "[>] Creating payer\n", - "IS SUBSCRIBED: False\n", - "[>] Paying for subscription\n", - "Wallet funds: 999967000000\n", - "Nilauth Public Key: did:nil:03520e70bd97a5fa6d70c614d50ee47bf445ae0b0941a1d61ddd5afa022b97ab14\n" - ] - } - ], - "source": [ - "from nuc.payer import Payer\n", - "from nuc.builder import NucTokenBuilder\n", - "from nuc.nilauth import NilauthClient\n", - "from nuc.envelope import NucTokenEnvelope\n", - "from nuc.token import Command, Did, InvocationBody, DelegationBody\n", - "from nuc.validate import NucTokenValidator\n", - "from cosmpy.crypto.keypairs import PrivateKey as NilchainPrivateKey\n", - "from cosmpy.aerial.wallet import LocalWallet\n", - "from cosmpy.aerial.client import LedgerClient, NetworkConfig\n", - "from secp256k1 import PrivateKey as NilAuthPrivateKey\n", - "import base64\n", - "import datetime\n", - "\n", - "\n", - "def get_wallet():\n", - " keypair = NilchainPrivateKey(\"U8LrusERG5Z30Dagnjm3/aNe53l/M6MBenW+mA3xwO4=\")\n", - " wallet = LocalWallet(keypair, prefix=\"nillion\")\n", - " return wallet, keypair\n", - "\n", - "\n", - "def get_private_key():\n", - " private_key = NilAuthPrivateKey(\n", - " base64.b64decode(\"U8LrusERG5Z30Dagnjm3/aNe53l/M6MBenW+mA3xwO4=\")\n", - " )\n", - " return private_key\n", - "\n", - "\n", - "builder_private_key = get_private_key()\n", - "\n", - "wallet, keypair = get_wallet()\n", - "address = wallet.address()\n", - "print(\"Paying for wallet:\", wallet.address())\n", - "\n", - "cfg = NetworkConfig(\n", - " chain_id=\"nillion-chain-devnet\",\n", - " url=\"grpc+http://localhost:30649\",\n", - " fee_minimum_gas_price=1,\n", - " fee_denomination=\"unil\",\n", - " staking_denomination=\"unil\",\n", - ")\n", - "\n", - "ledger_client = LedgerClient(cfg)\n", - "\n", - "balances = ledger_client.query_bank_balance(address, \"unil\")\n", - "\n", - "print(f\"Wallet balance: {balances} unil\")\n", - "print(\"[>] Creating nilauth client\")\n", - "nilauth_client = NilauthClient(\"http://localhost:30921\")\n", - "\n", - "print(\"[>] Creating payer\")\n", - "payer = Payer(\n", - " wallet_private_key=keypair,\n", - " chain_id=\"nillion-chain-devnet\",\n", - " grpc_endpoint=\"http://localhost:30649\",\n", - " gas_limit=1000000000000,\n", - ")\n", - "# Pretty print the subscription details\n", - "subscription_details = nilauth_client.subscription_status(builder_private_key)\n", - "print(f\"IS SUBSCRIBED: {subscription_details.subscribed}\")\n", - "\n", - "\n", - "if not subscription_details.subscribed:\n", - " print(\"[>] Paying for subscription\")\n", - " nilauth_client.pay_subscription(\n", - " key=builder_private_key,\n", - " payer=payer,\n", - " )\n", - "else:\n", - " print(\"[>] Subscription is already paid for\")\n", - "\n", - " print(\n", - " f\"EXPIRES IN: {subscription_details.details.expires_at - datetime.datetime.now(datetime.timezone.utc)}\"\n", - " )\n", - " print(\n", - " f\"CAN BE RENEWED IN: {subscription_details.details.renewable_at - datetime.datetime.now(datetime.timezone.utc)}\"\n", - " )\n", - "nilauth_public_key = Did(nilauth_client.about().public_key.serialize())\n", - "\n", - "print(f\"Nilauth Public Key: {nilauth_public_key}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "New Private Key: eb540d7477890364fe0c88159d550cf79d5d7944b826d5f31f1767ddecbb89bc\n", - "Delegated Key: 0230b806939879096581cba908d411cb13ae6568a1b39cedd88765ed4a66f08e68\n", - "New Private Key: aa9c6d8808d1fec4bfd85821821b675ebdbc9a756ed9998ca7e666d2a7caabc1\n", - "Delegated Key: 03cf80bed14919fdafce672893733b0af7bf4d7b5c99ab1c1f7d537060a6ffe95f\n" - ] - } - ], - "source": [ - "delegated_key = NilAuthPrivateKey()\n", - "print(\"New Private Key: \", delegated_key.serialize())\n", - "print(\"Delegated Key: \", delegated_key.pubkey.serialize().hex())\n", - "\n", - "nilai_public_key = NilAuthPrivateKey()\n", - "\n", - "print(\"New Private Key: \", nilai_public_key.serialize())\n", - "print(\"Delegated Key: \", nilai_public_key.pubkey.serialize().hex())" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Root Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDNhZWZmNmY0YjQ0Yzk1ZDA5NmE5ZjEyNDZjZWFmNDViNGE5MGU4NzY4MDVmZjBhNGZkNmJjNDA5YmViNzNmNTcxIiwic3ViIjoiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCJleHAiOjE3NDUzMzc3OTEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjY0MjAyYzlmZTFlZTJlZWExNzJjMDQ1YWI0ODc0MjlmIiwicHJmIjpbXX0.BAAHSDM0RP8Xci0ifOKaPPbEdUS3pQG-e6vv6oCIN2RIuoBdlCABtFCpXi5mEGUsU_pcZ4zRVNxSbV-pqkSA4Q\n", - "Root Token Envelope: \n" - ] - } - ], - "source": [ - "root_token = nilauth_client.request_token(key=builder_private_key)\n", - "print(f\"Root Token: {root_token}\")\n", - "root_token_envelope = NucTokenEnvelope.parse(root_token)\n", - "print(f\"Root Token Envelope: {root_token_envelope}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Delegation Token: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCAiYXVkIjogImRpZDpuaWw6MDIzMGI4MDY5Mzk4NzkwOTY1ODFjYmE5MDhkNDExY2IxM2FlNjU2OGExYjM5Y2VkZDg4NzY1ZWQ0YTY2ZjA4ZTY4IiwgInN1YiI6ICJkaWQ6bmlsOjAzYWVmZjZmNGI0NGM5NWQwOTZhOWYxMjQ2Y2VhZjQ1YjRhOTBlODc2ODA1ZmYwYTRmZDZiYzQwOWJlYjczZjU3MSIsICJjbWQiOiAiL25pbC9haS9nZW5lcmF0ZSIsICJwb2wiOiBbXSwgIm5vbmNlIjogIjNjZWU0OTBkMjM3YWQ4NTg1NThmZDMzZGViNGU0ZjkyIiwgInByZiI6IFsiMjczOTBjZmVkZmUyZGIxODJmYWM0ZjEyYTI0ZDc1NjczMTljNzU3NTg0MDMyNWM3YWI0NmQwOGZkZjQxZjUyNSJdfQ.UB6061VreW9a2hy03M6IWq9TjbpZgPuW4pj3HO3ziXEQ_XKasp--Q-rksEZ6VGQ72I7XS3Q3XV75b1kO7DxRYw/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDNhZWZmNmY0YjQ0Yzk1ZDA5NmE5ZjEyNDZjZWFmNDViNGE5MGU4NzY4MDVmZjBhNGZkNmJjNDA5YmViNzNmNTcxIiwic3ViIjoiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCJleHAiOjE3NDUzMzc3OTEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjY0MjAyYzlmZTFlZTJlZWExNzJjMDQ1YWI0ODc0MjlmIiwicHJmIjpbXX0.BAAHSDM0RP8Xci0ifOKaPPbEdUS3pQG-e6vv6oCIN2RIuoBdlCABtFCpXi5mEGUsU_pcZ4zRVNxSbV-pqkSA4Q\n", - "Delegated Token Envelope: \n", - "Validating Delegated Token Envelope\n", - "None\n" - ] - } - ], - "source": [ - "delegated_token = (\n", - " NucTokenBuilder.extending(root_token_envelope)\n", - " .body(DelegationBody(policies=[]))\n", - " .audience(Did(delegated_key.pubkey.serialize()))\n", - " .command(Command([\"nil\", \"ai\", \"generate\"]))\n", - " .build(builder_private_key)\n", - ")\n", - "\n", - "print(f\"Delegation Token: {delegated_token}\")\n", - "\n", - "delegated_token_envelope = NucTokenEnvelope.parse(delegated_token)\n", - "\n", - "print(\"Delegated Token Envelope: \", delegated_token_envelope)\n", - "\n", - "print(\"Validating Delegated Token Envelope\")\n", - "NucTokenValidator([nilauth_public_key]).validate(delegated_token_envelope)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Invocation: eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiAiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCAiYXVkIjogImRpZDpuaWw6MDNjZjgwYmVkMTQ5MTlmZGFmY2U2NzI4OTM3MzNiMGFmN2JmNGQ3YjVjOTlhYjFjMWY3ZDUzNzA2MGE2ZmZlOTVmIiwgInN1YiI6ICJkaWQ6bmlsOjAzYWVmZjZmNGI0NGM5NWQwOTZhOWYxMjQ2Y2VhZjQ1YjRhOTBlODc2ODA1ZmYwYTRmZDZiYzQwOWJlYjczZjU3MSIsICJjbWQiOiAiL25pbCIsICJhcmdzIjoge30sICJub25jZSI6ICI1MDcwMjM4MWVjYzRmNTk0YTg4NmEwZjQ2OWRiMjRmNyIsICJwcmYiOiBbIjI3MzkwY2ZlZGZlMmRiMTgyZmFjNGYxMmEyNGQ3NTY3MzE5Yzc1NzU4NDAzMjVjN2FiNDZkMDhmZGY0MWY1MjUiXX0.qV-Urb8exWFwLOzbMNH7N0Fq-Cj7BFQcMWsm2ahdnbJncDz5BTzVsHMBPEJ96sOd2AFcL2i7SqbRm4CL0Jam3w/eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6bmlsOjAzNTIwZTcwYmQ5N2E1ZmE2ZDcwYzYxNGQ1MGVlNDdiZjQ0NWFlMGIwOTQxYTFkNjFkZGQ1YWZhMDIyYjk3YWIxNCIsImF1ZCI6ImRpZDpuaWw6MDNhZWZmNmY0YjQ0Yzk1ZDA5NmE5ZjEyNDZjZWFmNDViNGE5MGU4NzY4MDVmZjBhNGZkNmJjNDA5YmViNzNmNTcxIiwic3ViIjoiZGlkOm5pbDowM2FlZmY2ZjRiNDRjOTVkMDk2YTlmMTI0NmNlYWY0NWI0YTkwZTg3NjgwNWZmMGE0ZmQ2YmM0MDliZWI3M2Y1NzEiLCJleHAiOjE3NDUzMzc3OTEsImNtZCI6Ii9uaWwiLCJwb2wiOltdLCJub25jZSI6IjY0MjAyYzlmZTFlZTJlZWExNzJjMDQ1YWI0ODc0MjlmIiwicHJmIjpbXX0.BAAHSDM0RP8Xci0ifOKaPPbEdUS3pQG-e6vv6oCIN2RIuoBdlCABtFCpXi5mEGUsU_pcZ4zRVNxSbV-pqkSA4Q\n", - "--------------------------------\n", - "Invocation Envelope: \n", - "Invocation Envelope Token Proofs: 1\n", - "Validating Invocation Envelope\n", - "None\n" - ] - } - ], - "source": [ - "invocation = (\n", - " NucTokenBuilder.extending(root_token_envelope)\n", - " .body(InvocationBody(args={}))\n", - " .audience(Did(nilai_public_key.pubkey.serialize()))\n", - " .build(builder_private_key)\n", - ")\n", - "\n", - "print(\"Invocation: \", invocation)\n", - "print(\"--------------------------------\")\n", - "\n", - "invocation_envelope = NucTokenEnvelope.parse(invocation)\n", - "\n", - "print(f\"Invocation Envelope: {invocation_envelope}\")\n", - "\n", - "print(f\"Invocation Envelope Token Proofs: {len(invocation_envelope.proofs)}\")\n", - "\n", - "\n", - "print(\"Validating Invocation Envelope\")\n", - "NucTokenValidator([nilauth_public_key]).validate(invocation_envelope)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Did(public_key=b'\\x03\\xae\\xffoKD\\xc9]\\tj\\x9f\\x12F\\xce\\xafE\\xb4\\xa9\\x0e\\x87h\\x05\\xff\\nO\\xd6\\xbc@\\x9b\\xebs\\xf5q')" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "invocation_envelope.token.token.subject" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Public Key (base64): A/1ubGp+RMoFrhD5ahy0zf5PszhzmoZbbNjV2SrZ+xtg\n", - "Signature (base64): MEQCIGco8+RDttKxejy0eBrbaCgQTRJpERO3X1LamVfL0hv8AiArle4uJ8SErO0v1q5deaEHSI35/yOesM3lah/wFpdBHg==\n", - "Is the signature valid? True\n" - ] - } - ], - "source": [ - "from base64 import b64encode, b64decode\n", - "from secp256k1 import PrivateKey, PublicKey\n", - "\n", - "# Generate a new private key\n", - "private_key = PrivateKey()\n", - "public_key = private_key.pubkey\n", - "\n", - "# Serialize the public key to base64 (compressed form)\n", - "verifying_key = b64encode(public_key.serialize()).decode()\n", - "print(\"Public Key (base64):\", verifying_key)\n", - "\n", - "# Message to sign\n", - "message = b\"Hello secp256k1\"\n", - "\n", - "# Sign the message\n", - "signature = private_key.ecdsa_sign(message)\n", - "serialized_sig = private_key.ecdsa_serialize(signature)\n", - "\n", - "# Encode the signature to base64 for transmission/storage\n", - "sig_base64 = b64encode(serialized_sig).decode()\n", - "print(\"Signature (base64):\", sig_base64)\n", - "\n", - "\n", - "# Verify the signature\n", - "is_valid = public_key.ecdsa_verify(message, signature)\n", - "print(\"Is the signature valid?\", is_valid)" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Is the signature valid? True\n" - ] - } - ], - "source": [ - "# ----------- Verification -----------\n", - "\n", - "# Decode public key and signature from base64\n", - "pubkey_bytes = b64decode(verifying_key)\n", - "sig_bytes = b64decode(sig_base64)\n", - "\n", - "# Reconstruct the public key object\n", - "pubkey = PublicKey(pubkey_bytes, raw=True)\n", - "\n", - "# Deserialize the signature\n", - "sig = pubkey.ecdsa_deserialize(sig_bytes)\n", - "\n", - "# Verify the signature\n", - "is_valid = pubkey.ecdsa_verify(message, sig)\n", - "print(\"Is the signature valid?\", is_valid)" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "b'\\xac\\xf7\\xa4\\x98\\x1d\\xd5\\xa7\\xf7\\xef\\x1e\\xfd0\\x07\\xf3\\x8d\\xc41\\xd1r\\xbd\\x04lx\\x13\\x0c\\x83\\xb5,\\x16\\xe1\\xc3\\x1d'\n", - "acf7a4981dd5a7f7ef1efd3007f38dc431d172bd046c78130c83b52c16e1c31d\n" - ] - }, - { - "ename": "AttributeError", - "evalue": "'str' object has no attribute 'bytes'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[42]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28mprint\u001b[39m(private_key.private_key)\n\u001b[32m 2\u001b[39m \u001b[38;5;28mprint\u001b[39m(private_key.serialize())\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[43mprivate_key\u001b[49m\u001b[43m.\u001b[49m\u001b[43mserialize\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbytes\u001b[49m()\n", - "\u001b[31mAttributeError\u001b[39m: 'str' object has no attribute 'bytes'" - ] - } - ], - "source": [ - "print(private_key.private_key)\n", - "print(private_key.serialize())\n", - "private_key.serialize().bytes()" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [], - "source": [ - "from base64 import b64encode\n", - "import os\n", - "\n", - "from secp256k1 import PrivateKey, PublicKey\n", - "\n", - "\n", - "def generate_key_pair() -> tuple[PrivateKey, PublicKey, str]:\n", - " \"\"\"\n", - " Generate a new key pair and return the private key, public key, and base64 encoded public key.\n", - "\n", - " Returns:\n", - " tuple[PrivateKey, PublicKey, str]: A tuple containing the private key, public key, and base64 encoded public key.\n", - " \"\"\"\n", - " if os.path.exists(\"private_key.pem\"):\n", - " with open(\"private_key.pem\", \"rb\") as f:\n", - " private_key = PrivateKey(f.read())\n", - " else:\n", - " private_key = PrivateKey()\n", - " with open(\"private_key.pem\", \"wb\") as f:\n", - " f.write(private_key.private_key)\n", - "\n", - " public_key = private_key.pubkey\n", - " b64_public_key = b64encode(public_key.serialize()).decode()\n", - "\n", - " return private_key, public_key, b64_public_key" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(,\n", - " ,\n", - " 'AquGL1Q0qjyEcT4K3sjOXGDEZSzyWvO4/5essOhvQjV5')" - ] - }, - "execution_count": 57, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "generate_key_pair()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.10" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py index 635143c5..b8b4b450 100644 --- a/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py @@ -137,11 +137,7 @@ def pay_for_subscription( # Pretty print the subscription details subscription_details = nilauth_client.subscription_status(private_key) logger.info(f"IS SUBSCRIBED: {subscription_details.subscribed}") - if ( - not subscription_details - or subscription_details.subscribed is None - or subscription_details.details is None - ): + if not subscription_details or subscription_details.subscribed is None: raise RuntimeError( f"User subscription details could not be retrieved: {subscription_details}, {subscription_details.subscribed}, {subscription_details.details}" ) @@ -155,6 +151,11 @@ def pay_for_subscription( else: logger.info("[>] Subscription is already paid for") + if subscription_details.details is None: + raise RuntimeError( + f"Subscription details could not be retrieved: {subscription_details}" + ) + logger.info( f"EXPIRES IN: {subscription_details.details.expires_at - datetime.datetime.now(datetime.timezone.utc)}" ) diff --git a/tests/e2e/config.py b/tests/e2e/config.py index 138b7cea..14d90078 100644 --- a/tests/e2e/config.py +++ b/tests/e2e/config.py @@ -1,7 +1,8 @@ import os ENVIRONMENT = os.getenv("ENVIRONMENT", "dev") -AUTH_TOKEN = os.environ["AUTH_TOKEN"] +# Left for API key for backwards compatibility +AUTH_TOKEN = os.getenv("AUTH_TOKEN", "") if ENVIRONMENT == "dev": BASE_URL = "http://localhost:8080/v1" diff --git a/tests/e2e/test_openai.py b/tests/e2e/test_openai.py index f4b944a9..374d8142 100644 --- a/tests/e2e/test_openai.py +++ b/tests/e2e/test_openai.py @@ -13,7 +13,7 @@ import pytest from openai import OpenAI from openai.types.chat import ChatCompletion -from .config import BASE_URL, AUTH_TOKEN, test_models +from .config import BASE_URL, test_models from .nuc import get_nuc_token @@ -337,11 +337,13 @@ def test_usage_endpoint(client): # The OpenAI client doesn't have a built-in method for this import requests + invocation_token = get_nuc_token() + url = BASE_URL + "/usage" response = requests.get( url, headers={ - "Authorization": f"Bearer {AUTH_TOKEN}", + "Authorization": f"Bearer {invocation_token.token}", "Content-Type": "application/json", }, ) @@ -373,10 +375,11 @@ def test_attestation_endpoint(client): import requests url = BASE_URL + "/attestation/report" + invocation_token = get_nuc_token() response = requests.get( url, headers={ - "Authorization": f"Bearer {AUTH_TOKEN}", + "Authorization": f"Bearer {invocation_token.token}", "Content-Type": "application/json", }, params={"nonce": "0" * 64}, From b756f2751210ff9db2a1c95ef694b10094688061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Fri, 9 May 2025 15:36:28 +0200 Subject: [PATCH 12/14] fix: corrections on typing and attestation parameters --- nilai-api/src/nilai_api/attestation/__init__.py | 4 ++-- nilai-attestation/gunicorn.conf.py | 1 + .../src/nilai_attestation/attestation/__init__.py | 1 + .../attestation/nvtrust/nv_attester.py | 4 ++-- .../attestation/nvtrust/nv_verifier.py | 6 ++++-- .../src/nilai_attestation/routers/private.py | 12 +++++++----- packages/nilai-common/src/nilai_common/config.py | 3 --- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/nilai-api/src/nilai_api/attestation/__init__.py b/nilai-api/src/nilai_api/attestation/__init__.py index 1eb4434e..795be454 100644 --- a/nilai-api/src/nilai_api/attestation/__init__.py +++ b/nilai-api/src/nilai_api/attestation/__init__.py @@ -26,8 +26,8 @@ async def verify_attestation_report(attestation_report: AttestationReport) -> bo try: attestation_url = f"http://{SETTINGS.attestation_host}:{SETTINGS.attestation_port}/attestation/verify" async with httpx.AsyncClient() as client: - response: httpx.Response = await client.post( - attestation_url, json=attestation_report.model_dump() + response: httpx.Response = await client.get( + attestation_url, params=attestation_report.model_dump() ) return response.json() except Exception as e: diff --git a/nilai-attestation/gunicorn.conf.py b/nilai-attestation/gunicorn.conf.py index e19b2758..fd58ad78 100644 --- a/nilai-attestation/gunicorn.conf.py +++ b/nilai-attestation/gunicorn.conf.py @@ -7,6 +7,7 @@ # Set the number of workers (2) workers = 1 + # Set the number of threads per worker (16) threads = 1 diff --git a/nilai-attestation/src/nilai_attestation/attestation/__init__.py b/nilai-attestation/src/nilai_attestation/attestation/__init__.py index 8c8758c6..d7ee6a7b 100644 --- a/nilai-attestation/src/nilai_attestation/attestation/__init__.py +++ b/nilai-attestation/src/nilai_attestation/attestation/__init__.py @@ -24,6 +24,7 @@ def get_attestation_report(nonce: Nonce | None = None) -> AttestationReport: logger.info(f"Nonce: {attestation_nonce}") load_sev_library() + return AttestationReport( nonce=attestation_nonce, verifying_key="", diff --git a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_attester.py b/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_attester.py index acec35c2..473d0573 100644 --- a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_attester.py +++ b/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_attester.py @@ -46,7 +46,7 @@ def is_nvidia_gpu_available() -> bool: return False -def nv_attest(nonce: Nonce) -> NVAttestationToken: +def nv_attest(nonce: Nonce, name: str = "thisNode1") -> NVAttestationToken: """Generate an attestation token from local evidence. Args: @@ -57,7 +57,7 @@ def nv_attest(nonce: Nonce) -> NVAttestationToken: """ # Create and configure the attestation client. client = attestation.Attestation() - client.set_name("thisNode1") + client.set_name(name) client.set_nonce(nonce) logger.info("Checking if NVIDIA GPU is available") diff --git a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_verifier.py b/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_verifier.py index dac7b7af..b0a73887 100644 --- a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_verifier.py +++ b/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_verifier.py @@ -43,7 +43,9 @@ } -def verify_attestation(attestation_report: AttestationReport) -> bool: +def verify_attestation( + attestation_report: AttestationReport, name: str = "thisNode1" +) -> bool: """Verify an NVIDIA attestation token against a policy. Args: @@ -57,7 +59,7 @@ def verify_attestation(attestation_report: AttestationReport) -> bool: # Create an attestation client instance for token verification. logger.info(f"Attestation report: {attestation_report}") client = attestation.Attestation() - client.set_name("thisNode1") + client.set_name(name) client.set_nonce(attestation_report.nonce) client.add_verifier( attestation.Devices.GPU, attestation.Environment.REMOTE, NRAS_URL, "" diff --git a/nilai-attestation/src/nilai_attestation/routers/private.py b/nilai-attestation/src/nilai_attestation/routers/private.py index fafbb2e9..90de1802 100644 --- a/nilai-attestation/src/nilai_attestation/routers/private.py +++ b/nilai-attestation/src/nilai_attestation/routers/private.py @@ -1,6 +1,6 @@ # Fast API and serving import logging -from fastapi import APIRouter +from fastapi import APIRouter, Depends # Internal libraries from nilai_attestation.attestation import ( @@ -35,12 +35,14 @@ async def get_attestation(nonce: Nonce | None = None) -> AttestationReport: return get_attestation_report(nonce) -@router.post("/attestation/verify", tags=["Attestation"]) -async def post_attestation(attestation_report: AttestationReport) -> bool: +@router.get("/attestation/verify", tags=["Attestation"]) +async def get_attestation_verification( + attestation_report: AttestationReport = Depends(), +) -> bool: """ - Verify a cryptographic attestation report. + Verify a cryptographic attestation report passed as query parameters. - - **attestation_report**: Attestation report to verify + - **attestation_report**: Attestation report to verify (fields passed as query parameters) - **Returns**: True if the attestation report is valid, False otherwise """ return verify_attestation_report(attestation_report) diff --git a/packages/nilai-common/src/nilai_common/config.py b/packages/nilai-common/src/nilai_common/config.py index e597b695..aad86b01 100644 --- a/packages/nilai-common/src/nilai_common/config.py +++ b/packages/nilai-common/src/nilai_common/config.py @@ -1,8 +1,5 @@ import os from pydantic import BaseModel -# from dotenv import load_dotenv - -# load_dotenv() # Only needed locally if using a .env file class HostSettings(BaseModel): From 2774cdee3a183d4d6d84b3fc39b0073026982500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Wed, 21 May 2025 12:59:27 +0200 Subject: [PATCH 13/14] Update .github/workflows/ci.yml Co-authored-by: Sergio Medina --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96493381..42a51ad1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,6 @@ jobs: - name: Run E2E tests run: | set -e - # export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 | jq ".apikey" -r) export ENVIRONMENT=ci uv run pytest -v tests/e2e From 1638629a81c17595b4711d645422260258897555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Cabrero-Holgueras?= Date: Wed, 21 May 2025 15:18:41 +0200 Subject: [PATCH 14/14] fix: various fixes to PR comments --- .../nilai-auth-server/src/nilai_auth_server/app.py | 4 ++-- .../nilai-auth-server/src/nilai_auth_server/config.py | 5 +---- nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py | 11 +++++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py index 1f0a571e..419cb1cc 100644 --- a/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/app.py @@ -3,7 +3,7 @@ from pydantic import BaseModel from secp256k1 import PublicKey as NilAuthPublicKey import base64 -from nilai_auth_server.config import NILAUTH_TRUSTED_ROOT_ISSUERS +from nilai_auth_server.config import NILAUTH_TRUSTED_ROOT_ISSUER from nuc_helpers import ( RootToken, @@ -36,7 +36,7 @@ def delegate(request: DelegateRequest) -> DelegationToken: server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( PRIVATE_KEY ) - nilauth_client = NilauthClient(f"http://{NILAUTH_TRUSTED_ROOT_ISSUERS}") + nilauth_client = NilauthClient(f"http://{NILAUTH_TRUSTED_ROOT_ISSUER}") # Pay for the subscription pay_for_subscription( diff --git a/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py b/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py index 3325e80d..4109136a 100644 --- a/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py +++ b/nilai-auth/nilai-auth-server/src/nilai_auth_server/config.py @@ -1,7 +1,4 @@ from dotenv import load_dotenv load_dotenv() -NILAUTH_TRUSTED_ROOT_ISSUERS = "localhost:30921" -# NILAUTH_TRUSTED_ROOT_ISSUERS = os.getenv("NILAUTH_TRUSTED_ROOT_ISSUERS", "").split(",") - -print("With trusted root issuers: ", NILAUTH_TRUSTED_ROOT_ISSUERS) +NILAUTH_TRUSTED_ROOT_ISSUER = "localhost:30921" diff --git a/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py index b8b4b450..8dc7b937 100644 --- a/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py +++ b/nilai-auth/nuc-helpers/src/nuc_helpers/helpers.py @@ -93,7 +93,7 @@ def get_unil_balance(address: Address, grpc_endpoint: str) -> int: Returns: The balance of the user in UNIL """ - print("grpc_endpoint", grpc_endpoint) + logger.info("grpc_endpoint", grpc_endpoint) cfg = NetworkConfig( chain_id="nillion-chain-devnet", @@ -124,7 +124,10 @@ def pay_for_subscription( grpc_endpoint: The endpoint of the grpc server """ - if get_unil_balance(wallet.address(), grpc_endpoint=grpc_endpoint) < 0: + if ( + get_unil_balance(wallet.address(), grpc_endpoint=grpc_endpoint) + < nilauth_client.subscription_cost() + ): raise RuntimeError("User does not have enough UNIL to pay for the subscription") payer = Payer( @@ -219,7 +222,7 @@ def get_invocation_token( nilai_public_key: The nilai public key delegated_key: The private key """ - print("Delegation token: ", delegation_token) + logger.info(f"Delegation token: {delegation_token}") delegated_token_envelope = NucTokenEnvelope.parse(delegation_token.token) invocation = ( @@ -260,4 +263,4 @@ def validate_token( validator.validate(NucTokenEnvelope.parse(token), validation_parameters) - print("[>] Token validated") + logger.info("[>] Token validated")