Skip to content

Latest commit

 

History

History
594 lines (495 loc) · 17.8 KB

File metadata and controls

594 lines (495 loc) · 17.8 KB

한국어 | English

C2PA PoC (Proof of Concept)

A Node.js PoC for signing and verifying provenance information (Manifest) on files based on C2PA (Coalition for Content Provenance and Authenticity).

Core Library

Project Structure

poc_c2pa/
├── assets/                   # Input folder (fixed)
│   ├── jwk.json              # Ed25519 JWK key file (.gitignore target)
│   └── sample.jpg            # Original file to sign
├── output/                   # Output folder (fixed, auto-created, .gitignore target)
│   ├── signed_sample.jpg     # File with embedded C2PA manifest (signed_ prefix)
│   ├── cert_chain.pem        # Certificate chain (cached)
│   └── private_key.pem       # Private key (cached)
├── src/
│   ├── constant.ts           # ★ Config file — file name, assertions, manifest settings
│   ├── crypto-utils.ts       # JWK → PEM conversion + certificate chain generation
│   ├── sign.ts               # Signing script (npm run sign)
│   └── verify.ts             # Verification script (npm run verify)
├── package.json
└── tsconfig.json

Quick Start

1. Install

npm install

Requirements: Node.js ≥ 20, openssl CLI (included by default on macOS/Linux)

2. JWK Key Setup

Create an assets/jwk.json file:

{
  "type": "jwk",
  "jwk": {
    "kty": "OKP",
    "d": "<Base64url private key>",
    "crv": "Ed25519",
    "kid": "<Key ID>",
    "x": "<Base64url public key>"
  }
}

assets/jwk.json is included in .gitignore and will not be committed.

3. Prepare Original File

Place the file to sign in the assets/ folder and set INPUT_FILE_NAME in constant.ts accordingly.

4. Sign

npm run sign

Processing flow:

  1. assets/jwk.json → PEM private key conversion
  2. Generate 3-level certificate chain via OpenSSL (Root CA → Intermediate CA → Signing Cert)
  3. Cache generated PEM files in output/ (reused on subsequent runs)
  4. Sign C2PA manifest (including assertions) into the file and output output/signed_<filename>

5. Verify

npm run verify

Reads the manifest from the signed file and outputs assertions, signature validity, etc.

npm scripts

Script Description
npm run sign Run C2PA signing
npm run verify Verify signed file
npm run clear Delete output/ folder (including PEM cache)
npm run test Run clear → sign → verify all at once

Configuration Guide (src/constant.ts)

All settings are managed in constant.ts. Just edit this file — no code changes needed.

File Settings

export const INPUT_FILE_NAME = 'sample.jpg';  // Original filename in assets/ folder
  • Input: assets/<INPUT_FILE_NAME>
  • Output: output/signed_<INPUT_FILE_NAME> (automatic)
  • Folders are fixed as assets/ and output/ — just change the filename.
  • mimeType is automatically inferred from the extension (.jpg, .png, .mp4, etc.).

Signing Algorithm

export const SIGNING_ALGORITHM = 'ed25519' as const;
// Change to 'es256' if using an EC P-256 key

Assertions (Data Embedded in Files)

In C2PA, the unit of information embedded in a file is called an Assertion. Each consists of a { label, data } pair.

Standard Assertions (Reference Examples)

Using standard labels in the CUSTOM_ASSERTIONS array allows you to add C2PA-compliant metadata:

// c2pa.actions — Record of actions performed on the file
{
  label: 'c2pa.actions',
  data: {
    actions: [{
      action: 'c2pa.created',
      digitalSourceType: 'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia',
      softwareAgent: 'MyApp/1.0',
    }],
  },
}

// stds.schema-org.CreativeWork — Schema.org content metadata
{
  label: 'stds.schema-org.CreativeWork',
  data: {
    '@type': 'CreativeWork',
    '@context': 'https://schema.org',
    author: [{ '@type': 'Person', name: 'Author Name' }],
  },
}

c2pa.actions is automatically generated when setIntent() is called, so you don't need to add it manually. stds.schema-org.CreativeWork is optional, but the @context field is required.

Custom Assertions

You can freely add any JSON to the CUSTOM_ASSERTIONS array:

export const CUSTOM_ASSERTIONS = [
  {
    label: 'org.newnal.project-info',
    data: {
      projectId: 'proj-abc-123',
      version: '1.0.0',
      tags: ['ai-generated', 'test', 'poc'],
      createdBy: { name: 'Test User', role: 'developer' },
    },
  },
  {
    label: 'org.newnal.training-info',
    data: {
      modelName: 'gpt-4',
      modelVersion: '2025-01',
      prompt: 'Generate a sample image',
      temperature: 0.7,
      seed: 42,
    },
  },
];

Changes (add/modify/delete) will be reflected on the next npm run sign.

Manifest Settings

CLAIM_GENERATOR — Free-form String

export const CLAIM_GENERATOR = 'newnal-c2pa-poc/1.0';

Software identifier for the manifest creator. The name/version format is conventional, but the content is arbitrary. The SDK automatically appends c2pa-rs/0.75.6 internally, so just put your app name here.

BUILDER_INTENT — IPTC Standard URI (Has Rules)

export const BUILDER_INTENT = {
  create: 'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia',
};
  • Key: Either create (new file) or update (modifying existing file)
  • Value: IPTC Digital Source Type URI — must use standardized values
URI (last segment) Meaning
trainedAlgorithmicMedia AI-generated content
algorithmicMedia Algorithm-generated (non-AI)
digitalCapture Captured directly by camera/scanner
digitalArt Digitally created by a person
compositeWithTrainedAlgorithmicMedia Composite with AI-generated content

Full list: https://cv.iptc.org/newscodes/digitalsourcetype/

The c2pa.actions assertion is automatically generated based on this value.

CERT_SUBJECT — X.509 DN Format

export const CERT_SUBJECT = 'CN=C2PA PoC Test, O=Newnal';

X.509 Distinguished Name used when generating self-signed certificates. Displayed in the signature_info of verification results.

Abbreviation Meaning Example
CN Common Name C2PA PoC Test
O Organization Newnal
OU Org Unit (optional) AI Team
C Country (optional) KR

⚠️ Important Notes

Custom Assertion Label Name Restrictions

The c2pa-rs SDK internally enforces JSON-LD format validation when the word metadata is included in a label. If the data doesn't contain a @context field, verification will fail.

# ❌ Avoid — causes "missing field @context" error during verification
org.newnal.metadata
com.example.metadata
my.custom.metadata

# ✅ Use alternative labels
org.newnal.project-info
org.newnal.meta
org.newnal.custom-data

Labels containing metadata can still be used if you include a @context field, but using different names is recommended to avoid confusion.

PEM Caching

  • On the first signing, a certificate chain and private key PEM are generated from the JWK and saved in output/.
  • Subsequent runs reuse the cached PEM files.
  • If you change the key, delete the PEM cache and re-sign:
npm run clear
npm run sign

Certificate Trust

  • This PoC uses a self-generated certificate chain (Self-signed Root CA).
  • Verification is configured with verify_trust: false, so certificate trust verification is skipped.
  • Changing to verify_trust: true will pass verification itself, but adds signingCredential.untrusted ("signing certificate untrusted") as a failure.
    • This is because the self-generated Root CA is not registered in the system/C2PA trust store.
    • Signature integrity (hash, certificate chain) is valid, but "can this signer be trusted?" fails.
  • In a production environment, certificates issued by C2PA-certified CAs (Adobe, DigiCert, etc.) are required to pass this verification.

Supported File Formats

mimeType is automatically inferred from the file extension (MIME_TYPES in constant.ts). Supported formats:

Extension mimeType
.jpg, .jpeg image/jpeg
.png image/png
.webp image/webp
.tiff, .tif image/tiff
.mp4 video/mp4
.mov video/quicktime

C2PA Manifest Structure (Reference)

┌──────────────────────────────────────────┐
│  File (JPEG, PNG, MP4, etc.)             │
│  ┌────────────────────────────────────┐  │
│  │  C2PA Manifest Store               │  │
│  │  ┌──────────────────────────────┐  │  │
│  │  │  Manifest                    │  │  │
│  │  │  ├─ claim_generator          │  │  │
│  │  │  ├─ signature_info           │  │  │
│  │  │  └─ assertions[]             │  │  │
│  │  │     ├─ c2pa.actions          │  │  │  ← Action records (auto)
│  │  │     ├─ stds.schema-org.*     │  │  │  ← Schema.org metadata (optional)
│  │  │     ├─ org.newnal.*          │  │  │  ← Custom data (free format)
│  │  │     └─ c2pa.hash.data        │  │  │  ← File integrity hash (auto)
│  │  └──────────────────────────────┘  │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘
  • Manifest Store: The overall container embedded in the file
  • Manifest: A single signing unit (a new Manifest is chained each time the file is edited)
  • Assertions: Array of { label, data } pairs — put the information you want to embed here
  • Signature: Digital signature over the entire manifest (tamper detection)

If the file binary is modified after signing, c2pa.hash.data verification will fail, detecting tampering.

Example Output

Sign Output Example

npm run sign

> newnal-poc-c2pa@1.0.0 sign
> tsx src/sign.ts

=== C2PA Signing Start ===

📄 Input file: /Users/evankim/dev/git/newnal/poc_c2pa/assets/sample.mp4
📁 Output path: /Users/evankim/dev/git/newnal/poc_c2pa/output/signed_sample.mp4

🔑 Preparing signing key and certificates...
   ✅ Newly generated from JWK → output/cert_chain.pem, output/private_key.pem saved
   ✅ LocalSigner created

📋 Composing manifest...
   ✅ Builder intent set: {"create":"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"}

✍️  Signing...
   ✅ Signing complete!

📊 Results:
   Original size: 51307.7 KB
   Signed file size: 51321.2 KB
   C2PA data size: 13.5 KB

✅ Signed file: /Users/evankim/dev/git/newnal/poc_c2pa/output/signed_sample.mp4
   → Verify with: npm run verify

Verify Output Example

npm run verify

> newnal-poc-c2pa@1.0.0 verify
> tsx src/verify.ts

=== C2PA Verification Start ===

📄 Verification target: /Users/evankim/dev/git/newnal/poc_c2pa/output/signed_sample.mp4

📋 Manifest store summary:
   Embedded: true

🔖 Active manifest:
   Claim Generator: N/A
   Title: N/A
   Format: N/A

📝 Assertions (4):

   [c2pa.hash.bmff.v3]
   {
     "exclusions": [
       {
         "xpath": "/uuid",
         "length": null,
         "data": [
           {
             "offset": 8,
             "value": "2P7D1hsOSDySl1goh37EgQ=="
           }
         ],
         "subset": null,
         "version": null,
         "flags": null,
         "exact": null
       },
       {
         "xpath": "/ftyp",
         "length": null,
         "data": null,
         "subset": null,
         "version": null,
         "flags": null,
         "exact": null
       },
       {
         "xpath": "/mfra",
         "length": null,
         "data": null,
         "subset": null,
         "version": null,
         "flags": null,
         "exact": null
       }
     ],
     "alg": "sha256",
     "hash": "f1t2PWcXvoBo+TlwpR07Eufq0fN+kRi9wC8yUmLK+cA=",
     "name": "jumbf manifest"
   }

   [org.newnal.project-info]
   {
     "tags": [
       "ai-generated",
       "test",
       "poc"
     ],
     "version": "1.0.0",
     "createdBy": {
       "name": "Test User",
       "role": "developer"
     },
     "projectId": "proj-abc-123"
   }

   [org.newnal.training-info]
   {
     "seed": 42,
     "prompt": "Generate a sample image",
     "modelName": "gpt-4",
     "temperature": 0.7,
     "modelVersion": "2025-01"
   }

   [c2pa.actions.v2]
   {
     "actions": [
       {
         "action": "c2pa.created",
         "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
       }
     ]
   }

--- Full Manifest JSON (for debugging) ---
{
  "active_manifest": "urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf",
  "manifests": {
    "urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf": {
      "claim_generator_info": [
        {
          "name": "c2pa-rs",
          "version": "0.75.6",
          "org.contentauth.c2pa_rs": "0.75.6"
        }
      ],
      "instance_id": "xmp:iid:2d527de9-9ed6-4f8a-97d6-ea1179e1865e",
      "assertions": [
        {
          "label": "c2pa.hash.bmff.v3",
          "data": {
            "exclusions": [
              {
                "xpath": "/uuid",
                "length": null,
                "data": [
                  {
                    "offset": 8,
                    "value": "2P7D1hsOSDySl1goh37EgQ=="
                  }
                ],
                "subset": null,
                "version": null,
                "flags": null,
                "exact": null
              },
              {
                "xpath": "/ftyp",
                "length": null,
                "data": null,
                "subset": null,
                "version": null,
                "flags": null,
                "exact": null
              },
              {
                "xpath": "/mfra",
                "length": null,
                "data": null,
                "subset": null,
                "version": null,
                "flags": null,
                "exact": null
              }
            ],
            "alg": "sha256",
            "hash": "f1t2PWcXvoBo+TlwpR07Eufq0fN+kRi9wC8yUmLK+cA=",
            "name": "jumbf manifest"
          },
          "created": true
        },
        {
          "label": "org.newnal.project-info",
          "data": {
            "tags": [
              "ai-generated",
              "test",
              "poc"
            ],
            "version": "1.0.0",
            "createdBy": {
              "name": "Test User",
              "role": "developer"
            },
            "projectId": "proj-abc-123"
          }
        },
        {
          "label": "org.newnal.training-info",
          "data": {
            "seed": 42,
            "prompt": "Generate a sample image",
            "modelName": "gpt-4",
            "temperature": 0.7,
            "modelVersion": "2025-01"
          }
        },
        {
          "label": "c2pa.actions.v2",
          "data": {
            "actions": [
              {
                "action": "c2pa.created",
                "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
              }
            ]
          }
        }
      ],
      "signature_info": {
        "alg": "Ed25519",
        "issuer": "Newnal",
        "common_name": "C2PA PoC Test",
        "cert_serial_number": "84608593711801546977401573933771680074522597368"
      },
      "label": "urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf",
      "claim_version": 2
    }
  },
  "validation_results": {
    "activeManifest": {
      "success": [
        {
          "code": "claimSignature.insideValidity",
          "url": "self#jumbf=/c2pa/urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf/c2pa.signature",
          "explanation": "claim signature valid"
        },
        {
          "code": "claimSignature.validated",
          "url": "self#jumbf=/c2pa/urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf/c2pa.signature",
          "explanation": "claim signature valid"
        },
        {
          "code": "assertion.hashedURI.match",
          "url": "self#jumbf=/c2pa/urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf/c2pa.assertions/c2pa.hash.bmff.v3",
          "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3"
        },
        {
          "code": "assertion.hashedURI.match",
          "url": "self#jumbf=/c2pa/urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf/c2pa.assertions/org.newnal.project-info",
          "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/org.newnal.project-info"
        },
        {
          "code": "assertion.hashedURI.match",
          "url": "self#jumbf=/c2pa/urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf/c2pa.assertions/org.newnal.training-info",
          "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/org.newnal.training-info"
        },
        {
          "code": "assertion.hashedURI.match",
          "url": "self#jumbf=/c2pa/urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf/c2pa.assertions/c2pa.actions.v2",
          "explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2"
        },
        {
          "code": "assertion.bmffHash.match",
          "url": "self#jumbf=/c2pa/urn:c2pa:d8ead370-32d0-4e92-a490-b38959784adf/c2pa.assertions/c2pa.hash.bmff.v3",
          "explanation": "BMFF hash valid"
        }
      ],
      "informational": [],
      "failure": []
    }
  },
  "validation_state": "Valid"
}