claim169-core

Crates.ioclaim169-core
lib.rsclaim169-core
version0.1.0-alpha.3
created_at2026-01-23 07:01:34.624881+00
updated_at2026-01-24 06:37:05.685879+00
descriptionMOSIP Claim 169 QR Code decoder library
homepage
repositoryhttps://github.com/jeremi/claim-169
max_upload_size
id2063675
size380,387
Jeremi Joslin (jeremi)

documentation

README

claim169-core

Alpha Software: This library is under active development. APIs may change without notice. Not recommended for production use without thorough testing.

Crates.io Documentation License: MIT

A Rust library for encoding and decoding MOSIP Claim 169 QR codes.

Features

  • Encode Claim 169 identity credentials to QR codes (Ed25519, ECDSA P-256)
  • Decode Claim 169 identity credentials from QR codes
  • Sign with Ed25519 or ECDSA P-256
  • Verify signatures (Ed25519, ECDSA P-256)
  • Encrypt/Decrypt with AES-GCM (128 or 256 bit)
  • Pluggable crypto backends for HSM integration
  • Comprehensive security: weak key rejection, decompression limits, timestamp validation

Installation

Add to your Cargo.toml:

[dependencies]
claim169-core = "0.1.0-alpha.3"

Feature Flags

Feature Default Description
software-crypto Yes Software implementations of Ed25519, ECDSA P-256, AES-GCM

Disable default features to use only custom crypto backends:

[dependencies]
claim169-core = { version = "0.1.0-alpha.3", default-features = false }

Encoding (Creating QR Codes)

Ed25519 Signed (Recommended)

use claim169_core::{Encoder, Claim169, CwtMeta};

// Assume you already have a 32-byte Ed25519 private key
let private_key: [u8; 32] = [/* your private key bytes */];

let claim = Claim169::default()
    .with_id("123456789")
    .with_full_name("John Doe")
    .with_date_of_birth("1990-01-15");

let meta = CwtMeta::new()
    .with_issuer("https://issuer.example.com")
    .with_expires_at(1800000000);

let qr_data = Encoder::new(claim, meta)
    .sign_with_ed25519(&private_key)?
    .encode()?;

// qr_data is a Base45 string ready for QR code generation

ECDSA P-256 Signed

use claim169_core::Encoder;

// Assume you already have a 32-byte ECDSA P-256 private key
let private_key: [u8; 32] = [/* your private key bytes */];

let qr_data = Encoder::new(claim169, cwt_meta)
    .sign_with_ecdsa_p256(&private_key)?
    .encode()?;

Signed and Encrypted

use claim169_core::Encoder;

// Assume you already have keys
let private_key: [u8; 32] = [/* your Ed25519 private key */];
let aes_key: [u8; 32] = [/* your AES-256 key */];

// Sign first, then encrypt (order enforced by library)
let qr_data = Encoder::new(claim169, cwt_meta)
    .sign_with_ed25519(&private_key)?
    .encrypt_with_aes256(&aes_key)?
    .encode()?;

Unsigned (Testing Only)

// Requires explicit opt-in - INSECURE
let qr_data = Encoder::new(claim169, cwt_meta)
    .allow_unsigned()
    .encode()?;

Custom Signer (HSM Integration)

use claim169_core::{Encoder, Signer, CryptoResult};
use coset::iana;

struct HsmSigner {
    hsm_client: HsmClient, // Your HSM client
}

impl Signer for HsmSigner {
    fn sign(
        &self,
        algorithm: iana::Algorithm,
        _key_id: Option<&[u8]>,
        data: &[u8],
    ) -> CryptoResult<Vec<u8>> {
        // Delegate to your HSM
        self.hsm_client.sign(data)
    }

    // Optional: override to provide a key ID
    fn key_id(&self) -> Option<&[u8]> {
        Some(b"my-key-id")
    }
}

let qr_data = Encoder::new(claim169, cwt_meta)
    .sign_with(hsm_signer, iana::Algorithm::EdDSA)
    .encode()?;

Decoding (Reading QR Codes)

With Ed25519 Verification (Recommended)

use claim169_core::Decoder;

let result = Decoder::new(qr_content)
    .verify_with_ed25519(&public_key)?
    .decode()?;

println!("ID: {:?}", result.claim169.id);
println!("Name: {:?}", result.claim169.full_name);
println!("Issuer: {:?}", result.cwt_meta.issuer);

With ECDSA P-256 Verification

let result = Decoder::new(qr_content)
    .verify_with_ecdsa_p256(&public_key)?
    .decode()?;

Decrypting Encrypted Credentials

let result = Decoder::new(qr_content)
    .decrypt_with_aes256(&aes_key)?
    .verify_with_ed25519(&public_key)?
    .decode()?;

Without Verification (Testing Only)

// Requires explicit opt-in - INSECURE
let result = Decoder::new(qr_content)
    .allow_unverified()
    .decode()?;

With Options

let result = Decoder::new(qr_content)
    .skip_biometrics()                    // Don't parse biometric data
    .max_decompressed_bytes(32768)        // 32KB limit (default: 64KB)
    .clock_skew_tolerance(60)             // 60 seconds tolerance
    .without_timestamp_validation()       // Disable exp/nbf validation
    .verify_with_ed25519(&public_key)?
    .decode()?;

Custom Verifier (HSM Integration)

use claim169_core::{Decoder, SignatureVerifier, CryptoResult, CryptoError};
use coset::iana;

struct HsmVerifier {
    hsm_client: HsmClient, // Your HSM client
}

impl SignatureVerifier for HsmVerifier {
    fn verify(
        &self,
        algorithm: iana::Algorithm,
        _key_id: Option<&[u8]>,
        data: &[u8],
        signature: &[u8],
    ) -> CryptoResult<()> {
        // Delegate to your HSM
        self.hsm_client
            .verify(data, signature)
            .map_err(|_| CryptoError::VerificationFailed)
    }
}

let result = Decoder::new(qr_content)
    .verify_with(hsm_verifier)
    .decode()?;

Data Model

Claim169

The main identity data structure containing:

  • Demographics: id, name, date of birth, gender, address, etc.
  • Biometrics: fingerprints, iris scans, face images, voice samples
  • Unknown fields: Forward-compatible storage for new spec fields
let claim = result.claim169;

// Demographics
println!("ID: {:?}", claim.id);
println!("Name: {:?}", claim.full_name);
println!("DOB: {:?}", claim.date_of_birth);

// Biometrics
if claim.has_biometrics() {
    println!("Has {} biometric entries", claim.biometric_count());
}

CwtMeta

CWT (CBOR Web Token) metadata:

let meta = result.cwt_meta;

println!("Issuer: {:?}", meta.issuer);
println!("Expires: {:?}", meta.expires_at);

// Check validity
if meta.is_expired(current_time) {
    println!("Credential expired!");
}

Error Handling

use claim169_core::{Decoder, Claim169Error};

match Decoder::new(qr_content).allow_unverified().decode() {
    Ok(result) => println!("Decoded: {:?}", result.claim169.full_name),
    Err(Claim169Error::Base45Decode(msg)) => println!("Invalid QR encoding: {}", msg),
    Err(Claim169Error::Expired(ts)) => println!("Expired at {}", ts),
    Err(Claim169Error::SignatureInvalid(msg)) => println!("Bad signature: {}", msg),
    Err(Claim169Error::DecodingConfig(msg)) => println!("Config error: {}", msg),
    Err(e) => println!("Error: {}", e),
}

Security

  • Always verify signatures in production
  • Always sign credentials when encoding
  • Weak keys are rejected (all-zeros, small-order points)
  • Decompression is limited to prevent zip bomb attacks
  • Timestamps are validated by default
  • Signing keys are zeroized on drop (where supported)

See SECURITY.md for detailed security information.

Key Generation (Optional Convenience)

You will need cryptographic keys, but in production those are typically provisioned and managed externally (HSM/KMS or secure key management). The examples below are provided for local development and testing convenience; for production, prefer integrating your existing key management and using the Signer/SignatureVerifier traits.

Ed25519 Keys

use claim169_core::Ed25519Signer;

// Generate a new random Ed25519 signing key
let signer = Ed25519Signer::generate();

// Get the public key for verification (32 bytes)
let public_key = signer.public_key_bytes();

// Or get a verifier directly
let verifier = signer.verifying_key();

// You can also create a signer from existing private key bytes (32 bytes)
let private_key_bytes: [u8; 32] = [/* your 32-byte private key */];
let signer = Ed25519Signer::from_bytes(&private_key_bytes)?;

ECDSA P-256 Keys

use claim169_core::EcdsaP256Signer;

// Generate a new random ECDSA P-256 signing key
let signer = EcdsaP256Signer::generate();

// Get the public key in uncompressed SEC1 format (65 bytes)
let public_key = signer.public_key_uncompressed();

// Or get a verifier directly
let verifier = signer.verifying_key();

// You can also create a signer from existing private key bytes (32 bytes)
let private_key_bytes: [u8; 32] = [/* your 32-byte private key */];
let signer = EcdsaP256Signer::from_bytes(&private_key_bytes)?;

AES-GCM Keys

AES keys must be generated using a secure random number generator. The library doesn't provide a key generation method, so use the rand crate:

use rand::RngCore;

// Generate a 32-byte key for AES-256-GCM
let mut aes256_key = [0u8; 32];
rand::thread_rng().fill_bytes(&mut aes256_key);

// Generate a 16-byte key for AES-128-GCM
let mut aes128_key = [0u8; 16];
rand::thread_rng().fill_bytes(&mut aes128_key);

Note: In production, use a cryptographically secure random number generator. The rand crate with OsRng is recommended:

use rand::{RngCore, rngs::OsRng};

let mut aes_key = [0u8; 32];
OsRng.fill_bytes(&mut aes_key);

Building

# Build
cargo build --release

# Run tests
cargo test --all-features

# Generate documentation
cargo doc --all-features --open

# Run clippy
cargo clippy --all-features

License

MIT License - See LICENSE for details.

Commit count: 104

cargo fmt