MOSIP Claim 169 QR Code decoder library
Project description
claim169
Alpha Software: This library is under active development. APIs may change without notice. Not recommended for production use without thorough testing.
A Python library for encoding and decoding MOSIP Claim 169 QR codes. Built on Rust for performance and security.
Installation
pip install claim169
Overview
MOSIP Claim 169 defines a standard for encoding identity data in QR codes using:
- CBOR encoding with numeric keys for compactness
- CWT (CBOR Web Token) for standard claims
- COSE_Sign1 for digital signatures
- COSE_Encrypt0 for optional encryption
- zlib compression + Base45 encoding for QR-friendly output
Quick Start
import claim169
# Decode a QR code (recommended: with signature verification)
qr_text = "6BF5YZB2..." # Base45-encoded QR content
result = claim169.decode(qr_text, verify_with_ed25519=public_key_bytes)
# Access identity data
print(f"ID: {result.claim169.id}")
print(f"Name: {result.claim169.full_name}")
print(f"DOB: {result.claim169.date_of_birth}")
# Access CWT metadata
print(f"Issuer: {result.cwt_meta.issuer}")
print(f"Expires: {result.cwt_meta.expires_at}")
Encoding
Create Claim 169 QR code data with various signing and encryption options. In production, keys are typically provisioned and managed externally (HSM/KMS or secure key management). The examples below assume you already have key bytes.
from claim169 import Claim169Input, CwtMetaInput, encode_with_ed25519, encode_signed_encrypted
# Create identity data
claim = Claim169Input(id="123456", full_name="John Doe")
claim.date_of_birth = "1990-01-15"
claim.email = "john@example.com"
# Create CWT metadata
meta = CwtMetaInput(issuer="https://issuer.example.com", expires_at=1800000000)
# Encode with Ed25519 signature (32-byte private key)
qr_data = encode_with_ed25519(claim, meta, private_key_bytes)
# Encode with signature and AES-256 encryption
qr_data = encode_signed_encrypted(claim, meta, sign_key_bytes, encrypt_key_bytes)
Encoding Functions
from claim169 import (
encode_with_ed25519, # Ed25519 signed
encode_with_ecdsa_p256, # ECDSA P-256 signed
encode_signed_encrypted, # Signed + AES-256-GCM encrypted
encode_unsigned, # Unsigned (testing only)
generate_nonce, # Generate random 12-byte nonce
)
# Ed25519 signed (recommended)
qr_data = encode_with_ed25519(claim, cwt_meta, private_key) # 32-byte key
# ECDSA P-256 signed
qr_data = encode_with_ecdsa_p256(claim, cwt_meta, private_key) # 32-byte key
# Signed and encrypted (AES-256-GCM)
qr_data = encode_signed_encrypted(claim, cwt_meta, sign_key, encrypt_key)
# Unsigned (for testing only - not recommended for production)
qr_data = encode_unsigned(claim, cwt_meta)
# Generate a random 12-byte nonce for encryption
nonce = generate_nonce()
Signature Verification
Ed25519 Verification (Recommended)
# Decode with Ed25519 signature verification
public_key = bytes.fromhex("d75a980182b10ab7...") # 32 bytes
result = claim169.decode_with_ed25519(qr_text, public_key)
if result.is_verified():
print("Signature is valid!")
ECDSA P-256 Verification
# Decode with ECDSA P-256 signature verification
public_key = bytes.fromhex("04...") # SEC1-encoded (33 or 65 bytes)
result = claim169.decode_with_ecdsa_p256(qr_text, public_key)
Custom Verifier (HSM Support)
For hardware security module (HSM) integration:
def my_hsm_verify(algorithm: str, key_id: bytes | None, data: bytes, signature: bytes):
"""Custom verifier callback for HSM integration.
Args:
algorithm: COSE algorithm name (e.g., "EdDSA", "ES256")
key_id: Optional key identifier from COSE header
data: The signed data (Sig_structure)
signature: The signature bytes
Raises:
Exception: If verification fails
"""
# Delegate to your HSM
hsm.verify(key_id, data, signature)
result = claim169.decode_with_verifier(qr_text, my_hsm_verify)
Encrypted Payloads
AES-GCM Decryption
# Decrypt with AES-256-GCM key
aes_key = bytes.fromhex("000102030405...") # 32 bytes for AES-256
result = claim169.decode_encrypted_aes(qr_text, aes_key, allow_unverified=True) # testing only
With Nested Signature Verification
def verify_callback(algorithm, key_id, data, signature):
public_key.verify(signature, data)
result = claim169.decode_encrypted_aes(
qr_text,
aes_key,
verifier=verify_callback
)
Custom Decryptor (HSM Support)
def my_hsm_decrypt(algorithm: str, key_id: bytes | None, nonce: bytes, aad: bytes, ciphertext: bytes) -> bytes:
"""Custom decryptor callback for HSM integration.
Args:
algorithm: COSE algorithm name (e.g., "A256GCM")
key_id: Optional key identifier from COSE header
nonce: IV/nonce from COSE header
aad: Additional authenticated data (Enc_structure)
ciphertext: The encrypted data
Returns:
Decrypted plaintext bytes
"""
return hsm.decrypt(key_id, nonce, aad, ciphertext)
# Provide a verifier for the inner COSE_Sign1 (recommended)
result = claim169.decode_with_decryptor(qr_text, my_hsm_decrypt, verifier=my_hsm_verify)
Decode Options
# Skip biometric data for faster parsing
result = claim169.decode(
qr_text,
verify_with_ed25519=public_key_bytes,
skip_biometrics=True,
)
# Limit decompressed size (default: 64KB)
result = claim169.decode(
qr_text,
verify_with_ed25519=public_key_bytes,
max_decompressed_bytes=32768,
)
Data Model
DecodeResult
result.claim169 # Claim169 - Identity data
result.cwt_meta # CwtMeta - Token metadata
result.verification_status # "verified", "skipped", or "failed"
# Helper methods
result.is_verified() # True if signature was verified
Claim169Input
Input class for encoding identity data into QR codes.
from claim169 import Claim169Input
claim = Claim169Input(id="123456", full_name="John Doe")
# Set optional fields
claim.first_name = "John"
claim.last_name = "Doe"
claim.date_of_birth = "1990-01-15"
claim.gender = 1 # 1=Male, 2=Female, 3=Other
claim.email = "john@example.com"
claim.phone = "+1234567890"
claim.address = "123 Main St"
claim.nationality = "US"
claim.marital_status = 1
CwtMetaInput
Input class for encoding CWT (CBOR Web Token) metadata.
from claim169 import CwtMetaInput
meta = CwtMetaInput(issuer="https://issuer.example.com", expires_at=1800000000)
# Set optional fields
meta.subject = "user-123"
meta.not_before = 1700000000
meta.issued_at = 1700000000
Claim169
claim = result.claim169
# Demographics
claim.id # Unique identifier
claim.full_name # Full name
claim.first_name # First name
claim.middle_name # Middle name
claim.last_name # Last name
claim.date_of_birth # ISO 8601 format
claim.gender # 1=Male, 2=Female, 3=Other
claim.address # Address
claim.email # Email address
claim.phone # Phone number
claim.nationality # Nationality code
claim.marital_status # Marital status code
# Biometrics (when present)
claim.face # List of face biometrics
claim.right_thumb # Right thumb fingerprint
# ... (all finger/iris/palm biometrics)
# Helper methods
claim.has_biometrics() # True if any biometric data present
claim.to_dict() # Convert to dictionary
CwtMeta
meta = result.cwt_meta
meta.issuer # Token issuer
meta.subject # Token subject
meta.expires_at # Expiration timestamp (Unix seconds)
meta.not_before # Not-before timestamp
meta.issued_at # Issued-at timestamp
# Helper methods
meta.is_valid_now() # True if token is currently valid
meta.is_expired() # True if token has expired
Biometric
bio = claim.face[0]
bio.data # Raw biometric data bytes
bio.format # Biometric format code
bio.sub_format # Sub-format code (optional)
bio.issuer # Issuer identifier (optional)
Exception Types
from claim169 import (
Claim169Exception, # Base exception
Base45DecodeError, # Invalid Base45 encoding
DecompressError, # zlib decompression failed
CoseParseError, # Invalid COSE structure
CwtParseError, # Invalid CWT structure
Claim169NotFoundError, # Missing claim 169
SignatureError, # Signature verification failed
DecryptionError, # Decryption failed
)
Development
Building from Source
# Install maturin
pip install maturin
# Build and install in development mode
cd core/claim169-python
maturin develop
Running Tests
cd core/claim169-python
uv run pytest tests/ -v
License
MIT License - See LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
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 claim169-0.1.0a0.tar.gz.
File metadata
- Download URL: claim169-0.1.0a0.tar.gz
- Upload date:
- Size: 145.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
89733eeb43d35ba22e609b36fdd7b2d3ff54ef3c80d2686e786ad1c95fc1ebe1
|
|
| MD5 |
787f7102acc5ab5ef58922b0a98c57f1
|
|
| BLAKE2b-256 |
82b68aa4a1f0301fa06bc3e79f8bc2d9119e71194bbe547ae1ca4a659b2cb19a
|
Provenance
The following attestation bundles were made for claim169-0.1.0a0.tar.gz:
Publisher:
publish-release.yml on jeremi/claim-169
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claim169-0.1.0a0.tar.gz -
Subject digest:
89733eeb43d35ba22e609b36fdd7b2d3ff54ef3c80d2686e786ad1c95fc1ebe1 - Sigstore transparency entry: 845884883
- Sigstore integration time:
-
Permalink:
jeremi/claim-169@933811bfd642654bfd40d60ba4d969bc1deff04e -
Branch / Tag:
refs/tags/v0.1.0-alpha - Owner: https://github.com/jeremi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-release.yml@933811bfd642654bfd40d60ba4d969bc1deff04e -
Trigger Event:
push
-
Statement type:
File details
Details for the file claim169-0.1.0a0-cp311-cp311-win_amd64.whl.
File metadata
- Download URL: claim169-0.1.0a0-cp311-cp311-win_amd64.whl
- Upload date:
- Size: 529.4 kB
- Tags: CPython 3.11, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27874dca2f36706ae5c9862089d2a08296a2ef552744e676a83380f97eab5c32
|
|
| MD5 |
993c3fdc84ceaadeccb4ab8d7c014957
|
|
| BLAKE2b-256 |
94119343b0982c16b3e5bf0f90436701b6d6fa9695ee7e1cacc13b0ef5df3559
|
Provenance
The following attestation bundles were made for claim169-0.1.0a0-cp311-cp311-win_amd64.whl:
Publisher:
publish-release.yml on jeremi/claim-169
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claim169-0.1.0a0-cp311-cp311-win_amd64.whl -
Subject digest:
27874dca2f36706ae5c9862089d2a08296a2ef552744e676a83380f97eab5c32 - Sigstore transparency entry: 845884885
- Sigstore integration time:
-
Permalink:
jeremi/claim-169@933811bfd642654bfd40d60ba4d969bc1deff04e -
Branch / Tag:
refs/tags/v0.1.0-alpha - Owner: https://github.com/jeremi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-release.yml@933811bfd642654bfd40d60ba4d969bc1deff04e -
Trigger Event:
push
-
Statement type:
File details
Details for the file claim169-0.1.0a0-cp311-cp311-macosx_11_0_arm64.whl.
File metadata
- Download URL: claim169-0.1.0a0-cp311-cp311-macosx_11_0_arm64.whl
- Upload date:
- Size: 615.3 kB
- Tags: CPython 3.11, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f528f7bf57f79be221d0eba9bfab732de3a60d43b800b59fa7204c413949c948
|
|
| MD5 |
74555b20af0b254b7549689986280049
|
|
| BLAKE2b-256 |
0cb6378f87cf4f85f365347ba6cacb7aef1018a380cd8ebb9894417816b1a066
|
Provenance
The following attestation bundles were made for claim169-0.1.0a0-cp311-cp311-macosx_11_0_arm64.whl:
Publisher:
publish-release.yml on jeremi/claim-169
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claim169-0.1.0a0-cp311-cp311-macosx_11_0_arm64.whl -
Subject digest:
f528f7bf57f79be221d0eba9bfab732de3a60d43b800b59fa7204c413949c948 - Sigstore transparency entry: 845884891
- Sigstore integration time:
-
Permalink:
jeremi/claim-169@933811bfd642654bfd40d60ba4d969bc1deff04e -
Branch / Tag:
refs/tags/v0.1.0-alpha - Owner: https://github.com/jeremi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-release.yml@933811bfd642654bfd40d60ba4d969bc1deff04e -
Trigger Event:
push
-
Statement type:
File details
Details for the file claim169-0.1.0a0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: claim169-0.1.0a0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 715.1 kB
- Tags: CPython 3.8, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c532c622a98b7c47212423ee9116d415cc061264986da0d7d732658d44258e5
|
|
| MD5 |
df20ffd94fb5a1d983b4bd6c136ff639
|
|
| BLAKE2b-256 |
90142e1fc70ee5e893dd98c979bd8a8ade4fdff642afe03076ae3f4d2aeeacc1
|
Provenance
The following attestation bundles were made for claim169-0.1.0a0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
publish-release.yml on jeremi/claim-169
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claim169-0.1.0a0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
0c532c622a98b7c47212423ee9116d415cc061264986da0d7d732658d44258e5 - Sigstore transparency entry: 845884896
- Sigstore integration time:
-
Permalink:
jeremi/claim-169@933811bfd642654bfd40d60ba4d969bc1deff04e -
Branch / Tag:
refs/tags/v0.1.0-alpha - Owner: https://github.com/jeremi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-release.yml@933811bfd642654bfd40d60ba4d969bc1deff04e -
Trigger Event:
push
-
Statement type: