Brendan recently did a short demo for a Project Origin workshop showing how the Content Authenticity Initiative’s open source toolkits for C2PA work with IPTC Photo Metadata.
It shows a practical way that developers and power users can start working with the C2PA Specification straight away.
Here we document the steps we took to create the demo, along with a few notes and suggestions for improvements. It includes several “manual” steps involving editing things with text editors.
Overall, it shows that it is possible to comply with the C2PA spec, but there is a long way to go before we can make it easy for users.
Step 1: Create an image with embedded IPTC Photo Metadata.
I used Adobe Photoshop but many other tools can be used - see IPTC’s list of software that supports IPTC Photo Metadata Standard.
2. Use exiftool to export the embedded metadata in XMP format
Using a Terminal window on my Mac, I ran the following command:
exiftool -xmp -b greenwich_flowers_with_metadata.jpg >greenwich_flowers_xmp.xml
Here is a description of what the extra arguments do:
-xmp
: Export the XMP metadata from the image file-b
: Show binary data. The XMP block includes byte-order markers which show up as binary data in exiftool output - you can see the<feff>
characters at the top of the output shown below.>greenwich_flowers_xmp.xml
: Send the output to a new file called “greenwich_flowers_xmp.xml”
We have just created an XML file that looks like this (actually the real version is all on one long line, so I have inserted line breaks here to make it slightly more readable):
<?xpacket begin="<feff>" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 7.2-c000 79.566ebc5b4, 2022/05/09-08:25:55 "> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/" xmlns:aux="http://ns.adobe.com/exif/1.0/aux/" xmlns:exifEX="http://cipa.jp/exif/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/" xmlns:xmpRights="http://ns.adobe.com/xap/1.0/rights/" xmlns:Iptc4xmpExt="http://iptc.org/std/Iptc4xmpExt/2008-02-29/" xmlns:plus="http://ns.useplus.org/ldf/xmp/1.0/" xmp:CreateDate="2022-07-09T17:47:17" xmp:CreatorTool="15.5" xmp:ModifyDate="2022-07-21T15:46:32+03:00" xmp:MetadataDate="2022-07-21T15:46:32+03:00" photoshop:DateCreated="2022-07-09T17:47:17.538" photoshop:LegacyIPTCDigest="74680B31492930D266D0FAE3B244D1BD" photoshop:ColorMode="3" photoshop:ICCProfile="Display P3" photoshop:AuthorsPosition="Managing Director" photoshop:Headline="Wildflowers at the Rose Garden, Greenwich Park, London" photoshop:City="London" photoshop:State="London" photoshop:Country="United Kingdom" photoshop:CaptionWriter="Brendan Quinn" ... and much more ... <?xpacket end="w"?>
If you’ve never seen an XMP packet before, this is probably quite confusing, but don’t worry - it just shows all the IPTC metadata properties in the XMP format that’s specified by ISO ISO 16884.
3. Convert the RDF/XML to JSON-LD
XMP’s standard data storage format is RDF/XML, which is a representation of the RDF data model in the XML format. To add these fields to a C2PA manifest, we need to convert the RDF/XML to another serialisation of RDF called JSON-LD.
There is a standard for expressing XMP in JSON-LD (ISO16884-3: JSON-LD serialisation of XMP) but exiftool doesn’t export in that format directly, so we have to do some conversion ourselves.
We can use a tool called riot
from the Apache Jena project to convert the RDF/XML to JSON-LD. If you don’t have it installed, download Jena or install it with Homebrew on a Mac (brew install jena
).
But riot
doesn't like the XML processing directives in the XMP packet. So first, we have to remove the XMP headers by hand. (Side note: if anyone knows of a good way of removing the processing headers using the command-line, please let me know!)
Open the XML file in a text editor and remove the <?xpacket ... ?>
sections at the start and end of the file. If you have it, you can also use xmllint
to make the xml look a bit nicer:
xmllint --format greenwich_flowers_xmp.xml >greenwich_flowers_xmp_pretty.xml
Then you can run riot
on the RDF/XML to create JSON-LD:
riot --syntax rdfxml --output jsonld greenwich_flowers_xmp_pretty.xml >greenwich_flowers_xmp.jsonld
riot converts between various syntaxes of RDF. This command converts the original RDF/XML format to JSON-LD, a JSON-friendly version of RDF.
Unfortunately that’s not the end of our story in preparing metadata for embedding into a C2PA assertion: riot
does a straight conversion to JSON-LD, including the structures such as rdf:Bag, rdf:Seq and various “blank nodes” used to handle multi-valued properties, but XMP's use of JSON-LD is much simpler. So we need to do some more conversion by hand.
The output at first looks like this:
{ "@graph": [ { "@id": "_:b0", "rdf:_1": "Brendan Quinn", "@type": "rdf:Seq" }, { "@id": "_:b1", "plus:ImageSupplierName": "IPTC" }, { "@id": "_:b2", "stEvt:changed": "/", "stEvt:softwareAgent": "Adobe Photoshop 23.4 (Macintosh)", "stEvt:when": "2022-07-21T15:46:32+03:00", "stEvt:instanceID": "xmp.iid:ef558a6e-e419-4aae-8890-f6b2b2bed77c", "stEvt:action": "saved" }, { "@id": "_:b3", "stEvt:changed": "/", "stEvt:softwareAgent": "Adobe Photoshop 23.4 (Macintosh)", "stEvt:when": "2022-07-21T15:46:32+03:00", "stEvt:instanceID": "xmp.iid:f9c65ac7-2ad9-4016-84b3-97065eb121b4", "stEvt:action": "saved" }, { "@id": "_:b4", "rdf:_1": { "@language": "x-default", "@value": "Wildflowers at the Rose Garden, Greenwich Park, London" }, "@type": "rdf:Alt" }, ... much more ...
All those _:b0,l _:b1, _:b2 objects are parts of the XMP packet that can actually be converted into simple arrays and objects in JSON. But right now there is no tool that can do this conversion, so I did it by hand! It took a while… It looks like it would be handy to create a Python and/or JavaScript tool to do it for us.
Ideally exiftool
would have an option that outputs XMP in ISO16884-3-compliant JSON-LD, but until it does, we need to do the work manually.
After a lot of manual reworking effort, here’s what I ended up with:
{ "@context": { "xmpRights": "http://ns.adobe.com/xap/1.0/rights/", "aux": "http://ns.adobe.com/exif/1.0/aux/", "exifEX": "http://cipa.jp/exif/1.0/", "stEvt": "http://ns.adobe.com/xap/1.0/sType/ResourceEvent#", "Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/", "photoshop": "http://ns.adobe.com/photoshop/1.0/", "plus": "http://ns.useplus.org/ldf/xmp/1.0/", "Iptc4xmpCore": "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/", "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "xmpMM": "http://ns.adobe.com/xap/1.0/mm/", "xmp": "http://ns.adobe.com/xap/1.0/", "dc": "http://purl.org/dc/elements/1.1/" }, "@graph": { "@id": "file:///Users/brendan/dev/iptc/c2pa/c2patool-play/greenwich_flowers_xmp_pretty.xml", "dc:description": "Wildflowers at the Rose Garden, Greenwich Park, London", "exifEX:LensModel": "iPhone 11 back dual wide camera 4.25mm f/1.8", "xmp:MetadataDate": "2022-07-21T15:46:32+03:00", "aux:LensInfo": "807365/524263 17/4 9/5 12/5", "exifEX:LensMake": "Apple", "xmp:rights/UsageTerms": "Usage permitted under Creative Commons Attribution (CC-BY 4.0) licence.", "xmp:ModifyDate": "2022-07-21T15:46:32+03:00", "photoshop:CaptionWriter": "Brendan Quinn", "xmp:mm/OriginalDocumentID": "4B267F2683ABA382F9E0C1DE10BF4210", "photoshop:ColorMode": "3", "photoshop:AuthorsPosition": "Managing Director", "plus:Licensor": { "plus:LicensorEmail": "office@iptc.org", "plus:LicensorURL": "www.iptc.org/", "plus:LicensorTelephoneType2": "http://ns.useplus.org/ldf/vocab/work", "plus:LicensorName": "Brendan Quinn" }, "dc:subject": [ "Rose Garden", "Greenwich", "flowers", "Wild flowers" ], "dc:creator": "Brendan Quinn", "Iptc4xmpCore:CountryCode": "GB", "Iptc4xmpCore:IntellectualGenre": "https://cv.iptc.org/newscodes/genre/Actuality", "Iptc4xmpExt:LocationShown": { "Iptc4xmpExt:WorldRegion": "Europe", "Iptc4xmpExt:CountryCode": "GB", "Iptc4xmpExt:CountryName": "United Kingdom", "Iptc4xmpExt:ProvinceState": "London", "Iptc4xmpExt:City": "London", "Iptc4xmpExt:Sublocation": "Greenwich" }, "photoshop:City": "London", "photoshop:DateCreated": "2022-07-09T17:47:17.538", "photoshop:ICCProfile": "Display P3", "plus:ModelReleaseStatus": "http://ns.useplus.org/ldf/vocab/MR-NAP", "photoshop:LegacyIPTCDigest": "74680B31492930D266D0FAE3B244D1BD", "photoshop:State": "London", "xmp:CreateDate": "2022-07-09T17:47:17", "dc:format": "image/jpeg", "plus:CopyrightOwner": { "plus:CopyrightOwnerName": "Brendan Quinn" }, "plus:ImageCreator": { "plus:ImageCreatorName": "Brendan Quinn" }, "xmp:CreatorTool": "15.5", "plus:ImageSupplier": { "plus:ImageSupplierName": "IPTC" }, "Iptc4xmpCore:Scene": "https://cv.iptc.org/newscodes/scene/011600", "Iptc4xmpCore:Location": "Greenwich", "Iptc4xmpCore:CreatorContactInfo": { "Iptc4xmpCore:CiAdrCtry": "United Kingdom", "Iptc4xmpCore:CiTelWork": "+44 (0)20 3178 4922 ", "Iptc4xmpCore:CiUrlWork": "https://www.iptc.org/", "Iptc4xmpCore:CiEmailWork": "mdirector@iptc.org", "Iptc4xmpCore:CiAdrPcode": "WC2A 1AL", "Iptc4xmpCore:CiAdrRegion": "London", "Iptc4xmpCore:CiAdrCity": "London", "Iptc4xmpCore:CiAdrExtadr": "25 Southampton Buildings" }, "Iptc4xmpExt:LocationCreated": { "Iptc4xmpExt:WorldRegion": "Europe", "Iptc4xmpExt:CountryCode": "GB", "Iptc4xmpExt:CountryName": "United Kingdom", "Iptc4xmpExt:ProvinceState": "London", "Iptc4xmpExt:City": "London", "Iptc4xmpExt:Sublocation": "Greenwich" }, "plus:PropertyReleaseStatus": "http://ns.useplus.org/ldf/vocab/PR-NAP", "xmp:mm/InstanceID": "xmp.iid:ef558a6e-e419-4aae-8890-f6b2b2bed77c", "photoshop:Headline": "Wildflowers at the Rose Garden, Greenwich Park, London", "photoshop:Country": "United Kingdom", "aux:Lens": "iPhone 11 back dual wide camera 4.25mm f/1.8", "dc:rights": "Copyright (C) Brendan Quinn 2022. Some rights reserved.", "Iptc4xmpExt:DigitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture" } }
4. Add the JSON-LD to a c2patool assertion block
Now we can finally start using c2patool
!
If you don’t yet have c2patool installed, and you’re on a Mac, I recommend installing the Homebrew version:
brew tap contentauth/tools
brew install c2patool
You can try running c2patool on the image file just to see what happens. As we only have IPTC embedded metadata but no C2PA claim included, it should show “No claim found”:
c2patool greenwich_flowers_with_metadata.jpg No claim found
Now we can follow the “manifest definition format” specification in the c2patool docs to turn our IPTC metadata structure into an assertion:
{ "alg": "es256", "private_key": "es256_private.key", "sign_cert": "es256_certs.pem", "ta_url": "http://timestamp.digicert.com", "vendor": "IPTC", "claim_generator": "BQ Test with c2patool/0.1", "ingredients": [], "assertions": [ { "label": "stds.iptc.photo-metadata", "data": { ... insert the XMP metadata packet in JSON-LD format here ... } } ] }
Save that as, say, test_assertion.json. Then we are told that we can run c2patool using this file as a parameter. So let’s try it:
c2patool test_assertion.json -p greenwich_flowers_with_metadata.jpg \ -o greenwich_flowers_with_c2pa.jpg
But we get the following response from c2patool:
----------- Claim creation requires key files "/Users/brendan/.cai/temp_key.pem" and "/Users/brendan/.cai/temp_key.pub" You can generate a throwaway RSAPSS SSH private key for testing by pasting the following line into a terminal and hitting enter mkdir -p ~/.x509 ; openssl req -new -newkey rsa:4096 -sigopt rsa_padding_mode:pss -days 180 -extensions v3_ca -addext "keyUsage = digitalSignature" -addext "extendedKeyUsage = emailProtection" -nodes -x509 -keyout ~/.x509/temp_key.pem -out ~/.x509/temp_key.pub -sha256 ; sudo chmod 644 ~/.x509/temp_key.pem You should only need to do this once. Set the environment var CAI_SIGNING_ALGORITHM=ps256 to set the signature algorithm The environment variable CAI_KEY_PATH can specify an alternate key folder. -----------
Note that this message contradicts itself: it requires a folder called ~/.cai
, NOT ~/.x509
as the example command says. Also the filenames for the key and certificate don’t match those given in the assertion example JSON, so I changed them in .
To make this work, on my MacOS system at least, I had to install openssl version 3 using homebrew:
brew install openssl@3
And then explicitly use the openssl 3 binary when running the command (see below).
So what I had to do (after installing openssl 3) was the following:
mkdir -p ~/.cai /opt/homebrew/opt/openssl@3/bin/openssl req -new -newkey rsa:4096 \ -sigopt rsa_padding_mode:pss -days 180 -extensions v3_ca \ -addext "keyUsage = digitalSignature" \ -addext "extendedKeyUsage = emailProtection" -nodes -x509 \ -keyout ~/.cai/temp_key.pem -out ~/.cai/temp_key.pub -sha256 sudo chmod 644 ~/.cai/temp_key.pem
When I ran this command, it asked me some questions about what information I wanted to put into the certificate:
...+.....+.+.........+..+.........+.+........+....+..+................+.................+...+.......+............+.....+....+......+......+...............+..+......+.+.........+.....+....+...............+..+....+......+...+...+..+.+.....+.......+........+......+.+...+.....+................+...+...+.................+.+..+.......+......+..+..........+.....+...+.+...+...+..+............+......+....+.....+......+...+.......+...+...........+...+.......+.........+..+...+.+........+.+..+.........+.........+......+....+.....+.+.....+......+...+.+......+............+...+..+...+....+.....+...+...+....+...+........+....+...+.....+......+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*...+......+............+...+..+...+....+..+...+......+....+..+...............+.+...+..+....+...+.....+...+...+.....................+.+.........+...........+...+....+...+.....+...+.+...+...+........+......+.............+.....+.+..+...+....+.....+......+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.+......+.......+..+.......+........+......+....+.....+......+..........+..+...............+....+...............+...+......+.....+..............................+.......+..+.......+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*...+......+.+......+.....+............+...............+.+..+...+.+...+.....+......+..........+......+........+..........+.....+......+............+...+......+....+..+.......+..+....+.....+.+...+............+...+..+.+.........+......+........+.+........+......+......+.+............+...+.........+...+..+....+..+.............+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*................+...+......................+......+.........+......+.....+..........+........+......+.............+.....+............+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:GB State or Province Name (full name) [Some-State]: Locality Name (eg, city) []:London Organization Name (eg, company) [Internet Widgits Pty Ltd]:IPTC Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []:iptc.org Email Address []:office@iptc.org
It then returned with no information, but my ~/.cai
folder now contained files called es256_certs.pem
and es256_private.key
as I had requested in the command.
Finally I could now run my c2patool command:
c2patool test_assertion.json -p greenwich_flowers_with_metadata.jpg \ -o greenwich_flowers_with_c2pa.jpg