Hardware-attested photo capture for React Native with embedded C2PA manifests.
Every photo taken through this SDK is signed by the device's tamper-resistant hardware, Secure Enclave on iOS and StrongBox/TEE on Android, and embedded with a C2PA manifest before the file is ever written to disk. The resulting JPEG can be verified with any standard C2PA tool.
npm install @rolobits/attestation-photo-mobile| Peer dependency | Version |
|---|---|
react |
>= 18 |
react-native |
>= 0.73 |
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add mobile targets
rustup target add aarch64-apple-ios # iOS device
rustup target add aarch64-apple-ios-sim # iOS simulator (Apple Silicon)
rustup target add aarch64-linux-android # Android
# Android builds require cargo-ndk
cargo install cargo-ndkThe Rust static library is compiled automatically via a CocoaPods script phase. No manual Xcode linking required.
cd ios && pod installThe Rust shared library is compiled automatically via a Gradle task. Just build your app as usual.
| Permission | iOS Info.plist |
Android AndroidManifest.xml |
Required? |
|---|---|---|---|
| Camera | NSCameraUsageDescription |
android.permission.CAMERA |
Yes |
| Location | NSLocationWhenInUseUsageDescription |
android.permission.ACCESS_FINE_LOCATION |
Only with includeLocation: true |
This library is headless, it handles attestation and signing, you bring your own camera (react-native-vision-camera, expo-camera, or anything that produces a JPEG path). See the example app for a full implementation.
import { useRef, useCallback } from "react";
import { Camera, useCameraDevice, useCameraPermission } from "react-native-vision-camera";
import { useAttestedCapture, saveToGallery } from "@rolobits/attestation-photo-mobile";
function CaptureScreen() {
const cameraRef = useRef<Camera>(null);
const device = useCameraDevice("back");
const { hasPermission, requestPermission } = useCameraPermission();
const { signPhoto, status, isReady } = useAttestedCapture({
includeLocation: true, // embed GPS in the C2PA manifest
appName: "My App", // shown in Content Credentials
nonce: "server-challenge-token", // replay prevention
});
const onPressCapture = useCallback(async () => {
// 1. Take a photo with VisionCamera
const rawPhoto = await cameraRef.current!.takePhoto();
// 2. Sign & embed the C2PA manifest — that's it
const signed = await signPhoto(rawPhoto.path);
// 3. (Optional) save to gallery
await saveToGallery({ filePath: signed.path });
}, [signPhoto]);
return <Camera ref={cameraRef} device={device!} isActive photo />;
}useAttestedCapture handles everything behind the scenes: hardware key provisioning, device integrity checks, location prefetch & refresh, file:// prefix stripping, and error wrapping into typed AttestedCameraError codes.
| Option | Type | Default | Description |
|---|---|---|---|
includeLocation |
boolean |
false |
Embed GPS coordinates in the C2PA manifest. |
requireTrustedHardware |
boolean |
true |
Reject signing on devices without Secure Enclave / StrongBox / TEE. |
appName |
string |
"Attestation Mobile" |
App name shown in C2PA Content Credentials. |
nonce |
string |
— | Server challenge token for replay prevention. |
| Property | Type | Description |
|---|---|---|
signPhoto |
(photoPath: string) => Promise<SignedPhoto> |
Sign a photo at the given path. Strips file:// prefix automatically. Rejects with AttestedCameraError on trust or signing failures. |
status |
AttestationStatus | null |
Current attestation status (null while loading on mount). |
isReady |
boolean |
true once key provisioning and status check have completed. |
For advanced use cases where you need full control over each step, every function the hook uses is also exported individually:
| Function | Returns | Description |
|---|---|---|
getAttestationStatus() |
Promise<AttestationStatus> |
Check device integrity and hardware trust level. |
ensureHardwareKey() |
Promise<{ trustLevel }> |
Provision the hardware-backed signing key. Idempotent. |
captureAndSignAtomic(params) |
Promise<SignedPhoto> |
Hash, sign, and embed a C2PA manifest into a JPEG. |
saveToGallery(params) |
Promise<{ uri }> |
Save a file to the device photo gallery. |
hashPhotoAtPath(params) |
Promise<{ sha256Hex }> |
Compute the SHA-256 hash of a photo file. |
signPayload(params) |
Promise<{ signatureBase64, trustLevel }> |
Sign arbitrary base64 data with the hardware key. |
prefetchLocation() |
Promise<{ latitude, longitude } | null> |
Pre-fetch GPS location in background for faster captures. |
The object returned by captureAndSignAtomic():
interface SignedPhoto {
path: string; // File path to the signed JPEG
signature: string; // SHA-256 hex of the original asset
algorithm: "ECDSA_P256_SHA256"; // Signing algorithm used
manifestFormat: "c2pa-jumbf"; // Always JUMBF
trustLevel: PlatformTrustLevel; // "secure_enclave" | "strongbox" | "tee" | "software_fallback"
embeddedManifest?: boolean; // true when real C2PA manifest is embedded
metadata: CaptureMetadata; // Device model, OS, timestamp, nonce, etc.
}Upload the output JPEG to verify.contentauthenticity.org or use the CLI:
cargo install c2patool
c2patool verify output.jpgThe verifier will show a valid signature with an unknown signer. This is expected for self-signed certificates. See Limitations below.
| Code | When |
|---|---|
E_COMPROMISED_DEVICE |
Device shows signs of jailbreak or root. Capture is blocked. |
E_NO_TRUSTED_HARDWARE |
No Secure Enclave, StrongBox, or TEE available and requireTrustedHardware is true. |
E_ATTESTATION_FAILED |
Hardware key provisioning failed. |
E_CAPTURE_FAILED |
Camera capture or file I/O failed. |
E_SIGNING_FAILED |
Hardware signing operation rejected. |
E_C2PA_EMBED_FAILED |
C2PA manifest could not be built or embedded. |
- Self-signed certificates — The signing key has a self-signed X.509 certificate with no CA chain. Verifiers will report an unknown signer. Tamper detection works, but attribution requires a CA integration (not yet implemented).
- Camera injection — On a compromised device, an attacker could feed synthetic frames to the camera API. The SDK signs whatever the camera returns.
- Heuristic root detection — Jailbreak/root detection uses basic signals. Sophisticated root hides are not detected. Consider layering with Play Integrity (Android) or App Attest (iOS) for higher assurance.
| Platform | Minimum | Recommended |
|---|---|---|
| iOS | 14.0 | 16.0+ |
| Android | API 28 / Android 9 | API 30+ / Android 11+ for higher StrongBox availability |
| Rust | 1.75.0 | stable |
| Node | 18 | 22 |