Is It Really You?
A hackathon prototype for binding a German EUDI Wallet holder presentation to a JPEG.
This hackathon MVP binds a German EUDI Wallet Verifiable Presentation to a JPEG. This can be useful, for example, to commit to a screenshot of a WhatsApp conversation. In the case of a request for something, this would give the receiver another signal whether or not the request is genuine.
While this may sound a lot like chat forensics, the security claim actually is attested image provenance:
This exact image is cryptographically bound to a fresh German EUDI Wallet holder presentation for the disclosed identity attributes, and the binding verified.
IIRY does not prove that a WhatsApp account belongs to the wallet holder, that the conversation content is true, or that a bank-transfer request by itself is legitimate. It proves image binding, credential presentation verification, holder-binding freshness, and verifier policy checks.
| Surface | Role |
|---|---|
| iPhone app | Captures or imports an image, shows the human challenge text, runs the wallet flow, and exports a signed C2PA JPEG using the protective .iiry extension. |
| Shared Swift core | Implements nonce encoding, asset hashing, CAWG identity assertion encoding, JPEG/C2PA insertion, SD-JWT/OpenID4VP holder-binding checks, and validation. |
| macOS CLI | Signs and verifies C2PA JPEGs through the same IIRYCore implementation used by the app. |
| FastAPI service | Drives the OpenID4VP relying-party flow and returns wallet response material to the app. |
ios/IIRY.swiftpm: SwiftUI iPhone app and the sharedIIRYCorecode.cli/Sources/IIRYCLI: macOS CLI that uses the sameIIRYCorepackage as the iPhone app.service/iiry_service: FastAPI relying-party service for OpenID4VP / German EUDI Wallet callbacks.docs/cawg-eudi-extension.md: lean CAWG extension proposal for OpenID4VP holder-bound EUDI presentations.
Content Credentials are the user-facing provenance experience built around the technical standards from the Coalition for Content Provenance and Authenticity (C2PA) for digital assets. The Creator Assertions Working Group (CAWG) adds identity assertions that can bind a credentialed actor to C2PA assertions.
CAWG's draft VC+VP identity assertion currently builds on the W3C standard for W3C Verifiable Credentials / Presentations. The German EUDI Wallet flow used here, however, returns an OpenID4VP presentation. That is not the same object shape as the CAWG draft's W3C VC/VP binding.
IIRY therefore uses an extension signature type:
io.github.ndurner.iiry.cawg.openid4vp.holder-binding.v3
The C2PA JPEG carries a literal cawg.identity assertion. Its signer_payload.sig_type is the IIRY namespace above, signer_payload.referenced_assertions includes the actual c2pa.hash.data hard-binding assertion, and its custom signature byte string carries canonical OpenID4VP evidence for the holder-bound wallet presentation.
The extension keeps the CAWG idea of an identity assertion whose signer_payload is presented to the credential holder and references the C2PA hard-binding assertion, but the holder proof is validated through OpenID4VP semantics:
- Verify the C2PA manifest and hard binding for the JPEG.
- Read the
cawg.identityassertion with the IIRY OpenID4VP signature type. - Decode the OpenID4VP nonce payload.
- Verify that the nonce contains both a digest of the CAWG
signer_payloadand a fresh random nonce. - Verify the Wallet presentation holder binding over the same nonce.
- Verify issuer trust, disclosure integrity, audience, freshness, and policy.
The OpenID4VP nonce is originally a verifier transaction challenge. In holder-bound presentations it helps bind the proof to this verifier and this transaction, preventing presentation injection and replay. IIRY extends this to include the CAWG/C2PA binding: the Wallet signs a holder-binding proof over the OpenID4VP nonce, and that nonce contains the digest of the exact CAWG signer_payload embedded in cawg.identity.
This follows CAWG's signer-payload model closely, including the draft's signer_payload presentation and c2pa.hash.data interaction patterns so the hard-binding assertion can be known before the Wallet presentation is requested. The main deviation is that current IIRY uses OpenID4VP nonce for this commitment, not OpenID4VP transaction_data, and the CAWG signature byte string carries typed OpenID4VP evidence rather than a direct COSE signature over signer_payload.
The nonce passed in the OpenID4VP request is built like this:
base64url(deterministic-cbor([
"io.github.ndurner.iiry.openid4vp-nonce.v3",
random_256_bits,
sha256(deterministic-cbor(cawg_identity.signer_payload))
]))
The human challenge text, for example:
Is it really you?
(2026-06-04 ABCD1234)
is intentionally not part of this nonce payload. It must be visible in the image itself and checked by the receiver. If the challenge is not captured in the image, IIRY can still prove that a wallet presentation was bound to those image bytes, but it cannot prove that the image answered the parent's latest challenge.
The app exports a signed C2PA JPEG with a protective .iiry extension:
IIRY-Commitment-YYYY-MM-DD-ABCD1234.jpg.c2pa.cawg.iiry
Commitment is deliberate: the file contains a cryptographic commitment and evidence, not a blanket confirmation that the content is true. The .jpg segment keeps the ordinary image nature visible, .c2pa.cawg makes the technical choices visible during a no-slides demo, and the final .iiry extension discourages messenger pipelines from treating the file as an ordinary JPEG and stripping C2PA metadata. Detached JSON .iiry proof carriers are not an interchange format.
The shared Swift core now implements a constrained IIRY JPEG/C2PA profile. It writes and reads JPEG APP11 C2PA/JUMBF segments, creates a c2pa.hash.data hard-binding assertion with exclusion ranges, embeds a CBOR cawg.identity assertion for the IIRY OpenID4VP signature type, writes a CBOR c2pa.claim.v2, and signs that claim as a detached COSE_Sign1 ES256 C2PA claim signature. The iPhone app and CLI use this same implementation.
For hackathon development the Swift core uses the same sample ES256 certificate/key material bundled with c2patool / c2pa-rs. That lets local reference tooling validate the C2PA mechanics, but it does not establish production signing-credential trust and must not be presented as a trusted Content Credential signer.
The Swift verifier used by both the iPhone app and CLI also verifies the embedded SD-JWT/OpenID4VP presentation: PID issuer JWT signature, disclosure hashes, holder key-binding signature, nonce, verifier audience, sd_hash, and issuer-chain trust against the German EUDIW sandbox PID provider trust list. For certificate-based relying-party deployments, the acceptable verifier audience is derived from the untracked RP access certificate at service/secrets/access-certificate.pem as x509_hash:base64url(sha256(access-certificate DER)). This follows the mock trust-list setup from the German EUDI Wallet Developer Guide / Blueprint and has only been tested with the German EUDIW Sandbox. It is not production PID issuer trust.
The CLI supports two verification modes for C2PA-bearing JPEGs:
iiry verify <image.jpg> --ownverifies the Swift IIRY profile.iiry verify <image.jpg> --c2patoolasks localc2patool -dwhether the asset satisfies the reference C2PA verifier.iiry verify <image.jpg> --bothruns both and exposes disagreement.iiry verify <image.jpg> --own --service-base-url <url>verifies the wallet key-bindingaudagainstredirect_uri:<url>.iiry verify <image.jpg> --own --audience <aud>verifies the wallet key-bindingaudagainst an explicit verifier identifier such as anx509_hash:...client ID.iiry verify <image.jpg> --own --access-cert <cert.pem>derives the acceptablex509_hash:...audience from a local RP access certificate.iiry verify <image.jpg> --c2patool --trust-c2pa-samplerepeats the reference check while explicitly trusting the C2PA ES256 sample root anchor for local development only.
For IIRY-generated JPEGs, the expected default c2patool development result is that assertion.dataHash.match, assertion.hashedURI.match, and claimSignature.validated pass, while signingCredential.untrusted fails. With --trust-c2pa-sample, the C2PA layer should report validation_state: Trusted; this is still sample trust, not production trust. Separately, generic C2PA tooling can see the cawg.identity assertion but will not understand IIRY's OpenID4VP holder-binding semantics until that extension is standardized or explicitly supported.
The web service drives the OpenID4VP relying-party flow, verifies the wallet response during issuance, and returns the Wallet response material to the app. Received .iiry files are verified again in Swift.
The iOS Xcode project runs scripts/generate-verifier-policy.py before compiling IIRYCore. If service/secrets/access-certificate.pem exists locally, the script writes the corresponding non-secret x509_hash:... into ios/IIRY.swiftpm/Sources/IIRYCore/VerifierPolicy.generated.swift so the app can verify certificate-based wallet audiences without querying the service for policy. The access certificate itself remains ignored by Git. Regenerate the Swift policy after RP access certificate rotation.
IIRY is a prototype implementation built on public standards and reference ecosystems:
- The C2PA/JUMBF/JPEG manifest work is implemented in Swift for this repository, guided by the Coalition for Content Provenance and Authenticity (C2PA) technical specification and checked for interoperability against
c2patool. - The CAWG identity model and the proposed OpenID4VP extension build on the Creator Assertions Working Group draft VC+VP identity assertion.
- The development-only ES256 certificate and private key embedded in
IIRYCoreare the sample testing material from thec2patool/c2pa-rsecosystem. They are included only so the hackathon prototype can exercise C2PA signing mechanics and reference-tool verification; they must not be treated as production signing credentials. - The relying-party service follows the OpenID4VP and German EUDI Wallet sandbox flow. Production deployments need real relying-party key material, certificates, trust policy, and operational review.
- The iOS app uses Apple platform frameworks including SwiftUI, PhotosUI, Uniform Type Identifiers, UIKit sharing, and CryptoKit. The service uses the Python FastAPI ecosystem listed in
service/requirements.txt.
The service can persist decrypted wallet presentation results for local CLI testing, but only when explicitly enabled:
IIRY_SERIALIZE_PRESENTATIONS=1By default it stores sessions only and does not write serialized VP artifacts for reuse.
Build and test the shared Swift core and macOS CLI:
swift test
swift run iiry --helpRun the service:
python3 -m venv .venv
. .venv/bin/activate
pip install -r service/requirements.txt
uvicorn iiry_service.app:app --app-dir service --reload --port 8110The service expects RP key and German EUDI sandbox certificate material through environment variables or service/secrets/, matching the structure described in service/iiry_service/app.py.
