Resposta curta. Um manifesto C2PA é uma declaração assinada sobre um arquivo de mídia: uma claim que lista asserções (o que foi feito, como “criado por uma câmera” ou “editado com IA generativa”, mais um hash dos bytes do arquivo), assinada com COSE por uma chave cujo certificado X.509 vai dentro da assinatura. Os validadores classificam um manifesto como Well-Formed (bem formado), Valid (válido: a assinatura e os hashes conferem) ou Trusted (confiável: o certificado do signatário se encadeia até uma âncora de confiança, por padrão a C2PA Trust List). Um carimbo do tempo RFC 3161 mantém o manifesto válido depois que o certificado expira. O que ele não faz: provar que a imagem mostra a verdade ou sobreviver a ferramentas que recodificam o arquivo. Trate uma credencial válida como “este signatário atesta estes bytes” e a falta dela como ausência total de informação.
O que há em um manifesto
A especificação C2PA (versão 2.4, abril de 2026) monta um manifesto a partir de algumas partes:
- Asserções, cada uma delas uma caixa CBOR ou JSON com rótulo.
c2pa.actions.v2lista o que aconteceu com o arquivo (desde a 2.1, “agora é obrigatório umc2pa.createdou umc2pa.opened”), com umdigitalSourceTypedo vocabulário IPTC que distingue uma captura de câmera de um conteúdo criado com IA generativa.c2pa.hash.dataé o vínculo rígido (hard binding): um hash sobre os bytes do arquivo, excluindo o intervalo em que o próprio manifesto está armazenado. Podem vir em seguida uma miniatura, ingredientes (os arquivos a partir dos quais este foi feito) e metadados. - A claim: “uma estrutura de dados assinada digitalmente e que evidencia adulterações, que referencia um conjunto de asserções”, por URI com hash; por isso, alterar qualquer asserção quebra a claim.
- A assinatura da claim: um
COSE_Sign1sobre a claim, com a cadeia de certificados do signatário no cabeçalho protegido. “Deve aparecer exatamente uma instância de uma credencial de identidade”, e, desde a versão 2.0, essa credencial é um certificado X.509. - Um carimbo do tempo opcional, de uma autoridade RFC 3161, sobre a assinatura.
Tudo isso é serializado como caixas JUMBF dentro do arquivo (um segmento APP11 no JPEG, uma caixa no MP4 ou no HEIF) ou armazenado remotamente e referenciado por URL. Quando um arquivo é editado, o editor adiciona um novo manifesto e registra o anterior como ingrediente; assim, o conjunto de manifestos (manifest store) vira uma cadeia de manifestos.
Os algoritmos de assinatura permitidos são ES256, ES384, ES512, PS256, PS384, PS512 e Ed25519 (§13.2.1); as chaves precisam corresponder ao algoritmo, e chaves RSA precisam ter pelo menos 2048 bits. Para vídeo ao vivo, a especificação recomenda algoritmos determinísticos, para que segmentos idênticos continuem armazenáveis em cache.
Assinatura de um JPEG
O laboratório cria uma CA de teste com o pyca/cryptography 49.0.0 e um certificado de signatário P-256 com os EKUs que um validador C2PA procura: c2pa-kp-claimSigning (1.3.6.1.4.1.62558.2.1) e, para validadores mais antigos, id-kp-emailProtection. Depois, assina um JPEG gerado de 640×360 com um carimbo do tempo da 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)
O perfil do certificado importa: o certificado do signatário tem digitalSignature em Key Usage, CA:FALSE e o EKU de assinatura de claims; a raiz é uma CA com keyCertSign. Um certificado sem um EKU adequado, ou um certificado folha autoassinado, não passa na validação, mesmo que a assinatura esteja matematicamente correta. O artigo Campos do certificado X.509 explica essas extensões.
Validação: Valid, Trusted, Invalid
O mesmo script lê o arquivo de volta quatro vezes: sem nenhuma configuração de confiança, com a raiz de teste como âncora de confiança, com um byte dos dados da imagem alterado e após a decodificação e uma nova codificação da imagem com o Pillow 12.0.0, como faria um pipeline que recebe imagens enviadas:
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
Como interpretar:
- Valid, mas não confiável. Todas as verificações criptográficas passaram (
claimSignature.validated, trêsassertion.hashedURI.matchpara as três asserções,assertion.dataHash.matchpara os bytes da imagem), mas veiosigningCredential.untrusted: qualquer pessoa pode criar uma CA e assinar. A especificação define esse estado com precisão: um manifesto é Valid quando está bem formado e não foi modificado, sua assinatura é validada “dentro da validade” e a credencial não foi revogada; ele só é Trusted quando, além disso, a credencial de assinatura “recebe o código de sucessosigningCredential.trusted” (§14.3). - Trusted assim que o validador conhece a raiz. Em produção, essa raiz vem da C2PA Trust List, “uma lista, mantida pelo C2PA, de âncoras de confiança de certificados X.509 que emitem certificados para signatários de hardware e software”, que os validadores devem incluir para o EKU
c2pa-kp-claimSigning; os usuários podem adicionar as próprias âncoras. - Invalid depois de um byte alterado: a assinatura continua correta, o vínculo rígido não (
assertion.dataHash.mismatch). Um recorte, um filtro ou uma nova compressão têm o mesmo efeito; por isso, os editores precisam gravar um novo manifesto em vez de manter o antigo. - Desaparece depois da recodificação. O manifesto fica nos metadados; um pipeline que decodifica os pixels e grava um novo arquivo o descarta, assim como o EXIF. Nada na saída indica que um dia houve uma credencial ali.
O manifesto acrescentou 39 KB a uma imagem de 13 KB: além da claim e da nossa cadeia de certificados de 981 bytes, ele traz uma miniatura gerada pelo c2pa-rs e o token de carimbo do tempo com os certificados da DigiCert. O timeStamp.untrusted aparece porque o laboratório não configurou a C2PA TSA Trust List: o carimbo do tempo é criptograficamente válido; a autoridade dele só não estava na lista do validador.
Carimbos do tempo e vida útil do certificado
A especificação é direta: “manifestos sem carimbos do tempo deixam de ser válidos quando a credencial de assinatura expira ou é revogada”, e, quando há um carimbo do tempo e ele é confiável, os validadores “devem usar o horário atestado, e não o horário atual” para verificar os certificados (§10.3.2.5.1, §15.8.2). É a mesma construção do Authenticode: o certificado só precisa ser válido no momento da assinatura, e a autoridade de carimbo do tempo prova quando foi esse momento. Dispositivos que assinam offline precisam de outra solução; a câmera do Pixel, do Google, por exemplo, usa “carimbos do tempo confiáveis no próprio dispositivo” para que as fotos “sejam confiáveis depois que o certificado expirar, mesmo que tenham sido capturadas quando o dispositivo estava offline” (Google Security Blog).
De onde vem a confiança
A identidade do signatário é o titular do certificado; portanto, o valor de uma credencial depende de quem emitiu esse certificado e do que essa entidade verificou. O C2PA mantém um programa de conformidade e as listas de confiança; os validadores as configuram por EKU e podem adicionar âncoras com datas notBefore e notAfter. O hardware também importa: o Google informa que o app de câmera do Pixel 10 “alcançou o nível de garantia 2 (Assurance Level 2), a classificação de segurança mais alta definida atualmente pelo programa de conformidade do C2PA”, com chaves de assinatura de claims “geradas e armazenadas com o Android StrongBox no chip de segurança Titan M2” e certificados emitidos só depois que o atestado de chave do Android (Android Key Attestation) comprova que o dispositivo e o app são genuínos. Chaves em hardware seguro são tratadas em chaves protegidas por hardware.
Só as asserções assinadas são declarações do signatário. A especificação dá um exemplo: “o signatário não é responsável pela exatidão de uma localização GPS que aparece nos metadados EXIF, mesmo que esses metadados estejam cobertos pelo vínculo rígido” (§9.2.6); só os metadados colocados em uma asserção c2pa.metadata são atribuídos ao signatário.
O que as credenciais de conteúdo não fazem
- Elas não provam que uma imagem é verdadeira. Uma credencial de câmera diz que este dispositivo produziu estes bytes; uma cena montada ou a foto de uma tela continua sendo uma captura de câmera. Uma credencial de IA generativa é uma declaração da ferramenta que decidiu adicioná-la.
- Elas são fáceis de remover. Nossa recodificação descartou o manifesto sem nenhum erro, e muitas vezes as pessoas removem metadados de propósito, por privacidade ou para reduzir o tamanho. A resposta da especificação é o vínculo flexível (soft binding): uma impressão digital ou uma marca d’água invisível que permite encontrar o manifesto de novo em um repositório. Uma “credencial de conteúdo durável” (Durable Content Credential) é aquela “para a qual existem um ou mais vínculos flexíveis que permitem descobri-la em um repositório de manifestos” (§2.4.1). Se uma plataforma armazena e busca manifestos dessa forma é uma decisão da própria plataforma.
- A ausência não significa nada. A maioria das imagens na web não tem credenciais; a falta de um manifesto não é evidência de manipulação.
- Elas podem expor mais do que o pretendido. Cadeias de ingredientes, nomes de dispositivos e localizações viajam com o arquivo. Os criadores podem remover asserções (redaction) quando um arquivo se torna ingrediente (§6.8), e convém que as ferramentas deixem as pessoas escolherem o que é incluído.
Se você desenvolve com C2PA
- Preserve ou assine de novo no seu pipeline. Se seu serviço redimensiona ou comprime arquivos enviados, guarde o original com a credencial dele ou crie um novo manifesto com o original como ingrediente e uma ação honesta
c2pa.editedouc2pa.resized, assinado com seu próprio certificado. - Mostre os estados de validação com fidelidade. Diferencie Trusted de apenas Valid, mostre o nome do signatário tirado do certificado e não mostre nada especial quando não houver manifesto.
- Valide com as listas de confiança do C2PA e com software atualizado. Códigos de status como
signingCredential.untrustedoutimeStamp.untrustedcostumam indicar um problema de configuração, não uma falsificação. - Acerte o certificado: EKU
c2pa-kp-claimSigning, chave em um HSM ou em hardware seguro e carimbos do tempo em todas as assinaturas. - Não tome decisões com base na ausência. Uma credencial ausente ou removida é o caso normal.
Fontes
- C2PA Technical Specification 2.4: §2.3.15 e §2.3.16 (listas de confiança), §2.4.1 (credenciais duráveis), §6.8 (remoção de asserções), §9.2.6 (metadados que não são C2PA), §10.3.2.5.1 e §15.8.2 (carimbos do tempo), §13.2.1 (algoritmos), §14.3 (estados de validação), §14.4 (listas de confiança)
- Anúncio do C2PA 2.3
- Google Security Blog: credenciais de conteúdo C2PA no Pixel e no Android (10 de setembro de 2025)
- RFC 9052: assinatura e criptografia de objetos CBOR (COSE), RFC 3161
- Ferramentas: c2pa-python 0.38.1 sobre o c2pa-rs 0.91.3, pyca/cryptography 49.0.0, Pillow 12.0.0