Skip to content

keispace/poc_c2pa

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

한국어 | English

C2PA PoC (Proof of Concept)

C2PA (Coalition for Content Provenance and Authenticity) 기반으로 파일에 출처 정보(Manifest) 를 서명하고 검증하는 Node.js PoC입니다.

핵심 라이브러리

프로젝트 구조

poc_c2pa/
├── assets/                   # 입력 폴더 (고정)
│   ├── jwk.json              # Ed25519 JWK 키 파일 (.gitignore 대상)
│   └── sample.jpg            # 서명할 원본 파일
├── output/                   # 출력 폴더 (고정, 자동 생성, .gitignore 대상)
│   ├── signed_sample.jpg     # C2PA 매니페스트가 포함된 파일 (signed_ 접두사)
│   ├── cert_chain.pem        # 인증서 체인 (캐싱)
│   └── private_key.pem       # 개인키 (캐싱)
├── src/
│   ├── constant.ts           # ★ 설정 파일 — 파일명, Assertions, 매니페스트 설정
│   ├── crypto-utils.ts       # JWK → PEM 변환 + 인증서 체인 생성
│   ├── sign.ts               # 서명 스크립트 (npm run sign)
│   └── verify.ts             # 검증 스크립트 (npm run verify)
├── package.json
└── tsconfig.json

빠른 시작

1. 설치

npm install

요구사항: Node.js ≥ 20, openssl CLI (macOS/Linux 기본 포함)

2. JWK 키 설정

assets/jwk.json 파일을 생성합니다:

{
  "type": "jwk",
  "jwk": {
    "kty": "OKP",
    "d": "<Base64url 개인키>",
    "crv": "Ed25519",
    "kid": "<키 ID>",
    "x": "<Base64url 공개키>"
  }
}

assets/jwk.json.gitignore에 포함되어 있어 커밋되지 않습니다.

3. 원본 파일 준비

assets/ 폴더에 서명할 파일을 넣고, constant.tsINPUT_FILE_NAME을 맞춰주세요.

4. 서명

npm run sign

처리 흐름:

  1. assets/jwk.json → PEM 개인키 변환
  2. OpenSSL로 3단계 인증서 체인 생성 (Root CA → Intermediate CA → Signing Cert)
  3. 생성된 PEM 파일을 output/에 캐싱 (다음 실행 시 재사용)
  4. C2PA 매니페스트(assertions 포함)를 파일에 서명하여 output/signed_<파일명> 생성

5. 검증

npm run verify

서명된 파일의 매니페스트를 읽고 assertions, 서명 유효성 등을 출력합니다.

npm scripts

스크립트 설명
npm run sign C2PA 서명 실행
npm run verify 서명된 파일 검증
npm run clear output/ 폴더 삭제 (PEM 캐시 포함)
npm run test clear → sign → verify 한 번에 실행

설정 가이드 (src/constant.ts)

모든 설정은 constant.ts에서 관리합니다. 코드 수정 없이 이 파일만 편집하면 됩니다.

파일 설정

export const INPUT_FILE_NAME = 'sample.jpg';  // assets/ 폴더 내 원본 파일명
  • 입력: assets/<INPUT_FILE_NAME>
  • 출력: output/signed_<INPUT_FILE_NAME> (자동)
  • 폴더는 assets/, output/ 고정이며, 파일명만 변경하면 됩니다.
  • mimeType은 확장자(.jpg, .png, .mp4 등)에서 자동 추론됩니다.

서명 알고리즘

export const SIGNING_ALGORITHM = 'ed25519' as const;
// EC P-256 키를 사용할 경우 'es256'으로 변경

Assertions (파일에 넣을 데이터)

C2PA에서 파일에 임베딩하는 정보 단위를 Assertion이라 합니다. { label, data } 쌍으로 구성됩니다.

표준 Assertions (참고용 예시)

CUSTOM_ASSERTIONS 배열에 표준 label을 사용하면 C2PA 규격에 맞는 메타데이터를 넣을 수 있습니다:

// c2pa.actions — 파일에 수행된 작업 기록
{
  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 콘텐츠 메타데이터
{
  label: 'stds.schema-org.CreativeWork',
  data: {
    '@type': 'CreativeWork',
    '@context': 'https://schema.org',
    author: [{ '@type': 'Person', name: 'Author Name' }],
  },
}

c2pa.actionssetIntent() 호출 시 자동 생성되므로 직접 넣지 않아도 됩니다. stds.schema-org.CreativeWork는 선택 사항이며, @context 필드가 필수입니다.

커스텀 Assertions

CUSTOM_ASSERTIONS 배열에 원하는 JSON을 자유롭게 추가할 수 있습니다:

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,
    },
  },
];

항목을 추가/수정/삭제하면 다음 npm run sign 시 반영됩니다.

매니페스트 설정

CLAIM_GENERATOR — 자유 문자열

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

매니페스트를 생성한 소프트웨어 식별값. 이름/버전 형태가 관례이며 내용은 자유입니다. SDK가 내부적으로 c2pa-rs/0.75.6도 자동 추가하므로, 여기에는 우리 앱 이름만 넣으면 됩니다.

BUILDER_INTENT — IPTC 규격 URI (규칙 있음)

export const BUILDER_INTENT = {
  create: 'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia',
};
  • : create (새 파일) 또는 update (기존 파일 수정) 중 하나
  • : IPTC Digital Source Type URI — 규격화된 값을 사용해야 합니다
URI (마지막 부분) 의미
trainedAlgorithmicMedia AI가 생성한 콘텐츠
algorithmicMedia 알고리즘 생성 (비AI)
digitalCapture 카메라/스캐너로 직접 촬영
digitalArt 사람이 디지털로 제작
compositeWithTrainedAlgorithmicMedia AI 생성물 합성

전체 목록: https://cv.iptc.org/newscodes/digitalsourcetype/

이 값에 따라 c2pa.actions assertion이 자동 생성됩니다.

CERT_SUBJECT — X.509 DN 형식

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

자기서명 인증서 생성 시 사용되는 X.509 Distinguished Name. 검증 결과의 signature_info에 표시됩니다.

약어 의미 예시
CN Common Name C2PA PoC Test
O Organization Newnal
OU Org Unit (선택) AI Team
C Country (선택) KR

⚠️ 주의사항

커스텀 Assertion label 이름 제약

c2pa-rs SDK 내부에서 metadata라는 단어가 label에 포함되면 JSON-LD 형식으로 강제 검증합니다. 이 경우 데이터에 @context 필드가 없으면 검증(verify)에서 실패합니다.

# ❌ 사용 금지 — 검증 시 "missing field @context" 오류 발생
org.newnal.metadata
com.example.metadata
my.custom.metadata

# ✅ 대체 label 사용
org.newnal.project-info
org.newnal.meta
org.newnal.custom-data

@context 필드를 포함하면 metadata label도 사용 가능하지만, 혼란을 피하기 위해 다른 이름을 권장합니다.

PEM 캐싱

  • 최초 서명 시 JWK로부터 인증서 체인과 개인키 PEM을 생성하고 output/에 저장합니다.
  • 이후 실행에서는 캐싱된 PEM을 재사용합니다.
  • 키를 변경한 경우 PEM 캐시를 삭제 후 다시 서명하세요:
npm run clear
npm run sign

인증서 신뢰 (Trust)

  • 이 PoC는 자체 생성한 인증서 체인(Self-signed Root CA)을 사용합니다.
  • 검증 시 verify_trust: false로 설정되어 있어 인증서 신뢰 검증은 건너뜁니다.
  • verify_trust: true로 변경하면 검증 자체는 통과하지만, failure에 signingCredential.untrusted ("signing certificate untrusted")가 추가됩니다.
    • 자체 생성한 Root CA가 시스템/C2PA 신뢰 저장소에 등록되어 있지 않기 때문입니다.
    • 서명의 무결성(해시, 인증서 체인)은 정상이지만, "이 서명자를 신뢰할 수 있는가"에서 실패합니다.
  • 프로덕션 환경에서는 C2PA 인증 CA(Adobe, DigiCert 등)에서 발급한 인증서를 사용해야 이 검증을 통과합니다.

지원 파일 형식

확장자에서 mimeType을 자동 추론합니다 (constant.tsMIME_TYPES). 지원 형식:

확장자 mimeType
.jpg, .jpeg image/jpeg
.png image/png
.webp image/webp
.tiff, .tif image/tiff
.mp4 video/mp4
.mov video/quicktime

C2PA 매니페스트 구조 (참고)

┌──────────────────────────────────────────┐
│  파일 (JPEG, PNG, MP4 등)                │
│  ┌────────────────────────────────────┐  │
│  │  C2PA Manifest Store               │  │
│  │  ┌──────────────────────────────┐  │  │
│  │  │  Manifest                    │  │  │
│  │  │  ├─ claim_generator          │  │  │
│  │  │  ├─ signature_info           │  │  │
│  │  │  └─ assertions[]             │  │  │
│  │  │     ├─ c2pa.actions          │  │  │  ← 생성/편집 등 작업 기록 (자동)
│  │  │     ├─ stds.schema-org.*     │  │  │  ← Schema.org 메타데이터 (선택)
│  │  │     ├─ org.newnal.*          │  │  │  ← 커스텀 데이터 (자유 형식)
│  │  │     └─ c2pa.hash.data        │  │  │  ← 파일 무결성 해시 (자동)
│  │  └──────────────────────────────┘  │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘
  • Manifest Store: 파일에 임베딩되는 전체 컨테이너
  • Manifest: 하나의 서명 단위 (편집될 때마다 새 Manifest가 체이닝)
  • Assertions: { label, data } 형태의 데이터 배열 — 파일에 넣고 싶은 정보를 여기에 담습니다
  • Signature: 매니페스트 전체에 대한 전자서명 (변조 감지)

서명 후 파일의 바이너리가 수정되면 c2pa.hash.data 검증이 실패하여 변조가 감지됩니다.

데이터 결과 예시

sign 결과 예시

npm run sign

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

=== C2PA 서명 시작 ===

📄 입력 파일: /Users/evankim/dev/git/newnal/poc_c2pa/assets/sample.mp4
📁 출력 경로: /Users/evankim/dev/git/newnal/poc_c2pa/output/signed_sample.mp4

🔑 서명키 및 인증서 준비 중...
   ✅ JWK에서 새로 생성 → output/cert_chain.pem, output/private_key.pem 저장
   ✅ LocalSigner 생성 완료

📋 매니페스트 구성 중...
   ✅ Builder intent 설정: {"create":"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"}

✍️  서명 중...
   ✅ 서명 완료!

📊 결과:
   원본 크기: 51307.7 KB
   서명 파일 크기: 51321.2 KB
   C2PA 데이터 크기: 13.5 KB

✅ 서명된 파일: /Users/evankim/dev/git/newnal/poc_c2pa/output/signed_sample.mp4
   → npm run verify 로 검증할 수 있습니다.

Verify 결과 예시

npm run verify

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

=== C2PA 검증 시작 ===

📄 검증 대상: /Users/evankim/dev/git/newnal/poc_c2pa/output/signed_sample.mp4

📋 매니페스트 스토어 요약:
   임베디드 여부: true

🔖 활성 매니페스트:
   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"
       }
     ]
   }

--- 전체 매니페스트 JSON (디버깅용) ---
{
  "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"
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors