@rolobits/attestation-photo-mobile
v1.3.0
Published
Atomic C2PA camera SDK for React Native
Maintainers
Readme
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.
Install
1. npm
npm install @rolobits/attestation-photo-mobile| Peer dependency | Version |
|---|---|
| react | >= 18 |
| react-native | >= 0.73 |
2. Rust toolchain, one-time setup
# 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-ndk3. iOS
The Rust static library is compiled automatically via a CocoaPods script phase. No manual Xcode linking required.
cd ios && pod install4. Android
The Rust shared library is compiled automatically via a Gradle task. Just build your app as usual.
5. Permissions
| 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 |
Quick start
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.
useAttestedCapture options
| 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. |
useAttestedCapture return value
| 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. |
Lower-level functions
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. |
SignedPhoto
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.
}Verifying output
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.
Error codes
| 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. |
Limitations
- 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 requirements
| 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 |
