Коротко. Манифест 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:
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, как это сделал бы конвейер загрузки:
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)
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
- Сохраняйте или переподписывайте в своём конвейере. Если ваш сервис меняет размер загруженных файлов или сжимает их, либо храните оригинал вместе с его Content Credentials, либо создавайте новый манифест, подписанный вашим собственным сертификатом, с оригиналом в качестве ингредиента и честным действием
c2pa.editedилиc2pa.resized. - Показывайте состояния проверки честно. Отличайте Trusted от просто Valid, показывайте имя подписанта из сертификата и не показывайте ничего особенного, если манифеста нет.
- Проверяйте по спискам доверия C2PA и актуальными версиями программ. Коды состояния вроде
signingCredential.untrustedилиtimeStamp.untrustedобычно говорят о настройках, а не о подделке. - Правильно оформите сертификат: EKU
c2pa-kp-claimSigning, ключ в HSM или в защищённом оборудовании, метки времени на каждой подписи. - Не принимайте решений на основании отсутствия. Отсутствующие или удалённые Content Credentials — обычная ситуация.
Источники
- Техническая спецификация C2PA 2.4 (C2PA Technical Specification): § 2.3.15 и § 2.3.16 (списки доверия), § 2.4.1 (устойчивые Content Credentials), § 6.8 (изъятие утверждений), § 9.2.6 (метаданные вне C2PA), § 10.3.2.5.1 и § 15.8.2 (метки времени), § 13.2.1 (алгоритмы), § 14.3 (состояния проверки), § 14.4 (списки доверия)
- Анонс C2PA 2.3
- Google Security Blog: C2PA Content Credentials в Pixel и Android (10 сентября 2025 года)
- RFC 9052: подпись и шифрование объектов CBOR (CBOR Object Signing and Encryption, COSE), RFC 3161
- Инструменты: c2pa-python 0.38.1 на c2pa-rs 0.91.3, pyca/cryptography 49.0.0, Pillow 12.0.0