In the last six months, I built something I felt needed to exist. It wasn’t a product idea that came out of a brainstorming session. It came from a simple observation that kept bothering me. Reality is becoming negotiable. A video can be dismissed as AI generated. A fake photo can go viral in minutes. A real eyewitness recording can be brushed off as synthetic.
I wanted a way for regular people to prove that their photos and videos were real at the moment they captured them. Not through forensic analysis after the fact. Not by trusting a cloud service. Not by hoping a platform labels things correctly.
So I built Witness by Reel Human, a mobile app that cryptographically signs photos and videos the moment the user taps the shutter, embedding a manifest directly inside the file.
This is the technical story of how it works.
The Core Problem
AI detection is a dead end. Even the best detectors fail as models improve. A real video can produce a false positive. A fake one can slip through. It becomes a trust black hole.
My approach is different. Instead of detecting what might be fake, Witness proves what is real. The goal is to anchor authenticity to the moment of capture itself.
To do that, the camera app has to do several things that smartphones normally don’t do:
1. Sign media inside the capture pipeline
2. Generate a cryptographic manifest
3. Bind the manifest to the operator and the device
4. Insert that manifest and signatures into the file structure
5. Make the entire thing independently verifiable by anyone
Everything else in the system exists to support these goals.
App Architecture Overview (iOS + Android)
Witness uses a dual-platform architecture:
iOS (Swift / AVFoundation)
· Custom capture pipeline
· On-device key generation using Secure Enclave where available
· Streaming-based hashing during recording
· Manifest construction before the file is finalized
On iOS, Witness assembles the manifest during the processing stage immediately after the camera writes the temporary media file. Videos use a streaming hash because loading 150–400 MB into memory is both unnecessary and unrealistic. Images use the original in-memory buffer. Once the manifest is assembled, the device signs it, the JSON is serialized, and the app embeds it directly into the final JPEG or MP4 container.
This is the part of the pipeline responsible for generating, signing, and embedding the manifest inside the media file:
do {
// Generate the manifest (streaming for video, in-memory for images)
let manifest = try await buildManifest(
fileURL: mediaURL,
originalData: originalImageData, // Only used for still images
mediaType: mediaExtension,
location: captureLocation
)
// Convert to JSON for embedding and signing
let jsonData = try encodeManifest(manifest)
let jsonString = String(data: jsonData, encoding: .utf8)!
print("Generated manifest for media type: \(mediaExtension)")
// Perform local processing and signature preparation
try await prepareManifestForEmbedding(manifest, jsonData: jsonData)
// Write the manifest into the final MP4 or JPEG container
return try embedManifest(
jsonData: jsonData,
jsonString: jsonString,
into: mediaURL,
originalImageData: originalImageData,
mediaType: mediaExtension
)
}
The embedding step (not shown here) writes the JSON manifest into a dedicated atom/segment inside the media file. The important part is that the manifest travels with the file itself instead of relying on cloud storage or external metadata. If the file is modified, missing, or corrupted, the signature chain breaks and Witness will reject the content.
Android (Kotlin / CameraX)
· Direct frame access via CameraX + MediaCodec
· Incremental hashing to avoid loading frames into memory
· Identical manifest layout and signature semantics to iOS
On Android, the Witness SDK handles manifest creation and media-type specific preparation. The app asks the SDK to generate a manifest for a captured file, then either returns raw JSON bytes (for MP4) or an XMP packet (for JPEG). If the device is not registered or its delegated signing certificate is exhausted, the SDK fails the operation. A simplified version of that flow looks like this:
try {
if (AppConfig.DEBUG) {
Log.d(TAG, "getBytesToInject called for type: $mediaType, file: ${inputFile.name}")
}
// Get SDK instance
val sdk = try {
WitnessSdk.getInstance()
} catch (e: IllegalStateException) {
Log.e(TAG, "Witness SDK not initialized")
return null
}
// Ensure this device is registered for witness capture
if (!sdk.isDeviceRegistered()) {
Log.e(TAG, "Device not registered for witness capture")
return null
}
// Ask the SDK to generate a signed manifest for this file
when (val result = sdk.generateManifestForFile(inputFile)) {
is ManifestResult.Success -> {
val manifest = result.manifest
val jsonManifest = Json.encodeToString(manifest)
if (AppConfig.DEBUG) {
Log.d(TAG, "Generated manifest v1.1 (length=${jsonManifest.length})")
Log.d(
TAG,
"Manifest: device=${manifest.deviceId}, operator=${manifest.operatorId}, seq=${manifest.sequenceNumber}"
)
}
return when (mediaType.lowercase()) {
"mp4" -> {
// MP4: embed raw JSON bytes into the container
jsonManifest.toByteArray(StandardCharsets.UTF_8)
}
"jpeg", "jpg" -> {
// JPEG: wrap JSON into an XMP packet for later APP1 insertion
generateXmpPacketFromJson(
jsonManifest,
xmpToolkitName = DEFAULT_XMP_TOOLKIT_NAME,
xpacketId = DEFAULT_XPACKET_ID
)
}
else -> {
Log.w(TAG, "Unsupported media type: $mediaType")
null
}
}
}
is ManifestResult.Error -> {
Log.e(TAG, "Witness manifest generation failed: ${result.message}")
// High-level hints for common error categories
when {
result.message.contains("quota", ignoreCase = true) -> {
Log.w(TAG, "Capture quota exhausted – device may need to refresh its signing certificate.")
}
result.message.contains("certificate", ignoreCase = true) -> {
Log.w(TAG, "Signing certificate issue – device may need to reconnect.")
}
result.message.contains("not registered", ignoreCase = true) -> {
Log.w(TAG, "Device registration issue – re-registration may be required.")
}
}
return null
}
}
}
Both platforms enforce the same rules so that verification does not depend on the device type.
The Cryptographic Model and the Witness manifest
Witness uses three cooperating elements:
1. Device Signing Key: Generated and stored on the device. Never leaves it.
2. Operator Key (WitnessID): Each person has a unique WitnessID linked to a keypair.
3. Delegated Signing Certificate (DSC): Issued by the server at registration. Validates that a device and operator are linked at the moment of capture.
So the final signed manifest includes:
· Device signature of content
· Operator signature
· Server-issued DSC
This creates a chain of accountability without requiring identity disclosure.
Below is a sanitized example of a Witness manifest embedded directly inside an MP4 file. Identifiers and cryptographic values are truncated, but the structure is unchanged.
{
"rhVersion": "1.1",
"timestamp": "2025-12-04T05:34:00Z",
"deviceId": "DEVICE-ID-REDACTED",
"operatorId": "OPERATOR-ID-REDACTED",
"authPairId": "AUTH-PAIR-ID-REDACTED",
"hash": "a3f…c12",
"signature": "MEYCIQD…CIQIhAKR…Cz",
"sequenceNumber": 3,
"nonce": "f19…c31",
"dsc": {
"authPairId": "AUTH-PAIR-ID-REDACTED",
"exp": 1765431233,
"maxCaptures": 100,
"pub": "BEoO…Q+c=",
"sig": "eyJh…4g"
},
"contentSig": {
"kid": "KEY-ID-REDACTED",
"sig": "ZqIv…Iw=="
}
}
Every capture produces a JSON manifest embedded inside the file. It contains:
· Hash of the raw content stream
· Timestamps
· Device ID
· Operator ID (aka WitnessID)
· DSC reference
· Public keys
· Signature block
The manifest is placed inside the MP4 or JPEG using a dedicated atom/segment. It travels with the file no matter how it is shared.
Witness does not store this data in the cloud. The user controls the file completely.
Embedding Into MP4 and JPEG
This is the part that forced me to write a lot of custom tooling.
For MP4:
· A new atom is created in the meta section
· Manifest + signatures are injected after the file is finalized
· File offsets are recalculated to maintain structural validity
For JPEG:
· A custom APP marker stores the manifest
· Data is placed before the Start of Scan
None of this breaks compatibility with standard players. The files open normally everywhere.
On Android we follow the same pattern: locate moov, strip any existing metadata atoms, add our own authenticated meta atom, and stream-rewrite the file. The code lives in Kotlin and uses the same structural rules as the iOS injector, shown here.
func injectMetadata(into fileURL: URL, meta: Data) throws -> URL {
// Open file for streaming (avoid loading entire video into memory)
let fileHandle = try FileHandle(forReadingFrom: fileURL)
defer { fileHandle.closeFile() }
// Locate the 'moov' atom (simplified)
guard let moovOffset = findMoovAtom(in: fileURL) else {
throw NSError(domain: "Witness", code: 1,
userInfo: [NSLocalizedDescriptionKey: "'moov' atom not found"])
}
// Read size and inspect children (illustrative only)
let moovSize = readAtomSize(from: fileHandle, at: moovOffset)
let moovContent = readAtomPayload(handle: fileHandle,
offset: moovOffset,
size: moovSize)
let children = parseAtoms(in: moovContent)
// Remove existing metadata atoms (simplified rule)
let filtered = children.filter { $0.type != "meta" }
// Build a new metadata atom containing the signed manifest
let newMetaAtom = buildMetaAtom(from: meta)
// Reconstruct the 'moov' atom with updated children
let newMoovAtom = buildContainerAtom(
type: "moov",
children: filtered + [newMetaAtom]
)
// Stream-write the new file with the updated 'moov' structure
return try writeUpdatedFile(
original: fileHandle,
moovOffset: moovOffset,
oldMoovSize: moovSize,
newMoov: newMoovAtom
)
}
Backend Architecture
Witness uses a minimal but secure backend:
· Node.js / Express API
· PostgreSQL (DigitalOcean Managed DB)
· Serverless backups into GCP + offsite storage
· AWS App Runner for API hosting
Key backend responsibilities:
1. Issue DSC certificates: These bind device keys to operator keys.
2. Validate signatures for the validator: A small stateless endpoint checks:
· Hash match
· Signature match
· DSC validity
· Sequence numbers
3. Manage operator accounts: Account registration, recovery, and deletion.
One of the most used functions in the backend is validating the device and operator pair (authPairID). That function checks the database for the current DSC assigned to that authPairID, returning the key metrics if the DSC is valid (stripped down for clarity, real implementation includes additional validation, auditing, and defensive protections).
async function getActiveDSC(authPairId, db) {
if (!isValidUUID(authPairId)) {
throw { statusCode: 400, message: "Invalid identifier format" };
}
// Query a summarized view of the current DSC:
// - most recently issued
// - not expired
// - not revoked
// - with capture usage counted
const row = await db.query(
`
SELECT
certificate_id,
max_captures,
expires_at,
issued_at,
used_captures
FROM dsc_overview
WHERE auth_pair_id = $1
ORDER BY issued_at DESC
LIMIT 1
`,
[authPairId]
);
if (row.rows.length === 0) {
throw { statusCode: 404, message: "No active certificate found" };
}
const cert = row.rows[0];
const remaining = cert.max_captures - cert.used_captures;
return {
certificateId: cert.certificate_id,
maxCaptures: cert.max_captures,
usedCaptures: cert.used_captures,
remainingCaptures: remaining,
expiresAt: cert.expires_at,
issuedAt: cert.issued_at
};
}
Independent Web Validation
Anyone can drop a Witness photo or video into the browser and verify:
· Device signature
· Operator signature
· DSC trust chain
· Timestamp
· Tamper detection
No upload. The validation is client-side.
The client-side web validator follows the same trust model as the mobile apps, but with one key constraint: because browsers can’t stream video frames or compute full content hashes efficiently, the web version focuses on cryptographic checks that are safe and feasible to perform locally. The validator performs four layered checks: it verifies that the manifest conforms to the v1.1 schema, validates the Delegated Signing Certificate (DSC) and extracts its ephemeral public key, verifies the content signature using that key, and (when possible) performs an auxiliary device signature check.
async function validateManifestV11(manifest, fileBytes, fileName, isJpeg) {
// 1. Check that required v1.1 fields are present and well-formed
const isVersionValid = validateV11Structure(manifest, fileName);
// 2. Content hash validation is intentionally omitted in the web validator
// (it does not have streaming access to the original capture pipeline).
const isContentHashValid = null;
// 3. Validate Delegated Signing Certificate (DSC):
// - time window
// - revocation / status
// - extract ephemeral public key for content verification
const dscValidation = await validateDSC(manifest, fileName);
// 4. Validate content signature using the ephemeral key from the DSC
const isSignatureValid = dscValidation.publicKey
? await validateContentSignature(
manifest,
dscValidation.publicKey,
fileBytes,
fileName
)
: false;
// 5. Optionally validate the device-level signature, if a device key is registered
const isDeviceSignatureValid = await validateDeviceSignature(manifest);
// Decide on a single, human-readable error message
const errorMessage =
!isVersionValid
? "Manifest is missing required v1.1 fields"
: !dscValidation.isValid
? "Delegated certificate validation failed"
: !isSignatureValid
? "Content signature verification failed"
: null;
return new ValidationResult({
manifest,
isVersionValid,
isContentHashValid, // null = not checked in web validator
isSignatureValid,
isDSCValid: dscValidation.isValid,
isDeviceSignatureValid,
errorMessage,
version: "1.1",
});
}
Why Build All of This Alone
Witness wasn’t a weekend project. It took rewriting the mobile capture stack on both platforms, building a PKI system, embedding atoms inside MP4 files, writing a JPG parser/injector, creating a verifier, and designing a backend from scratch.
I built it because the problem wouldn’t leave me alone. Authenticity is becoming harder for ordinary people to defend. I didn’t want a solution that relied on platform gatekeepers, cloud storage, proprietary AI, or fragile heuristics.
I wanted something that simply works because the math works.
What’s Next
These are the next major steps:
· Public verification API (DONE!)
· Richer gallery and metadata views
· Signing for edited media
· Support for livestream signing
· SDKs for journalists and camera makers
The foundation is here. Now I want to see what people do with it.
Try it, test it, break it
Witness is live now on both app stores. If you’re a developer, researcher, or just someone who cares about truth in media, experiment with it. Pull apart a Witness file. Inspect the manifest. Verify it yourself.
Bring real-world authenticity back into digital media.
That’s why I built Witness.
What I want people to know about Witness (Key Takeaways)
· Media is signed at capture, not after.
· The manifest lives *inside the file*, not on the cloud.
· The server never sees the media, the hashes, or the manifest. Signing happens locally; the backend issues certificates only.
· Verification is independent and local.
· The system uses a three-key trust chain (device + operator + DSC).
· MP4/JPEG formats remain fully compatible with existing players.