Zurück zum Entwicklerportal (Developer Portal)
Mobile Integration

React Native / Expo Integrationsleitfaden (Integration Guide)

C2PA-Signierung pro Installation (per-install signing) für mobile Anwendungen

Überblick (Overview)

Dieser Leitfaden führt durch die Integration der Trusteddit-PKI (Public Key Infrastructure) in eine React Native- oder Expo-Anwendung für Pro-Installation-C2PA-Content-Credentials. Jede App-Installation generiert ihr eigenes ECDSA P-256-Schlüsselpaar in der sicheren Enklave (secure enclave) des Geräts, bezieht ein kurzlebiges Zertifikat (short-lived certificate, 90 Tage) von der Trusteddit-Zertifizierungsstelle (CA) und signiert damit alle erfassten Medien vor dem Upload.

Prüfende Anwendungen (verifying applications wie Browser, soziale Plattformen und News-Reader) können das im Medium eingebettete C2PA-Manifest auslesen, die Zertifikatskette bis zur C2PA-Vertrauensliste (C2PA Trust List) auflösen und dem Betrachter ein Herkunftsabzeichen (provenance badge) anzeigen.

Datenschutzmodell (Privacy Model)

Opake Signer-IDs (Opaque Signer IDs)

Der allgemeine Name (Common Name, CN) des Zertifikats ist eine opake Kennung im Format:

Dieser Token wird mittels HMAC-SHA256 über die interne Benutzer-ID erzeugt, unter Verwendung eines Schlüssels, der ausschließlich in Ihrem Anwendungs-Backend liegt. Er kann von keiner externen Stelle auf eine reale Benutzeridentität zurückgeführt werden: weder von Trusteddit noch von prüfenden Anwendungen oder der C2PA-Vertrauensliste (C2PA Trust List).

Nur Ihr Backend hält die signer_id → user_id-Zuordnung. Diese Architektur ist konstruktionsbedingt DSGVO-konform (GDPR-compliant by construction): Das Zertifikat enthält keine personenbezogenen Daten, und die Zuordnung verbleibt innerhalb Ihrer Datengrenzen.

Registrierungs- und Signierablauf (Enrollment & Signing Flow)

1
Authentifizierung (Auth): Benutzer authentifiziert sich. Cognito-JWT wird abgerufen.
2
Signer-ID (Signer ID): App fordert opake Signer-ID von Ihrem Backend an.
3
Schlüsselerzeugung (Key Gen): ECDSA P-256-Schlüsselpaar wird im sicheren Speicher erzeugt.
4
CSR: Zertifikatsignieranforderung (CSR) mit Signer-ID als CN erstellt.
5
Registrierung (Enroll): CSR und JWT werden an enroll.trusteddit.com übermittelt.
6
Signierung (Sign): Gerätezertifikat signiert erfasste Medien (C2PA-Manifest).
7
Badge: Signierte Medien hochladen; Backend vergibt Prüfabzeichen (verification badge).
Authentifizierung (Auth)
Signer-ID (Signer ID)
Schlüsselerzeugung (Key Gen)
CSR
Registrierung (Enroll)
Signierung (Sign)
Badge

Abhängigkeiten (Dependencies)

# Install required packages
npx expo install expo-crypto expo-secure-store

npm install react-native-quick-crypto @peculiar/x509

# iOS: rebuild native modules
npx expo prebuild --platform ios

expo-crypto

Kryptografische Grundfunktionen (ECDSA-Schlüsselerzeugung)

expo-secure-store

Sicherer, hardwaregestützter Schlüsselspeicher

react-native-quick-crypto

Web Crypto API-Polyfill für die CSR-Erstellung

@peculiar/x509

Erstellung von X.509-Zertifikaten und CSRs (DER/PEM)

Schritt 1: Schlüsselerzeugung (Key Pair Generation)

Erzeugen Sie ein ECDSA P-256-Schlüsselpaar und speichern Sie den privaten Schlüssel über expo-secure-store in der sicheren Enklave (secure enclave) des Geräts.

// src/lib/deviceKey.ts
import * as Crypto from 'expo-crypto';
import * as SecureStore from 'expo-secure-store';

const PRIVATE_KEY_ALIAS = 'trusteddit.device.privkey';
const PUBLIC_KEY_ALIAS  = 'trusteddit.device.pubkey';

/**
 * Generate an ECDSA P-256 key pair and store the private key
 * in the device secure enclave. Returns the public key as a
 * base64url-encoded DER SubjectPublicKeyInfo blob.
 */
export async function generateDeviceKeyPair(): Promise<string> {
  const keyPair = await Crypto.generateKeyPairAsync(
    Crypto.CryptoAlgorithm.ECDSA,
    {
      extractable: false,  // private key cannot be exported
      namedCurve: 'P-256',
    }
  );

  // Export public key as SPKI (DER) for CSR construction
  const publicKeyDer = await Crypto.exportKeyAsync(
    Crypto.CryptoKeyFormat.SubjectPublicKeyInfo,
    keyPair.publicKey
  );
  const publicKeyB64 = Buffer.from(publicKeyDer).toString('base64url');

  // Persist keys – private key stored in Secure Enclave
  await SecureStore.setItemAsync(PRIVATE_KEY_ALIAS, JSON.stringify(keyPair.privateKey));
  await SecureStore.setItemAsync(PUBLIC_KEY_ALIAS, publicKeyB64);

  return publicKeyB64;
}

/**
 * Retrieve the stored public key. Returns null if not yet enrolled.
 */
export async function getStoredPublicKey(): Promise<string | null> {
  return SecureStore.getItemAsync(PUBLIC_KEY_ALIAS);
}

Schritt 2: Signer-ID anfordern (Request Signer ID)

Rufen Sie Ihr Anwendungs-Backend auf, um eine opake Signer-ID für den authentifizierten Benutzer abzufragen. Das Backend leitet diesen Token per HMAC ab und speichert die Zuordnung: siehe Infrastruktur-Leitfaden (Infrastructure Guide) für die Backend-Implementierung.

// src/lib/enrollment.ts
import { Auth } from 'aws-amplify';

/**
 * Retrieve an opaque signer ID from the app backend.
 * The backend derives this via HMAC-SHA256(secret, userId).
 */
export async function requestSignerId(): Promise<string> {
  const session = await Auth.currentSession();
  const jwt = session.getIdToken().getJwtToken();

  const response = await fetch('https://api.thephenom.app/api/signer-id', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${jwt}`,
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error(`Signer ID request failed: ${response.status}`);
  }

  const { signerId } = await response.json() as { signerId: string };
  return signerId; // e.g. "[email protected]"
}

Schritt 3: CSR erstellen (Build the CSR)

Erstellen Sie eine PKCS#10-Zertifikatsignieranforderung (Certificate Signing Request, CSR) mit der opaken Signer-ID als allgemeinem Namen (Common Name, CN). Der CSR wird mit dem privaten Schlüssel auf dem Gerät signiert.

// src/lib/csr.ts
import * as x509 from '@peculiar/x509';
import * as Crypto from 'expo-crypto';
import * as SecureStore from 'expo-secure-store';

const PRIVATE_KEY_ALIAS = 'trusteddit.device.privkey';

/**
 * Build a DER-encoded CSR with the opaque signer ID as the CN.
 * The CSR is self-signed with the on-device ECDSA P-256 private key.
 */
export async function buildCSR(
  signerId: string,
  publicKeyDerB64: string
): Promise<string> {
  const privateKeyRaw = await SecureStore.getItemAsync(PRIVATE_KEY_ALIAS);
  if (!privateKeyRaw) throw new Error('No device private key found');

  const privateKey = JSON.parse(privateKeyRaw) as CryptoKey;

  // Reconstruct the public key from stored DER
  const publicKeyDer = Buffer.from(publicKeyDerB64, 'base64url');

  const csr = await x509.Pkcs10CertificateRequestGenerator.create({
    name: `CN=${signerId}`,
    keys: {
      privateKey,
      publicKey: await crypto.subtle.importKey(
        'spki',
        publicKeyDer,
        { name: 'ECDSA', namedCurve: 'P-256' },
        true,
        ['verify']
      ),
    },
    signingAlgorithm: { name: 'ECDSA', hash: 'SHA-256' },
    extensions: [
      // Mark as non-CA end-entity
      new x509.BasicConstraintsExtension(false, undefined, true),
    ],
  });

  // Return PEM-encoded CSR for transmission
  return csr.toString('pem');
}

Schritt 4: Bei Trusteddit registrieren (Enroll with Trusteddit)

Übermitteln Sie den CSR und das Cognito-JWT an die Trusteddit-Registrierungs-API (enrollment API). Nach erfolgreicher Prüfung liefert die API ein signiertes Zertifikat und die vollständige Trusteddit-CA-Kette zurück. Speichern Sie beides im sicheren Speicher für zukünftige Signiervorgänge.

// src/lib/enrollment.ts (continued)
import * as SecureStore from 'expo-secure-store';
import { Auth } from 'aws-amplify';

const CERT_ALIAS  = 'trusteddit.device.cert';
const CHAIN_ALIAS = 'trusteddit.device.chain';

const ENROLL_URL = 'https://enroll.trusteddit.com/api/v1/enroll';

export interface EnrollmentResponse {
  certificate: string;     // PEM-encoded device certificate
  chain: string[];         // PEM-encoded CA chain (intermediate → root)
  expiresAt: string;       // ISO 8601 expiry of the issued certificate
}

/**
 * Submit the CSR to the Trusteddit enrollment API.
 * Stores the returned certificate and chain in secure storage.
 */
export async function enrollDevice(csrPem: string): Promise<EnrollmentResponse> {
  const session = await Auth.currentSession();
  const jwt = session.getIdToken().getJwtToken();

  const response = await fetch(ENROLL_URL, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${jwt}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ csr: csrPem }),
  });

  if (!response.ok) {
    const error = await response.json() as { code: string; message: string };
    throw new Error(`Enrollment failed [${error.code}]: ${error.message}`);
  }

  const result = await response.json() as EnrollmentResponse;

  // Persist certificate and chain for future signing operations
  await SecureStore.setItemAsync(CERT_ALIAS, result.certificate);
  await SecureStore.setItemAsync(CHAIN_ALIAS, JSON.stringify(result.chain));

  return result;
}

Schritt 5: Erfasste Medien signieren (Sign Captured Media)

Erstellen Sie nach der Aufnahme ein C2PA-Manifest mit dem eingebetteten Gerätezertifikat und signieren Sie es mit dem privaten Geräteschlüssel, bevor die Datei gespeichert oder für den Upload vorbereitet wird.

// src/lib/signing.ts
import * as SecureStore from 'expo-secure-store';
import * as Crypto from 'expo-crypto';

const PRIVATE_KEY_ALIAS = 'trusteddit.device.privkey';
const CERT_ALIAS        = 'trusteddit.device.cert';
const CHAIN_ALIAS       = 'trusteddit.device.chain';

export interface C2PAManifest {
  /** The signed C2PA JUMBF box as a base64-encoded string */
  manifestBox: string;
  /** ISO 8601 timestamp of the signing operation */
  signedAt: string;
}

/**
 * Sign a media file with the on-device certificate.
 *
 * @param mediaUri - File URI of the captured media (video/image)
 * @param mimeType - MIME type of the media, e.g. "video/mp4"
 * @returns A C2PA manifest box to embed in the file prior to upload
 */
export async function signMedia(
  mediaUri: string,
  mimeType: string
): Promise<C2PAManifest> {
  const [privateKeyRaw, certPem, chainRaw] = await Promise.all([
    SecureStore.getItemAsync(PRIVATE_KEY_ALIAS),
    SecureStore.getItemAsync(CERT_ALIAS),
    SecureStore.getItemAsync(CHAIN_ALIAS),
  ]);

  if (!privateKeyRaw || !certPem || !chainRaw) {
    throw new Error('Device not enrolled. Call enrollDevice() first.');
  }

  const privateKey = JSON.parse(privateKeyRaw) as CryptoKey;
  const chain = JSON.parse(chainRaw) as string[];

  // Compute SHA-256 hash of the media file for inclusion in the manifest
  const fileHash = await Crypto.digestStringAsync(
    Crypto.CryptoDigestAlgorithm.SHA256,
    mediaUri, // production: read file bytes
    { encoding: Crypto.CryptoEncoding.BASE64 }
  );

  // Build the C2PA claim structure
  const claim = {
    claim_generator: 'com.thephenom.app/1.0',
    format: mimeType,
    assertions: [
      { label: 'c2pa.hash.data', data: { alg: 'sha256', hash: fileHash } },
    ],
    signature_info: {
      alg: 'ES256',
      cert: certPem,
      chain,
      tsa_url: 'https://tsa.trusteddit.com/rfc3161',
    },
  };

  // Sign the canonical JSON representation of the claim
  const claimBytes = new TextEncoder().encode(JSON.stringify(claim));
  const signature = await crypto.subtle.sign(
    { name: 'ECDSA', hash: 'SHA-256' },
    privateKey,
    claimBytes
  );

  const manifestBox = Buffer.from(JSON.stringify({
    claim,
    signature: Buffer.from(signature).toString('base64url'),
  })).toString('base64');

  return { manifestBox, signedAt: new Date().toISOString() };
}

Schritt 6: Upload und Prüfabzeichen empfangen (Upload & Receive Authorship Badge)

Laden Sie die signierten Medien in Ihr Backend hoch. Das Backend überprüft das C2PA-Manifest, ruft die Trusteddit-Badge-Signierdienst-API auf, um eine Verifikationssignatur der Plattform hinzuzufügen, und gibt das Abzeichen zur Anzeige in der App zurück.

// src/lib/upload.ts
import { Auth } from 'aws-amplify';
import { C2PAManifest } from './signing';

export interface UploadResult {
  mediaId: string;
  badgeUrl: string;        // URL of the rendered authorship badge image
  badgeManifest: string;   // JSON-encoded C2PA manifest including badge signature
  verificationUrl: string; // Public URL for verifiers (e.g. contentcredentials.org)
}

/**
 * Upload a signed media file plus its C2PA manifest to the app backend.
 * Returns the media ID and authorship badge on success.
 */
export async function uploadSignedMedia(
  mediaUri: string,
  mimeType: string,
  manifest: C2PAManifest
): Promise<UploadResult> {
  const session = await Auth.currentSession();
  const jwt = session.getIdToken().getJwtToken();

  const formData = new FormData();
  formData.append('file', {
    uri: mediaUri,
    name: 'media',
    type: mimeType,
  } as unknown as Blob);
  formData.append('manifestBox', manifest.manifestBox);
  formData.append('signedAt', manifest.signedAt);

  const response = await fetch('https://api.thephenom.app/api/media/upload', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${jwt}` },
    body: formData,
  });

  if (!response.ok) {
    const err = await response.json() as { message: string };
    throw new Error(`Upload failed: ${err.message}`);
  }

  return response.json() as Promise<UploadResult>;
}

Zwei-Signaturen-Modell (Two-Signature Model)

Jedes authentifizierte Medium trägt zwei unabhängige C2PA-Signaturen. Prüfende Anwendungen lösen beide auf und zeigen sie dem Betrachter an.

Signatur (Signature)Signaturschlüssel (Signing Key)Ausgestellt von (Issued By)Nachweis (Proves)Zeitpunkt (Applied At)
Gerätezertifikat-Signatur (Device Cert Signature)Privater ECDSA P-256-Schlüssel auf dem GerätTrusteddit-Zertifizierungsstelle (Trusteddit CA)Dieses Gerät hat dieses Medium aufgenommenAufnahmezeitpunkt, auf dem Gerät (on-device)
Badge-Signierdienst-Signatur (Badge-Signer Signature)Trusteddit-Badge-Signierdienst (serverseitig)Trusteddit-Badge-Signierdienst-APIPlattform hat das Medium geprüft und authentifiziertUpload-Zeitpunkt, serverseitig

Die Gerätezertifikat-Signatur beweist die Entstehungsherkunft (creation provenance): Dieses spezifische Gerät hat dieses Medium erfasst. Die Badge-Signatur beweist die Plattformprüfung (platform verification): Ihre Plattform hat das Medium und die Gerätezertifikatskette vor der Veröffentlichung geprüft.

Weiter: Infrastruktur-Leitfaden (Infrastructure Guide)