Alpha — The ProofMode plugin is in early development. See below for what’s available and what’s coming next.
ProofMode plugin
ProofMode is a mobile app (iOS and Android) that collects device evidence — GPS coordinates, network context, device metadata, PGP signatures, and SafetyNet/Play Integrity attestations. The @location-proofs/plugin-proofmode package integrates ProofMode into the Astral SDK’s standard plugin pipeline and verifies stamp consistency.
How it works
- The user captures evidence in the ProofMode app on their phone
- They export a ZIP bundle from the app
- Your application passes the ZIP data through the standard SDK pipeline
- The plugin parses it into an
UnsignedLocationStamp and can verify its structure and signals
ProofMode App → ZIP export → stamps.create() → stamps.sign() → stamps.verify()
Installation
npm install @location-proofs/plugin-proofmode
GitHub: github.com/location-proofs/plugin-proofmode
Registration
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ProofModePlugin } from '@location-proofs/plugin-proofmode';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
astral.plugins.register(new ProofModePlugin());
Plugin properties
plugin.name // 'proofmode'
plugin.version // '0.1.0'
plugin.runtimes // ['node', 'browser']
plugin.description // 'ProofMode device-based location proofs with PGP signatures
// and hardware attestation'
Standard pipeline
ProofMode implements the standard create() and verify() interface methods, so it works with the SDK’s StampsModule.
Collecting evidence
Evidence collection happens in the ProofMode app on the user’s device. The app captures GPS coordinates, sensor data, PGP signatures, and hardware attestations, then exports a ZIP bundle. Your application receives this ZIP and passes it through the SDK pipeline below.
create()
Pass the ZIP bundle as signals.data.zipData:
import { readFileSync } from 'fs';
const zipData = new Uint8Array(readFileSync('proofmode-export.zip'));
const unsigned = await astral.stamps.create(
{ plugin: 'proofmode' },
{
plugin: 'proofmode',
timestamp: Math.floor(Date.now() / 1000),
data: { zipData }
}
);
Under the hood, create() parses the ZIP and extracts GPS coordinates, timestamps, PGP signatures, SafetyNet tokens, and device metadata into a structured UnsignedLocationStamp.
verify()
Verify a ProofMode stamp’s internal validity:
const result = await astral.stamps.verify(stamp);
Checks performed:
- Structure — LP version is
'0.2', plugin is 'proofmode', location/signals/temporalFootprint present
- Signatures — At least one signature exists with non-empty value and signer info
- Signal consistency:
- Latitude and longitude are in valid ranges
- Location provider and accuracy are consistent
- SafetyNet JWT structure is valid (base64url-encoded, 3 parts)
Location.Time matches temporalFootprint within 3600 seconds
Returns StampVerificationResult:
{
valid: boolean,
signaturesValid: boolean,
structureValid: boolean,
signalsConsistent: boolean,
details: { ... } // ProofMode-specific verification details
}
Lower-level API
For more control over the parsing step, you can use the plugin’s helpers directly:
parseBundle()
Parse a ProofMode ZIP export into a structured bundle.
const plugin = new ProofModePlugin();
const bundle = plugin.parseBundle(zipData);
| Parameter | Type | Description |
|---|
zipData | Uint8Array | Raw ZIP file data from ProofMode export |
Returns ParsedBundle:
interface ParsedBundle {
metadata: ProofModeMetadata; // Parsed signal data
publicKey?: string; // ASCII-armored PGP public key
metadataSignature?: Uint8Array; // PGP detached signature of metadata
mediaSignature?: Uint8Array; // PGP detached signature of media
safetyNetToken?: string; // Google SafetyNet/Play Integrity JWT
otsProof?: Uint8Array; // OpenTimestamps proof
mediaFile?: Uint8Array; // The media file data
mediaFileName?: string;
expectedHash?: string; // SHA-256 hash from bundle filename
files: BundleFile[]; // All raw files in bundle
}
createStampFromBundle()
Create an unsigned location stamp from a parsed bundle:
const unsigned = plugin.createStampFromBundle(bundle);
This is what create() calls internally after parsing.
What the ZIP bundle contains
A ProofMode export ZIP typically includes:
| File | Description |
|---|
<hash>.proof.csv or .proof.json | Location and device metadata signals |
pubkey.asc | ASCII-armored PGP public key |
<file>.asc | PGP detached signatures |
<file>.gst | Google SafetyNet/Play Integrity JWT |
<file>.ots | OpenTimestamps proof |
| Media file | Photo or video captured during proof |
Signal fields
The metadata file contains 30+ signal fields:
| Category | Fields |
|---|
| Location | Location.Latitude, Location.Longitude, Location.Provider, Location.Accuracy, Location.Altitude, Location.Bearing, Location.Speed, Location.Time |
| Network | CellInfo, WiFi.MAC, IPv4, IPv6, Network |
| Device | DeviceID, Hardware, Manufacturer, Model |
| ProofMode | ProofHash, FileHash, MimeType, File.Name, File.Size |
| Timestamps | DateCreated, Timestamp |
Verification scope
What verify checks today
- Stamp structure conforms to Location Protocol v0.2
- Signatures array is non-empty with valid signer info
- GPS coordinates are within valid ranges
- Signal consistency (provider/accuracy, timestamps alignment)
- SafetyNet JWT structure (3-part base64url format)
Deferred to v1+
- Full PGP cryptographic signature verification (checking signatures against the public key)
- SafetyNet/Play Integrity certificate chain verification
- OpenTimestamps proof verification
Coming soon
collect() — In-app evidence collection is planned for developers building React Native apps. This will allow triggering ProofMode evidence collection directly from your app, rather than requiring the user to export a ZIP. This is under active development.
Example
import { AstralSDK } from '@decentralized-geo/astral-sdk';
import { ProofModePlugin } from '@location-proofs/plugin-proofmode';
import { readFileSync } from 'fs';
const astral = new AstralSDK({ chainId: 84532, signer: wallet });
astral.plugins.register(new ProofModePlugin());
// Load a ProofMode ZIP export
const zipData = new Uint8Array(readFileSync('proofmode-export.zip'));
// Create stamp through the standard pipeline
const unsigned = await astral.stamps.create(
{ plugin: 'proofmode' },
{ plugin: 'proofmode', timestamp: Math.floor(Date.now() / 1000), data: { zipData } }
);
console.log('Stamp location:', unsigned.location);
console.log('Stamp time:', unsigned.temporalFootprint);
// Sign the stamp
const stamp = await astral.stamps.sign(
{ plugin: 'proofmode' },
unsigned,
signer
);
// Verify
const result = await astral.stamps.verify(stamp);
console.log('Valid:', result.valid);
console.log('Structure:', result.structureValid);
console.log('Signals consistent:', result.signalsConsistent);
// Use in a proof with stamps from other plugins
const proof = astral.proofs.create(claim, [stamp, otherStamp]);
const vector = await astral.proofs.verify(proof);