DSPIP Developer Documentation
Complete reference for implementing the Digital Signing of Physical Items Protocol. Version 1.0 | Internet-Draft: draft-midwestcyber-dspip-00
DSPIP is a cryptographic protocol for authenticating physical items using digitally signed QR codes. This specification focuses on the SHIP type for shipping and logistics, providing cryptographic authentication of packages with chain of custody tracking, privacy-preserving delivery, and anti-cloning protections through split-key labels.
DSPIP is currently an Internet-Draft submitted to the IETF. The specification is stable for implementation.
Quick Start
1. Install the SDK
2. Generate a Key Pair
import { generateKeyPair } from '@dspip/core';
const { privateKey, publicKey } = generateKeyPair();
// privateKey: 32 bytes (hex encoded)
// publicKey: 33 bytes compressed (base64 for DNS)
3. Publish Your Public Key to DNS
warehouse._dspip.example.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1; p=YOUR_PUBLIC_KEY_BASE64"
4. Create a Signed QR Code
import { createSignedQR } from '@dspip/core';
const qrData = createSignedQR({
privateKey: 'your-private-key',
keyLocator: 'warehouse._dspip.example.com',
payload: {
type: 'SHIP',
issuer: {
organization: 'ACME Corp',
address: { city: 'Omaha', state: 'NE', country: 'US' }
},
subject: {
name: 'Bob Jones',
address: { city: 'Lincoln', state: 'NE', country: 'US' }
},
itemId: 'TRACK-2025-000123',
timestamp: Date.now(),
typeData: {
privacyMode: 'standard',
parcelId: 'TRACK-2025-000123',
carrier: 'ACME',
service: 'Ground'
}
}
});
5. Verify a QR Code
import { verify } from '@dspip/core';
const result = await verify(qrData);
if (result.valid) {
console.log('Signature verified!');
console.log('Type:', result.type); // "SHIP"
console.log('Issuer:', result.payload.issuer);
console.log('Item ID:', result.payload.itemId);
} else {
console.error('Verification failed:', result.error);
}
Installation
JavaScript / TypeScript
npm install @dspip/core
# or
yarn add @dspip/core
Python
pip install dspip
Go
go get github.com/MidwestCyberLLC/dspip-go
Java (Maven)
<dependency>
<groupId>io.dspip</groupId>
<artifactId>dspip-java</artifactId>
<version>1.0.0</version>
</dependency>
Rust
cargo add dspip
# or add to Cargo.toml:
[dependencies]
dspip = "1.0"
C / Embedded
git clone https://github.com/MidwestCyberLLC/dspip-c.git
cd dspip-c
mkdir build && cd build
cmake ..
make
sudo make install
Flutter / Dart
flutter pub add dspip
# or add to pubspec.yaml:
dependencies:
dspip: ^1.0.0
From Source
git clone https://github.com/MidwestCyberLLC/dspip.git
cd dspip
npm install
npm run build
Digital Envelope Model
DSPIP follows a "digital envelope" paradigm for shipping labels. Understanding this model is key to implementing the protocol correctly.
Envelope Exterior (Public)
Like the address on a physical envelope, the following information is encoded but not encrypted. Anyone can read this data:
- Issuer (sender) identity and address
- Subject (recipient or last mile provider destination)
- Tracking number (itemId)
- Timestamp
- Type (SHIP for shipping)
Signature (Authenticity Seal)
A cryptographic signature proves the label was created by the claimed sender and has not been tampered with. Similar to a shipping seal.
Private Contents (Encrypted)
For privacy modes, the actual recipient address and delivery instructions are encrypted for the last mile provider, like the contents of a sealed package that only the authorized recipient can access.
This model protects recipient privacy while maintaining package routability and authenticity verification. The last mile provider decrypts the final delivery address only when needed.
Protocol Flow
The DSPIP shipping protocol consists of these steps:
-
Payload Creation
Sender creates a JSON payload containing type (SHIP), issuer info, subject info, itemId, and timestamp. -
Privacy Mode Selection
Choose privacy mode (standard, encrypted, or split-key) based on security requirements. -
Recipient Encryption (Privacy Modes)
For encrypted/split-key modes, encrypt recipient info with last mile provider's public key using ECIES. -
Base64 Encoding
Payload is Base64 encoded for consistent handling. -
Signing
Sender signs protocol|version|type|keyLocator|encodedPayload using their private key (ECDSA or Ed25519 for split-key). -
QR Generation
Complete 6-7 field structure is serialized with pipe delimiters and encoded as a QR code. -
Label Application
QR code is printed on shipping label and attached to package. -
Transit Scanning
Carriers scan QR code at each custody transfer point. -
DNS Lookup / Zone B Reveal
Scanner queries DNS TXT record for public key (or reveals Zone B for split-key labels). -
Verification
Scanner verifies signature and checks revocation status. -
Privacy Decryption
Last mile provider decrypts recipient information using their ECIES private key. -
Delivery Confirmation
Cryptographic proof of delivery recorded via challenge-response protocol.
Privacy Modes
DSPIP supports three privacy modes for shipping to accommodate different security requirements:
- Full recipient address in cleartext
- Standard ECDSA signature (secp256k1)
- Business-to-business shipments
- Transparency required scenarios
- Recipient encrypted with ECIES
- Last mile provider decrypts at delivery
- Consumer privacy protection
- AES-256-GCM encryption
- Ed25519 signature from physical label
- Private key under Zone A scratch-off
- Public key under Zone B for verification
- Prevents QR code duplication
Privacy Mode Selection
| Use Case | Recommended Mode | Reason |
|---|---|---|
| Business shipments | Standard | Transparency, easy verification |
| Consumer deliveries | Encrypted | Protects recipient privacy |
| High-value packages | Split-Key | Prevents cloning attacks |
| International shipping | Encrypted | Privacy with customs compliance |
For encrypted and split-key modes, packages route to a "last mile provider" (e.g., local post office, corporate mailroom) who decrypts the actual recipient address for final delivery.
Payload Structure
The payload contains shipping information encoded as JSON. DSPIP uses issuer for the sender and subject for the recipient/delivery destination:
{
"type": "SHIP", // REQUIRED: Fixed for shipping
"issuer": { // REQUIRED: Sender information
"name": "string", // Optional
"organization": "string", // Optional
"address": {
"street1": "string", // Optional
"street2": "string", // Optional
"city": "string", // Optional
"state": "string", // Optional
"postalCode": "string", // Optional
"country": "string" // REQUIRED: ISO 3166-1 alpha-2
},
"email": "string", // Optional
"publicKeyId": "string" // Optional
},
"subject": { // REQUIRED: Recipient/Provider
"name": "string", // Optional (may be encrypted)
"organization": "string", // Optional
"lastMileProvider": "string", // For privacy modes
"address": { // Optional (may be encrypted)
"street1": "string",
"city": "string",
"state": "string",
"postalCode": "string",
"country": "string"
},
"publicKeyId": "string" // Optional
},
"itemId": "string", // REQUIRED: Tracking number
"timestamp": number, // REQUIRED: Unix timestamp (ms)
"message": "string", // Optional: Public note
"typeData": {} // SHIP-specific fields (see below)
}
typeData Object (SHIP Type)
The typeData object contains shipping-specific information:
{
"typeData": {
"privacyMode": "standard|encrypted|split-key",
"parcelId": "string", // Tracking number
"carrier": "string", // Shipping company
"service": "string", // Express, ground, etc.
"lastMileProvider": "string", // DNS key locator for privacy modes
"encryptedRecipient": "string", // Base64 ECIES encrypted data
"authenticationProfile": "string", // e.g., "PHYSICAL-SPLIT-KEY"
"labelSerial": "string", // For split-key labels
"gs1": "string", // Optional: GS1 identifier
"edi": "string" // Optional: EDI reference
}
}
Required Fields
| Field | Type | Description |
|---|---|---|
| type | string | Fixed to "SHIP" for shipping protocol |
| itemId | string | Unique tracking identifier for the package |
| timestamp | number | Unix timestamp in milliseconds |
| issuer.address.country | string | ISO 3166-1 alpha-2 country code |
Organizations MAY include additional fields in typeData for compatibility with their systems.
Scanners MUST ignore unknown fields to maintain forward compatibility.
QR Serialization
QR data MUST be serialized using pipe (|) delimiters:
DSPIP|<version>|<type>|<keyLocator>|<encodedPayload>|<signature>[|<privateMessage>]
Fields
| Field | Description | Example |
|---|---|---|
| protocol | Fixed string "DSPIP" | DSPIP |
| version | Semantic version | 1.0 |
| type | Protocol type (SHIP for shipping) | SHIP |
| keyLocator | DNS path for public key | warehouse._dspip.example.com |
| encodedPayload | Base64-encoded JSON | eyJ0eXBlIjoiU0hJUCIuLi4= |
| signature | Hex-encoded ECDSA (DER) | 3045022100... |
| privateMessage | Optional ECIES-encrypted message (Base64-encoded ciphertext) | IQLx4j2...kF9w== (ECIES compact format) |
Examples
// Standard shipping (6 fields)
DSPIP|1.0|SHIP|warehouse._dspip.example.com|eyJ0eXBlIjoiU0hJUCJ9...|3045022...
// Privacy-mode shipping (6 fields)
DSPIP|1.0|SHIP|warehouse._dspip.example.com|eyJwcml2YWN5TW9kZSI6...|3045022...
// With ECIES-encrypted private message (7 fields)
DSPIP|1.0|SHIP|warehouse._dspip.example.com|eyJ0eXBlIjoiU0hJUCJ9...|3045022...|IQLx4j2Rk8...
The privateMessage field must be encrypted using ECIES (Elliptic Curve Integrated Encryption Scheme)
with the recipient's public key before Base64 encoding. Use eciesEncryptCompact() from the SDK
to encrypt messages. The compact format contains: ephemeralPublicKey (33 bytes) || iv (12 bytes) || ciphertext || mac (16 bytes).
QR Code Requirements
| Error Correction | Level M (15% recovery) recommended |
| Version | 40 or automatic selection |
| Encoding Mode | Binary for optimal density |
| Max Capacity (Level M) | 2,331 bytes |
Implementations MUST validate that exactly 6 or 7 pipe-delimited fields are present, and that the type field equals "SHIP" for shipping protocol. The pipe delimiter was chosen for its low frequency in Base64 and domain names.
DNS Records
Key Locator Format
Key locators MUST follow this pattern:
<selector>._dspip.<domain>
Where:
- selector: Unique identifier for the key (e.g., "warehouse-a", "omaha-main")
- _dspip: Fixed protocol subdomain (underscore prefix per RFC 8552)
- domain: Organization's domain name
Examples
// Warehouse/sender keys
warehouse._dspip.example.com
shipping._dspip.acmelogistics.com
// Last mile provider keys
omaha-main._dspip.usps.gov
receiving-lincoln._dspip.fedex.com
// Corporate mailroom keys
mailroom._dspip.acmecorp.com
DNS TXT Record Format
The DNS TXT record MUST contain semicolon-separated tag=value pairs:
v=DSPIP1; k=ec; c=secp256k1; p=<base64_public_key>; [optional tags]
Required Tags
Optional Tags
Record Signature (rsig)
The rsig field provides cryptographic authentication of lifecycle metadata
within the DNS record itself. This protects against unauthorized modification of lifecycle
fields (t, exp, exp-v, s, seq) by intermediate DNS resolvers or caches.
Threat Model
Without rsig, an attacker with access to an intermediate DNS resolver could modify lifecycle fields without invalidating QR code signatures:
- Changing
s=activetos=revokedto cause denial of service - Changing
s=revokedtos=activeto bypass revocation - Modifying
seqto prevent key rotation from taking effect
Signable Content
The rsig signature covers a canonical string of lifecycle fields:
rsig_content = selector | t | exp | exp-v | s | seq
// Example:
warehouse|1703548800|1735084800|1766620800|active|1
Example Record with rsig
warehouse._dspip.example.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1;
p=AzmjYBMwFZfa70H75ZOgLMUT0LVVJ+wt8QUOLo/0nIXC;
t=1703548800; exp=1735084800; exp-v=1766620800;
s=active; seq=1; rsig=MEUCIQDx...base64...;
n=Main%20Warehouse%20Key%202025"
Verification
When rsig is present, verifiers SHOULD:
- Extract the rsig value from the DNS record
- Reconstruct the signable content from lifecycle fields
- Compute SHA-256 hash of the signable content
- Verify the rsig signature using the public key from the p= field
- If verification fails, treat the record as untrusted
DNSSEC and rsig serve complementary purposes. DNSSEC proves the record came from the authoritative zone. rsig proves the signing key holder authorized the lifecycle values. For maximum security, use both DNSSEC and rsig. DNSSEC alone is sufficient if you control DNS infrastructure end-to-end.
Address Field Format
The address field specifies the physical location of signing facilities, primarily for last mile providers. This enables verifiers to display facility location on shipping labels for encrypted privacy mode, where the recipient address is hidden.
The address field uses a scheme prefix to indicate the encoding:
Note: If no scheme prefix is present, implementations MUST interpret the value as a Plus Code (plus: scheme).
Address Scheme Examples
// Plus Code (default scheme) - ~3m precision
address=plus:86HJW222+22
address=849VCWC8+R9 // implicit plus: scheme
address=plus:CWC8+R9,Omaha // short code with locality
// Street address (percent-encoded)
address=street:1234%20Post%20Office%20Way%2C%20Omaha%2C%20NE%2068101
// Geographic coordinates
address=geo:41.2565,-95.9345
// Facility reference
address=facility:USPS-OMAHA-MAIN
Address Field Requirements
- The address field is OPTIONAL for key records
- Last mile providers SHOULD include address for encrypted mode label display
- Implementations SHOULD prefer Plus Codes for new deployments
- Verifiers MUST support all four schemes
- When displaying addresses, verifiers MAY render Plus Codes as clickable links to mapping services
Example Records
// Shipping warehouse with lifecycle fields
warehouse._dspip.example.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1;
p=AzmjYBMwFZfa70H75ZOgLMUT0LVVJ+wt8QUOLo/0nIXC;
t=1703548800; exp=1735084800; exp-v=1766620800; s=active; seq=1;
types=SHIP; auth=enterprise; n=Main%20Warehouse"
// Last mile provider using Plus Code (RECOMMENDED)
omaha-main._dspip.usps.gov. IN TXT "v=DSPIP1; k=ec; c=secp256k1;
p=BxkiXPQwFZfe80J86AOhMMVU1MWWK+xu9RVPMp/1oJYD;
t=1703548800; exp=1735084800; exp-v=1766620800; s=active;
types=SHIP; auth=government; address=86HJW222+22;
coverage=68101,68102,68103,68104,68105"
// Last mile provider using street address scheme
nyc-hub._dspip.fedex.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1;
p=BxkiXPQwFZfe80J86AOhMMVU1MWWK+xu9RVPMp/1oJYD;
types=SHIP; auth=enterprise;
address=street:640%20W%2052nd%20St%2C%20New%20York;
coverage=10019,10020"
// Corporate mailroom using geo coordinates
mailroom._dspip.acmecorp.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1;
p=CylmZCNxGafb91I86BPhNNVV2NXYL+yu8SVQNq/2pKZE;
t=1703548800; exp=1735084800; s=active; seq=1;
types=SHIP; auth=organization; address=geo:41.2565,-95.9345"
Key Generation
DSPIP uses secp256k1 for standard/encrypted modes and Ed25519 for split-key labels.
secp256k1 (Standard/Encrypted Mode)
| Curve | secp256k1 (SEC 2) - same as Bitcoin/Ethereum |
| Private Key Size | 256 bits (32 bytes) |
| Public Key Size | 264 bits (33 bytes compressed) |
| Signature Algorithm | ECDSA with SHA-256 |
Ed25519 (Split-Key Mode)
| Curve | Curve25519 |
| Private Key Size | 256 bits (32 bytes) - Zone A |
| Public Key Size | 256 bits (32 bytes) - Zone B |
| Signature Algorithm | Ed25519 |
Code Example
import { generateKeyPair, deriveEthAddress } from '@dspip/core';
// Generate secp256k1 key pair for standard/encrypted mode
const { privateKey, publicKey, publicKeyBase64 } = generateKeyPair();
// Optional: derive Ethereum address for blockchain
const ethAddress = deriveEthAddress(publicKey);
console.log('Private Key:', privateKey);
console.log('Public Key (base64):', publicKeyBase64);
// For split-key labels, Ed25519 keys are generated during manufacturing
// and printed under scratch-off zones (Zone A: private, Zone B: public)
secp256k1 keys can be used for DSPIP signatures, Ethereum/Polygon transactions, and Bitcoin transactions. Ed25519 split-key labels provide physical anti-cloning through one-time-use keys.
Signature Creation
Standard signatures are created using ECDSA over the secp256k1 curve. Split-key labels use Ed25519.
Standard Mode (ECDSA)
-
Construct Signable Content
Concatenate with pipe delimiters:protocol|version|type|keyLocator|encodedPayload -
Calculate Hash
Compute SHA-256 hash of the signable content -
Sign
Sign the hash using ECDSA with the private key -
Encode
Encode signature in DER format, then convert to hexadecimal string
Split-Key Mode (Ed25519)
-
Reveal Zone A
Sender scratches Zone A on label to reveal Ed25519 private key -
Construct Signable Content
Same format:protocol|version|type|keyLocator|encodedPayload -
Sign
Sign using Ed25519 with the revealed private key -
Destroy Key
Private key is destroyed after signing (single-use)
What Gets Signed
The signature covers: protocol identifier, version, type, key locator, and encoded payload. It does NOT cover the signature field itself or the optional private message.
// Standard ECDSA signing
const signableContent = `${protocol}|${version}|${type}|${keyLocator}|${encodedPayload}`;
const hash = sha256(signableContent);
const signature = ecdsaSign(hash, privateKey);
const signatureHex = derEncode(signature).toString('hex');
// Split-key Ed25519 signing
const signableContent = `${protocol}|${version}|${type}|${keyLocator}|${encodedPayload}`;
const signature = ed25519Sign(signableContent, zoneAPrivateKey);
const signatureHex = signature.toString('hex');
Verification
Verification Algorithm
-
Parse QR Data
Split by pipe delimiter. Validate exactly 6 or 7 fields. Extract protocol, version, type, keyLocator, encodedPayload, signature, [privateMessage]. -
Validate Protocol
Protocol MUST equal "DSPIP". Version MUST be compatible (1.0). Type MUST equal "SHIP". -
Decode Payload
Base64 decode encodedPayload. Parse JSON. Check privacyMode if present. -
DNS Lookup (Standard/Encrypted)
Query DNS TXT record for keyLocator. Verify types includes "SHIP". Extract public key. -
Zone B Reveal (Split-Key Only)
For split-key mode, recipient scratches Zone B to reveal Ed25519 public key. -
Verify Signature
Reconstruct signable content:protocol|version|type|keyLocator|encodedPayload. Verify signature with public key. -
Revocation Check
Query revocation list for itemId. Verify within 180-day window. -
Privacy Decryption (Last Mile Provider)
If keyLocator matches verifier, decrypt encryptedRecipient using ECIES private key.
Code Example
import { verify } from '@dspip/core';
const result = await verify(qrData, {
cacheEnabled: true,
checkRevocation: true,
recordToBlockchain: false
});
if (result.valid) {
console.log('Valid!');
console.log('Type:', result.type); // "SHIP"
console.log('Issuer:', result.payload.issuer);
console.log('Item ID:', result.payload.itemId);
console.log('Privacy Mode:', result.payload.typeData?.privacyMode);
} else {
console.error('Error:', result.errorCode, result.errorMessage);
}
Error Conditions
| Error Code | Description | Severity |
|---|---|---|
| INVALID_PROTOCOL | Protocol not "DSPIP" | Fatal |
| INVALID_TYPE | Type not "SHIP" | Fatal |
| PARSE_ERROR | Cannot parse QR structure (wrong field count) | Fatal |
| SIGNATURE_INVALID | Signature verification failed | Fatal |
| REVOKED | Package has been revoked | Fatal |
| DECRYPTION_FAILED | Cannot decrypt recipient (for LMP) | Fatal |
Implementations MUST accept signatures from expired keys for packages created before the expiration date. Check the payload timestamp against key expiration.
Selector Discovery
Organizations with multiple signing keys can publish a discovery record at the base
_dspip.<domain> location. This allows verifiers to enumerate available
selectors and understand key organization.
Discovery Record Format
_dspip.example.com. IN TXT "v=DSPIP1; selectors=warehouse,shipping,returns;
delegate=geo:region._dspip.example.com"
Delegation Schemes
The delegate tag supports various selector organization strategies:
| Scheme | Pattern | Use Case |
|---|---|---|
| list | selectors=a,b,c |
Small number of static selectors |
| geo | delegate=geo:{region}._dspip.example.com |
Geographic distribution (us-east, eu-west) |
| postal | delegate=postal:{zip}._dspip.example.com |
Postal code based routing |
| region | delegate=region:{state}._dspip.example.com |
State/province organization |
| service | delegate=service:{type}._dspip.example.com |
Service level differentiation |
Discovery Lookup Flow
-
Query Base Record
Query_dspip.<domain>to check for discovery record -
Parse Delegation
Ifdelegatetag present, determine scheme and template -
Resolve Selector
Substitute variables in template to construct final key locator -
Fallback
If no discovery record, fall back to direct<selector>._dspip.<domain>lookup
Example: Geographic Delegation
// Discovery record
_dspip.acmelogistics.com. IN TXT "v=DSPIP1; delegate=geo:{region}._dspip.acmelogistics.com"
// Regional records
us-east._dspip.acmelogistics.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1; p=..."
us-west._dspip.acmelogistics.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1; p=..."
eu-central._dspip.acmelogistics.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1; p=..."
Key Lifecycle Management
DSPIP supports comprehensive key lifecycle management through DNS record fields that indicate key status, creation time, and expiration policies.
Lifecycle States
- s=active (or omitted, default)
- Current time < exp timestamp
- Normal operation mode
- s=verify-only
- Current time < exp-v timestamp
- Used during key rotation
- Current time >= exp-v timestamp
- Signatures should be rejected
- Record may be removed
Lifecycle DNS Tags
Recommended Lifecycle Policy
| Phase | Duration | Description |
|---|---|---|
| Active Signing | 1 year | Key can sign new packages |
| Verify-Only | 1 year | Key validates in-transit packages |
| Total Lifetime | 2 years | Covers full shipping lifecycle |
Example: Full Lifecycle Record
warehouse._dspip.example.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1;
p=AzmjYBMwFZfa70H75ZOgLMUT0LVVJ+wt8QUOLo/0nIXC;
t=1703548800; exp=1735084800; exp-v=1766620800;
s=active; seq=1; types=SHIP; n=Main%20Warehouse"
Key Rotation Process
-
Generate New Key
Create new key pair with incremented sequence number -
Publish New Record
Add new key record with s=active -
Transition Old Key
Update old key to s=verify-only -
Update Signing Systems
Configure systems to sign with new key -
Monitor & Retire
After exp-v passes, old record may be removed
When transitioning a key to verify-only status, ensure all in-transit packages signed with that key will reach their destination before the exp-v timestamp. The recommended 1-year verify-only window accommodates international shipping delays.
Revocation
DSPIP supports three types of revocation records:
- Key Revocation (
_revoked-key) - Revoke compromised or retired signing keys - Item Revocation (
_revoked) - Revoke individual packages (lost, stolen, etc.) - Bulk Revocation (
revocation) - Pointer to bulk package revocation lists
Key Revocation (_revoked-key._dspip.<domain>)
When a signing key is compromised or must be retired, publish a key revocation record for fast propagation. This provides a dedicated revocation channel that operates independently of key record caching.
_revoked-key._dspip.example.com. IN TXT "v=DSPIP1; type=key-revocation;
selector=warehouse; revoked=1703548900; reason=compromised;
replacement=warehouse-v2"
Key Revocation Fields
Additionally, update the key record with s=revoked for authoritative status:
warehouse._dspip.example.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1;
p=AzmjYBMwFZfa70H75ZOgLMUT0LVVJ+wt8QUOLo/0nIXC;
s=revoked; types=SHIP"
For fastest propagation: (1) Publish _revoked-key record immediately,
(2) Update key record with s=revoked. Verifiers MUST treat a key as
revoked if EITHER indicates revocation.
Item Revocation (_revoked._dspip.<domain>)
Individual packages can be revoked without revoking the signing key:
_revoked._dspip.example.com. IN TXT "v=DSPIP1; type=item-revocation;
itemId=TRACK-2025-000123; revoked=1703548900; reason=lost"
Item Revocation Reasons
lost- Package lost in transitstolen- Package was stolendamaged- Package damaged and reshippedrecalled- Package recalled by senderfraud- Fraudulent QR code detected
Bulk Revocation (revocation._dspip.<domain>)
For high-volume scenarios, point to an external revocation list:
revocation._dspip.example.com. IN TXT "v=DSPIP1; type=revocation;
url=https://example.com/dspip/revoked.json; format=json;
updated=1703548800"
Revocation Freshness
To prevent stale revocation information, revocation records include freshness metadata:
Verification Priority
Verifiers MUST check revocation in this order:
-
Check Key Revocation
Query_revoked-key._dspip.<domain>- if selector appears, REJECT immediately -
Check Key Record Status
If key record hass=revoked, REJECT -
Check Item Revocation
Query_revoked._dspip.<domain>and/or bulk revocation list -
Proceed with Verification
If no revocation found, continue with signature verification
Always check revocation status before accepting a package. Compromised keys or canceled shipments may be exploited if revocation checks are skipped. Use short TTLs (60-300 seconds) when caching key revocation records.
Caching Strategy
High-volume verification environments benefit from multi-tier caching to reduce DNS lookups and improve verification speed. This section describes recommended caching architectures for different deployment scales.
Three-Tier Architecture
- Store 1,000-5,000 frequently used keys
- LRU eviction policy
- 5-minute TTL for active keys
- Persist across restarts
- Serve 10-50 devices per hub
- 50,000+ key capacity
- 15-minute TTL
- Redis or similar in-memory store
- Single source of truth
- Millions of keys capacity
- Respect DNS TTL
- Force refresh on revocation
Cache Invalidation
| Event | Action | Propagation |
|---|---|---|
| Key revocation | Immediate invalidation | Push to all tiers |
| Key expiration | Remove on access | Lazy propagation |
| TTL expiry | Background refresh | Pull from upstream |
| New key | Cache on first access | Pull on demand |
Offline Operation
When network connectivity is unavailable, devices should operate with cached data while indicating staleness to users:
// Staleness thresholds
const CACHE_WARNING_THRESHOLD = 3600; // 1 hour - show warning
const CACHE_ERROR_THRESHOLD = 86400; // 24 hours - show error
const CACHE_REJECT_THRESHOLD = 604800; // 7 days - reject verification
function verifyWithCache(qrData, cache) {
const cacheAge = Date.now() - cache.lastSync;
const result = verify(qrData, { cache });
if (cacheAge > CACHE_REJECT_THRESHOLD) {
return { valid: false, error: 'CACHE_TOO_OLD' };
}
if (cacheAge > CACHE_ERROR_THRESHOLD) {
result.warning = 'CACHE_STALE';
result.flags = ['OFFLINE_MODE'];
} else if (cacheAge > CACHE_WARNING_THRESHOLD) {
result.warning = 'CACHE_AGING';
}
return result;
}
Performance Metrics
Expected cache hit rates for a well-tuned deployment:
| Tier | Hit Rate | Latency |
|---|---|---|
| Device (L1) | 70-80% | < 1ms |
| Regional (L2) | 15-25% | 1-5ms |
| Central (L3) | 3-8% | 5-20ms |
| DNS (origin) | 1-3% | 20-100ms |
Pre-warm caches with keys from expected shippers before shift start. A logistics hub receiving packages from 50 regular shippers can pre-fetch those keys during quiet periods, achieving 95%+ L1 hit rates during peak hours.
Blockchain Integration
DSPIP supports optional blockchain recording for immutable custody tracking. Since DSPIP uses secp256k1 keys, the same keys can sign both QR codes and blockchain transactions.
Enabling Blockchain
Add blockchain information to your DNS TXT record:
v=DSPIP1; k=ec; c=secp256k1; p=...;
eth=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0;
chain=polygon
Custody Event Structure
{
"parcelId": "sha256(DSPIP-parcelId)", // Hashed for privacy
"custodian": "ethereum_address",
"timestamp": "block.timestamp",
"location": "geohash_or_facility_id",
"previousCustodyHash": "bytes32"
}
Only hashed parcel IDs should be stored on public blockchains. Personal information MUST NOT be recorded on immutable ledgers.
Offline Verification
For environments without network connectivity, DSPIP supports offline verification using cached DNS record bundles.
Cache Bundle Format
{
"version": "1.0",
"generated": 1703548800,
"expires": 1704153600,
"records": {
"warehouse._dspip.example.com": "v=DSPIP1; k=ec; ...",
"emp-jsmith._dspip.example.com": "v=DSPIP1; k=ec; ..."
},
"signature": "<bundle_signature>"
}
Requirements
- Cache bundles MUST be updated at least weekly
- Bundles MUST be signed by organization's root key
- Critical revocations SHOULD use out-of-band distribution
DNS Sharding
Large organizations with thousands of keys MAY implement DNS sharding to distribute records across multiple subdomains.
Root Discovery Record
_dspip.example.org. IN TXT "v=DSPIP1; mode=sharded;
shards=_dspip01,_dspip02,_dspip03,_dspip04; count=4"
Shard Distribution Algorithm
- Calculate SHA-256 hash of selector
- Take hash modulo shard count
- Map to shard subdomain with zero-padded index
Lookup Procedure
- Query
_dspip.<domain>for root record - If
mode=direct, key is at<selector>._dspip.<domain> - If
mode=sharded:- Calculate shard from selector hash
- Query
<selector>.<shard>.<domain>
- If not found in calculated shard, MAY query other shards
Error Handling
Error Codes
Test Vectors
The private keys below are PUBLIC and MUST NOT be used for any production purpose. They are provided solely for verifying implementation correctness.
secp256k1 Key Pair (Standard/Encrypted Mode)
Private Key (hex)
e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35
Public Key Compressed (hex)
0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2
Public Key Base64 (for DNS)
AzmjYBMwFZfa70H75ZOgLMUT0LVVJ+wt8QUOLo/0nIXC
Ed25519 Key Pair (Split-Key Mode)
Zone A Private Key (hex)
9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60
Zone B Public Key (hex)
d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a
Standard Mode Sample Payload
{
"type": "SHIP",
"issuer": {
"organization": "ACME Logistics",
"address": {
"city": "Omaha",
"state": "NE",
"country": "US"
}
},
"subject": {
"name": "Bob Jones",
"address": {
"street1": "456 Main Street",
"city": "Lincoln",
"state": "NE",
"postalCode": "68501",
"country": "US"
}
},
"itemId": "TRACK-2025-000123",
"timestamp": 1703548800000,
"typeData": {
"privacyMode": "standard",
"parcelId": "TRACK-2025-000123",
"carrier": "ACME",
"service": "Ground"
}
}
DNS TXT Records
// Warehouse with lifecycle fields
warehouse._dspip.example.com. IN TXT "v=DSPIP1; k=ec; c=secp256k1;
p=AzmjYBMwFZfa70H75ZOgLMUT0LVVJ+wt8QUOLo/0nIXC;
t=1703548800; exp=1735084800; exp-v=1766620800; s=active; seq=1;
types=SHIP; n=Main%20Warehouse"
// Last mile provider
omaha-main._dspip.usps.gov. IN TXT "v=DSPIP1; k=ec; c=secp256k1;
p=AzmjYBMwFZfa70H75ZOgLMUT0LVVJ+wt8QUOLo/0nIXC;
t=1703548800; exp=1735084800; s=active; types=SHIP;
coverage=68101,68102,68103,68104,68105"
Complete QR Data (6-field format)
DSPIP|1.0|SHIP|warehouse._dspip.example.com|eyJ0eXBlIjoiU0hJUCIsImlzc3VlciI6ey...}|304502203a8b4c9d2e1f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f022100f9e8d7c6b5a4938271605f4e3d2c1b0a9988776655443322110fedcba987654
Live Test DNS Record
Query this record to test your implementation:
dig TXT test._dspip.dspip.io
Implementation Checklist
Use this checklist to verify your implementation is complete:
Parsing
- Parse QR data with exactly 6 or 7 pipe-delimited fields
- Validate protocol field equals "DSPIP"
- Validate version compatibility (1.0)
- Validate type field equals "SHIP"
Payload Handling
- Base64 decode the payload
- Parse JSON payload
- Validate required fields: type, itemId, timestamp, issuer.address.country
- Check privacyMode in typeData if present
- Ignore unknown fields for forward compatibility
DNS
- Query DNS TXT record at key locator
- Parse semicolon-delimited tags
- Validate v=DSPIP1, k=ec, c=secp256k1
- Verify types includes "SHIP"
- Base64 decode public key (33 bytes)
- Check key status (s tag) - reject if revoked
- Check signing expiration (exp) for new signatures
- Check verification expiration (exp-v) for verification
- Implement caching based on TTL
Cryptography
- Reconstruct signable content: protocol|version|type|keyLocator|encodedPayload
- SHA-256 hash the signable content
- Hex decode the signature
- For standard/encrypted: Verify ECDSA signature using secp256k1
- For split-key: Verify Ed25519 signature using Zone B public key
- Check key lifecycle status (s, exp, exp-v tags)
Privacy Modes
- Handle standard mode (cleartext recipient)
- Handle encrypted mode (ECIES decryption for last mile provider)
- Handle split-key mode (Zone B reveal for verification)
Validation
- Verify test vectors produce expected results
- Test against live DNS record at test._dspip.dspip.io
- Handle all error conditions appropriately
- Check revocation lists for itemId
Revocation Lists (Section 5.5)
DSPIP supports revocation of individual item IDs through signed revocation lists. This is used for lost, stolen, damaged, or recalled packages. Revocation lists include an optional Bloom filter for O(1) probabilistic lookup and automatic 180-day pruning of stale entries.
Revocation Entry Structure
{
"itemId": "TRACK-2025-001", // Item being revoked
"timestamp": 1703548800, // When revoked (Unix time)
"reason": "lost" // lost | stolen | damaged | recalled | other
}
Signed Revocation List
{
"version": "1.0",
"issuer": "warehouse._dspip.acme.com",
"timestamp": 1703548800,
"expires": 1703635200,
"entries": [...],
"bloomFilter": {
"bits": "base64-encoded-bitarray",
"size": 10000,
"hashCount": 7
},
"signature": "..."
}
Bloom Filter
For large revocation lists, a Bloom filter enables O(1) "definitely not revoked" checks. The optimal filter size depends on expected entries and desired false positive rate:
| Expected Items | False Positive Rate | Optimal Size (bits) | Hash Functions |
|---|---|---|---|
| 1,000 | 1% | 9,585 | 7 |
| 10,000 | 1% | 95,851 | 7 |
| 100,000 | 0.1% | 1,437,759 | 10 |
JavaScript Example
import {
createRevocationList,
checkRevocation,
calculateBloomFilterSize
} from '@dspip/core';
// Create a signed revocation list with Bloom filter
const list = createRevocationList({
issuer: 'warehouse._dspip.acme.com',
privateKey: privateKeyHex,
entries: [
{ itemId: 'TRACK-001', reason: 'lost' },
{ itemId: 'TRACK-002', reason: 'stolen' }
],
includeBloomFilter: true
});
// Check if an item is revoked (O(1) with Bloom filter)
const result = checkRevocation('TRACK-001', list);
if (result.revoked) {
console.log('Item revoked:', result.entry.reason);
}
// Calculate optimal Bloom filter parameters
const { size, hashCount } = calculateBloomFilterSize(10000, 0.01);
Automatic Pruning
Revocation entries older than 180 days (REVOCATION_MAX_AGE_SECONDS) should be automatically removed. This ensures lists don't grow indefinitely while maintaining sufficient history for in-transit packages.
// Prune old entries and re-sign
const prunedList = pruneRevocationList(list, privateKey);
console.log('Pruned entries:', list.entries.length - prunedList.entries.length);
Always verify the revocation list signature before trusting its contents. An attacker could provide a forged list to prevent detection of stolen packages.
Delivery Confirmation (Section 7.4)
DSPIP provides cryptographic proof of delivery through a challenge-response protocol. This creates non-repudiable evidence that a package was delivered to the intended recipient.
Protocol Flow
-
Carrier Creates Challenge
At delivery, carrier generates a random nonce and creates a signed challenge with the item ID and timestamp. -
Recipient Signs Challenge
Recipient signs the challenge hash with their private key, creating a response. -
Carrier Verifies and Creates Proof
Carrier verifies the recipient's signature and creates a complete delivery proof. -
Proof Stored for Audit
The proof can be stored on-chain or in traditional systems for later verification.
Challenge Structure
{
"version": "1.0",
"itemId": "TRACK-2025-001",
"carrierKeyLocator": "driver._dspip.usps.gov",
"nonce": "random-16-bytes-base64",
"timestamp": 1703548800,
"expires": 1703549100, // 5 minutes validity
"signature": "carrier-signature"
}
Delivery Proof
{
"version": "1.0",
"itemId": "TRACK-2025-001",
"challenge": { ... },
"response": {
"challengeHash": "sha256-of-challenge",
"recipientPubkey": "recipient-public-key",
"timestamp": 1703548850,
"signature": "recipient-signature"
},
"proofHash": "sha256-of-proof",
"carrierSignature": "carrier-attestation",
"valid": true
}
JavaScript Example
import {
createDeliveryChallenge,
respondToChallenge,
verifyDeliveryResponse
} from '@dspip/core';
// Step 1: Carrier creates challenge
const challenge = createDeliveryChallenge({
itemId: 'TRACK-2025-001',
carrierKeyLocator: 'driver._dspip.usps.gov',
carrierPrivateKey: carrierPrivKey,
validitySeconds: 300
});
// Step 2: Recipient signs the challenge
const response = respondToChallenge({
challenge,
recipientPrivateKey: recipientPrivKey,
metadata: {
recipientName: 'John Doe',
location: 'geo:41.2565,-95.9345'
}
});
// Step 3: Carrier verifies and creates proof
const proof = verifyDeliveryResponse({
challenge,
response,
carrierPrivateKey: carrierPrivKey
});
if (proof.valid) {
console.log('Delivery confirmed! Proof:', proof.proofHash);
}
Multi-Party Attestation
For high-value deliveries, DSPIP supports multi-party attestation requiring signatures from multiple witnesses (carrier, recipient, third-party witness).
import {
createMultiPartyAttestation,
addAttestation,
verifyMultiPartyAttestation
} from '@dspip/core';
// Create attestation requiring 3 signatures
const attestation = createMultiPartyAttestation(proof, 3);
// Add attestations from different parties
addAttestation(attestation, 'carrier._dspip.usps.gov', 'carrier', carrierPrivKey);
addAttestation(attestation, 'recipient', 'recipient', recipientPrivKey);
addAttestation(attestation, 'witness._dspip.acme.com', 'witness', witnessPrivKey);
console.log('Complete:', attestation.complete); // true when 3+ signatures
// Verify all attestations
const valid = verifyMultiPartyAttestation(attestation, publicKeyMap);
Compact Format for QR
Challenges can be serialized to a compact format for display as QR codes:
DSPIP-DC|1.0|TRACK-001|driver._dspip.usps.gov|1703548800|1703549100|nonce|signature
Delivery proofs provide cryptographic evidence that cannot be forged. The recipient's signature on the challenge proves they received the package at the specified time and location.
DNSSEC Validation (Section 8)
DNSSEC provides cryptographic authentication for DNS responses, creating a chain of trust from the root zone down to the key locator record. When DNSSEC is enabled, verifiers can be confident the public key was published by the domain owner.
Chain of Trust
DNSSEC validation verifies a chain from the DNS root to the key record:
-
Root Zone
Trust anchor (hardcoded root keys) validates the root zone's DNSKEY. -
TLD Zone
DS record in root points to TLD's DNSKEY. Validate TLD zone. -
Domain Zone
DS record in TLD points to domain's DNSKEY. Validate domain zone. -
Key Record
RRSIG on TXT record validates the DSPIP key data.
DNS-over-HTTPS (DoH)
DNSSEC validation typically uses DNS-over-HTTPS for secure transport. Common DoH providers:
| Provider | Endpoint |
|---|---|
| Cloudflare | https://cloudflare-dns.com/dns-query |
| https://dns.google/dns-query | |
| Quad9 | https://dns.quad9.net/dns-query |
JavaScript Example
import {
validateDNSSEC,
validateKeyLocatorDNSSEC,
hasDNSSEC
} from '@dspip/core';
// Quick check: does domain have DNSSEC?
const enabled = await hasDNSSEC('cloudflare.com');
console.log('DNSSEC enabled:', enabled);
// Full validation for a key locator
const result = await validateKeyLocatorDNSSEC(
'warehouse._dspip.example.com',
{ fullChainValidation: true }
);
console.log('Secure:', result.secure);
console.log('Chain:', result.chainOfTrust);
// { root: true, tld: true, domain: true, subdomain: true }
// Get human-readable recommendation
console.log('Recommendation:', result.recommendation);
// "DNSSEC validated - key lookup is cryptographically authenticated"
Validation Result
{
"secure": true,
"chainOfTrust": {
"root": true,
"tld": true,
"domain": true,
"subdomain": true
},
"algorithm": 13, // ECDSAP256SHA256
"keyTag": 12345,
"validFrom": 1703548800,
"validUntil": 1704153600,
"recommendation": "DNSSEC validated..."
}
Key Tag Calculation
The key tag is a 16-bit identifier for DNSKEY records, calculated per RFC 4034:
import { calculateKeyTag } from '@dspip/core';
const keyTag = calculateKeyTag({
flags: 257, // KSK
protocol: 3,
algorithm: 13, // ECDSAP256SHA256
publicKey: 'base64-key'
});
console.log('Key tag:', keyTag);
Security Considerations
- Chain of trust validated to root
- RRSIG signatures verified
- Recommended for production
- Use DoH for transport security
- Display warning to users
- Acceptable for low-value items
Not all domains have DNSSEC enabled. When DNSSEC is not available, DSPIP still provides signature verification but without DNS authentication. Display appropriate warnings to users about the reduced assurance level.
Resources
Official Links
- Protocol Website - dspip.io
- Developer Portal - dspip.dev
- GitHub Repository - Reference implementation
SDKs
- JavaScript/TypeScript: dspip-javascript - Full browser and Node.js support
- Python: dspip-python - Server-side and scripting
- Go: dspip-go - High-performance services
- Java: dspip-java - Enterprise and Android
- Rust: dspip-rust - Systems programming and WASM
- C: dspip-c - Embedded systems and IoT
- Flutter/Dart: dspip-flutter - Cross-platform mobile apps
SDK Feature Support
| Feature | JS | Python | Go | Java | Rust | C | Flutter |
|---|---|---|---|---|---|---|---|
| Key Generation | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Payload Creation | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Verification | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Revocation Lists | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Delivery Confirmation | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| DNSSEC Validation | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| QR Code Generation | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
Standards References
- RFC 6376 - DKIM Signatures
- RFC 8552 - Underscored DNS Naming
- SEC 2 - secp256k1 Curve Parameters
- ISO 18004 - QR Code Specification
Community
- Issue Tracker
- Discussions
- Security Reports - security@dspip.io
- General Contact - contact@dspip.io