Beltic logo
Python SDK Guide

Signing & Verification

Sign credentials as JWS tokens and verify signatures with the Python SDK.

Key Generation

Generate Ed25519 or ES256 key pairs for signing credentials.

from beltic import generate_key_pair

# Generate Ed25519 key pair (recommended)
private_key, public_key = generate_key_pair("EdDSA")

# Generate ES256 (P-256) key pair
private_key, public_key = generate_key_pair("ES256")

Key Import/Export

Export Keys to PEM

from beltic import export_key_to_pem

# Export private key
private_pem = export_key_to_pem(private_key, private=True)
with open("private.pem", "wb") as f:
    f.write(private_pem)

# Export public key
public_pem = export_key_to_pem(public_key, private=False)
with open("public.pem", "wb") as f:
    f.write(public_pem)

Import Keys from PEM

from beltic import import_key_from_pem

# Import private key
with open("private.pem", "rb") as f:
    private_key = import_key_from_pem(f.read(), "EdDSA", private=True)

# Import public key
with open("public.pem", "rb") as f:
    public_key = import_key_from_pem(f.read(), "EdDSA", private=False)

Export/Import JWK

from beltic import export_public_key, import_public_key

# Export public key as JWK
jwk = export_public_key(public_key)
# {"kty": "OKP", "crv": "Ed25519", "x": "..."}

# Import public key from JWK
key = import_public_key(jwk, "EdDSA")

Signing Credentials

Sign a DeveloperCredential

from beltic import sign_credential, SignOptions

token = sign_credential(
    credential,
    private_key,
    SignOptions(
        alg="EdDSA",
        issuer_did="did:web:beltic.com",
        subject_did="did:web:developer.example.com",
        key_id="did:web:beltic.com#key-1",
    ),
)

print(token)
# eyJhbGciOiJFZERTQSIsInR5cCI6ImFwcGxpY2F0aW9uL2JlbHRpYy1kZXZlbG9wZXIrand0IiwiY...

Sign an AgentCredential

token = sign_credential(
    agent_credential,
    private_key,
    SignOptions(
        alg="EdDSA",
        issuer_did="did:web:beltic.com",
        subject_did="did:web:agent.example.com",
        key_id="did:web:beltic.com#key-1",
        audience="did:web:platform.example.com",  # Optional
    ),
)

SignOptions

class SignOptions:
    alg: Literal["EdDSA", "ES256"]  # Signature algorithm
    issuer_did: str                 # iss claim
    subject_did: str                # sub claim
    key_id: str | None = None       # kid header
    audience: str | None = None     # aud claim

JWT Structure

Signed credentials follow this structure:

Header:

{
  "alg": "EdDSA",
  "typ": "application/beltic-developer+jwt",
  "kid": "did:web:beltic.com#key-1"
}

Payload:

{
  "iss": "did:web:beltic.com",
  "sub": "did:web:developer.example.com",
  "jti": "credential-id-uuid",
  "iat": 1699876800,
  "nbf": 1699876800,
  "exp": 1731412800,
  "vc": { /* Full credential object */ }
}

Verifying Credentials

Basic Verification

from beltic import verify_credential, VerifyOptions

async def key_resolver(input):
    # Resolve public key from DID or key registry
    # input.kid, input.alg, input.iss are available
    return public_key

verified = await verify_credential(
    token,
    VerifyOptions(key_resolver=key_resolver),
)

print(f"Algorithm: {verified.algorithm}")
print(f"Issuer: {verified.payload['iss']}")
print(f"Credential: {verified.credential['legalName']}")

Verification with Constraints

verified = await verify_credential(
    token,
    VerifyOptions(
        key_resolver=key_resolver,
        expected_issuer="did:web:beltic.com",
        expected_audience="did:web:platform.example.com",
        allowed_algorithms=["EdDSA", "ES256"],
    ),
)

VerifyOptions

class VerifyOptions:
    key_resolver: Callable[[KeyResolverInput], Awaitable[CryptoKey]]
    expected_issuer: str | None = None      # Require specific issuer
    expected_audience: str | None = None    # Require specific audience
    allowed_algorithms: list[str] | None = None  # Restrict algorithms

VerifiedCredential Result

class VerifiedCredential:
    credential: dict          # The verified credential (vc claim)
    algorithm: str            # Algorithm used (EdDSA or ES256)
    header: dict              # Full JWS protected header
    payload: dict             # Full JWT payload (iss, sub, vc, etc.)

Decode Without Verification

For debugging, you can decode a token without verification:

from beltic import decode_token

decoded = decode_token(token)
print(decoded.header)   # {"alg": "EdDSA", "typ": "...", "kid": "..."}
print(decoded.payload)  # {"iss": "...", "sub": "...", "vc": {...}}

Never trust decoded tokens without verification. Always use verify_credential for production code.

Get Credential Type

Determine if a token is a developer or agent credential:

from beltic import get_credential_type

cred_type = get_credential_type(token)
# Returns: "developer" | "agent" | None

Error Handling

SignatureError

from beltic import verify_credential, SignatureError

try:
    verified = await verify_credential(token, options)
except SignatureError as e:
    print(f"Step: {e.step}")  # PARSE, KEY_RESOLUTION, SIGNATURE, CLAIMS, SCHEMA
    print(f"Code: {e.code}")  # SIG-001, SIG-008, etc.
    print(f"Message: {e.message}")

Common Error Codes

CodeDescription
SIG-001Invalid JWS structure
SIG-002Unsupported algorithm
SIG-003Algorithm "none" not allowed
SIG-008Signature verification failed
SIG-009Token expired
SIG-010Token not yet valid
SIG-014Schema validation failed

Complete Example

import asyncio
from beltic import (
    validate_developer_credential,
    sign_credential,
    verify_credential,
    generate_key_pair,
    ValidationError,
    SignOptions,
    VerifyOptions,
)

async def main():
    # 1. Generate keys
    private_key, public_key = generate_key_pair("EdDSA")
    
    # 2. Create credential data
    credential_data = {
        "credentialId": "550e8400-e29b-41d4-a716-446655440000",
        "legalName": "Acme AI Solutions Inc.",
        "entityType": "corporation",
        "kybTier": "tier_2_standard",
        # ... other required fields
    }
    
    # 3. Validate
    result = validate_developer_credential(credential_data)
    if not result.ok:
        raise ValidationError(result.errors)
    
    # 4. Sign
    token = sign_credential(
        result.value,
        private_key,
        SignOptions(
            alg="EdDSA",
            issuer_did="did:web:beltic.com",
            subject_did="did:web:acme.example.com",
            key_id="did:web:beltic.com#key-1",
        ),
    )
    
    print(f"Signed token: {token[:50]}...")
    
    # 5. Verify
    async def resolver(input):
        return public_key
    
    verified = await verify_credential(
        token,
        VerifyOptions(key_resolver=resolver),
    )
    
    print(f"Verified: {verified.credential['legalName']}")

asyncio.run(main())

Next Steps