Skip to main content
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

  1. The user captures evidence in the ProofMode app on their phone
  2. They export a ZIP bundle from the app
  3. Your application passes the ZIP data through the standard SDK pipeline
  4. 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);
ParameterTypeDescription
zipDataUint8ArrayRaw 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:
FileDescription
<hash>.proof.csv or .proof.jsonLocation and device metadata signals
pubkey.ascASCII-armored PGP public key
<file>.ascPGP detached signatures
<file>.gstGoogle SafetyNet/Play Integrity JWT
<file>.otsOpenTimestamps proof
Media filePhoto or video captured during proof

Signal fields

The metadata file contains 30+ signal fields:
CategoryFields
LocationLocation.Latitude, Location.Longitude, Location.Provider, Location.Accuracy, Location.Altitude, Location.Bearing, Location.Speed, Location.Time
NetworkCellInfo, WiFi.MAC, IPv4, IPv6, Network
DeviceDeviceID, Hardware, Manufacturer, Model
ProofModeProofHash, FileHash, MimeType, File.Name, File.Size
TimestampsDateCreated, 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);