Digital Signing of Physical Items Protocol SDK - Official Python implementation
Project description
DSPIP Python SDK
Official Python implementation of the Digital Signing of Physical Items Protocol (DSPIP) per Internet-Draft draft-midwestcyber-dspip-02.
⚠️ BETA VERSION: This is a pre-release implementation of the DSPIP protocol, currently under review by the IETF. The API may change based on feedback.
Overview
DSPIP provides cryptographic authentication for physical items using digitally signed QR codes, with a focus on shipping and logistics applications.
Installation
pip install dspip
Or install from source:
git clone https://github.com/MidwestCyberLLC/dspip-python.git
cd dspip-python
pip install -e .
Quick Start
from dspip import (
generate_key_pair,
create_standard_payload,
verify_offline,
generate_qr_code_data_url,
)
# Generate a key pair
keys = generate_key_pair()
print(f"Public Key: {keys.public_key_base64}")
# Create a shipping label
qr_data = create_standard_payload(
issuer={"name": "Acme Corp", "address": {"country": "US"}},
recipient={"name": "John Doe", "address": {"country": "US", "city": "New York"}},
item_id="PKG-001",
key_locator="warehouse._dspip.example.com",
sender_private_key_hex=keys.private_key_hex,
)
# Generate QR code
qr_image = generate_qr_code_data_url(qr_data)
# Verify offline
result = verify_offline(qr_data, keys.public_key_base64)
print(f"Valid: {result.valid}")
Features
Key Generation
from dspip import (
generate_key_pair,
generate_secp256k1_key_pair,
generate_ed25519_key_pair,
)
# Generate secp256k1 key pair (default for standard/encrypted mode)
secp_keys = generate_secp256k1_key_pair()
# Generate Ed25519 key pair (for split-key mode)
ed_keys = generate_ed25519_key_pair()
# Auto-detect based on curve parameter
keys = generate_key_pair("ed25519")
Privacy Modes
DSPIP supports three privacy modes:
Standard Mode
Recipient information is visible to all parties.
from dspip import create_standard_payload
qr_data = create_standard_payload(
issuer={"name": "Sender", "address": {"country": "US"}},
recipient={"name": "Recipient", "address": {"country": "US"}},
item_id="PKG-001",
key_locator="warehouse._dspip.example.com",
sender_private_key_hex=keys.private_key_hex,
)
Encrypted Mode
Recipient information is encrypted for the carrier.
from dspip import create_encrypted_payload
qr_data = create_encrypted_payload(
issuer={"name": "Sender", "address": {"country": "US"}},
recipient={"name": "Recipient", "address": {"country": "US"}},
item_id="PKG-001",
key_locator="warehouse._dspip.example.com",
sender_private_key_hex=sender_keys.private_key_hex,
carrier_public_key_hex=carrier_keys.public_key_hex,
lmp_address="plus:87G8Q2JM+HV",
)
Split-Key Mode
Recipient information is encrypted for the last-mile provider using Ed25519/X25519.
from dspip import create_split_key_payload, generate_ed25519_key_pair
sender_keys = generate_ed25519_key_pair()
lmp_keys = generate_ed25519_key_pair()
qr_data = create_split_key_payload(
issuer={"name": "Sender", "address": {"country": "US"}},
recipient={"name": "Recipient", "address": {"country": "US"}},
item_id="PKG-001",
key_locator="warehouse._dspip.example.com",
sender_private_key_hex=sender_keys.private_key_hex,
lmp_public_key_hex=lmp_keys.public_key_hex,
lmp_address="plus:87G8Q2JM+HV",
)
DNS Records
from dspip import (
create_dns_record,
format_dns_record,
parse_dns_record,
create_key_lifecycle,
)
# Create a DNS record
record = create_dns_record(
public_key_base64=keys.public_key_base64,
curve="secp256k1",
)
# Format for DNS TXT record
txt_record = format_dns_record(record)
# v=DSPIP1; k=ec; c=secp256k1; p=...
# Create with lifecycle management
record = create_key_lifecycle(
public_key_base64=keys.public_key_base64,
private_key_hex=keys.private_key_hex,
signing_days=365,
verification_grace_days=90,
)
Address Schemes
from dspip import (
create_plus_code_address,
create_street_address,
create_geo_address,
create_facility_address,
parse_address_field,
)
# Plus Code (Google Open Location Code)
addr = create_plus_code_address("87G8Q2JM+HV")
# Street address (percent-encoded)
addr = create_street_address("123 Main St, New York, NY 10001")
# Geo coordinates (RFC 5870)
addr = create_geo_address(40.7128, -74.0060, uncertainty=10)
# Facility identifier
addr = create_facility_address("NYC-SORT-01")
# Parse any address format
parsed = parse_address_field("plus:87G8Q2JM+HV")
print(f"Scheme: {parsed.scheme}, Value: {parsed.value}")
Verification
import asyncio
from dspip import verify, verify_offline, verify_and_decrypt
# Offline verification with known public key
result = verify_offline(qr_data, public_key_base64)
# Online verification with DNS lookup
async def verify_label():
result = await verify(qr_data)
if result.valid:
print(f"Verified! Signed by: {result.signed_by}")
else:
for error in result.errors:
print(f"Error: {error.message}")
asyncio.run(verify_label())
# Verify and decrypt (for encrypted mode)
async def verify_and_decrypt_label():
result = await verify_and_decrypt(qr_data, carrier_private_key_hex)
if result.valid and result.decrypted_recipient:
print(f"Recipient: {result.decrypted_recipient['recipientName']}")
asyncio.run(verify_and_decrypt_label())
QR Code Generation
from dspip import (
generate_qr_code_data_url,
generate_qr_code_svg,
generate_qr_code_terminal,
save_qr_code_to_file,
will_fit_in_qr_code,
)
# Generate as data URL (for HTML img src)
data_url = generate_qr_code_data_url(qr_data)
# Generate as SVG
svg = generate_qr_code_svg(qr_data)
# Generate for terminal display
terminal = generate_qr_code_terminal(qr_data)
print(terminal)
# Save to file
save_qr_code_to_file(qr_data, "label.png")
# Check if data fits in QR code
if will_fit_in_qr_code(qr_data):
print("Data fits in QR code")
API Reference
Types
KeyPair- Key pair with hex and base64 formatsDSPIPPayload- Main payload structureDSPIPQRData- QR code data structureDSPIPDNSRecord- DNS TXT record structureVerificationResult- Verification result with errors/warningsPrivacyMode- Enum: STANDARD, ENCRYPTED, SPLIT_KEYKeyStatus- Enum: ACTIVE, VERIFY_ONLY, REVOKEDAddressScheme- Enum: PLUS, STREET, GEO, FACILITY
Crypto Functions
generate_key_pair(curve)- Generate key pairsign(message_hash, private_key_hex, curve)- Sign messageverify_signature(message_hash, signature, public_key, curve)- Verify signatureecies_encrypt(plaintext, public_key_hex)- ECIES encryptionecies_decrypt(ciphertext, private_key_hex)- ECIES decryptionencrypt_for_lmp(data, sender_private_key, lmp_public_key)- Split-key encryptiondecrypt_as_lmp(encrypted, sender_public_key, lmp_private_key)- Split-key decryption
Payload Functions
create_standard_payload(...)- Create standard mode payloadcreate_encrypted_payload(...)- Create encrypted mode payloadcreate_split_key_payload(...)- Create split-key mode payloadencode_payload(payload)- Encode payload to Base64decode_payload(encoded)- Decode payload from Base64serialize_qr_data(qr_data)- Serialize to QR stringparse_qr_data(data)- Parse QR string
DNS Functions
create_dns_record(...)- Create DNS record structureformat_dns_record(record)- Format as TXT record stringparse_dns_record(txt)- Parse TXT record stringresolve_dns_record(key_locator)- Resolve via DNS-over-HTTPScheck_key_status(record)- Check if key can sign/verify
Verification Functions
verify(qr_data)- Verify with DNS lookupverify_offline(qr_data, public_key)- Verify with known keyverify_and_decrypt(qr_data, private_key)- Verify and decrypt
QR Functions
generate_qr_code_data_url(data)- Generate as data URLgenerate_qr_code_svg(data)- Generate as SVGgenerate_qr_code_terminal(data)- Generate for terminalsave_qr_code_to_file(data, path)- Save to filewill_fit_in_qr_code(data)- Check if data fits
Revocation Lists
Revocation lists allow revoking item IDs (e.g., for lost, stolen, or damaged packages):
import asyncio
from dspip import (
create_revocation_list,
check_revocation,
verify_revocation_list,
calculate_bloom_filter_size,
)
# Create a signed revocation list with Bloom filter
revocation_list = create_revocation_list(
issuer="warehouse._dspip.acme.com",
entries=[
{"item_id": "TRACK-001", "revoked": int(time.time()), "reason": "lost"},
{"item_id": "TRACK-002", "revoked": int(time.time()), "reason": "stolen"},
],
private_key_hex=sender_keys.private_key_hex,
include_bloom_filter=True,
)
# Verify the list signature
is_valid = verify_revocation_list(revocation_list, sender_keys.public_key_hex)
# Check if an item is revoked
async def check():
result = await check_revocation(
"TRACK-001",
cached_list=revocation_list,
issuer_public_key_hex=sender_keys.public_key_hex,
)
print(f"Revoked: {result.revoked}") # True
print(f"Reason: {result.entry.reason}") # 'lost'
asyncio.run(check())
# Calculate optimal Bloom filter size
params = calculate_bloom_filter_size(10000, 0.01) # 10k items, 1% false positive
print(f"Size: {params['size']} bits, Hash count: {params['hash_count']}")
Delivery Confirmation
Cryptographic proof of delivery using challenge-response protocol:
from dspip import (
create_delivery_challenge,
respond_to_challenge,
verify_delivery_response,
create_multi_party_attestation,
add_attestation,
)
# Step 1: Carrier creates challenge
challenge = create_delivery_challenge(
item_id="TRACK-2025-001",
carrier_key_locator="driver._dspip.usps.gov",
carrier_private_key_hex=carrier_keys.private_key_hex,
)
# Step 2: Recipient signs the challenge
response = respond_to_challenge(
challenge=challenge,
recipient_private_key_hex=recipient_keys.private_key_hex,
recipient_public_key_hex=recipient_keys.public_key_hex,
metadata={
"recipient_name": "Bob Jones",
"location": "41.2565,-95.9345",
},
)
# Step 3: Carrier verifies and creates proof
proof = verify_delivery_response(
challenge=challenge,
response=response,
carrier_private_key_hex=carrier_keys.private_key_hex,
)
print(f"Delivery verified: {proof.valid}")
print(f"Proof hash: {proof.proof_hash}") # For blockchain recording
# Multi-party attestation for high-value deliveries
attestation = create_multi_party_attestation(proof, required_attestations=3)
attestation = add_attestation(attestation, "carrier", "carrier", carrier_keys.private_key_hex)
attestation = add_attestation(attestation, "recipient", "recipient", recipient_keys.private_key_hex)
attestation = add_attestation(attestation, "witness", "witness", witness_keys.private_key_hex)
print(f"Complete: {attestation.complete}") # True (3/3 attestations)
DNSSEC Validation
Validate the chain of trust from DNS root to key locator:
import asyncio
from dspip import (
validate_dnssec,
validate_key_locator_dnssec,
has_dnssec,
)
async def check_dnssec():
# Quick check: does domain have DNSSEC?
dnssec_enabled = await has_dnssec("example.com")
print(f"DNSSEC enabled: {dnssec_enabled}")
# Full validation for a key locator
result = await validate_key_locator_dnssec(
"warehouse._dspip.example.com",
full_chain_validation=True,
)
print(f"Trusted: {result['trusted']}")
print(f"Recommendation: {result['recommendation']}")
# "DNSSEC validated - key lookup is cryptographically authenticated"
# OR "DNSSEC not enabled - key lookup relies on network security only"
# Detailed validation result
dnssec_result = await validate_dnssec("cloudflare.com")
print(f"Chain of trust: {dnssec_result.chain_of_trust.to_dict()}")
# {'root': True, 'tld': True, 'domain': True, 'subdomain': True}
asyncio.run(check_dnssec())
Dependencies
coincurve- secp256k1 operationspynacl- Ed25519/X25519 operationscryptography- AES-GCM encryptionqrcode[pil]- QR code generationhttpx- Async HTTP for DNS resolution
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 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 dspip-1.0.0.tar.gz.
File metadata
- Download URL: dspip-1.0.0.tar.gz
- Upload date:
- Size: 47.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fee4c875b41958d44b016e79a4b9452e5ddc4ac706e181aa4f650665bed8fa6a
|
|
| MD5 |
36d644d9eb29cf5022a0814bccbc9415
|
|
| BLAKE2b-256 |
99bf35cbc1eff0d243b1fc7fb4745d48c8026be033f8f0748402540213ae433c
|
File details
Details for the file dspip-1.0.0-py3-none-any.whl.
File metadata
- Download URL: dspip-1.0.0-py3-none-any.whl
- Upload date:
- Size: 48.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1da6ab12320e923a5e5b84fecc79ffd82b3f422e3fd0f7085975d21368ac118
|
|
| MD5 |
0c1362c287aea94ce42c0a7d752cfa02
|
|
| BLAKE2b-256 |
70ab0ca56c08aa37829d1bc8b73c7e24177ae943278346e994263f18fbc73dcd
|