Get Started

Learn how to use our command-line interface to sign and verify media files on the server-side

To begin, download and unzip the CLI somewhere on your system, navigate to the directory in your terminal, and try executing it:

./truepic

👨‍💻

Did macOS pop up a warning dialog?

If you're running the CLI for the first time on macOS, you may need to follow this quick 3-step process to grant an exception for it to run, as we're not (yet) registered with Apple as an identified developer.

💻

Missing the .exe on Windows?

Whenever you see truepic, simply replace it with truepic.exe in the CLI commands (don't rename the file itself). Even though we use the former throughout our documentation, the CLI works just as well on Windows!

If everything is working, you will see the help output with explanations of the various commands and options:

$ ./truepic
Cryptographically sign and verify history/origin details in media files that complies with the C2PA open standard

Usage: truepic <COMMAND>

Commands:
  enroll    Create a key and request a certificate to use for signing
  sign      Cryptographically sign history/origin details into a media file
  verify    Verify the cryptographically signed history/origin details in a media file
  manifest  Extract the manifest store from a media file
  help      Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

Now that we've confirmed that the CLI runs on your system, let's walk through the two-step process of enrollment and signing.

Enroll a New Key with a Key Provider

❗️

Security Review

Before proceeding with file system or inline enrollment, please review our security considerations.

The first command we're going to use is enroll. The most important detail is where you want the keys and their certificates to be stored. For demo testing, we're going to store them in the file system. It's the simplest to get started with, as it doesn't depend on any outside service, but for production deployment, the CLI supports a number of "key providers" (as we call them) that you can choose from, including AWS and Azure. Check out our Key Providers guide for details.

Each key provider is organized as a sub-command of enroll. This makes it easy to see exactly what options are required and available for a specific key provider. Since we're going to use the file system, we can see these options by passing file-system as the sub-command, along with the --help option:

./truepic enroll file-system --help

As mentioned, the file system key provider is the simplest to get started with, only requiring an API key to enroll:

./truepic enroll file-system --api-key <API_KEY>

When you execute this command, a lot happens under the hood: a new key is created with the key provider – since we're using the file system key provider, it stores it in the file system of your machine – the public portion of the key is sent to us, and a certificate for the key is requested from our certificate authority (CA). The private key is never sent to us – ever – and, depending on your chosen key provider, may never even be seen by the CLI.

And with that, we're now ready to sign!

❗️

Action required: manual certificate re-enrollment

The CLI does not automatically renew certificates upon expiry. To avoid disruptions, please track the expiry dates of all certificates and plan re-enrollment accordingly. By default, certificates are valid for one year.

🔐

Need to manage multiple keys and certificates?

The enroll command – as well as the sign command, as we'll soon see – has a --profile option that gives the key and certificate a name. Since we didn't specify the option in the example above, it was given the name default. If you need to manage more than one key and certificate, however, you can run the enroll command again (and again), but this time with the --profile option to give it a unique name. Then you simply specify the --profilewhen signing to use the specific key and certificate with that name.

Sign a Media File

☝️

Tip

To sign files with C2PA 2.0, include the--target-spec-version "2.0" option in your command.

Signing media files with the sign command is the main attraction here, so we're going to walk through a number of examples that build on each other, starting with the simplest.

Sign for the First Time

There are a lot of details that can go into the C2PA manifest that's signed into a file. To start, however, let's not worry about any of that, and rely on the CLI to automatically add the bare minimum: a hash of the file and a thumbnail (if it's an image):

./truepic sign file_to_sign.jpg --output signed_file.jpg

To explain the two file here: file_to_sign.jpg is the input file to sign, and signed_file.jpg is where the signed version will be output as a new file. Both of these files are (or will be) in the same directory as the CLI, but they can be paths anywhere on the file system, such as ~/Documents/file_to_sign.jpg or /path/to/file_to_sign.jpg. The CLI just needs permission to read/write to them.

If all goes well, you should see output similar to the following:

$ ./truepic sign file_to_sign.jpg --output signed_file.jpg
Signing: /path/to/file_to_sign.jpg
Creating output file: /path/to/signed_file.jpg
Creating C2PA manifest and signing into file
Signed!

Congrats on signing your first media file! Now, let's build on that to add some of your own details.

Sign with Assertions

If you're new to C2PA, an assertion is a statement about the media file, typically added when it's created or modified by a piece of software. These statements can be displayed back to the viewer to help them understand more about what they are seeing. In Add Assertions, we review the schema and supported assertion types.

You can add your own assertions in one of two ways: the --assertions option accepts a path to a JSON file, or the --assertions-inline option accepts a string of JSON. The JSON itself is the same either way, it's just the delivery method that differs.

We're going to use --assertions, as it's a bit more manageable than properly formatting a string of JSON on the command line. Go ahead and create an assertions.json file in the same directory as the CLI. Here's a Creative Work assertion – identifying the author as a person named "Joe Demo" – that you can copy and paste as the contents. This assertion was supported in C2PA 1.3 and below, but is deprecated in 1.4 and above.

{
  "assertions": [
    {
      "label": "stds.schema-org.CreativeWork",
      "data": {
        "@context": "https://schema.org",
        "@type": "CreativeWork",
        "author": [
          {
            "@type": "Person",
            "name": "Joe Demo"
          }
        ]
      }
    }
  ]
}

To add this assertion, we'll use the same command we used previously, but with an additional --assertions option that points to the file we just created. We'll also split the command onto multiple lines with \ to make it more readable:

./truepic sign file_to_sign.jpg \
  --output signed_file.jpg \
  --assertions assertions.json

Again, if all goes well, you should see the same "Signed!" output as before.

Sign with Ingredients

The final signing example we’ll look at adds one or more ingredients to the file we’re signing using the --ingredient option. Again, if you're new to C2PA, an ingredient is a media file that the file we're signing has been created or derived from. You can learn more about that in Understand C2PA.

In order to use an ingredient, it's required to add a c2pa.actions.v2 assertion detailing how the ingredient was used or modified in the media file that you're signing.

For example, say you've taken an original photo and resized it for the web. The resized photo would be the file you're signing, with the original photo being the single ingredient.

We'll first add a c2pa.resized action to the previous assertions.json file that describes how it was resized:

{
  "assertions": [
    {
      "label": "stds.schema-org.CreativeWork",
      "data": {
        "@context": "https://schema.org",
        "@type": "CreativeWork",
        "author": [
          {
            "@type": "Person",
            "name": "Joe Demo"
          }
        ]
      }
    },
    {
      "label": "c2pa.actions.v2",
      "data": {
        "actions": [
          {
            "action": "c2pa.resized",
            "when": "2023-05-11T20:38:00Z",
            "softwareAgent": {
              "name": "sharp",
              "version": "0.32.1"
            },
            "description": "Resized the image width to 960",
            "parameters": {
              "width": "960"
            }
          }
        ]
      }
    }
  ]
}

Then, building on the previous command, we'll add an --ingredient option that points to the path of the original photo:

./truepic sign file_to_sign.jpg \
  --output signed_file.jpg \
  --assertions assertions.json \
  --ingredient original_file.jpg

As a more complex example, say you've created a composite image of many other images. The composite image would be the file you're signing, with all of the images that went into the composite being ingredients. The --ingredient option is simply repeated with the path to each one:

./truepic sign file_to_sign.jpg \
  --output signed_file.jpg \
  --assertions assertions.json \
  --ingredient ingredient_file.jpg \
  --ingredient another_ingredient_file.jpg \
  --ingredient one_more_ingredient_file.jpg

One final time, if all goes well, you should see the same "Signed!" output as before.

Verify a Media File

The final command we'll look at is verify, because what kind of CLI would this be if you couldn't also validate a C2PA signed file?

We've signed a number of times up to this point, so you should have a signed_file.jpg file in the same directory as the CLI. Simply pass it (or any other path) as a single argument to the verify command:

./truepic verify signed_file.jpg

The output should be JSON that looks similar to this:

{
  "manifest_store": [
    {
      "URI": "com.truepic:urn:uuid:27e41225-ccba-4b66-aac1-a54b0107d4cc",
      "assertions": {
        "com.truepic.libc2pa": [
          {
            "status": "VALID",
            "instance_index": 0,
            "data": {
              "lib_name": "Truepic C2PA C++ Library",
              "lib_version": "3.8.12",
              "target_spec_version": "1.4",
              "git_hash": "v3.8.12"
            }
          }
        ],
        "c2pa.thumbnail.claim.jpeg": [
          {
            "status": "VALID",
            "instance_index": 0,
            "thumbnail_id": "ba3eb086479f7793dc1b80f8e2aee3d5fb41d9f64e249ec203280e3fdae8df1e"
          }
        ],
        "c2pa.hash.data": [
          {
            "status": "VALID",
            "instance_index": 0
          }
        ]
      },
      "certificate": {
        "subject_name": "Your Common Name (TRUEPIC SIGN DEMO)",
        "organization_name": "Your Org (TRUEPIC SIGN DEMO)",
        "organization_unit_name": "Your Org Unit (TRUEPIC SIGN DEMO)",
        "issuer_name": "WebClaimSigningCA",
        "valid_not_before": "2023-05-19T15:28:41Z",
        "valid_not_after": "2025-05-18T15:28:40Z",
        "cert_der": "MIIDYzCCAkugAwIBAgIUGFEXBQ85FWHbrgyJUBYQDt0CYzYwDQYJKoZIhvcNAQEMBQAwSjEaMBgGA1UEAwwRV2ViQ2xhaW1TaWduaW5nQ0ExDTALBgNVBAsMBExlbnMxEDAOBgNVBAoMB1RydWVwaWMxCzAJBgNVBAYTAlVTMB4XDTIzMDUxOTE1Mjg0MVoXDTI1MDUxODE1Mjg0MFowgY8xCzAJBgNVBAYTAlVTMSUwIwYDVQQKDBxZb3VyIE9yZyAoVFJVRVBJQyBTSUdOIERFTU8pMSowKAYDVQQLDCFZb3VyIE9yZyBVbml0IChUUlVFUElDIFNJR04gREVNTykxLTArBgNVBAMMJFlvdXIgQ29tbW9uIE5hbWUgKFRSVUVQSUMgU0lHTiBERU1PKTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJutR+CkUhBdotClWrlPVom9zXhZDnK4fQjEcXZ6NpRWyBDrG+Zr3YgKSciuoqQv6Oj/xJWCSygBmvTXEfPKqi+jgcUwgcIwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRaH2tm05TnsEGDfZwMe13Fc0tLszBNBggrBgEFBQcBAQRBMD8wPQYIKwYBBQUHMAGGMWh0dHA6Ly92YS50cnVlcGljLmNvbS9lamJjYS9wdWJsaWN3ZWIvc3RhdHVzL29jc3AwEwYDVR0lBAwwCgYIKwYBBQUHAwQwHQYDVR0OBBYEFBWkGBm55y05716FAJFKCSMdQkVEMA4GA1UdDwEB/wQEAwIFoDANBgkqhkiG9w0BAQwFAAOCAQEAOwqlq7SpkmoKPF/9hqsQsEBiRzMI1HwKuG5vJKv6WdBQUy+DhHNgV47fxhERLCxZlu0VDnrzNeptjw6Ap7Nh4lG2a4/O70pEXnPXvKK6FLp5gqBhLhy1pEER0Dwr5+5Cd4CxdHTQMxtmfk7qLu4iHKKe9Wf90Zz4+IY645HJoebXE0QF6mjJJwq+DimPVmmWHpKQQNZWmRDlUiDXYaBGS9UqdGhn6g78GSaVO8Qd8QG+677EtfMP71GgLv2q4OhqpCJPnC3hxsUirWIzRGioHhdP8rO7gN+/EvdQRVMx1fpbrPK3tHOd3HOKX5qImal/1qFLB5Uhw868pXmzvca2ag==",
        "status": "VALID"
      },
      "certificate_chain": [
        {
          "cert_der": "MIIEejCCAmKgAwIBAgIUafyQxMyJUII6Hqhf0oL/KNX9k5AwDQYJKoZIhvcNAQEMBQAwPzEPMA0GA1UEAwwGUm9vdENBMQ0wCwYDVQQLDARMZW5zMRAwDgYDVQQKDAdUcnVlcGljMQswCQYDVQQGEwJVUzAeFw0yMTEyMDkyMDM5NDZaFw0yNjEyMDgyMDM5NDVaMEoxGjAYBgNVBAMMEVdlYkNsYWltU2lnbmluZ0NBMQ0wCwYDVQQLDARMZW5zMRAwDgYDVQQKDAdUcnVlcGljMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMEWEsOnUMGYzM5r+I6k8cVq+nKWiNgFM/uK7ILyZYDnQZyaxOFgFccE6Chr9cfa3gqKIvrFp6P2Cij+B2I7CssJeWV5DlialDyWLy9i1RZYzIqol8pIkAJZ6wg2568vpT37f5Pvd7G+6Ho4+BQeRBdQaOH5Z6kXSfW/Tcr79ryBoZ9kSOFYCHpcq3pB+4aGOgGh7qZy3iCi3cKoUTWdjJercnQy+RObm/q7Wf3U2FdMyK3voXEfhWwf59gd8L0q5DRmiL6ZE7B9sd9hbc2+btbz3+gzF1Mq/wN1lqOa2+cWKpEdGMdLtgMRVNbzmcZxi5O+cFK5EuXGhX1oGMECsm8CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRYuvGp8g3nRQYKsCmnWpcw6ic9CzAdBgNVHQ4EFgQUWh9rZtOU57BBg32cDHtdxXNLS7MwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQB1OIZ6FxFC8Fd8BrC7d907jYXKacXkQVozjCF6hnF/Re2LfFPQqucxuHM/d1NhoGGfpk6F6vPwyD3bjOeQVxWwX3yRNmOTqWhW6UXHTzsnFIqckmsBXYIrB0fL0QRWP6vUQxsuNBbq0lPQog0K5Y2XF0QOGbv/2WGGBsJ7TVtafw5xWV841f924Y7fnSkzQGLqJaPaJhVVyeV8UDChP0qhuN2Rekt8C6gkyNQr4pXTlgLMqgLVD7XGwrL3wkAAILPiyz7R1snJrUKLYV2svkPn96tQB6GOu4Ltk29B6myonIwHHPQflsQl4V28xw2lrALtuZOtaSr47Cs2OGs/wn6IiW0cEFCed8smoUe05BvZOEq+S4O2PSKy3QQ/UoWib7QQia87XqXoOXT8Bi5vI8Ul+5IzqxezpmAQEXPfvT6LtSDtOS6odwROQsS8Fra4LUEiVJyeHkzAXJoSf1XdhKKcQJhoiuVp/+Syu5uTuf9KS3VdcyzuRMpmwWEncexQqSPTIVE2gY2rVo+meAkb3VXydFMz+RXnMKdJE0y5qCOysar+pdTfytTFN7c8idg+s67OSW9MbMlIe+vzUY7fjNfTfADQaZgypZQxlpjBJuchCR0a57dacDbg1SkSn6TCb4rFbeO7CSn/gop4Va5hiSq7e+mf/VD/nlxEYrbdgifp0Q=="
        }
      ],
      "claim": {
        "claim_generator": "Truepic_Lens_CLI/0.7.1 libc2pa/3.8.12",
        "claim_generator_info": [
          {
            "name": "Truepic Lens CLI",
            "version": "0.7.1"
          }
        ],
        "dc:format": "image/jpeg",
        "instanceID": "45adda1e-fd6b-4b67-825e-2e0480e8bb9c",
        "dc:title": "test_signed.jpg"
      },
      "is_active": true,
      "signature": {
        "signed_by": "Your Org (TRUEPIC SIGN DEMO)",
        "status": "VALID",
        "signed_on": "2024-03-12T20:51:01Z"
      },
      "trusted_timestamp": {
        "TSA": "Truepic Lens Time-Stamping Authority",
        "status": "VALID",
        "timestamp": "2024-03-12T20:51:01Z"
      },
      "validation_statuses": [
        {
          "code": "assertion.hashedURI.match",
          "URI": "self#jumbf=/c2pa/com.truepic:urn:uuid:27e41225-ccba-4b66-aac1-a54b0107d4cc/c2pa.assertions/com.truepic.libc2pa",
          "success": true
        },
        {
          "code": "assertion.hashedURI.match",
          "URI": "self#jumbf=/c2pa/com.truepic:urn:uuid:27e41225-ccba-4b66-aac1-a54b0107d4cc/c2pa.assertions/c2pa.thumbnail.claim.jpeg",
          "success": true
        },
        {
          "code": "assertion.hashedURI.match",
          "URI": "self#jumbf=/c2pa/com.truepic:urn:uuid:27e41225-ccba-4b66-aac1-a54b0107d4cc/c2pa.assertions/c2pa.hash.data",
          "success": true
        },
        {
          "code": "timeStamp.trusted",
          "URI": "self#jumbf=/c2pa/com.truepic:urn:uuid:27e41225-ccba-4b66-aac1-a54b0107d4cc/c2pa.signature",
          "success": true
        },
        {
          "code": "signingCredential.trusted",
          "URI": "self#jumbf=/c2pa/com.truepic:urn:uuid:27e41225-ccba-4b66-aac1-a54b0107d4cc/c2pa.signature",
          "success": true
        },
        {
          "code": "claimSignature.validated",
          "URI": "self#jumbf=/c2pa/com.truepic:urn:uuid:27e41225-ccba-4b66-aac1-a54b0107d4cc/c2pa.signature",
          "success": true
        },
        {
          "code": "signingCredential.notRevoked",
          "URI": "self#jumbf=/c2pa/com.truepic:urn:uuid:27e41225-ccba-4b66-aac1-a54b0107d4cc/c2pa.signature",
          "success": true
        },
        {
          "code": "assertion.dataHash.match",
          "URI": "self#jumbf=/c2pa/com.truepic:urn:uuid:27e41225-ccba-4b66-aac1-a54b0107d4cc/c2pa.assertions/c2pa.hash.data",
          "success": true
        }
      ],
      "ai": {
        "contains_ai": false,
        "is_ai_generated": false
      }
    }
  ]
}

If the file is not C2PA signed, an error will be output instead:

Error: "Failed to validate file: Error: No manifest present in jpeg"

To learn more about what each of the validation response objects mean, see Understand C2PA and Validate C2PA.

Next Steps

That concludes our quick tour of the command-line interface! You've learned the basics of signing and verifying, and are ready to begin integrating the CLI into your own infrastructure and workflows. And remember, if you need any help at all as you dive in further, we're happy to assist!