tamga-sdk
Official Python SDK for Tamga. Integrate license activation, offline verification, and machine management into your Python applications.
Pure Python — no Rust extension, no native build step. Every cryptographic primitive (Ed25519,
RSA-PKCS1/PSS, ECDSA-P256, AES-256-GCM, HKDF-SHA256) comes from the
cryptography package; HTTP transport is
httpx.
Install
pip install tamga-sdk
Requires Python 3.9+. The distribution is named tamga-sdk (the bare tamga name on PyPI
belongs to an unrelated logging library); the importable package is tamga:
import tamga
Quickstart
from tamga import TamgaClient, TamgaConfig
from tamga.transport import LicenseAuth
config = TamgaConfig(
account_id="your-account-id",
host="api.tamga.sh",
default_auth=LicenseAuth(key="YOUR-LICENSE-KEY"),
)
with TamgaClient(config) as client:
result = client.licenses.validate_by_key("YOUR-LICENSE-KEY")
if result.meta.valid:
print("License is valid:", result.meta.code.value)
else:
print("License is not valid:", result.meta.code.value, result.meta.detail)
result.meta.code is a ValidationCode enum member (VALID, EXPIRED, SUSPENDED,
TOO_MANY_MACHINES, …). An unrecognized code from a newer server deserializes to
ValidationCode.UNKNOWN instead of raising.
Runnable end-to-end scripts live in examples/:
validate_license.py— validate by key, by ID with a scope, and the lightweight quick-validateGET.checkout_and_verify.py— offline.liccheckout, plain and encrypted, through the full verify pipeline.machine_activation_flow.py— create machine → validate → roll back on over-limit.heartbeat_scheduler.py— machine (600s window) vs. process (30s window) heartbeat scheduling side by side.offline_proof.py— air-gapped machine proof generation and verification.
Auth transports
Four of the server's five transports are modeled (src/tamga/transport.py::apply_auth).
Session-cookie auth is browser/portal-only — it requires a matching Origin header and is
deliberately out of scope for a non-browser SDK.
from tamga import TamgaConfig
from tamga.transport import BasicAuth, BearerAuth, LicenseAuth, QueryParamAuth
# 1. Bearer token
TamgaConfig(account_id="...", host="api.tamga.sh", default_auth=BearerAuth(token="tok-..."))
# 2. HTTP Basic — three sub-forms
TamgaConfig(
account_id="...",
host="api.tamga.sh",
default_auth=BasicAuth(email="you@example.com", password="..."),
)
TamgaConfig(account_id="...", host="api.tamga.sh", default_auth=BasicAuth(token="tok-..."))
TamgaConfig(account_id="...", host="api.tamga.sh", default_auth=BasicAuth(license_key="..."))
# 3. License key — the primary transport for embedded/client apps
TamgaConfig(account_id="...", host="api.tamga.sh", default_auth=LicenseAuth(key="YOUR-KEY"))
# 4. Query parameter
TamgaConfig(account_id="...", host="api.tamga.sh", default_auth=QueryParamAuth(value="tok-..."))
Every issued token carries a tok- prefix regardless of its documented type — treat tokens as
opaque strings and do not build prefix-based type detection.
licenses.validate_by_key(key) falls back to Authorization: License <key> for the key being
validated when no default_auth is configured, since it already holds the credential
(src/tamga/client.py::LicensesClient.validate_by_key).
Offline verification
.lic license files and machine files verify entirely offline once the account's public key is
embedded in your application — no network round-trip per check.
from tamga.checkout.license_file import LicenseFile, LicenseFileExpired
# The account's raw 32-byte Ed25519 public key, embedded in your application.
ACCOUNT_PUBLIC_KEY = b"...32 bytes..."
with TamgaClient(config) as client:
checkout = client.licenses.check_out(license_id, ttl=86_400)
assert not isinstance(checkout, bytes) # the POST variant returns a LicenseFileResource
license_file = LicenseFile.parse(checkout.certificate)
try:
license_resource = license_file.verify(ACCOUNT_PUBLIC_KEY)
except LicenseFileExpired as exc:
print("license file expired at unix timestamp", exc.exp)
else:
print("verified:", license_resource.id)
Pass as_bytes=True to use the GET variant instead, which returns the raw .lic bytes with no
surrounding metadata. For an encrypted checkout, supply the license key so the AES key can be
derived, and use verify_with_claims when you want the signed jti (replay detection) or kid
(key rotation):
encrypted = client.licenses.check_out(license_id, encrypt=True, ttl=86_400)
assert not isinstance(encrypted, bytes)
license_resource, claims = LicenseFile.parse(encrypted.certificate).verify_with_claims(
ACCOUNT_PUBLIC_KEY,
license_key="YOUR-LICENSE-KEY",
)
print(claims.iat, claims.exp, claims.jti, claims.kid)
⚠️ Compatibility break: license files must be format v2.
algmust bebase64+ed25519+v2oraes-256-gcm+ed25519+v2; every v1-issued.licfile is rejected with aValueErrorand there is no fallback path (src/tamga/checkout/license_file.py::LicenseFile.parse). If you hold v1 files, re-check them out against a v2 server. In v1 the requestedttl/expirylived only in the JSON:API envelope around the certificate, so a 24-hour trial file stayed cryptographically valid forever; accepting both formats would hand that behavior back.
Machine files use the same {enc, sig, alg} envelope but dispatch signature verification on the
license's own scheme (ED25519_SIGN, RSA_2048_PKCS1_SIGN, RSA_2048_PKCS1_PSS_SIGN,
ECDSA_P256_SIGN) via src/tamga/checkout/machine_file.py::MachineFile.verify, and they are not
part of the +v2 alg vocabulary. src/tamga/proof.py::ProofResult.verify covers the lighter
air-gapped machine offline proof.
Security notes
- Both offline-file AES keys are HKDF-SHA256 derived. License file:
salt = "tamga:license-file-key-v1",ikm = the license key,info = "license-file"(src/tamga/crypto/hkdf.py::derive_license_file_key). Machine file:salt = "tamga:machine-file-key-v1",ikm = the license key,info = the machine's fingerprint(src/tamga/crypto/hkdf.py::derive_machine_file_key), so a machine file only decrypts on the machine it was issued for. The former zero-pad/truncate license-file transform was removed, not deprecated — the module that implemented it no longer exists. - Signed expiry is enforced, not advisory. Format v2 moves
iat/exp/jti/kidinside the signed bytes, andsrc/tamga/checkout/license_file.py::LicenseFile.verifyrejects an expired file withLicenseFileExpiredusing a deliberately small 60-second clock-skew tolerance (CLOCK_SKEW_TOLERANCE_SECONDS). The client's clock is under the attacker's control, so passverify(..., now=<server-supplied timestamp>)if you are defending against a rewound clock.LicenseFile.is_expired()reads the unsignedexpirymetadata and is advisory only. - Signatures cover
enc's base64 string, not its decoded bytes. Both file types signenc.encode("ascii")(src/tamga/checkout/license_file.py::LicenseFile.verify). It is the easiest thing to get backwards when reimplementing verification. schememust come from an authenticated response. FeedMachineFile.verify(..., scheme=...)from the license's ownschemefield, never from the certificate's ownalgstring —algsits in the unsigned outer envelope and is not covered by the signature (src/tamga/checkout/machine_file.py, module docstring).RSA_2048_JWT_RS256is rejected up front withSchemeNotSupportedError, never falling through to another verifier.- HTTP 429 is live and handled.
src/tamga/client.py::_request_with_retryretries while the server answers429.src/tamga/client.py::_retry_delayprefers the server'sRetry-Afterbut caps it at 60s, otherwise using jittered exponential backoff so a fleet does not reconverge into the spike it was backing off from.src/tamga/client.py::_is_retryablescopes auto-retry to everyGETplus exactly fivePOSTactions —validate,validate-key,check-in,check-out,ping— because those are the calls a client makes on a timer. Creates are deliberately excluded: retryingPOST /machinesrisks burning a second seat. Tune withTamgaConfig(max_retries=...);0disables retries and the raisedtamga.errors.RateLimitedErrorstill carriesretry_after. - Verification failures stay uniform inside a step. A wrong key, a malformed key, and a
tampered message all collapse to one
InvalidSignature(src/tamga/crypto/ed25519.py::verify). The steps themselves remain distinguishable on purpose:InvalidSignature(not authentic),InvalidTag(authentic but decryption failed),LicenseFileExpired(authentic but expired),ValueError(malformed input that never reached a cryptographic operation).
Report suspected vulnerabilities privately to security@tamga.sh — see
SECURITY.md.
Known gaps
- Sync only.
TamgaClientwrapshttpx.Client; there is no async client yet. - No session-cookie transport. Browser/portal only, out of scope here.
- No
Tamga-Environmentheader. No server code path reads it yet, so the SDK does not send it. - No releases/auto-update sub-client. The upgrade-check endpoint is not usable server-side.
X-RateLimit-*response headers are not sent.Retry-Afteron a429is the only server-side rate-limit signal available (src/tamga/transport.py::parse_retry_after), and only its delta-seconds form is honored — the HTTP-date form is ignored rather than risking a date being misread as a duration.- 10 of the 24
ValidationCodemembers are declared but never emitted today (BANNED,ENTITLEMENTS_MISSING,TOO_MANY_USERS,HEARTBEAT_DEAD,HEARTBEAT_NOT_STARTED, theFINGERPRINT/COMPONENTS/CHECKSUM/VERSIONscope mismatches, andNOT_FOUND, which comes back as a raw HTTP 404). Per-member reachability is documented insrc/tamga/models/validation.py. - Only four
LicenseScopefields are enforced server-side —product,policy,user,environment.entitlements,fingerprint,version, andchecksumare sent and silently ignored. - Pagination cursors are inferred.
components.list/entitlements.listreturnnext_after=Noneunless you pass an explicitlimit, because the server exposes no cursor metadata (src/tamga/client.py::_next_after_cursor). - No CLI. The package ships a library only.
Documentation
- tamga.sh — product documentation and the account console.
SECURITY.md— the crypto assumptions an integrator is trusting, and how to report a vulnerability.CLAUDE.md— dense, gotcha-first architecture/crypto reference for anyone modifying this codebase.CONTRIBUTING.md— dev setup, test/lint/type-check commands, PR expectations.- Every public symbol carries a Google-style docstring;
help(tamga.TamgaClient)and your IDE are the API reference until a generated docs site lands.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tamga_sdk-1.0.2.tar.gz.
File metadata
- Download URL: tamga_sdk-1.0.2.tar.gz
- Upload date:
- Size: 172.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eaf4fad6fbfe9b0c4c904c2c61c31275b25b92d4445c4540b49f86ba3894324e
|
|
| MD5 |
c8f2f5a56c1d7ba26d853f3e9ded394c
|
|
| BLAKE2b-256 |
c9b23f1bdbe9851e7670e200bdee524298dc4cf2c2344801429f7c143ef0059a
|
Provenance
The following attestation bundles were made for tamga_sdk-1.0.2.tar.gz:
Publisher:
release.yml on tamga-sh/tamga-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tamga_sdk-1.0.2.tar.gz -
Subject digest:
eaf4fad6fbfe9b0c4c904c2c61c31275b25b92d4445c4540b49f86ba3894324e - Sigstore transparency entry: 2500318107
- Sigstore integration time:
-
Permalink:
tamga-sh/tamga-python@36116f26d1234d16fcc757b8a973d0b0bec6ad41 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/tamga-sh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@36116f26d1234d16fcc757b8a973d0b0bec6ad41 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tamga_sdk-1.0.2-py3-none-any.whl.
File metadata
- Download URL: tamga_sdk-1.0.2-py3-none-any.whl
- Upload date:
- Size: 57.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3720e6efadbda5f66f97e12136a231fbf0664573e5ebc7ed20fca16a198638f
|
|
| MD5 |
723c2722aa456a8681a5f5fd3cfc6e83
|
|
| BLAKE2b-256 |
31906fe9abec6968deb482582d4f29b1111f9a370c155af02c4b6937938f5f89
|
Provenance
The following attestation bundles were made for tamga_sdk-1.0.2-py3-none-any.whl:
Publisher:
release.yml on tamga-sh/tamga-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tamga_sdk-1.0.2-py3-none-any.whl -
Subject digest:
b3720e6efadbda5f66f97e12136a231fbf0664573e5ebc7ed20fca16a198638f - Sigstore transparency entry: 2500318111
- Sigstore integration time:
-
Permalink:
tamga-sh/tamga-python@36116f26d1234d16fcc757b8a973d0b0bec6ad41 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/tamga-sh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@36116f26d1234d16fcc757b8a973d0b0bec6ad41 -
Trigger Event:
push
-
Statement type: