Перейти к содержимому
ecdsa.com
← Все статьи

C2PA Content Credentials: манифесты, подписи, ограничения

Мы подписали JPEG с помощью c2pa-python 0.38.1 (c2pa-rs 0.91.3), проверили его сначала без доверия к подписанту, затем с доверием, изменили один байт и пропустили файл через обычный конвейер обработки изображений. Спецификация цитируется по версии C2PA 2.4, актуальной на сегодня.

Коротко. Манифест C2PA — подписанное заявление о медиафайле: claim, который перечисляет утверждения (assertions: что было сделано, например «снято камерой» или «отредактировано с помощью генеративного ИИ», плюс хеш байтов файла) и подписан в формате COSE ключом, чей сертификат X.509 вложен в подпись. Валидаторы присваивают манифесту состояние Well-Formed (корректно сформирован), Valid (подпись и хеши сходятся) или Trusted (цепочка сертификата подписанта доходит до якоря доверия, по умолчанию из списка доверия C2PA, C2PA Trust List). Метка времени RFC 3161 сохраняет манифест действительным и после истечения срока действия сертификата. Чего он не делает: не доказывает, что снимок показывает правду, и не переживает инструменты, которые перекодируют файл. Считайте действительные Content Credentials заявлением «этот подписант ручается за эти байты», а их отсутствие — отсутствием какой-либо информации.

Что входит в манифест

Спецификация C2PA (версия 2.4, апрель 2026 года) собирает манифест из нескольких частей:

  • Утверждения (assertions), каждое в отдельном блоке CBOR или JSON со своей меткой. c2pa.actions.v2 перечисляет, что происходило с файлом (с версии 2.1 «теперь обязательно одно из действий: c2pa.created или c2pa.opened»), и указывает digitalSourceType из словаря IPTC, по которому снимок с камеры отличают от результата генеративного ИИ. c2pa.hash.data — жёсткая привязка (hard binding): хеш байтов файла без диапазона, в котором хранится сам манифест. Дальше могут идти миниатюра, ингредиенты (файлы, из которых сделан этот) и метаданные.
  • Claim: «структура данных с цифровой подписью, в которой заметно любое изменение и которая ссылается на набор утверждений». Ссылки задаются URI с хешем (hashed URI), поэтому изменение любого утверждения нарушает claim.
  • Подпись claim: структура COSE_Sign1, которая подписывает claim; цепочка сертификатов подписанта лежит в её защищённом заголовке. «Должен присутствовать ровно один экземпляр удостоверения (identity credential)», и с версии 2.0 таким удостоверением служит сертификат X.509.
  • Необязательная метка времени от службы RFC 3161, которая покрывает подпись.

Всё это сериализуется в блоки JUMBF внутри файла (сегмент APP11 в JPEG, отдельный блок (box) в MP4 или HEIF) либо хранится удалённо, и тогда на манифест ссылаются по URL. Когда файл редактируют, редактор добавляет новый манифест и записывает предыдущий как ингредиент, так что хранилище манифестов превращается в цепочку.

Допустимые алгоритмы подписи — ES256, ES384, ES512, PS256, PS384, PS512 и Ed25519 (§ 13.2.1); ключ должен соответствовать алгоритму, а длина ключа RSA должна быть не меньше 2048 бит. Для видео в прямом эфире спецификация рекомендует детерминированные алгоритмы, чтобы одинаковые сегменты можно было кешировать.

Подпись JPEG

Лабораторный скрипт создаёт тестовый центр сертификации (CA) с помощью pyca/cryptography 49.0.0 и сертификат подписанта P-256 с теми EKU, которые ищет валидатор C2PA: c2pa-kp-claimSigning (1.3.6.1.4.1.62558.2.1) и, для старых валидаторов, id-kp-emailProtection. Затем он подписывает сгенерированный JPEG размером 640×360 с меткой времени от DigiCert:

lab.py (фрагмент): определение манифеста и подпись (c2pa-python 0.38.1)
manifest = {
    "claim_generator_info": [{"name": "ecdsa.com C2PA lab", "version": "1.0"}],
    "title": "sunrise.jpg",
    "assertions": [
        {"label": "c2pa.actions.v2", "data": {"actions": [
            {"action": "c2pa.created", "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"}
        ]}}
    ],
}
signer = c2pa.Signer.from_info(c2pa.C2paSignerInfo(alg=b"es256", sign_cert=chain_pem, private_key=key_pem, ta_url=b"http://timestamp.digicert.com"))
builder = c2pa.Builder.from_json(manifest)
signed = io.BytesIO()
builder.sign(signer, "image/jpeg", io.BytesIO(original), signed)

Профиль сертификата важен: в сертификате подписанта стоят digitalSignature в расширении Key Usage (использование ключа), CA:FALSE и EKU для подписи claim; корневой сертификат — сертификат CA с keyCertSign. Сертификат без подходящего EKU или самоподписанный конечный сертификат не проходит проверку, даже если подпись математически верна. Эти расширения разобраны в статье Поля сертификата X.509.

Проверка: Valid, Trusted, Invalid

Тот же скрипт затем четыре раза читает файл: без каких-либо настроек доверия, с тестовым корневым сертификатом в роли якоря доверия, с одним изменённым байтом данных изображения и после декодирования и повторного кодирования изображения в Pillow 12.0.0, как это сделал бы конвейер загрузки:

lab.py (фрагмент): чтение с якорем доверия, изменение байта, перекодирование
trusted = c2pa.Context.from_dict({"trust": {"trust_anchors": pem(root).decode()}, "verify": {"verify_trust": True}})
read("signed, test root as trust anchor", signed, trusted)

# Change one byte of the compressed image data (after the JPEG Start of Scan marker).
sos = signed.rfind(b"\xff\xda")
tampered = bytearray(signed)
tampered[sos + 200] ^= 0x01
read("one byte of image data changed", bytes(tampered), trusted)

# What many upload pipelines do: decode and re-encode, dropping metadata.
Image.open(io.BytesIO(signed)).save(out := io.BytesIO(), "JPEG", quality=85)
read("re-encoded by an image pipeline", out.getvalue(), trusted)
Вывод (python:3.14.8-slim-trixie)
c2pa-python 0.38.1, 0.91.3
JPEG: 13376 bytes unsigned, 52438 bytes with the manifest (+39062)
--- signed, no trust anchors configured: validation_state = Valid
    status codes: timeStamp.validated, claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match, assertion.hashedURI.match, assertion.hashedURI.match, assertion.dataHash.match, signingCredential.ocsp.skipped, timeStamp.untrusted, signingCredential.untrusted
    signer: Example Newsroom / Example Newsroom Photo Desk, alg Es256, time 2026-10-22T06:43:11+00:00
    assertions: ['c2pa.actions.v2']
    assertion store: ['c2pa.actions.v2', 'c2pa.hash.data', 'c2pa.thumbnail.claim']
    certificate chain in the signature: 981 bytes DER
--- signed, test root as trust anchor: validation_state = Trusted
    status codes: timeStamp.validated, signingCredential.trusted, claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match, assertion.hashedURI.match, assertion.hashedURI.match, assertion.dataHash.match, signingCredential.ocsp.skipped, timeStamp.untrusted
--- one byte of image data changed: validation_state = Invalid
    status codes: timeStamp.validated, signingCredential.trusted, claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match, assertion.hashedURI.match, assertion.hashedURI.match, signingCredential.ocsp.skipped, timeStamp.untrusted, assertion.dataHash.mismatch
--- re-encoded by an image pipeline: no C2PA manifest found

Как это читать:

  • Valid, но без доверия. Все криптографические проверки пройдены (claimSignature.validated, три assertion.hashedURI.match для трёх утверждений, assertion.dataHash.match для байтов изображения), но есть signingCredential.untrusted: создать CA и подписать файл может кто угодно. Спецификация определяет это состояние точно: манифест получает состояние Valid, если он корректно сформирован, не изменён, его подпись проходит проверку «в пределах срока действия» и удостоверение подписанта не отозвано; Trusted — только если вдобавок удостоверение подписанта «получает код успеха signingCredential.trusted» (§ 14.3).
  • Trusted, как только валидатор знает корневой сертификат. В рабочей среде этот корневой сертификат берётся из списка доверия C2PA (C2PA Trust List) — «управляемого C2PA списка якорей доверия в виде сертификатов X.509, которые выпускают сертификаты для аппаратных и программных подписантов»; валидаторы должны включать этот список для EKU c2pa-kp-claimSigning, а пользователи могут добавлять свои якоря.
  • Invalid после изменения одного байта: подпись по-прежнему в порядке, а жёсткая привязка — нет (assertion.dataHash.mismatch). Обрезка, фильтр или повторное сжатие дают тот же эффект, поэтому редакторы должны записывать новый манифест, а не сохранять старый.
  • Манифест пропал после перекодирования. Он хранится в метаданных; конвейер, который декодирует пиксели и записывает новый файл, отбрасывает его, как и EXIF. Ничто в полученном файле не говорит о том, что Content Credentials в нём когда-то были.

Манифест добавил 39 КБ к изображению размером 13 КБ: помимо claim и нашей цепочки сертификатов размером 981 байт, в нём лежат миниатюра, которую сгенерировал c2pa-rs, и токен метки времени с сертификатами DigiCert. timeStamp.untrusted появляется потому, что в лабораторном скрипте не настроен список доверенных служб меток времени C2PA (C2PA TSA Trust List): метка времени криптографически действительна, просто её службы не было в списке валидатора.

Метки времени и срок действия сертификата

Спецификация говорит прямо: «манифесты без меток времени перестают быть действительными, когда срок действия удостоверения подписанта истекает или оно отзывается», а если метка времени есть и ей доверяют, валидаторы при проверке сертификатов «должны использовать засвидетельствованное время, а не текущее» (§ 10.3.2.5.1, § 15.8.2). Это та же конструкция, что и в Authenticode: сертификат должен быть действителен только в момент подписи, а служба меток времени доказывает, когда этот момент наступил. Устройствам, которые подписывают офлайн, нужно другое решение; камера Google Pixel, например, использует «доверенные метки времени на устройстве», чтобы фотографиям «можно было доверять и после истечения срока действия сертификата, даже если они сняты, когда устройство было офлайн» (Google Security Blog).

Откуда берётся доверие

Идентификационные данные подписанта — это субъект сертификата, поэтому ценность Content Credentials зависит от того, кто выпустил этот сертификат и что он при этом проверил. C2PA ведёт программу соответствия (conformance program) и списки доверия; валидаторы настраивают их по EKU и могут добавлять якоря с датами notBefore и notAfter. Оборудование тоже имеет значение: по данным Google, приложение Pixel 10 Camera «получило уровень гарантии 2 (Assurance Level 2), высшую оценку безопасности, которую на данный момент определяет программа соответствия C2PA», а ключи для подписи claim «генерируются и хранятся с помощью Android StrongBox в чипе безопасности Titan M2»; сертификаты выдаются только после того, как аттестация ключей Android (Android Key Attestation) подтвердит подлинность устройства и приложения. О ключах в защищённом оборудовании рассказывает статья Аппаратно защищённые ключи.

От имени подписанта говорят только подписанные утверждения. Спецификация приводит пример: «подписант не отвечает за точность координат GPS в метаданных EXIF, даже если эти метаданные покрыты жёсткой привязкой» (§ 9.2.6); подписанту приписываются только метаданные, помещённые в утверждение c2pa.metadata.

Чего Content Credentials не делают

  • Они не доказывают, что изображение правдиво. Content Credentials камеры говорят, что эти байты создало это устройство; постановочная сцена или фото экрана — всё равно снимок с камеры. Content Credentials с отметкой о генеративном ИИ — это заявление инструмента, который решил их добавить.
  • Их легко удалить. Наше перекодирование отбросило манифест без какой-либо ошибки, а метаданные часто удаляют намеренно, ради конфиденциальности или чтобы уменьшить размер. Ответ спецификации — мягкая привязка (soft binding): отпечаток или невидимый водяной знак, по которому манифест можно снова найти в репозитории. «Устойчивыми» (Durable Content Credential) называются Content Credentials, «для которых существует одна или несколько мягких привязок, позволяющих найти их в репозитории манифестов» (§ 2.4.1). Хранит ли конкретная платформа манифесты и ищет ли их таким способом, решает сама платформа.
  • Их отсутствие ничего не значит. У большинства изображений в интернете нет Content Credentials; отсутствие манифеста не говорит о том, что изображение подделано.
  • Они могут раскрыть больше, чем задумано. Цепочки ингредиентов, названия устройств и местоположение передаются вместе с файлом. Авторы могут изымать утверждения, когда файл становится ингредиентом (§ 6.8), а инструментам следует давать людям выбирать, что включать.

Если вы работаете с C2PA

  1. Сохраняйте или переподписывайте в своём конвейере. Если ваш сервис меняет размер загруженных файлов или сжимает их, либо храните оригинал вместе с его Content Credentials, либо создавайте новый манифест, подписанный вашим собственным сертификатом, с оригиналом в качестве ингредиента и честным действием c2pa.edited или c2pa.resized.
  2. Показывайте состояния проверки честно. Отличайте Trusted от просто Valid, показывайте имя подписанта из сертификата и не показывайте ничего особенного, если манифеста нет.
  3. Проверяйте по спискам доверия C2PA и актуальными версиями программ. Коды состояния вроде signingCredential.untrusted или timeStamp.untrusted обычно говорят о настройках, а не о подделке.
  4. Правильно оформите сертификат: EKU c2pa-kp-claimSigning, ключ в HSM или в защищённом оборудовании, метки времени на каждой подписи.
  5. Не принимайте решений на основании отсутствия. Отсутствующие или удалённые Content Credentials — обычная ситуация.

Источники