├── docs ├── .gitkeep ├── nightly-builds │ ├── nightly-build-runs.png │ ├── nightly-build-summary.png │ └── README.md ├── project-contributions.md ├── x_509.md ├── release-notes.md ├── manifest.md └── usage.md ├── sample ├── C.jpg ├── image.jpg ├── store.cfg ├── es256_private.key ├── test.json ├── es256_certs.pem ├── trust_anchors.pem ├── ps256.pem ├── allowed_list.pem └── ps256.pub ├── tests ├── fixtures │ ├── C.jpg │ ├── verify.jpeg │ ├── libpng-test.png │ ├── earth_apollo17.jpg │ ├── trust │ │ ├── store.cfg │ │ ├── no-match.pem │ │ └── anchors.pem │ ├── ingredient │ │ └── ingredient.json │ ├── do_not_train.json │ ├── ingredient_test.json │ └── sample1.svg └── integration.rs ├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── repo-closed.yml ├── rustfmt.toml ├── README.md ├── LICENSE-MIT ├── SECURITY.md ├── deny.toml ├── Cargo.toml ├── Makefile ├── schemas ├── ingredient.json └── manifest-definition.json ├── CODE_OF_CONDUCT.md ├── src ├── info.rs ├── tree.rs ├── signer.rs ├── callback_signer.rs └── main.rs ├── CONTRIBUTING.md ├── CHANGELOG.md └── LICENSE-APACHE /docs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/C.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentauth/c2patool/HEAD/sample/C.jpg -------------------------------------------------------------------------------- /sample/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentauth/c2patool/HEAD/sample/image.jpg -------------------------------------------------------------------------------- /tests/fixtures/C.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentauth/c2patool/HEAD/tests/fixtures/C.jpg -------------------------------------------------------------------------------- /tests/fixtures/verify.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentauth/c2patool/HEAD/tests/fixtures/verify.jpeg -------------------------------------------------------------------------------- /tests/fixtures/libpng-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentauth/c2patool/HEAD/tests/fixtures/libpng-test.png -------------------------------------------------------------------------------- /tests/fixtures/earth_apollo17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentauth/c2patool/HEAD/tests/fixtures/earth_apollo17.jpg -------------------------------------------------------------------------------- /docs/nightly-builds/nightly-build-runs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentauth/c2patool/HEAD/docs/nightly-builds/nightly-build-runs.png -------------------------------------------------------------------------------- /docs/nightly-builds/nightly-build-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentauth/c2patool/HEAD/docs/nightly-builds/nightly-build-summary.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | 3 | **/*.rs.bk 4 | 5 | .DS_Store 6 | .idea 7 | 8 | .x509 9 | .ec 10 | .rsa 11 | .ed 12 | .es509 13 | 14 | .vscode 15 | -------------------------------------------------------------------------------- /sample/store.cfg: -------------------------------------------------------------------------------- 1 | 2 | //id-kp-emailProtection 3 | 1.3.6.1.5.5.7.3.4 4 | //id-kp-documentSigning 5 | 1.3.6.1.5.5.7.3.36 6 | //id-kp-timeStamping 7 | 1.3.6.1.5.5.7.3.8 8 | //id-kp-OCSPSigning 9 | 1.3.6.1.5.5.7.3.9 -------------------------------------------------------------------------------- /tests/fixtures/trust/store.cfg: -------------------------------------------------------------------------------- 1 | //id-kp-emailProtection 2 | 1.3.6.1.5.5.7.3.4 3 | //id-kp-documentSigning 4 | 1.3.6.1.5.5.7.3.36 5 | //id-kp-timeStamping 6 | 1.3.6.1.5.5.7.3.8 7 | //id-kp-OCSPSigning 8 | 1.3.6.1.5.5.7.3.9 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | PLEASE DO NOT FILE PULL REQUESTS IN THIS REPO. 2 | 3 | As of 10 December 2024, all work on c2patool is hosted in the `cli` folder of the `contentauth/c2pa-rs` repo. Please file your pull request in that repo instead. 4 | -------------------------------------------------------------------------------- /sample/es256_private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgfNJBsaRLSeHizv0m 3 | GL+gcn78QmtfLSm+n+qG9veC2W2hRANCAAQPaL6RkAkYkKU4+IryBSYxJM3h77sF 4 | iMrbvbI8fG7w2Bbl9otNG/cch3DAw5rGAPV7NWkyl3QGuV/wt0MrAPDo 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/fixtures/ingredient/ingredient.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "test ingredient", 3 | "format": "image/jpeg", 4 | "instance_id": "xmp:iid:a60dfda7-0606-42db-a4f0-398ab4fb79cf", 5 | "thumbnail": { 6 | "format": "image/png", 7 | "identifier": "../libpng-test.png" 8 | }, 9 | "relationship": "componentOf" 10 | } -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # IMPORTANT: PR validation uses nightly version of 2 | # rustfmt. 3 | 4 | # Invoke by using: 5 | # 6 | # rustup toolchain add nightly 7 | # cargo +nightly fmt 8 | 9 | blank_lines_upper_bound = 1 10 | edition = "2018" 11 | format_code_in_doc_comments = true 12 | group_imports = "StdExternalCrate" 13 | hex_literal_case = "Lower" 14 | imports_granularity = "Crate" 15 | reorder_impl_items = true 16 | unstable_features = true 17 | -------------------------------------------------------------------------------- /.github/workflows/repo-closed.yml: -------------------------------------------------------------------------------- 1 | name: Repo CLOSED 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - synchronize 9 | - labeled 10 | push: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | tests: 16 | name: c2patool moved 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Please file your PR in contentauth/c2pa-rs 21 | run: | 22 | echo "This repo is no longer in use. Please file your PR in contentauth/c2pa-rs instead." 23 | exit 1 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # c2patool HAS MOVED 2 | 3 | `c2patool` is a command line tool for working with C2PA [manifests](https://c2pa.org/specifications/specifications/1.3/specs/C2PA_Specification.html#_manifests) and media assets (audio, image or video files). 4 | 5 | As of 10 December 2024, we've transitioned work on the c2patool project to the [contentauth/c2pa-rs](https://github.com/contentauth/c2pa-rs/blob/main/cli) repo. 6 | 7 | We have moved all existing open PRs and issues to the c2pa-rs repo. If you have _new_ issues or PRs, please file them in the c2pa-rs repo. 8 | -------------------------------------------------------------------------------- /tests/fixtures/do_not_train.json: -------------------------------------------------------------------------------- 1 | { 2 | "ta_url": "http://timestamp.digicert.com", 3 | 4 | "claim_generator": "TestApp/0.0.1", 5 | "claim_generator_info": [{ 6 | "name": "Test App", 7 | "version": "0.0.1" 8 | }], 9 | "assertions": [ 10 | { 11 | "label": "c2pa.training-mining", 12 | "data": { 13 | "entries": { 14 | "c2pa.ai_generative_training": { "use": "notAllowed" }, 15 | "c2pa.ai_inference": { "use": "notAllowed" }, 16 | "c2pa.ai_training": { "use": "notAllowed" }, 17 | "c2pa.data_mining": { "use": "notAllowed" } 18 | } 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/fixtures/trust/no-match.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICSDCCAfqgAwIBAgIUb+aBTX1CsjJ1iuMJ9kRudz/7qEcwBQYDK2VwMIGMMQsw 3 | CQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCVNvbWV3aGVyZTEnMCUG 4 | A1UECgweQzJQQSBUZXN0IEludGVybWVkaWF0ZSBSb290IENBMRkwFwYDVQQLDBBG 5 | T1IgVEVTVElOR19PTkxZMRgwFgYDVQQDDA9JbnRlcm1lZGlhdGUgQ0EwHhcNMjIw 6 | NjEwMTg0NjQxWhcNMzAwODI2MTg0NjQxWjCBgDELMAkGA1UEBhMCVVMxCzAJBgNV 7 | BAgMAkNBMRIwEAYDVQQHDAlTb21ld2hlcmUxHzAdBgNVBAoMFkMyUEEgVGVzdCBT 8 | aWduaW5nIENlcnQxGTAXBgNVBAsMEEZPUiBURVNUSU5HX09OTFkxFDASBgNVBAMM 9 | C0MyUEEgU2lnbmVyMCowBQYDK2VwAyEAMp5+0e83nNgQhdhBW8Rshkjy90sa1A9J 10 | IzkItcDqCuKjeDB2MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH 11 | AwQwDgYDVR0PAQH/BAQDAgbAMB0GA1UdDgQWBBTuLrYRqW4wu6yjIK1/iW8ud7dm 12 | kTAfBgNVHSMEGDAWgBRXTAfC/JxQvRlk/bCbdPMDbsSfqTAFBgMrZXADQQB2R6vb 13 | I+X8CTRC54j3NTvsUj454G1/bdzbiHVgl3n+ShOAJ85FJigE7Eoav7SeXeVnNjc8 14 | QZ1UrJGwgBBEP84G 15 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /docs/project-contributions.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The information in this page is primarily for those who wish to contribute to the c2patool project itself, rather than those who simply wish to use it as a tool. For general contribution guidelines, see [CONTRIBUTING.md](../CONTRIBUTING.md). 4 | 5 | ## Building from source 6 | 7 | To build the project from source, enter these commands: 8 | 9 | ```shell 10 | cargo install c2patool 11 | ``` 12 | 13 | To build the tool on a Windows machine, you need to install the [7zip](https://www.7-zip.org/) tool. 14 | 15 | NOTE: If you encounter errors installing, you may need to update your Rust installation by entering this command: 16 | 17 | ``` 18 | rustup update 19 | ``` 20 | 21 | ## Nightly builds 22 | 23 | Interim binaries are generated every day around 05:30 UTC (overnight for our US-based team) and are available for roughly two weeks thereafter. These can be helpful for testing purposes. For more information, see the documentation on [nightly builds](https://github.com/contentauth/c2patool/tree/main/docs/nightly-builds/README.md). 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | © Copyright 2020 Adobe. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sample/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "alg": "ps256", 3 | "private_key": "ps256.pem", 4 | "sign_cert": "ps256.pub", 5 | "ta_url": "http://timestamp.digicert.com", 6 | "claim_generator": "TestApp", 7 | "title": "My Title", 8 | "assertions": [ 9 | { 10 | "label": "stds.schema-org.CreativeWork", 11 | "data": { 12 | "@context": "https://schema.org", 13 | "@type": "CreativeWork", 14 | "author": [ 15 | { 16 | "@type": "Person", 17 | "name": "Joe Bloggs" 18 | } 19 | ] 20 | } 21 | }, 22 | { 23 | "label": "c2pa.actions", 24 | "data": { 25 | "actions": [ 26 | { 27 | "action": "c2pa.opened" 28 | } 29 | ], 30 | "metadata": { 31 | "reviewRatings": [ 32 | { 33 | "code": "c2pa.unknown", 34 | "explanation": "Something untracked happened", 35 | "value": 4 36 | } 37 | ] 38 | } 39 | } 40 | }, 41 | { 42 | "label": "my.assertion", 43 | "data": { 44 | "any_tag": "whatever I want" 45 | } 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | This C2PA open-source library is maintained in partnership with Adobe. At this time, Adobe is taking point on accepting security reports through its HackerOne portal and public bug bounty program. 4 | 5 | ## Reporting a vulnerability 6 | 7 | Please do not create a public GitHub issue for any suspected security vulnerabilities. Instead, please file an issue through [Adobe's HackerOne page](https://hackerone.com/adobe?type=team). If for some reason this is not possible, reach out to cai-security@adobe.com. 8 | 9 | 10 | ## Vulnerability SLAs 11 | 12 | Once we receive an actionable vulnerability (meaning there is an available patch, or a code fix is required), we will acknowledge the vulnerability within 24 hours. Our target SLAs for resolution are: 13 | 14 | 1. 72 hours for vulnerabilities with a CVSS score of 9.0-10.0 15 | 2. 2 weeks for vulnerabilities with a CVSS score of 7.0-8.9 16 | 17 | Any vulnerability with a score below 6.9 will be resolved when possible. 18 | 19 | 20 | ## C2PA Vulnerabilities 21 | 22 | This library is not meant to address any potential vulnerabilities within the C2PA specification itself. It is only an implementation of the spec as written. Any suspected vulnerabilities within the spec can be reported [here](https://github.com/c2pa-org/specifications/issues). 23 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # Configuration used for dependency checking with cargo-deny. 2 | # 3 | # For further details on all configuration options see: 4 | # https://embarkstudios.github.io/cargo-deny/checks/cfg.html 5 | 6 | [graph] 7 | targets = [ 8 | { triple = "x86_64-unknown-linux-gnu" }, 9 | { triple = "x86_64-apple-darwin" }, 10 | { triple = "x86_64-pc-windows-msvc" }, 11 | { triple = "aarch64-apple-darwin" }, 12 | { triple = "wasm32-unknown-unknown" }, 13 | ] 14 | 15 | # Deny all advisories unless explicitly ignored. 16 | [advisories] 17 | yanked = "deny" 18 | ignore = [ 19 | "RUSTSEC-2021-0127", # serde_cbor 20 | "RUSTSEC-2023-0071", # rsa Marvin Attack: (https://jira.corp.adobe.com/browse/CAI-5104) 21 | "RUSTSEC-2024-0384", # instant (https://github.com/contentauth/c2pa-rs/issues/663) 22 | "RUSTSEC-2024-0399", # tokio-rustls server: https://rustsec.org/advisories/RUSTSEC-2024-0399 23 | ] 24 | # Deny multiple versions unless explicitly skipped. 25 | [bans] 26 | multiple-versions = "allow" # "deny" # TODO: Re-enable when possible. 27 | wildcards = "allow" 28 | 29 | # List of allowed licenses. 30 | [licenses] 31 | allow = [ 32 | "Apache-2.0", 33 | "BSD-2-Clause", 34 | "BSD-3-Clause", 35 | "CC0-1.0", 36 | "ISC", 37 | "LicenseRef-ring", 38 | "MIT", 39 | "MPL-2.0", 40 | "Unicode-3.0", 41 | "Unicode-DFS-2016", 42 | "Zlib", 43 | ] 44 | confidence-threshold = 0.8 45 | 46 | [[licenses.clarify]] 47 | name = "ring" 48 | expression = "LicenseRef-ring" 49 | license-files = [ 50 | { path = "LICENSE", hash = 3171872035 } 51 | ] 52 | 53 | [sources] 54 | unknown-registry = "deny" 55 | unknown-git = "deny" 56 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 57 | allow-git = [] 58 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "c2patool" 3 | default-run = "c2patool" 4 | 5 | # Please do not manually edit `version`. Version updates will be generated 6 | # automatically when c2patool is published. Remember to use (MINOR) or (MAJOR) 7 | # tags in the PR title to trigger non-patch updates as needed. 8 | version = "0.9.12" 9 | 10 | description = "Tool for displaying and creating C2PA manifests." 11 | authors = [ 12 | "Gavin Peacock ", 13 | "Maurice Fisher ", 14 | ] 15 | license = "MIT OR Apache-2.0" 16 | documentation = "https://opensource.contentauthenticity.org/docs/c2patool" 17 | readme = "README.md" 18 | keywords = ["c2pa", "xmp", "metadata"] 19 | edition = "2018" 20 | homepage = "https://contentauthenticity.org" 21 | repository = "https://github.com/contentauth/c2patool" 22 | 23 | [dependencies] 24 | anyhow = "1.0" 25 | atree = "0.5.2" 26 | c2pa = { version = "0.38.0", features = [ 27 | "fetch_remote_manifests", 28 | "file_io", 29 | "add_thumbnails", 30 | "pdf", 31 | "unstable_api", 32 | ] } 33 | clap = { version = "4.5.10", features = ["derive", "env"] } 34 | env_logger = "0.11.4" 35 | glob = "0.3.1" 36 | log = "0.4" 37 | serde = { version = "1.0", features = ["derive"] } 38 | serde_derive = "1.0" 39 | serde_json = "1.0" 40 | tempfile = "3.3" 41 | treeline = "0.1.0" 42 | pem = "3.0.3" 43 | openssl = { version = "0.10.61", features = ["vendored"] } 44 | reqwest = { version = "0.12.4", features = ["blocking"] } 45 | url = "2.5.0" 46 | 47 | [dev-dependencies] 48 | assert_cmd = "2.0.14" 49 | httpmock = "0.7.0" 50 | predicates = "3.1" 51 | mockall = "0.13.0" 52 | 53 | [package.metadata.binstall] 54 | # Use defaults 55 | 56 | [profile.release] 57 | strip = true # Automatically strip symbols from the binary. 58 | opt-level = 3 59 | lto = "thin" # Link time optimization. 60 | -------------------------------------------------------------------------------- /docs/x_509.md: -------------------------------------------------------------------------------- 1 | # Using an X.509 certificate 2 | 3 | The c2patool uses some custom properties in the manifest definition file for signing: 4 | 5 | - `private_key`: Path to the private key file. 6 | - `sign_cert`: Path to the signing certificate file. 7 | - `alg`: Algorithm to use, if not the default ES256. 8 | 9 | Both the private key and signing certificate must be in PEM (privacy-enhanced mail) format. The signing certificate must contain a PEM certificate chain starting with the end-entity certificate used to sign the claim ending with the intermediate certificate before the root CA certificate. 10 | 11 | If the manifest definition file doesn't include the `sign_cert` and `private_key` properties, c2patool uses a built-in certificate and private key. An example certifcate and private key file are also provided in the [c2patool repo sample folder](https://github.com/contentauth/c2patool/tree/main/sample). 12 | 13 | If you are using a signing algorithm other than the default `es256`, specify it in the manifest definition field `alg` with one of the following values: 14 | 15 | - `ps256` 16 | - `ps384` 17 | - `ps512` 18 | - `es256` 19 | - `es384` 20 | - `es512` 21 | - `ed25519` 22 | 23 | The specified algorithm must be compatible with the values of private key and signing certificate. For more information, see [Signing manfiests](https://opensource.contentauthenticity.org/docs/signing-manifests). 24 | 25 | Instead of specifying the values in manifest definition file properties, you can put the values of the key and cert chain in two environment variables: `C2PA_PRIVATE_KEY` for the private key and `C2PA_SIGN_CERT` for the public certificates. For example, to sign with ES256 signatures using the content of a private key file and certificate file: 26 | 27 | ```shell 28 | set C2PA_PRIVATE_KEY=$(cat my_es256_private_key) 29 | set C2PA_SIGN_CERT=$(cat my_es256_certs) 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /sample/es256_certs.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIChzCCAi6gAwIBAgIUcCTmJHYF8dZfG0d1UdT6/LXtkeYwCgYIKoZIzj0EAwIw 3 | gYwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU29tZXdoZXJl 4 | MScwJQYDVQQKDB5DMlBBIFRlc3QgSW50ZXJtZWRpYXRlIFJvb3QgQ0ExGTAXBgNV 5 | BAsMEEZPUiBURVNUSU5HX09OTFkxGDAWBgNVBAMMD0ludGVybWVkaWF0ZSBDQTAe 6 | Fw0yMjA2MTAxODQ2NDBaFw0zMDA4MjYxODQ2NDBaMIGAMQswCQYDVQQGEwJVUzEL 7 | MAkGA1UECAwCQ0ExEjAQBgNVBAcMCVNvbWV3aGVyZTEfMB0GA1UECgwWQzJQQSBU 8 | ZXN0IFNpZ25pbmcgQ2VydDEZMBcGA1UECwwQRk9SIFRFU1RJTkdfT05MWTEUMBIG 9 | A1UEAwwLQzJQQSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQPaL6R 10 | kAkYkKU4+IryBSYxJM3h77sFiMrbvbI8fG7w2Bbl9otNG/cch3DAw5rGAPV7NWky 11 | l3QGuV/wt0MrAPDoo3gwdjAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsG 12 | AQUFBwMEMA4GA1UdDwEB/wQEAwIGwDAdBgNVHQ4EFgQUFznP0y83joiNOCedQkxT 13 | tAMyNcowHwYDVR0jBBgwFoAUDnyNcma/osnlAJTvtW6A4rYOL2swCgYIKoZIzj0E 14 | AwIDRwAwRAIgOY/2szXjslg/MyJFZ2y7OH8giPYTsvS7UPRP9GI9NgICIDQPMKrE 15 | LQUJEtipZ0TqvI/4mieoyRCeIiQtyuS0LACz 16 | -----END CERTIFICATE----- 17 | -----BEGIN CERTIFICATE----- 18 | MIICajCCAg+gAwIBAgIUfXDXHH+6GtA2QEBX2IvJ2YnGMnUwCgYIKoZIzj0EAwIw 19 | dzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTb21ld2hlcmUx 20 | GjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBGT1IgVEVTVElO 21 | R19PTkxZMRAwDgYDVQQDDAdSb290IENBMB4XDTIyMDYxMDE4NDY0MFoXDTMwMDgy 22 | NzE4NDY0MFowgYwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJ 23 | U29tZXdoZXJlMScwJQYDVQQKDB5DMlBBIFRlc3QgSW50ZXJtZWRpYXRlIFJvb3Qg 24 | Q0ExGTAXBgNVBAsMEEZPUiBURVNUSU5HX09OTFkxGDAWBgNVBAMMD0ludGVybWVk 25 | aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHllI4O7a0EkpTYAWfPM 26 | D6Rnfk9iqhEmCQKMOR6J47Rvh2GGjUw4CS+aLT89ySukPTnzGsMQ4jK9d3V4Aq4Q 27 | LsOjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW 28 | BBQOfI1yZr+iyeUAlO+1boDitg4vazAfBgNVHSMEGDAWgBRembiG4Xgb2VcVWnUA 29 | UrYpDsuojDAKBggqhkjOPQQDAgNJADBGAiEAtdZ3+05CzFo90fWeZ4woeJcNQC4B 30 | 84Ill3YeZVvR8ZECIQDVRdha1xEDKuNTAManY0zthSosfXcvLnZui1A/y/DYeg== 31 | -----END CERTIFICATE----- 32 | 33 | -------------------------------------------------------------------------------- /docs/nightly-builds/README.md: -------------------------------------------------------------------------------- 1 | # About c2patool nightly builds 2 | 3 | Interim binaries are generated every day around 0530 UTC (i.e. overnight for our US-based team) and are available for roughly two weeks thereafter. These can be helpful for testing purposes. 4 | 5 | ## Finding nightly builds 6 | 7 | Start by clicking on the [Actions](https://github.com/contentauth/c2patool/actions) tab for the repo and then click on the ["Nightly build" entry on the left side](https://github.com/contentauth/c2patool/actions/workflows/nightly.yml). 8 | 9 | In this screen, you'll see a history of recent nightly builds, something like this: 10 | 11 | ![Nightly build runs](./nightly-build-runs.png) 12 | 13 | Click on the build description (for example, "Use v2 of rust-cache" in this list). That will lead to a summary screen similar to the following: 14 | 15 | ![Nightly build summary](./nightly-build-summary.png) 16 | 17 | In the **"Artifacts"** section at the bottom are archives containing the `c2patool` binary and relevant sample files. 18 | 19 | Note that each build contains a unique build number. If you file a bug against a nightly version, we would very much appreciate it if you would include that version ID: 20 | 21 | ``` 22 | $ c2patool --version 23 | c2patool 0.6.2-nightly+2023-08-29-24601f5 24 | ``` 25 | 26 | This build number contains the date of the nightly version and the commit ID (from `main` branch) that was built. 27 | 28 | ## Important notes about nightly builds 29 | 30 | * CAI team members may occasionally trigger "nightly" builds at other times of the day if there are particularly interesting changes that we need to test. 31 | * Nightly builds of `c2patool` are built with the latest available nightly build of `c2pa-rs`. Typically this is a snapshot of `c2pa-rs` taken at 0500 UTC (i.e. very shortly before the corresponding `c2patool` build). 32 | * This repo contains a `nightly` branch which contains the code for the latest build that was triggered (typically the Cargo.toml and Cargo.lock changes). This branch is force-pushed by the nightly build process; you should not rely on its contents and should not target this build for any pull requests. 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile to aid with local development and testing 2 | # This is not required for automated builds 3 | 4 | ifeq ($(OS),Windows_NT) 5 | PLATFORM := win 6 | else 7 | UNAME := $(shell uname) 8 | ifeq ($(UNAME),Linux) 9 | PLATFORM := linux 10 | endif 11 | ifeq ($(UNAME),Darwin) 12 | PLATFORM := mac 13 | endif 14 | endif 15 | 16 | check-format: 17 | cargo +nightly fmt -- --check 18 | 19 | clippy: 20 | cargo clippy --all-features --all-targets -- -D warnings 21 | 22 | test-local: 23 | cargo test --all-features 24 | 25 | # Full local validation, build and test all features including wasm 26 | # Run this before pushing a PR to pre-validate 27 | test: check-format clippy test-local 28 | 29 | fmt: 30 | cargo +nightly fmt 31 | 32 | # Creates a folder wtih c2patool bin, samples and readme 33 | c2patool-package: 34 | rm -rf target/c2patool* 35 | mkdir -p target/c2patool 36 | mkdir -p target/c2patool/sample 37 | cp target/release/c2patool target/c2patool/c2patool 38 | cp README.md target/c2patool/README.md 39 | cp sample/* target/c2patool/sample 40 | cp CHANGELOG.md target/c2patool/CHANGELOG.md 41 | 42 | # These are for building the c2patool release bin on various platforms 43 | build-release-win: 44 | cargo build --release 45 | 46 | build-release-mac-arm: 47 | rustup target add aarch64-apple-darwin 48 | MACOSX_DEPLOYMENT_TARGET=11.1 cargo build --target=aarch64-apple-darwin --release 49 | 50 | build-release-mac-x86: 51 | rustup target add x86_64-apple-darwin 52 | MACOSX_DEPLOYMENT_TARGET=10.15 cargo build --target=x86_64-apple-darwin --release 53 | 54 | build-release-mac-universal: build-release-mac-arm build-release-mac-x86 55 | lipo -create -output target/release/c2patool target/aarch64-apple-darwin/release/c2patool target/x86_64-apple-darwin/release/c2patool 56 | 57 | build-release-linux: 58 | cargo build --release 59 | 60 | # Builds and packages a zip for c2patool for each platform 61 | ifeq ($(PLATFORM), mac) 62 | release: build-release-mac-universal c2patool-package 63 | cd target && zip -r c2patool_mac_universal.zip c2patool && cd .. 64 | endif 65 | ifeq ($(PLATFORM), win) 66 | release: build-release-win c2patool-package 67 | cd target && 7z a -r c2patool_win_intel.zip c2patool && cd .. 68 | endif 69 | ifeq ($(PLATFORM), linux) 70 | release: build-release-linux c2patool-package 71 | cd target && tar -czvf c2patool_linux_intel.tar.gz c2patool && cd .. 72 | endif 73 | -------------------------------------------------------------------------------- /schemas/ingredient.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "http://ns.adobe.com/c2patool/ingredient-definition/v1", 4 | "type": "object", 5 | "description": "Definition format for an ingredient created with c2patool", 6 | "examples": [ 7 | { 8 | "title": "C.jpg", 9 | "format": "image/jpeg", 10 | "instance_id": "xmp:iid:01712cdc-26b8-4902-ad53-1cdbd5370b1b", 11 | "relationship": "componentOf", 12 | "thumbnail": { 13 | "format": "image/jpeg", 14 | "identifier": "thumb.jpg" 15 | }, 16 | "active_manifest": "manifest.c2pa" 17 | } 18 | ], 19 | "required": [ 20 | "title" 21 | ], 22 | "properties": { 23 | "title": { 24 | "type": "string", 25 | "description": "A human-readable string to be displayed as the title for this Ingredient (generally the file name of the asset)." 26 | }, 27 | "format": { 28 | "type": "string", 29 | "description": "The MIME content type of the associated ingredient" 30 | }, 31 | "instance_id": { 32 | "type": "string", 33 | "description": "A unique identifier for the ingredient instance. Often from `xmpMM:InstanceID` in XMP metadata" 34 | }, 35 | "document_id": { 36 | "type": "string", 37 | "description": "Optionally a copy from `xmpMM:DocumentID` in XMP metadata" 38 | }, 39 | "relationship": { 40 | "type": "string", 41 | "description": "Either `componentOf`(default) or `parentOf`" 42 | }, 43 | "provenance": { 44 | "type": "string", 45 | "description": "URI to the associated C2PA manifest. My echo `dcterms:provenance` in XMP metadata." 46 | }, 47 | "hash": { 48 | "type": "string", 49 | "description": "A hash value of the asset at import, used to deduplicate ingredients" 50 | }, 51 | "thumbnail": { 52 | "type": "object", 53 | "format": "ResourceReference", 54 | "description": "Identifies a thumbnail file with `format` holding MIME type, and `identifier` with a path to the file (relative to Ingredient definition)" 55 | }, 56 | "active_manifest": { 57 | "type": "string", 58 | "description": "Manifest label of for the most recently added manifest in the manifest store" 59 | }, 60 | "validation_status": { 61 | "type": "object", 62 | "format": "ValidationStatus", 63 | "description": "Validation status record for any issues related to this ingredient or its validation" 64 | }, 65 | "manifest_data": { 66 | "type": "object", 67 | "format": "ResourceRef", 68 | "description": "Identifies a c2pa file the extracted manifest store from an asset with `format` holding MIME type, and `identifier` with a path to the .c2pa file (relative to Ingredient definition)" 69 | } 70 | }, 71 | "additionalProperties": false 72 | } -------------------------------------------------------------------------------- /sample/trust_anchors.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICUzCCAfmgAwIBAgIUdmkq4byvgk2FSnddHqB2yjoD68gwCgYIKoZIzj0EAwIw 3 | dzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTb21ld2hlcmUx 4 | GjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBGT1IgVEVTVElO 5 | R19PTkxZMRAwDgYDVQQDDAdSb290IENBMB4XDTIyMDYxMDE4NDY0MFoXDTMyMDYw 6 | NzE4NDY0MFowdzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlT 7 | b21ld2hlcmUxGjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBG 8 | T1IgVEVTVElOR19PTkxZMRAwDgYDVQQDDAdSb290IENBMFkwEwYHKoZIzj0CAQYI 9 | KoZIzj0DAQcDQgAEre/KpcWwGEHt+mD4xso3xotRnRx2IEsMoYwVIKI7iEJrDEye 10 | PcvJuBywA0qiMw2yvAvGOzW/fqUTu1jABrFIk6NjMGEwHQYDVR0OBBYEFF6ZuIbh 11 | eBvZVxVadQBStikOy6iMMB8GA1UdIwQYMBaAFF6ZuIbheBvZVxVadQBStikOy6iM 12 | MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA0gA 13 | MEUCIHBC1xLwkCWSGhVXFlSnQBx9cGZivXzCbt8BuwRqPSUoAiEAteZQDk685yh9 14 | jgOTkp4H8oAmM1As+qlkRK2b+CHAQ3k= 15 | -----END CERTIFICATE----- 16 | -----BEGIN CERTIFICATE----- 17 | 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 52 | -----END CERTIFICATE----- 53 | 54 | 55 | -------------------------------------------------------------------------------- /sample/ps256.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 52 | G/dZR0Qj3j0T5F9GX719XpmzYbVFKIKPTsKF 53 | -----END PRIVATE KEY----- 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language. 18 | * Being respectful of differing viewpoints and experiences. 19 | * Gracefully accepting constructive criticism. 20 | * Focusing on what is best for the community. 21 | * Showing empathy towards other community members. 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances. 27 | * Trolling, insulting/derogatory comments, and personal or political attacks. 28 | * Public or private harassment. 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission. 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting. 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version]. 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /src/info.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Adobe. All rights reserved. 2 | // This file is licensed to you under the Apache License, 3 | // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 | // or the MIT license (http://opensource.org/licenses/MIT), 5 | // at your option. 6 | // Unless required by applicable law or agreed to in writing, 7 | // this software is distributed on an "AS IS" BASIS, WITHOUT 8 | // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 9 | // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 10 | // specific language governing permissions and limitations under 11 | // each license. 12 | 13 | use std::{io::Cursor, path::Path}; 14 | 15 | use anyhow::Result; 16 | use c2pa::{IngredientOptions, Reader}; 17 | 18 | /// display additional C2PA information about the asset (not json formatted) 19 | pub fn info(path: &Path) -> Result<()> { 20 | struct Options {} 21 | impl IngredientOptions for Options { 22 | fn thumbnail(&self, _path: &Path) -> Option<(String, Vec)> { 23 | None 24 | } 25 | } 26 | let ingredient = c2pa::Ingredient::from_file_with_options(path, &Options {})?; 27 | println!("Information for {}", ingredient.title()); 28 | let mut is_cloud_manifest = false; 29 | //println!("instanceID = {}", ingredient.instance_id()); 30 | if let Some(provenance) = ingredient.provenance() { 31 | is_cloud_manifest = !provenance.starts_with("self#jumbf="); 32 | if is_cloud_manifest { 33 | println!("Cloud URL = {provenance}"); 34 | } else { 35 | println!("Provenance URI = {provenance}"); 36 | } 37 | } 38 | 39 | let file_size = std::fs::metadata(path).unwrap().len(); 40 | if let Some(manifest_data) = ingredient.manifest_data() { 41 | if is_cloud_manifest { 42 | println!( 43 | "Remote manifest store size = {} (file size = {})", 44 | manifest_data.len(), 45 | file_size 46 | ); 47 | } else { 48 | println!( 49 | "Manifest store size = {} ({:.2}% of file size {})", 50 | manifest_data.len(), 51 | (manifest_data.len() as f64 / file_size as f64) * 100f64, 52 | file_size 53 | ); 54 | } 55 | if let Some(validation_status) = ingredient.validation_status() { 56 | println!("Validation issues:"); 57 | for status in validation_status { 58 | println!(" {}", status.code()); 59 | } 60 | } else { 61 | println!("Validated"); 62 | } 63 | let reader = Reader::from_stream("c2pa", Cursor::new(manifest_data.into_owned()))?; 64 | 65 | let manifests: Vec<_> = reader.iter_manifests().collect(); 66 | match manifests.len() { 67 | 0 => println!("No manifests"), 68 | 1 => println!("One manifest"), 69 | n => println!("{n} manifests"), 70 | } 71 | } else if is_cloud_manifest { 72 | println!("Unable to fetch cloud manifest. (file size = {file_size})"); 73 | } else { 74 | println!("No C2PA Manifests. (file size = {file_size})"); 75 | } 76 | Ok(()) 77 | } 78 | 79 | #[cfg(test)] 80 | pub mod tests { 81 | #![allow(clippy::expect_used)] 82 | 83 | use super::*; 84 | 85 | #[test] 86 | fn test_manifest_config() { 87 | const SOURCE_PATH: &str = "tests/fixtures/C.jpg"; 88 | 89 | info(&std::path::PathBuf::from(SOURCE_PATH)).expect("info"); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docs/release-notes.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | This page highlights noteworthy changes in each release. 4 | 5 | Refer to the [CHANGELOG](https://github.com/contentauth/c2patool/blob/main/CHANGELOG.md) for detailed Git changes. 6 | 7 | ## 0.9.x 8 | 9 | * Update c2pa-rs for RegionOfInterest support. ([#269](https://github.com/contentauth/c2patool/pull/269)) 10 | * Fix broken link that was causing os site workflow to fail ([#266](https://github.com/contentauth/c2patool/pull/266)) 11 | * Document fragment subcommand ([#236](https://github.com/contentauth/c2patool/pull/236)) 12 | * Initial fragment support ([#230](https://github.com/contentauth/c2patool/pull/230)) 13 | * Add warning about accessing a private key directly ([#218](https://github.com/contentauth/c2patool/pull/218)) 14 | * Update c2patool with c2pa-rs fixes ([#190](https://github.com/contentauth/c2patool/pull/190)): 15 | - Merkle trees with empty proofs 16 | - Support for missing metadata for Claims object 17 | - OCSP fixes 18 | * Document how to specify an icon ([#182](https://github.com/contentauth/c2patool/pull/182)) 19 | * Add better support for cargo-binstall ([#177](https://github.com/contentauth/c2patool/pull/177)) 20 | * Add HTTP source option for trust config ([#174](https://github.com/contentauth/c2patool/pull/174)) 21 | 22 | ## 0.8.x 23 | 24 | * fixed c2patool asset name ([#171](https://github.com/contentauth/c2patool/pull/171)) 25 | * Allow clients to sign with a process outside of c2patool ([#169](https://github.com/contentauth/c2patool/pull/169)) 26 | * Add trust and verification options to c2pa_tool ([#168](https://github.com/contentauth/c2patool/pull/168)) 27 | * Add version to c2patool artifact names ([#158](https://github.com/contentauth/c2patool/pull/158)) 28 | 29 | ## 0.7.x 30 | 31 | * Update to c2pa-rs v0.28.2 ([#153](https://github.com/contentauth/c2patool/pull/153)) 32 | * Fix windows release ([#132](https://github.com/contentauth/c2patool/pull/132)) 33 | * Use compress-archive instead of tar ([#130](https://github.com/contentauth/c2patool/pull/130)) 34 | 35 | ## 0.6.0 36 | 37 | * Validates 1.3 signatures but will not generate them. 38 | * Supports other 1.3 features such as actions v2 and ingredients v2. 39 | * Supports adding `claim_generator_info` to a manifest. 40 | * Icons for `claim_generator_info` can be added as resource references. 41 | * The SDK will create v2 actions or ingredients if required, but defaults to v1. 42 | 43 | ## 0.5.4 44 | 45 | NOTE: This release introduced a 1.3 required change in signature format that is not compatible with previous verify code. 46 | We want to give some time for developers to integrate 1.3 validation before using 1.3 signatures. 47 | Please avoid using 0.5.4 and update to 0.6.0 which can validate the new format but does not create it. 48 | 49 | ## 0.5.3 50 | 51 | * Fixes a bug where ingredient thumbnails were not generated. 52 | * You can now pass an `ingredient.json` file or folder using the command line `--parent` option. If a folder is passed as an ingredient, the tool will look for an ingredient.json fle in that folder. 53 | * Fixes `--parent` is no longer relative to the `--manifest` path 54 | 55 | ## 0.5.2 56 | 57 | * Removes manifest preview feature 58 | * Tests for similar extensions 59 | * Adds `.svg` support 60 | 61 | ## 0.5.1 62 | 63 | * Updates the sample certs which had expired 64 | * Updates to the README for 0.5.0 changes 65 | 66 | ## 0.5.0 67 | 68 | * Adds support for many new file formats, see [supported file formats](https://opensource.contentauthenticity.org/docs/c2patool/#supported-file-formats). 69 | * Manifests and Ingredients can read and write thumbnail and c2pa resource files. 70 | * Adds `-i/--ingredient` option to generate an ingredient report or folder. 71 | * Changes to Manifest Definition: 72 | * `ingredients` now requires JSON Ingredient definitions. 73 | * `ingredient_paths` accepts file paths, including JSON Ingredient definitions. 74 | * `base_path` no longer supported. File paths are relative to the containing JSON file. 75 | -------------------------------------------------------------------------------- /src/tree.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Adobe. All rights reserved. 2 | // This file is licensed to you under the Apache License, 3 | // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 | // or the MIT license (http://opensource.org/licenses/MIT), 5 | // at your option. 6 | // Unless required by applicable law or agreed to in writing, 7 | // this software is distributed on an "AS IS" BASIS, WITHOUT 8 | // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 9 | // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 10 | // specific language governing permissions and limitations under 11 | // each license. 12 | 13 | use std::path::Path; 14 | 15 | use atree::{Arena, Token}; 16 | use c2pa::{Reader, Result}; 17 | use treeline::Tree; 18 | 19 | fn populate_node( 20 | tree: &mut Arena, 21 | reader: &Reader, 22 | manifest_label: &str, 23 | current_token: &Token, 24 | name_only: bool, 25 | ) -> Result<()> { 26 | if let Some(manifest) = reader.get_manifest(manifest_label) { 27 | for assertion in manifest.assertions().iter() { 28 | let label = assertion.label_with_instance(); 29 | current_token.append(tree, format!("Assertion:{label}")); 30 | } 31 | 32 | for ingredient in manifest.ingredients().iter() { 33 | if let Some(label) = ingredient.active_manifest() { 34 | // create new node 35 | let data = if name_only { 36 | format!("{}_{}", ingredient.title(), label) 37 | } else { 38 | format!("Asset:{}, Manifest:{}", ingredient.title(), label) 39 | }; 40 | 41 | let new_token = current_token.append(tree, data); 42 | 43 | populate_node(tree, reader, label, &new_token, name_only)?; 44 | } else { 45 | let asset_name = ingredient.title(); 46 | let data = if name_only { 47 | asset_name.to_string() 48 | } else { 49 | format!("Asset:{asset_name}") 50 | }; 51 | current_token.append(tree, data); 52 | } 53 | } 54 | } 55 | Ok(()) 56 | } 57 | 58 | fn walk_tree(tree: &Arena, token: &Token) -> Tree { 59 | let result = token.children_tokens(tree).fold( 60 | Tree::root(tree[*token].data.clone()), 61 | |mut root, entry_token| { 62 | if entry_token.is_leaf(tree) { 63 | root.push(Tree::root(tree[entry_token].data.clone())); 64 | } else { 65 | root.push(walk_tree(tree, &entry_token)); 66 | } 67 | root 68 | }, 69 | ); 70 | 71 | result 72 | } 73 | 74 | /// Prints tree view of manifest store 75 | pub fn tree>(path: P) -> Result { 76 | let os_filename = path 77 | .as_ref() 78 | .file_name() 79 | .ok_or_else(|| crate::Error::BadParam("bad filename".to_string()))?; 80 | let asset_name = os_filename.to_string_lossy().into_owned(); 81 | 82 | let reader = Reader::from_file(path)?; 83 | 84 | // walk through the manifests and show the contents 85 | Ok(if let Some(manifest_label) = reader.active_label() { 86 | let data = format!("Asset:{}, Manifest:{}", asset_name, manifest_label); 87 | let (mut tree, root_token) = Arena::with_data(data); 88 | populate_node(&mut tree, &reader, manifest_label, &root_token, false)?; 89 | // print tree 90 | format!("Tree View:\n {}", walk_tree(&tree, &root_token)) 91 | } else { 92 | format!("Tree View:\n Asset:{asset_name}") 93 | }) 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn test_tree() -> Result<()> { 102 | let result = tree("tests/fixtures/C.jpg")?; 103 | assert!(result.contains("Tree View:")); 104 | assert!(result.contains("Assertion:c2pa.actions")); 105 | Ok(()) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions to this project! 4 | 5 | Before you start, we ask that you understand the following guidelines. 6 | 7 | ## Code of conduct 8 | 9 | This project adheres to the Adobe [code of conduct](CODE_OF_CONDUCT.md). By participating, 10 | you are expected to uphold this code. Please report unacceptable behavior to 11 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 12 | 13 | ## Have a question? 14 | 15 | Start by filing an issue. The existing committers on this project work to reach 16 | consensus around project direction and issue solutions within issue threads 17 | (when appropriate). 18 | 19 | ## Contributor license agreement 20 | 21 | All third-party contributions to this project must be accompanied by a signed contributor 22 | license agreement (CLA). This gives Adobe permission to redistribute your contributions 23 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You 24 | only need to submit an Adobe CLA one time, so if you have submitted one previously, 25 | you are good to go! 26 | 27 | ## Code reviews 28 | 29 | All submissions should come in the form of pull requests and need to be reviewed 30 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) 31 | for more information on sending pull requests. 32 | 33 | Code submissions will need to pass all automated tests in place at the time of submission. 34 | These include such things as Rust code format, Clippy/lint checks, and unit test coverage. 35 | 36 | We encourage you to raise an issue in GitHub before starting work on a major addition to the crate. 37 | This will give us an opportunity to discuss API design and avoid duplicate efforts. 38 | 39 | ### Pull request titles 40 | 41 | Titles of pull requests that target a long-lived branch such as _main_ or a release-specific branch should follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#specification). The repository's [commit lint rules](https://github.com/contentauth/c2pa-rs/blob/main/.commitlintrc.yml) require that the first word of the pull request title must be one of the following: 42 | 43 | - `fix` 44 | - `feat` 45 | - `chore` 46 | - `update` 47 | - `doc` 48 | 49 | Optionally, but preferred, a scope can be added in parentheses after the type. The scope should be the name of the module or component that the commit affects. For example, `feat(api): Introduce a new API to validate 1.0 claims`. 50 | 51 | If more detail is warranted, add a blank line and then continue with sentences (these sentences should be punctuated as such) and paragraphs as needed to provide that detail. There is no need to word-wrap this message. 52 | 53 | For example: 54 | 55 | ```text 56 | feat(api): Introduce a new API to validate 1.0 claims 57 | 58 | Repurpose existing v2 API for 0.8 compatibility (read: no validation) mode. 59 | ``` 60 | 61 | The conventional commit message requirement does not apply to individual commits within a pull request, provided that those commits will be squashed when the PR is merged and the resulting squash commit does follow the conventional commit requirement. This may require the person merging the PR to verify the commit message syntax when performing the squash merge. 62 | 63 | TIP: For single-commit PRs, ensure the commit message conforms to the conventional commit requirement, since by default that will also be the title of the PR. 64 | 65 | ## From contributor to committer 66 | 67 | We love contributions from our community! If you'd like to go a step beyond contributor 68 | and become a committer with full write access and a say in the project, you must 69 | be invited to the project. The existing committers employ an internal nomination 70 | process that must reach lazy consensus (silence is approval) before invitations 71 | are issued. If you feel you are qualified and want to get more deeply involved, 72 | feel free to reach out to existing committers to have a conversation about that. 73 | 74 | ## Security issues 75 | 76 | Do not create a public GitHub issue for any suspected security vulnerabilities. Instead, please file an issue through [Adobe's HackerOne page](https://hackerone.com/adobe?type=team). 77 | For more information on reporting security issues, see [SECURITY.md](SECURITY.md). 78 | -------------------------------------------------------------------------------- /schemas/manifest-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "http://ns.adobe.com/c2patool/claim-definition/v1", 4 | "type": "object", 5 | "description": "Definition format for claim created with c2patool", 6 | "examples": [ 7 | { 8 | "alg": "es256", 9 | "private_key": "es256_private.key", 10 | "sign_cert": "es256_certs.pem", 11 | "ta_url": "http://timestamp.digicert.com", 12 | "vendor": "myvendor", 13 | "claim_generator": "MyApp/0.1", 14 | "thumbnail": { 15 | "identifier": "thumb.jpg", 16 | "format": "image/jpeg" 17 | }, 18 | "ingredients": [ 19 | { 20 | "title": "A.jpg", 21 | "instanceId": "12345", 22 | "thumbnail": { 23 | "identifier": "A_thumb.jpg", 24 | "format": "image/jpeg" 25 | }, 26 | "relationship": "parentOf" 27 | } 28 | ], 29 | "ingredient_paths": [ 30 | "C.jpg" 31 | ], 32 | "assertions": [ 33 | { 34 | "label": "my.assertion", 35 | "data": { 36 | "any_tag": "whatever I want" 37 | } 38 | } 39 | ] 40 | } 41 | ], 42 | "required": [ 43 | "assertions" 44 | ], 45 | "properties": { 46 | "vendor": { 47 | "type": "string", 48 | "description": "Typically an Internet domain name (without the TLD) for the vendor (i.e. `adobe`, `nytimes`). If provided this will be used as a prefix on generated manifest labels." 49 | }, 50 | "claim_generator": { 51 | "type": "string", 52 | "description": "A UserAgent string that will let a user know what software/hardware/system produced this Manifest - names should not contain spaces (defaults to c2patool)." 53 | }, 54 | "title": { 55 | "type": "string", 56 | "description": "A human-readable string to be displayed as the title for this Manifest (defaults to the name of the file this manifest was embedded in)." 57 | }, 58 | "credentials": { 59 | "type": "object", 60 | "description": "An array of W3C verifiable credentials objects defined in the c2pa assertion specification. Section 7." 61 | }, 62 | "thumbnail": { 63 | "type": "object", 64 | "format": "JSON resource definition", 65 | "description": "An object with an identifier field with a file path, and a format with the mime type of that file." 66 | }, 67 | "ingredients": { 68 | "type": "array", 69 | "format": "Array of JSON ingredients, such as those produced with the --ingredient option", 70 | "description": "Ingredients that were used to modify the asset referenced by this Manifest (if any)." 71 | }, 72 | "ingredient_paths": { 73 | "type": "array", 74 | "format": "Array of local file system paths", 75 | "description": "File paths to assets that were used to modify the asset referenced by this Manifest (if any). This may be a JSON Ingredient definition file." 76 | }, 77 | "assertions": { 78 | "type": "object", 79 | "description": "Objects with label, and data - standard c2pa labels must match values as defined in the c2pa assertion specification." 80 | }, 81 | "alg": { 82 | "type": "string", 83 | "format": "Local file system path", 84 | "description": "Signing algorithm: one of [ ps256 | ps384 | ps512 | es256 | es384 | es512 | ed25519]. Defaults to es256." 85 | }, 86 | "ta_url": { 87 | "type": "string", 88 | "format": "http URL", 89 | "description": "A URL to an RFC3161 compliant Time Stamp Authority. If missing there will no secure timestamp." 90 | }, 91 | "private_key": { 92 | "type": "string", 93 | "format": "Local file system path", 94 | "description": "File path to a private key file." 95 | }, 96 | "sign_cert": { 97 | "type": "string", 98 | "format": "Local file system path", 99 | "description": "File path to signing cert file." 100 | } 101 | }, 102 | "additionalProperties": false 103 | } -------------------------------------------------------------------------------- /docs/manifest.md: -------------------------------------------------------------------------------- 1 | # Manifest definition file 2 | 3 | C2PA Tool reads a manifest definition JSON file with a `.json` extension. This file defines a single manifest to be added to an asset's manifest store. 4 | In the manifest definition file, file paths are relative to the location of the file unless you specify a `base_path` field. 5 | 6 | ## JSON format 7 | 8 | The C2PA specification describes a manifest that has a binary structure in JPEG universal metadata box format (JUMBF). However, C2PA Tool works with a JSON manifest structure that's easier to understand and work with. It's a declarative language for representing and creating a manifest in binary format. For more information on the JSON manifest, see [Working with manifests](https://opensource.contentauthenticity.org/docs/manifest/understanding-manifest). 9 | 10 | See also: 11 | 12 | * Manifest store reference 13 | * Manifest store schema 14 | * Manifest definition schema 15 | * Ingredient schema 16 | 17 | ## Adding a claim generator icon 18 | 19 | You can specify an icon to be displayed by tools such as [Verify](https://contentcredentials.org/verify) to indicate the signer of the manifest. 20 | 21 | To do this, add a `claim_generator_info` property to the manifest definition. The `claim_generator_info.icon` property contains information on the icon: 22 | - `icon.format` specifies the MIME type of the icon file. SVG format is preferred, but you can also use PNG or JPEG formats. 23 | - `icon.identifier` specifies the name of the icon file. 24 | 25 | For example: 26 | 27 | ```json 28 | "claim_generator_info": [ 29 | { 30 | "name": "My App", 31 | "version": "0.1.0", 32 | "icon": { 33 | "format": "image/svg+xml", 34 | "identifier": "logo.svg" 35 | } 36 | } 37 | ], 38 | ``` 39 | 40 | To add the icon using C2PA Tool, make sure the icon file and the manifest definition file are in the same directory where you are running `c2patool`. Then, you can add the icon by using a command like this: 41 | 42 | ```shell 43 | c2patool image_to_sign.jpg -m manifest.json -o signed_with_icon.jpg 44 | ``` 45 | 46 | NOTE: The [Verify](https://contentcredentials.org/verify) tool will not display an icon for a signing certificate that is not on the temporary certificate list, such as the C2PA Tool test certificate. 47 | 48 | ## Example 49 | 50 | The example below is a snippet of a manifest definition that inserts a CreativeWork author assertion. This example uses the default testing certificates in the [sample folder](https://github.com/contentauth/c2patool/tree/main/sample) that are also built into the c2patool binary. Copy this JSON into a file to use as a test manifest. 51 | 52 | **NOTE**: When you don't specify a key or certificate in the manifest `private_key` and `sign_cert` fields, the tool will use the built-in key and cert. You'll see a warning message, since they are meant for development purposes only. For actual use, provide a permanent key and certificate in the manifest definition or environment variables; see [Creating and using an X.509 certificate](x_509.md). 53 | 54 | The following manifest properties are specific to c2patool and used for signing manifests: 55 | 56 | - `alg`: Signing algorithm to use. See [Creating and using an X.509 certificate](x_509.md) for possible values. Default: `es256`. 57 | - `private_key`: Private key to use. Default: `es256_private.key` 58 | - `sign_cert`: Signing certificate to use. Default: `es256_certs.pem` 59 | - `ta_url`: Time Authority URL for getting a time-stamp (for example, `http://timestamp.digicert.com`). A time-stamp provides a way to confirm that the manifest was signed when the certificate was valid, even if the certificate has since expired. Howver, the Time Authority URL requires a live online connection for confirmation, which may not always be available. 60 | 61 | ```json 62 | { 63 | "alg": "es256", 64 | "private_key": "es256_private.key", 65 | "sign_cert": "es256_certs.pem", 66 | "ta_url": "http://timestamp.digicert.com", 67 | 68 | "claim_generator": "TestApp", 69 | "assertions": [ 70 | { 71 | "label": "stds.schema-org.CreativeWork", 72 | "data": { 73 | "@context": "https://schema.org", 74 | "@type": "CreativeWork", 75 | "author": [ 76 | { 77 | "@type": "Person", 78 | "name": "Joe Bloggs" 79 | } 80 | ] 81 | } 82 | } 83 | ] 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /tests/fixtures/ingredient_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "ta_url": "http://timestamp.digicert.com", 3 | 4 | "claim_generator": "TestApp", 5 | "claim_generator_info": [{ 6 | "name": "Test App", 7 | "version": "0.0.1", 8 | "icon": { 9 | "format": "image/png", 10 | "identifier": "libpng-test.png" 11 | } 12 | }], 13 | "thumbnail": { 14 | "format": "image/jpeg", 15 | "identifier": "earth_apollo17.jpg" 16 | }, 17 | "assertions": [ 18 | { 19 | "label": "c2pa.actions", 20 | "data": { 21 | "actions": [ 22 | { 23 | "action": "c2pa.created", 24 | "instanceId": "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d", 25 | "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia", 26 | "softwareAgent": { 27 | "name": "TestApp", 28 | "version": "1.0", 29 | "something": "else" 30 | } 31 | }, 32 | { 33 | "action": "c2pa.opened", 34 | "instanceId": "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d", 35 | "parameters": { 36 | "description": "import" 37 | }, 38 | "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia", 39 | "softwareAgent": { 40 | "name": "TestApp", 41 | "version": "1.0", 42 | "icon": { 43 | "format": "image/svg+xml", 44 | "identifier": "sample1.svg" 45 | }, 46 | "something": "else" 47 | }, 48 | "changes": [ 49 | { 50 | "region" : [ 51 | { 52 | "type" : "temporal", 53 | "time" : {} 54 | }, 55 | { 56 | "type" : "identified", 57 | "item" : { 58 | "identifier" : "https://bioportal.bioontology.org/ontologies/FMA", 59 | "value" : "lips" 60 | } 61 | } 62 | ], 63 | "description": "lip synced area" 64 | } 65 | ] 66 | }, 67 | { 68 | "action" : "c2pa.edited", 69 | "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/minorHumanEdits", 70 | "changes" : [ 71 | { 72 | "region" : [ 73 | { 74 | "type" : "identified", 75 | "item" : { 76 | "identifier" : "track_id", 77 | "value" : "transcript" 78 | } 79 | } 80 | ] 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | }, 87 | { 88 | "label": "stds.schema-org.CreativeWork", 89 | "data": { 90 | "@context": "https://schema.org", 91 | "@type": "CreativeWork", 92 | "author": [ 93 | { 94 | "@type": "Person", 95 | "name": "Joe Bloggs" 96 | } 97 | ] 98 | }, 99 | "kind": "Json" 100 | }, 101 | { 102 | "label": "cawg.training-mining", 103 | "data": { 104 | "entries": { 105 | "c2pa.ai_training" : { 106 | "use" : "allowed" 107 | }, 108 | "c2pa.ai_generative_training" : { 109 | "use" : "notAllowed" 110 | } 111 | } 112 | } 113 | } 114 | ], 115 | "ingredients": [ 116 | { 117 | "title": "earth_apollo17.jpg", 118 | "format": "image/jpeg", 119 | "document_id": "adobe:docid:photoshop:e1c344db-c371-ef45-a4fe-9b5ba6346544", 120 | "instance_id": "xmp.iid:9867c3b4-465e-42e4-8064-76c0c0fe3d16", 121 | "thumbnail": { 122 | "format": "image/jpeg", 123 | "identifier": "verify.jpeg" 124 | }, 125 | "relationship": "componentOf" 126 | } 127 | ], 128 | "ingredient_paths": [ 129 | "C.jpg", "ingredient/ingredient.json" 130 | ] 131 | } 132 | 133 | -------------------------------------------------------------------------------- /src/signer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Adobe. All rights reserved. 2 | // This file is licensed to you under the Apache License, 3 | // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 | // or the MIT license (http://opensource.org/licenses/MIT), 5 | // at your option. 6 | 7 | // Unless required by applicable law or agreed to in writing, 8 | // this software is distributed on an "AS IS" BASIS, WITHOUT 9 | // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 | // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 | // specific language governing permissions and limitations under 12 | // each license. 13 | 14 | use std::{ 15 | env, 16 | path::{Path, PathBuf}, 17 | }; 18 | 19 | use anyhow::{Context, Result}; 20 | use c2pa::{create_signer, Signer, SigningAlg}; 21 | use serde::Deserialize; 22 | 23 | // Pull in default certs so the binary can self config 24 | const DEFAULT_CERTS: &[u8] = include_bytes!("../sample/es256_certs.pem"); 25 | const DEFAULT_KEY: &[u8] = include_bytes!("../sample/es256_private.key"); 26 | 27 | pub fn get_ta_url() -> Option { 28 | std::env::var("C2PA_TA_URL").ok() 29 | } 30 | #[derive(Debug, Default, Deserialize)] 31 | pub struct SignConfig { 32 | /// Signing algorithm to use - must match the associated certs 33 | /// 34 | /// Must be one of [ ps256 | ps384 | ps512 | es256 | es384 | es512 | ed25519 ] 35 | /// Defaults to es256 36 | pub alg: Option, 37 | /// A path to a file containing the private key required for signing 38 | pub private_key: Option, 39 | /// A path to a file containing the signing cert required for signing 40 | pub sign_cert: Option, 41 | /// A Url to a Time Authority to use when signing the manifest 42 | pub ta_url: Option, 43 | } 44 | 45 | impl SignConfig { 46 | pub fn from_json(json: &str) -> Result { 47 | serde_json::from_str(json).context("reading manifest configuration") 48 | } 49 | 50 | // set a base for all non-absolute paths 51 | pub fn set_base_path>(&mut self, base: P) -> &Self { 52 | if let Some(path) = self.private_key.as_ref() { 53 | if !path.is_absolute() { 54 | self.private_key = Some(base.as_ref().join(path)); 55 | } 56 | } 57 | if let Some(path) = self.sign_cert.as_ref() { 58 | if !path.is_absolute() { 59 | self.sign_cert = Some(base.as_ref().join(path)); 60 | } 61 | } 62 | self 63 | } 64 | 65 | pub fn signer(&self) -> Result> { 66 | let alg = self.alg.as_deref().unwrap_or("es256").to_lowercase(); 67 | let alg: SigningAlg = alg.parse().map_err(|_| c2pa::Error::UnsupportedType)?; 68 | let tsa_url = self.ta_url.clone().or_else(get_ta_url); 69 | 70 | let mut private_key = None; 71 | let mut sign_cert = None; 72 | 73 | if let Some(path) = self.private_key.as_deref() { 74 | private_key = 75 | Some(std::fs::read(path).context(format!("Reading private key: {:?}", &path))?); 76 | } 77 | 78 | if private_key.is_none() { 79 | if let Ok(key) = env::var("C2PA_PRIVATE_KEY") { 80 | private_key = Some(key.as_bytes().to_vec()); 81 | } 82 | }; 83 | 84 | if let Some(path) = self.sign_cert.as_deref() { 85 | sign_cert = 86 | Some(std::fs::read(path).context(format!("Reading sign cert: {:?}", &path))?); 87 | } 88 | 89 | if sign_cert.is_none() { 90 | if let Ok(cert) = env::var("C2PA_SIGN_CERT") { 91 | sign_cert = Some(cert.as_bytes().to_vec()); 92 | } 93 | }; 94 | 95 | if let Some(private_key) = private_key { 96 | if let Some(sign_cert) = sign_cert { 97 | let signer = create_signer::from_keys(&sign_cert, &private_key, alg, tsa_url) 98 | .context("Invalid certification data")?; 99 | return Ok(signer); 100 | } 101 | } 102 | 103 | eprintln!( 104 | "\n\n-----------\n\n\ 105 | Note: Using default private key and signing certificate. This is only valid for development.\n\ 106 | A permanent key and cert should be provided in the manifest definition or in the environment variables.\n"); 107 | 108 | let signer = create_signer::from_keys(DEFAULT_CERTS, DEFAULT_KEY, alg, tsa_url) 109 | .context("Invalid certification data")?; 110 | 111 | Ok(signer) 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | pub mod tests { 117 | #![allow(clippy::unwrap_used)] 118 | 119 | use super::*; 120 | const CONFIG: &str = r#"{ 121 | "alg": "es256", 122 | "private_key": "es256_private.key", 123 | "sign_cert": "es256_certs.pem", 124 | "ta_url": "http://timestamp.digicert.com" 125 | }"#; 126 | 127 | #[test] 128 | fn test_sign_config() { 129 | let mut sign_config = SignConfig::from_json(CONFIG).expect("from_json"); 130 | sign_config.set_base_path("sample"); 131 | 132 | let signer = sign_config.signer().expect("get signer"); 133 | assert_eq!(signer.alg(), SigningAlg::Es256); 134 | } 135 | 136 | #[test] 137 | fn test_sign_default() { 138 | let sign_config = SignConfig::default(); 139 | 140 | let signer = sign_config.signer().expect("get signer"); 141 | assert_eq!(signer.alg(), SigningAlg::Es256); 142 | } 143 | 144 | #[test] 145 | fn test_sign_from() { 146 | let sign_config = SignConfig::default(); 147 | 148 | let signer = sign_config.signer().expect("get signer"); 149 | assert_eq!(signer.alg(), SigningAlg::Es256); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /sample/allowed_list.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIChzCCAi6gAwIBAgIUcCTmJHYF8dZfG0d1UdT6/LXtkeYwCgYIKoZIzj0EAwIw 3 | gYwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU29tZXdoZXJl 4 | MScwJQYDVQQKDB5DMlBBIFRlc3QgSW50ZXJtZWRpYXRlIFJvb3QgQ0ExGTAXBgNV 5 | BAsMEEZPUiBURVNUSU5HX09OTFkxGDAWBgNVBAMMD0ludGVybWVkaWF0ZSBDQTAe 6 | Fw0yMjA2MTAxODQ2NDBaFw0zMDA4MjYxODQ2NDBaMIGAMQswCQYDVQQGEwJVUzEL 7 | MAkGA1UECAwCQ0ExEjAQBgNVBAcMCVNvbWV3aGVyZTEfMB0GA1UECgwWQzJQQSBU 8 | ZXN0IFNpZ25pbmcgQ2VydDEZMBcGA1UECwwQRk9SIFRFU1RJTkdfT05MWTEUMBIG 9 | A1UEAwwLQzJQQSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQPaL6R 10 | kAkYkKU4+IryBSYxJM3h77sFiMrbvbI8fG7w2Bbl9otNG/cch3DAw5rGAPV7NWky 11 | l3QGuV/wt0MrAPDoo3gwdjAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsG 12 | AQUFBwMEMA4GA1UdDwEB/wQEAwIGwDAdBgNVHQ4EFgQUFznP0y83joiNOCedQkxT 13 | tAMyNcowHwYDVR0jBBgwFoAUDnyNcma/osnlAJTvtW6A4rYOL2swCgYIKoZIzj0E 14 | AwIDRwAwRAIgOY/2szXjslg/MyJFZ2y7OH8giPYTsvS7UPRP9GI9NgICIDQPMKrE 15 | LQUJEtipZ0TqvI/4mieoyRCeIiQtyuS0LACz 16 | -----END CERTIFICATE----- 17 | -----BEGIN CERTIFICATE----- 18 | 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 54 | -----END CERTIFICATE----- 55 | -----BEGIN CERTIFICATE----- 56 | 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 91 | wvq03sA= 92 | -----END CERTIFICATE----- 93 | -------------------------------------------------------------------------------- /sample/ps256.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 38 | -----END CERTIFICATE----- 39 | -----BEGIN CERTIFICATE----- 40 | MIIGkTCCBEWgAwIBAgIUeTn90WGAkw2fOJHBNX6EhnB7FZ4wQQYJKoZIhvcNAQEK 41 | MDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEF 42 | AKIDAgEgMHcxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU29t 43 | ZXdoZXJlMRowGAYDVQQKDBFDMlBBIFRlc3QgUm9vdCBDQTEZMBcGA1UECwwQRk9S 44 | IFRFU1RJTkdfT05MWTEQMA4GA1UEAwwHUm9vdCBDQTAeFw0yMjA2MTAxODQ2MjZa 45 | Fw0zMDA4MjcxODQ2MjZaMIGMMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQ 46 | BgNVBAcMCVNvbWV3aGVyZTEnMCUGA1UECgweQzJQQSBUZXN0IEludGVybWVkaWF0 47 | ZSBSb290IENBMRkwFwYDVQQLDBBGT1IgVEVTVElOR19PTkxZMRgwFgYDVQQDDA9J 48 | bnRlcm1lZGlhdGUgQ0EwggJWMEEGCSqGSIb3DQEBCjA0oA8wDQYJYIZIAWUDBAIB 49 | BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAIBBQCiAwIBIAOCAg8AMIICCgKC 50 | AgEAqlafkrMkDom4SFHQBGwqODnuj+xi7IoCxADsKs9rDjvEB7qK2cj/d7sGhp4B 51 | vCTu6I+2xUmfz+yvJ/72+HnQvoUGInPp8Rbvb1T3LcfyDcY4WHqJouKNGa4T4ZVN 52 | u3HdgbaD/S3BSHmBJZvZ6YH0pWDntbNra1WR0KfCsA+jccPfCI3NTVCjEnFlTSdH 53 | UasJLnh9tMvefk1QDUp3mNd3x7X1FWIZquXOgHxDNVS+GDDWfSN20dwyIDvotleN 54 | 5bOTQb3Pzgg0D/ZxKb/1oiRgIJffTfROITnU0Mk3gUwLzeQHaXwKDR4DIVst7Git 55 | A4yIIq8xXDvyKlYde6eRY1JV/H0RExTxRgCcXKQrNrUmIPoFSuz05TadQ93A0Anr 56 | EaPJOaY20mJlHa6bLSecFa/yW1hSf/oNKkjqtIGNV8k6fOfdO6j/ZkxRUI19IcqY 57 | Ly/IewMFOuowJPay8LCoM0xqI7/yj1gvfkyjl6wHuJ32e17kj1wnmUbg/nvmjvp5 58 | sPZjIpIXJmeEm2qwvwOtBJN8EFSI4emeIO2NVtQS51RRonazWNuHRKf/hpCXsJpI 59 | snZhH3mEqQAwKuobDhL+9pNnRag8ssCGLZmLGB0XfSFufMp5/gQyZYj4Q6wUh/OI 60 | O/1ZYTtQPlnHLyFBVImGlCxvMiDuh2ue7lYyrNuNwDKXMI8CAwEAAaNjMGEwDwYD 61 | VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFAwygMrllm1P 62 | H/v5CwHWqeUM3ADiMB8GA1UdIwQYMBaAFEVvG+J0LmYCLXksOfn6Mk2UKxlQMEEG 63 | CSqGSIb3DQEBCjA0oA8wDQYJYIZIAWUDBAIBBQChHDAaBgkqhkiG9w0BAQgwDQYJ 64 | YIZIAWUDBAIBBQCiAwIBIAOCAgEAqkYEUJP1BJLY55B7NWThZ31CiTiEnAUyR3O6 65 | F2MBWfXMrYEAIG3+vzQpLbbAh2u/3W4tzDiLr9uS7KA9z6ruwUODgACMAHZ7kfT/ 66 | Ze3XSmhezYVZm3c4b/F0K/d92GDAzjgldBiKIkVqTrRSrMyjCyyJ+kR4VOWz8EoF 67 | vdwvrd0SP+1l9V5ThlmHzQ3cXT1pMpCjj+bw1z7ScZjYdAotOk74jjRXF5Y0HYra 68 | bGh6tl0sn6WXsYZK27LuQ/iPJrXLVqt/+BKHYtqD73+6dh8PqXG1oXO9KoEOwJpt 69 | 8R9IwGoAj37hFpvZm2ThZ6TKXM0+HpByZamExoCiL2mQWRbKWPSyJjFwXjLScWSB 70 | IJg1eY45+a3AOwhuSE34alhwooH2qDEuGK7KW1W5V/02jtsbYc2upEfkMzd2AaJb 71 | 2ALDGCwa4Gg6IkEadNBdXvNewG1dFDPOgPiJM9gTGeXMELO9sBpoOvZsoVj2wbVC 72 | +5FFnqm40bPy0zeR99CGjgZBMr4siCLRJybBD8sX6sE0WSx896Q0PlRdS4Wniu+Y 73 | 8QCS293tAyD7tWztko5mdVGfcYYfa2UnHqKlDZOpdMq/rjzXtPVREq+dRKld3KLy 74 | oqiZiY7ceUPTraAQ3pK535dcX3XA7p9RsGztyl7jma6HO2WmO9a6rGR2xCqW5/g9 75 | wvq03sA= 76 | -----END CERTIFICATE----- 77 | -----BEGIN CERTIFICATE----- 78 | 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 113 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /tests/fixtures/trust/anchors.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 38 | -----END CERTIFICATE----- 39 | -----BEGIN CERTIFICATE----- 40 | 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 75 | wvq03sA= 76 | -----END CERTIFICATE----- 77 | -----BEGIN CERTIFICATE----- 78 | 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 113 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Adobe. All rights reserved. 2 | // This file is licensed to you under the Apache License, 3 | // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 | // or the MIT license (http://opensource.org/licenses/MIT), 5 | // at your option. 6 | 7 | // Unless required by applicable law or agreed to in writing, 8 | // this software is distributed on an "AS IS" BASIS, WITHOUT 9 | // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 | // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 | // specific language governing permissions and limitations under 12 | // each license. 13 | 14 | use std::{error::Error, fs, path::PathBuf, process::Command}; 15 | 16 | // Add methods on commands 17 | use assert_cmd::prelude::*; 18 | use httpmock::{prelude::*, Mock}; 19 | use predicate::str; 20 | use predicates::prelude::*; 21 | 22 | const TEST_IMAGE_WITH_MANIFEST: &str = "C.jpg"; // save for manifest tests 23 | 24 | fn fixture_path(name: &str) -> PathBuf { 25 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 26 | path.push("tests/fixtures"); 27 | path.push(name); 28 | fs::canonicalize(path).expect("canonicalize") 29 | } 30 | 31 | #[test] 32 | // c2patool tests/fixtures/C.jpg trust --trust_anchors=tests/fixtures/trust/anchors.pem --trust_config=tests/fixtures/trust/store.cfg 33 | fn tool_load_trust_settings_from_file_trusted() -> Result<(), Box> { 34 | Command::cargo_bin("c2patool")? 35 | .arg(fixture_path(TEST_IMAGE_WITH_MANIFEST)) 36 | .arg("trust") 37 | .arg("--trust_anchors") 38 | .arg(fixture_path("trust/anchors.pem")) 39 | .arg("--trust_config") 40 | .arg(fixture_path("trust/store.cfg")) 41 | .assert() 42 | .success() 43 | .stdout(str::contains("C2PA Test Signing Cert")) 44 | .stdout(str::contains("signingCredential.untrusted").not()); 45 | Ok(()) 46 | } 47 | 48 | #[test] 49 | // c2patool tests/fixtures/C.jpg trust --trust_anchors=tests/fixtures/trust/no-match.pem --trust_config=tests/fixtures/trust/store.cfg 50 | fn tool_load_trust_settings_from_file_untrusted() -> Result<(), Box> { 51 | Command::cargo_bin("c2patool")? 52 | .arg(fixture_path(TEST_IMAGE_WITH_MANIFEST)) 53 | .arg("trust") 54 | .arg("--trust_anchors") 55 | .arg(fixture_path("trust/no-match.pem")) 56 | .arg("--trust_config") 57 | .arg(fixture_path("trust/store.cfg")) 58 | .assert() 59 | .success() 60 | .stdout(str::contains("C2PA Test Signing Cert")) 61 | .stdout(str::contains("signingCredential.untrusted")); 62 | Ok(()) 63 | } 64 | 65 | fn create_mock_server<'a>( 66 | server: &'a MockServer, 67 | anchor_source: &str, 68 | config_source: &str, 69 | ) -> Vec> { 70 | let anchor_path = fixture_path(anchor_source).to_str().unwrap().to_owned(); 71 | let trust_mock = server.mock(|when, then| { 72 | when.method(GET).path("/trust/anchors.pem"); 73 | then.status(200) 74 | .header("content-type", "text/plain") 75 | .body_from_file(anchor_path); 76 | }); 77 | let config_path = fixture_path(config_source).to_str().unwrap().to_owned(); 78 | let config_mock = server.mock(|when, then| { 79 | when.method(GET).path("/trust/store.cfg"); 80 | then.status(200) 81 | .header("content-type", "text/plain") 82 | .body_from_file(config_path); 83 | }); 84 | 85 | vec![trust_mock, config_mock] 86 | } 87 | 88 | #[test] 89 | fn tool_load_trust_settings_from_url_arg_trusted() -> Result<(), Box> { 90 | let server = MockServer::start(); 91 | let mocks = create_mock_server(&server, "trust/anchors.pem", "trust/store.cfg"); 92 | 93 | // Test flags 94 | Command::cargo_bin("c2patool")? 95 | .arg(fixture_path(TEST_IMAGE_WITH_MANIFEST)) 96 | .arg("trust") 97 | .arg("--trust_anchors") 98 | .arg(server.url("/trust/anchors.pem")) 99 | .arg("--trust_config") 100 | .arg(server.url("/trust/store.cfg")) 101 | .assert() 102 | .success() 103 | .stdout(str::contains("C2PA Test Signing Cert")) 104 | .stdout(str::contains("signingCredential.untrusted").not()); 105 | 106 | mocks.iter().for_each(|m| m.assert()); 107 | 108 | Ok(()) 109 | } 110 | 111 | #[test] 112 | fn tool_load_trust_settings_from_url_arg_untrusted() -> Result<(), Box> { 113 | let server = MockServer::start(); 114 | let mocks = create_mock_server(&server, "trust/no-match.pem", "trust/store.cfg"); 115 | 116 | Command::cargo_bin("c2patool")? 117 | .arg(fixture_path(TEST_IMAGE_WITH_MANIFEST)) 118 | .arg("trust") 119 | .arg("--trust_anchors") 120 | .arg(server.url("/trust/anchors.pem")) 121 | .arg("--trust_config") 122 | .arg(server.url("/trust/store.cfg")) 123 | .assert() 124 | .success() 125 | .stdout(str::contains("C2PA Test Signing Cert")) 126 | .stdout(str::contains("signingCredential.untrusted")); 127 | 128 | mocks.iter().for_each(|m| m.assert()); 129 | 130 | Ok(()) 131 | } 132 | 133 | #[test] 134 | fn tool_load_trust_settings_from_url_env_trusted() -> Result<(), Box> { 135 | let server = MockServer::start(); 136 | let mocks = create_mock_server(&server, "trust/anchors.pem", "trust/store.cfg"); 137 | 138 | // Test flags 139 | Command::cargo_bin("c2patool")? 140 | .arg(fixture_path(TEST_IMAGE_WITH_MANIFEST)) 141 | .arg("trust") 142 | .env("C2PATOOL_TRUST_ANCHORS", server.url("/trust/anchors.pem")) 143 | .env("C2PATOOL_TRUST_CONFIG", server.url("/trust/store.cfg")) 144 | .assert() 145 | .success() 146 | .stdout(str::contains("C2PA Test Signing Cert")) 147 | .stdout(str::contains("signingCredential.untrusted").not()); 148 | 149 | mocks.iter().for_each(|m| m.assert()); 150 | 151 | Ok(()) 152 | } 153 | 154 | #[test] 155 | fn tool_load_trust_settings_from_url_env_untrusted() -> Result<(), Box> { 156 | let server = MockServer::start(); 157 | let mocks = create_mock_server(&server, "trust/no-match.pem", "trust/store.cfg"); 158 | 159 | // Test flags 160 | Command::cargo_bin("c2patool")? 161 | .arg(fixture_path(TEST_IMAGE_WITH_MANIFEST)) 162 | .arg("trust") 163 | .env("C2PATOOL_TRUST_ANCHORS", server.url("/trust/anchors.pem")) 164 | .env("C2PATOOL_TRUST_CONFIG", server.url("/trust/store.cfg")) 165 | .assert() 166 | .success() 167 | .stdout(str::contains("C2PA Test Signing Cert")) 168 | .stdout(str::contains("signingCredential.untrusted")); 169 | 170 | mocks.iter().for_each(|m| m.assert()); 171 | 172 | Ok(()) 173 | } 174 | 175 | #[test] 176 | // c2patool tests/fixtures/C.jpg --tree 177 | fn tool_tree() -> Result<(), Box> { 178 | Command::cargo_bin("c2patool")? 179 | .arg(fixture_path(TEST_IMAGE_WITH_MANIFEST)) 180 | .arg("--tree") 181 | .assert() 182 | .success() 183 | .stdout(str::contains("Asset:C.jpg, Manifest:contentauth:urn:uuid:")) 184 | .stdout(str::contains("Assertion:c2pa.actions")); 185 | Ok(()) 186 | } 187 | 188 | #[test] 189 | // c2patool tests/fixtures/C.jpg --info 190 | fn tool_info() -> Result<(), Box> { 191 | Command::cargo_bin("c2patool")? 192 | .arg(fixture_path(TEST_IMAGE_WITH_MANIFEST)) 193 | .arg("--info") 194 | .assert() 195 | .success() 196 | .stdout(str::contains( 197 | "Provenance URI = self#jumbf=/c2pa/contentauth:urn:uuid:", 198 | )) 199 | .stdout(str::contains("Manifest store size = 51217")); 200 | Ok(()) 201 | } 202 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All changes to this project are documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org), except that – as is typical in the Rust community – the minimum supported Rust version may be increased without a major version increase. 6 | 7 | Do not manually edit this file. It will be automatically updated when a new release is published. 8 | 9 | ## 0.9.12 10 | _18 October 2024_ 11 | 12 | * fix: Update c2pa-rs for RegionOfInterest support. ([#269](https://github.com/contentauth/c2pa-rs/pull/269)) 13 | * Fix broken link that was causing os site workflow to fail ([#266](https://github.com/contentauth/c2pa-rs/pull/266)) 14 | * Bump codecov/codecov-action from 3 to 4 ([#242](https://github.com/contentauth/c2pa-rs/pull/242)) 15 | * chore: Run all CI jobs when user is dependabot[bot] 16 | * chore: Debug CI again 17 | * chore: Format for consistency with c2pa-rs CI workflow ([#265](https://github.com/contentauth/c2pa-rs/pull/265)) 18 | * chore: Don't skip CI jobs for non-pull-request events 19 | * chore: Retry debug 20 | * chore: Debug action context 21 | * chore: Skip CodeCov upload for non-member PRs ([#263](https://github.com/contentauth/c2pa-rs/pull/263)) 22 | * Bump EmbarkStudios/cargo-deny-action from 1 to 2 ([#245](https://github.com/contentauth/c2pa-rs/pull/245)) 23 | * chore: Adjust conditions for running CI jobs ([#261](https://github.com/contentauth/c2pa-rs/pull/261)) 24 | ## 0.9.11 25 | _16 October 2024_ 26 | 27 | * Merge hardening bug fixes ([#260](https://github.com/contentauth/c2pa-rs/pull/260)) 28 | ## 0.9.10 29 | _07 October 2024_ 30 | 31 | * Update c2ptool to use latest c2pa-rs ([#258](https://github.com/contentauth/c2pa-rs/pull/258)) 32 | ## 0.9.9 33 | _17 September 2024_ 34 | 35 | * Pull in latest bug fixes ([#237](https://github.com/contentauth/c2pa-rs/pull/237)) 36 | * Document fragment subcommand ([#236](https://github.com/contentauth/c2pa-rs/pull/236)) 37 | * Switch back to using `pull_request` instead of `pull_request_target` trigger 38 | * Bump actions/checkout from 3 to 4 ([#243](https://github.com/contentauth/c2pa-rs/pull/243)) 39 | * Remove no-longer-maintained clippy-check action ([#238](https://github.com/contentauth/c2pa-rs/pull/238)) 40 | ## 0.9.8 41 | _30 August 2024_ 42 | 43 | * Initial fragment support ([#230](https://github.com/contentauth/c2pa-rs/pull/230)) 44 | * Add warning about accessing a private key directly ([#218](https://github.com/contentauth/c2pa-rs/pull/218)) 45 | ## 0.9.7 46 | _15 August 2024_ 47 | 48 | * Update to latest c2pa SDK ([#222](https://github.com/contentauth/c2pa-rs/pull/222)) 49 | * Remove rust toolchain version lock ([#221](https://github.com/contentauth/c2pa-rs/pull/221)) 50 | * Update security guidance to link to SECURITY.md ([#217](https://github.com/contentauth/c2pa-rs/pull/217)) 51 | ## 0.9.6 52 | _30 July 2024_ 53 | 54 | * Pull latest c2pa-rs bug fixes into c2patool ([#212](https://github.com/contentauth/c2pa-rs/pull/212)) 55 | * only run tests/clippy if labeled ([#205](https://github.com/contentauth/c2pa-rs/pull/205)) 56 | * Bump env_logger from 0.10.2 to 0.11.4 ([#204](https://github.com/contentauth/c2pa-rs/pull/204)) 57 | * Updates cargo packages and cargo.deny file. ([#200](https://github.com/contentauth/c2pa-rs/pull/200)) 58 | ## 0.9.5 59 | _18 July 2024_ 60 | 61 | * Update to lastest c2pa-rs ([#197](https://github.com/contentauth/c2pa-rs/pull/197)) 62 | * added security.md ([#196](https://github.com/contentauth/c2pa-rs/pull/196)) 63 | ## 0.9.4 64 | _25 June 2024_ 65 | 66 | * Update c2patool ([#190](https://github.com/contentauth/c2pa-rs/pull/190)) 67 | * Match c2pa-rs minimum toolchain version and test in CI ([#188](https://github.com/contentauth/c2pa-rs/pull/188)) 68 | * Document how to specify an icon ([#182](https://github.com/contentauth/c2pa-rs/pull/182)) 69 | ## 0.9.3 70 | _29 May 2024_ 71 | 72 | * Remove binary modules ([#179](https://github.com/contentauth/c2pa-rs/pull/179)) 73 | ## 0.9.2 74 | _24 May 2024_ 75 | 76 | * Remove integration tests for now due to extraneous binaries ([#178](https://github.com/contentauth/c2pa-rs/pull/178)) 77 | ## 0.9.1 78 | _22 May 2024_ 79 | 80 | * Add better support for cargo-binstall ([#177](https://github.com/contentauth/c2pa-rs/pull/177)) 81 | ## 0.9.0 82 | _07 May 2024_ 83 | 84 | * Integrate with c2pa-rs 0.32.0, various test case fixes. ([#175](https://github.com/contentauth/c2pa-rs/pull/175)) 85 | * (MINOR) Add HTTP source option for trust config ([#174](https://github.com/contentauth/c2pa-rs/pull/174)) 86 | ## 0.8.2 87 | _28 March 2024_ 88 | 89 | * fixed c2patool asset name ([#171](https://github.com/contentauth/c2pa-rs/pull/171)) 90 | ## 0.8.1 91 | _25 March 2024_ 92 | 93 | * use c2pa-rs 0.31.1 for actions.changes support ([#170](https://github.com/contentauth/c2pa-rs/pull/170)) 94 | ## 0.8.0 95 | _20 March 2024_ 96 | 97 | * (MINOR) allow clients to sign with a process outside of c2patool ([#169](https://github.com/contentauth/c2pa-rs/pull/169)) 98 | * Add trust and verification options to c2pa_tool ([#168](https://github.com/contentauth/c2pa-rs/pull/168)) 99 | * adds version to c2patool artifact names ([#158](https://github.com/contentauth/c2pa-rs/pull/158)) 100 | ## 0.7.0 101 | _22 November 2023_ 102 | 103 | * (MINOR) updates to c2pa-rs v0.28.2 ([#153](https://github.com/contentauth/c2pa-rs/pull/153)) 104 | * Update to c2pa-rs 0.28.1 105 | ## 0.6.2 106 | _05 October 2023_ 107 | 108 | * update to c2pa 0.27.1 ([#146](https://github.com/contentauth/c2pa-rs/pull/146)) 109 | * Merge branch 'main' of https://github.com/contentauth/c2patool 110 | * Add Do not train example 111 | * Upgrade to c2pa-rs 0.26.0 ([#143](https://github.com/contentauth/c2pa-rs/pull/143)) 112 | * Fix issue with docusaurus styling and fix broken links ([#138](https://github.com/contentauth/c2pa-rs/pull/138)) 113 | * Updates to c2pa-rs 0.25.1 ([#128](https://github.com/contentauth/c2pa-rs/pull/128)) 114 | * Fix windows release ([#132](https://github.com/contentauth/c2pa-rs/pull/132)) 115 | ## 0.6.1 116 | _24 July 2023_ 117 | 118 | * use compress-archive instead of tar ([#130](https://github.com/contentauth/c2pa-rs/pull/130)) 119 | 120 | ## 0.6.0 121 | _22 June 2023_ 122 | 123 | * (MINOR) update to c2pa-rs 0.24.0 ([#127](https://github.com/contentauth/c2pa-rs/pull/127)) 124 | 125 | ## 0.5.4 126 | _13 June 2023_ 127 | 128 | * integrate c2pa 23.0 bump version ([#126](https://github.com/contentauth/c2pa-rs/pull/126)) 129 | * Merge branch 'main' of https://github.com/contentauth/c2patool 130 | * c2pa-rs 23.0 + updated test 131 | * Update README.md ([#124](https://github.com/contentauth/c2pa-rs/pull/124)) 132 | 133 | ## 0.5.3 134 | _04 May 2023_ 135 | 136 | * Parent Ingredient JSON ([#123](https://github.com/contentauth/c2pa-rs/pull/123)) 137 | 138 | ## 0.5.2 139 | _19 April 2023_ 140 | 141 | * Ingredient thumbnails, extension cleanup, toolkit update ([#120](https://github.com/contentauth/c2pa-rs/pull/120)) 142 | 143 | ## 0.5.1 144 | _10 April 2023_ 145 | 146 | * Update README.md ([#118](https://github.com/contentauth/c2pa-rs/pull/118)) 147 | * Update expired sample certs ([#113](https://github.com/contentauth/c2pa-rs/pull/113)) 148 | 149 | ## 0.5.0 150 | _28 March 2023_ 151 | 152 | * (MINOR) New ingredient support and c2pa file formats ([#111](https://github.com/contentauth/c2pa-rs/pull/111)) 153 | * Leverage new Manifest & Ingredient, add Ingredient creation. ([#107](https://github.com/contentauth/c2pa-rs/pull/107)) 154 | 155 | ## 0.4.0 156 | _01 March 2023_ 157 | 158 | * (MINOR) Add --certs and --tree options ([#106](https://github.com/contentauth/c2pa-rs/pull/106)) 159 | * update to cp2pa 0.17.0 ([#105](https://github.com/contentauth/c2pa-rs/pull/105)) 160 | * Update for Clippy in Rust 1.67 ([#101](https://github.com/contentauth/c2pa-rs/pull/101)) 161 | 162 | ## 0.3.9 163 | _06 December 2022_ 164 | 165 | * update to c2pa-rs 0.16.0 166 | * allows clients to output manifest report to specified directory ([#91](https://github.com/contentauth/c2pa-rs/pull/91)) 167 | 168 | ## 0.3.8 169 | _09 November 2022_ 170 | 171 | * Bump c2pa from 0.13.2 to 0.15.0 ([#87](https://github.com/contentauth/c2pa-rs/pull/87)) 172 | * Build infrastructure improvements ([#85](https://github.com/contentauth/c2pa-rs/pull/85)) 173 | * Fix new Clippy warning in Rust 1.65 ([#84](https://github.com/contentauth/c2pa-rs/pull/84)) 174 | * Readme updates ([#62](https://github.com/contentauth/c2pa-rs/pull/62)) 175 | 176 | ## 0.3.7 177 | _22 September 2022_ 178 | 179 | * Treat a source asset with a manifest store as a default parent ([#76](https://github.com/contentauth/c2pa-rs/pull/76)) 180 | * Fetch remote manifests for --info ([#75](https://github.com/contentauth/c2pa-rs/pull/75)) 181 | 182 | ## 0.3.6 183 | _16 September 2022_ 184 | 185 | * Update Cargo.lock when publishing crate ([#71](https://github.com/contentauth/c2pa-rs/pull/71)) 186 | * [IGNORE] update readme --info ([#70](https://github.com/contentauth/c2pa-rs/pull/70)) 187 | * Update Cargo.lock to 0.3.5 188 | 189 | ## 0.3.5 190 | _15 September 2022_ 191 | 192 | * Upgrade cpufeatures to non-yanked version ([#68](https://github.com/contentauth/c2pa-rs/pull/68)) 193 | * Add --info option ([#65](https://github.com/contentauth/c2pa-rs/pull/65)) 194 | * Updated publish workflow to upload binaries to GitHub ([#58](https://github.com/contentauth/c2pa-rs/pull/58)) 195 | * Fix Make release script & update readme ([#55](https://github.com/contentauth/c2pa-rs/pull/55)) 196 | * (Some version history omitted as we worked on some release process issues) 197 | 198 | ## 0.3.0 199 | _18 August 2022_ 200 | 201 | * (MINOR) Rework c2patool parameters ([#53](https://github.com/contentauth/c2pa-rs/pull/53)) 202 | * Update to 0.11.0 c2pa-rs ([#38](https://github.com/contentauth/c2pa-rs/pull/38)) 203 | * Remove Homebrew, Git installation methods, and add "update" wording ([#33](https://github.com/contentauth/c2pa-rs/pull/33)) 204 | 205 | ## 0.2.1 206 | _29 June 2022_ 207 | 208 | * Add BMFF support for video & etc ([#25](https://github.com/contentauth/c2pa-rs/pull/25)) 209 | 210 | ## 0.2.0 211 | _28 June 2022_ 212 | 213 | * (MINOR) Upgrade to c2pa Rust SDK version 0.6.0 ([#24](https://github.com/contentauth/c2pa-rs/pull/24)) 214 | * Fix an error in the README documentation ([#23](https://github.com/contentauth/c2pa-rs/pull/23)) 215 | * Display help if there are no arguments on the command line ([#21](https://github.com/contentauth/c2pa-rs/pull/21)) 216 | * Bump anyhow from 1.0.57 to 1.0.58 ([#17](https://github.com/contentauth/c2pa-rs/pull/17)) 217 | * Updates examples to use ta_url instead of ta ([#15](https://github.com/contentauth/c2pa-rs/pull/15)) 218 | 219 | ## 0.1.3 220 | _17 June 2022_ 221 | 222 | * Update to latest c2pa Rust SDK ([#12](https://github.com/contentauth/c2pa-rs/pull/12)) 223 | * Add built-in default certs to make getting started easier ([#9](https://github.com/contentauth/c2pa-rs/pull/9)) 224 | 225 | ## 0.1.2 226 | _10 June 2022_ 227 | 228 | * Update crate's description field 229 | 230 | ## 0.1.1 231 | _10 June 2022_ 232 | 233 | * Initial public release 234 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Adobe 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/callback_signer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Adobe. All rights reserved. 2 | // This file is licensed to you under the Apache License, 3 | // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 | // or the MIT license (http://opensource.org/licenses/MIT), 5 | // at your option. 6 | // Unless required by applicable law or agreed to in writing, 7 | // this software is distributed on an "AS IS" BASIS, WITHOUT 8 | // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 9 | // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 10 | // specific language governing permissions and limitations under 11 | // each license. 12 | 13 | use std::{ 14 | io::Write, 15 | path::PathBuf, 16 | process::{Command, Stdio}, 17 | }; 18 | 19 | use anyhow::{bail, Context}; 20 | use c2pa::{Error, Signer, SigningAlg}; 21 | 22 | use crate::signer::SignConfig; 23 | 24 | /// A struct that implements [SignCallback]. This struct will call out to the client provided 25 | /// external signer to get the signed bytes for the asset. 26 | pub(crate) struct ExternalProcessRunner { 27 | config: CallbackSignerConfig, 28 | signer_path: PathBuf, 29 | } 30 | 31 | impl ExternalProcessRunner { 32 | pub fn new(config: CallbackSignerConfig, signer_path: PathBuf) -> Self { 33 | Self { 34 | config, 35 | signer_path, 36 | } 37 | } 38 | } 39 | 40 | impl SignCallback for ExternalProcessRunner { 41 | /// Runs the client-provided [Command], passing to it, via stdin, the bytes to be signed. We 42 | /// also pass the `reserve-size`, `sign-cert`, and `alg` as CLI arguments to the [Command]. 43 | fn sign(&self, bytes: &[u8]) -> anyhow::Result> { 44 | let sign_cert = self 45 | .config 46 | .sign_cert_path 47 | .as_os_str() 48 | .to_str() 49 | .context("Unable to read sign_certs. Is the sign_cert path valid?")?; 50 | 51 | // Spawn external process provided by the `c2patool` client. 52 | let mut child = Command::new(&self.signer_path) 53 | .stdin(Stdio::piped()) 54 | .stdout(Stdio::piped()) 55 | .stderr(Stdio::piped()) 56 | .args(["--reserve-size", &self.config.reserve_size.to_string()]) 57 | .args(["--alg", &format!("{}", &self.config.alg)]) 58 | .args(["--sign-cert", sign_cert]) 59 | .spawn() 60 | .context(format!("Failed to run command at {:?}", self.signer_path))?; 61 | 62 | // Write claim bytes to spawned processes' `stdin`. 63 | child 64 | .stdin 65 | .take() 66 | .context("Failed to access `stdin` of external process")? 67 | .write_all(bytes) 68 | .context("Failed to write data to the provided external process")?; 69 | 70 | let output = child 71 | .wait_with_output() 72 | .context(format!("Failed to read stdout from {:?}", self.signer_path))?; 73 | 74 | if !output.status.success() { 75 | bail!(format!( 76 | "User supplied signer process failed. It's stderr output was: \n{}", 77 | String::from_utf8(output.stderr).unwrap_or_default() 78 | )); 79 | } 80 | 81 | let bytes = output.stdout; 82 | if bytes.is_empty() { 83 | bail!("User supplied process succeeded, but the external process did not write signature bytes to stdout"); 84 | } 85 | 86 | Ok(bytes) 87 | } 88 | } 89 | 90 | /// A config containing the required values for signing an asset with an external command. 91 | #[derive(Clone, Debug)] 92 | pub(crate) struct CallbackSignerConfig { 93 | /// Signing algorithm to use - must match the associated certs 94 | /// 95 | /// Must be one of [ ps256 | ps384 | ps51024 | es256 | es384 | es51024 | ed25519 ] 96 | pub alg: SigningAlg, 97 | /// A path to a file containing the signing cert required for signing 98 | pub sign_cert_path: PathBuf, 99 | /// Size of the claim bytes. 100 | pub reserve_size: usize, 101 | pub tsa_url: Option, 102 | } 103 | 104 | impl CallbackSignerConfig { 105 | /// Constructs a new [CallbackSignerConfig] using a manifest sign config, the name of an 106 | /// external process, and the reserve_size. 107 | pub fn new(sign_config: &SignConfig, reserve_size: usize) -> anyhow::Result { 108 | let alg = sign_config 109 | .alg 110 | .clone() 111 | .and_then(|alg| alg.parse::().ok()) 112 | .context("Invalid signing algorithm provided")?; 113 | 114 | let sign_cert_path = sign_config 115 | .sign_cert 116 | .clone() 117 | .context("Unable to load the provided sign_cert_path")?; 118 | 119 | Ok(CallbackSignerConfig { 120 | alg, 121 | sign_cert_path, 122 | reserve_size, 123 | tsa_url: sign_config.ta_url.clone(), 124 | }) 125 | } 126 | } 127 | 128 | #[cfg_attr(test, mockall::automock)] 129 | pub(crate) trait SignCallback { 130 | /// Method which will be called with the `data` to be signed. Implementors 131 | /// should return the signed bytes as an [anyhow::Result]. 132 | fn sign(&self, data: &[u8]) -> anyhow::Result>; 133 | } 134 | 135 | /// A [Signer] implementation that allows clients to provide their own function 136 | /// to sign the manifest bytes. 137 | pub(crate) struct CallbackSigner<'a> { 138 | callback: Box, 139 | config: CallbackSignerConfig, 140 | } 141 | 142 | impl<'a> CallbackSigner<'a> { 143 | pub fn new(callback: Box, config: CallbackSignerConfig) -> Self { 144 | Self { callback, config } 145 | } 146 | } 147 | 148 | impl Signer for CallbackSigner<'_> { 149 | fn sign(&self, data: &[u8]) -> c2pa::Result> { 150 | self.callback.sign(data).map_err(|e| { 151 | eprintln!("Unable to embed signature into asset. {}", e); 152 | Error::EmbeddingError 153 | }) 154 | } 155 | 156 | fn alg(&self) -> SigningAlg { 157 | self.config.alg 158 | } 159 | 160 | fn certs(&self) -> c2pa::Result>> { 161 | let cert_contents = std::fs::read(&self.config.sign_cert_path) 162 | .map_err(|_| Error::FileNotFound(format!("{:?}", self.config.sign_cert_path)))?; 163 | 164 | let mut pems = pem::parse_many(cert_contents).map_err(|_| Error::CoseInvalidCert)?; 165 | // [pem::parse_many] returns an empty vector if you supply invalid contents, like json, for example. 166 | // Check here if the pems vector is empty. 167 | if pems.is_empty() { 168 | return Err(Error::CoseInvalidCert); 169 | } 170 | 171 | let sign_cert = pems 172 | .drain(..) 173 | .map(|p| p.into_contents()) 174 | .collect::>>(); 175 | 176 | Ok(sign_cert) 177 | } 178 | 179 | fn reserve_size(&self) -> usize { 180 | self.config.reserve_size 181 | } 182 | 183 | fn time_authority_url(&self) -> Option { 184 | self.config.tsa_url.clone() 185 | } 186 | } 187 | 188 | #[cfg(test)] 189 | mod test { 190 | use anyhow::anyhow; 191 | 192 | use super::*; 193 | 194 | #[test] 195 | fn test_signing_succeeds_returns_bytes() { 196 | let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 197 | sign_cert_path.push("sample/es256_certs.pem"); 198 | 199 | let sign_config = SignConfig { 200 | alg: Some(SigningAlg::Es256.to_string()), 201 | sign_cert: Some(sign_cert_path), 202 | ..Default::default() 203 | }; 204 | 205 | let result = vec![1, 2, 3]; 206 | let expected = result.clone(); 207 | 208 | let mut mock_callback_signer = MockSignCallback::default(); 209 | mock_callback_signer 210 | .expect_sign() 211 | .returning(move |_| Ok(result.clone())); 212 | 213 | let config = CallbackSignerConfig::new(&sign_config, 1024).unwrap(); 214 | let callback = Box::new(mock_callback_signer); 215 | let signer = CallbackSigner::new(callback, config); 216 | 217 | assert_eq!(signer.sign(&[]).unwrap(), expected); 218 | } 219 | 220 | #[test] 221 | fn test_signing_succeeds_returns_error_embedding() { 222 | let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 223 | sign_cert_path.push("sample/es256_certs.pem"); 224 | 225 | let sign_config = SignConfig { 226 | alg: Some(SigningAlg::Es256.to_string()), 227 | sign_cert: Some(sign_cert_path), 228 | ..Default::default() 229 | }; 230 | 231 | let mut mock_callback_signer = MockSignCallback::default(); 232 | mock_callback_signer 233 | .expect_sign() 234 | .returning(|_| Err(anyhow!(""))); 235 | 236 | let config = CallbackSignerConfig::new(&sign_config, 1024).unwrap(); 237 | let callback = Box::new(mock_callback_signer); 238 | let signer = CallbackSigner::new(callback, config); 239 | 240 | assert!(matches!(signer.sign(&[]), Err(Error::EmbeddingError))); 241 | } 242 | 243 | #[test] 244 | fn test_sign_config_to_external_sign_config_fails() { 245 | let sign_config = SignConfig::default(); 246 | assert!(CallbackSignerConfig::new(&sign_config, 1024).is_err()); 247 | } 248 | 249 | #[test] 250 | fn test_sign_config_to_external_sign_config_fails_with_invalid_signing_alg() { 251 | let sign_config = SignConfig { 252 | alg: Some("invalid_signing_alg".to_owned()), 253 | ..Default::default() 254 | }; 255 | 256 | let result = CallbackSignerConfig::new(&sign_config, 1024); 257 | let error = result.err().unwrap(); 258 | assert_eq!(format!("{error}"), "Invalid signing algorithm provided") 259 | } 260 | 261 | #[test] 262 | fn test_sign_config_to_external_sign_config_fails_with_missing_sign_certs() { 263 | let sign_config = SignConfig { 264 | alg: Some(SigningAlg::Es256.to_string()), 265 | sign_cert: None, 266 | ..Default::default() 267 | }; 268 | 269 | let result = CallbackSignerConfig::new(&sign_config, 1024); 270 | let error = result.err().unwrap(); 271 | assert_eq!( 272 | format!("{error}"), 273 | "Unable to load the provided sign_cert_path" 274 | ) 275 | } 276 | 277 | #[test] 278 | fn test_try_from_succeeds_for_valid_sign_config() { 279 | let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 280 | sign_cert_path.push("sample/es256_certs.pem"); 281 | 282 | let expected_alg = SigningAlg::Es256; 283 | let sign_config = SignConfig { 284 | alg: Some(expected_alg.to_string()), 285 | sign_cert: Some(sign_cert_path), 286 | ..Default::default() 287 | }; 288 | 289 | let expected_reserve_size = 10248; 290 | let esc = CallbackSignerConfig::new(&sign_config, expected_reserve_size).unwrap(); 291 | let callback = Box::::default(); 292 | let signer = CallbackSigner::new(callback, esc); 293 | 294 | assert_eq!(signer.alg(), expected_alg); 295 | assert_eq!(signer.reserve_size(), expected_reserve_size); 296 | } 297 | 298 | #[test] 299 | fn test_callback_signer_error_file_not_found() { 300 | let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 301 | sign_cert_path.push("sample/NOT-HERE"); 302 | 303 | let sign_config = SignConfig { 304 | alg: Some(SigningAlg::Es256.to_string()), 305 | sign_cert: Some(sign_cert_path), 306 | ..Default::default() 307 | }; 308 | 309 | let config = CallbackSignerConfig::new(&sign_config, 10248).unwrap(); 310 | let callback = Box::::default(); 311 | let signer = CallbackSigner::new(callback, config); 312 | 313 | assert!(matches!(signer.certs(), Err(Error::FileNotFound(_)))); 314 | } 315 | 316 | #[test] 317 | fn test_callback_signer_error_invalid_cert() { 318 | let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 319 | sign_cert_path.push("sample/test.json"); 320 | 321 | let sign_config = SignConfig { 322 | alg: Some(SigningAlg::Es256.to_string()), 323 | sign_cert: Some(sign_cert_path), 324 | ..Default::default() 325 | }; 326 | 327 | let config = CallbackSignerConfig::new(&sign_config, 1024).unwrap(); 328 | let callback = Box::::default(); 329 | let signer = CallbackSigner::new(callback, config); 330 | 331 | assert!(matches!(signer.certs(), Err(Error::CoseInvalidCert))); 332 | } 333 | 334 | #[test] 335 | fn test_callback_signer_valid_sign_certs() { 336 | let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 337 | sign_cert_path.push("sample/es256_certs.pem"); 338 | 339 | let sign_config = SignConfig { 340 | alg: Some(SigningAlg::Es256.to_string()), 341 | sign_cert: Some(sign_cert_path), 342 | ..Default::default() 343 | }; 344 | 345 | let config = CallbackSignerConfig::new(&sign_config, 1024).unwrap(); 346 | let callback = Box::::default(); 347 | let signer = CallbackSigner::new(callback, config); 348 | 349 | assert_eq!(signer.certs().unwrap().len(), 2); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Using C2PA Tool 2 | 3 | C2PA Tool's command-line syntax is: 4 | 5 | ``` 6 | c2patool [OPTIONS] [COMMAND] 7 | ``` 8 | 9 | Where: 10 | - `OPTIONS` is one or more of the command-line options described in following table. 11 | - `` is the (relative or absolute) file path to the asset to read or embed a manifest into. 12 | - `[COMMAND]` is one of the optional subcommands: `trust`, `fragment`, or `help`. 13 | 14 | By default, c2patool writes a JSON representation of C2PA manifests found in the asset to the standard output. 15 | 16 | ## Subcommands 17 | 18 | The tool supports the following subcommands: 19 | - `trust` [configures trust support](#configuring-trust-support) for certificates on a "known certificate list." With this subcommand, several additional options are available. 20 | - `fragment` [adds a manifest to fragmented BMFF content](#adding-a-manifest-to-fragmented-bmff-content). With this subcommand, one additional option is available. 21 | - `help` displays command line help information. 22 | 23 | ## Options 24 | 25 | The following options are available with any (or no) subcommand. Additional options are available with each subcommand. 26 | 27 | | CLI option          | Short version | Argument | Description | 28 | |-----|----|----|----| 29 | | `--certs` | | N/A | Extract a certificate chain to standard output (stdout). | 30 | | `--config` | `-c` | `` | Specify a manifest definition as a JSON string. See [Providing a manifest definition on the command line](#providing-a-manifest-definition-on-the-command-line). | 31 | | `--detailed` | `-d` | N/A | Display detailed C2PA-formatted manifest data. See [Displaying a detailed manifest report](#detailed-manifest-report). | 32 | | `--force` | `-f` | N/A | Force overwriting output file. See [Forced overwrite](#forced-overwrite). | 33 | | `--help` | `-h` | N/A | Display CLI help information. | 34 | | `--info` | | N/A | Display brief information about the file. | 35 | | `--ingredient` | `-i` | N/A | Create an Ingredient definition in --output folder. | 36 | | `--output` | `-o` | `` | Path to output folder or file. See [Adding a manifest to an asset file](#adding-a-manifest-to-an-asset-file). | 37 | | `--manifest` | `-m` | `` | Specify a manifest file to add to an asset file. See [Adding a manifest to an asset file](#adding-a-manifest-to-an-asset-file). 38 | | `--no_signing_verify` | None | N/A | Do not validate the signature after signing an asset, which speeds up signing. See [Speeding up signing](#speeding-up-signing) | 39 | | `--parent` | `-p` | `` | Path to parent file. See [Specifying a parent file](#specifying-a-parent-file). | 40 | | `--remote` | `-r` | `` | URL for remote manifest available over HTTP. See [Generating a remote manifest](#generating-a-remote-manifest)| N/A? | 41 | | `--reserve-size` | N/A | Only valid with `--signer-path` argument. The amount of memory to reserve for signing. Default: 20000. For more information, see CLI help. | 42 | | `--sidecar` | `-s` | N/A | Put manifest in external "sidecar" file with `.c2pa` extension. See [Generating an external manifest](#generating-an-external-manifest). | 43 | | `--signer-path` | N/A | Specify path to command-line executable for signing. See [Signing claim bytes with your own signer](#signing-claim-bytes-with-your-own-signer). | 44 | | `--tree` | | N/A | Create a tree diagram of the manifest store. | 45 | | `--version` | `-V` | N/A | Display version information. | 46 | 47 | ## Displaying manifest data 48 | 49 | To display the manifest associated with an asset file, provide the path to the file as the argument; for example: 50 | 51 | ```shell 52 | c2patool sample/C.jpg 53 | ``` 54 | 55 | The tool displays the manifest JSON to standard output (stdout). 56 | 57 | Use the `--output` argument to write the contents of the manifest, (including the manifest's assertion and ingredient thumbnails) to the specified directory. 58 | 59 | ```shell 60 | c2patool sample/C.jpg --output ./report 61 | ``` 62 | 63 | ### Detailed manifest report 64 | 65 | Use the `-d` option to display a detailed report describing the internal C2PA format of manifests contained in the asset; for example, using one of the example images in the `sample` directory: 66 | 67 | ```shell 68 | c2patool sample/C.jpg -d 69 | ``` 70 | 71 | By default, the tool displays the detailed report to standard output (stdout). If you specify an output folder, the tool saves it to a file named `detailed.json` in that folder. 72 | 73 | ### Displaying an information report 74 | 75 | Use the `--info` option to print a high-level report about the asset file and related C2PA data. 76 | For a cloud manifest the tool displays the URL to the manifest. 77 | Displays the size of the manifest store and number of manifests. 78 | It will report if the manifest validated or show any errors encountered in validation. 79 | 80 | 81 | ```shell 82 | c2patool sample/C.jpg --info 83 | ``` 84 | 85 | The tool displays the report to standard output (stdout). 86 | 87 | ## Creating an ingredient from a file 88 | 89 | The `--ingredient` option creates an ingredient report. When used with the `--output` folder, it extracts or creates a thumbnail image and a binary `.c2pa` manifest store containing the C2PA data from the file. The JSON ingredient this produces can be added to a manifest definition to carry the full history and validation record of that asset into a newly-created manifest. 90 | 91 | Provide the path to the file as the argument; for example: 92 | 93 | ```shell 94 | c2patool sample/C.jpg --ingredient --output ./ingredient 95 | ``` 96 | 97 | ## Adding a manifest to an asset file 98 | 99 | Use the `--manifest` / `-m` option to add the C2PA manifest definition file specified in the argument to the asset file to be signed. Specify the output file as the argument to the `--output` / `-o` option. The output extension type must match the source. The tool will not convert between file types. For example: 100 | 101 | ```shell 102 | c2patool sample/image.jpg -m sample/test.json -o signed_image.jpg 103 | ``` 104 | 105 | The tool generates a new manifest using the values given in the file and displays the manifest store to standard output (stdout). 106 | 107 | CAUTION: If the output file is the same as the source file, the tool will overwrite the source file. 108 | 109 | If the manifest definition file has `private_key` and `sign_cert` fields, then the tool signs the manifest using the private key and certificate they specify, respectively. Otherwise, the tool uses the built-in test certificate and key, which is suitable ONLY for development and testing. You can also specify the private key and certificate using environment variables; for more information, see [Creating and using an X.509 certificate](x_509.md). 110 | 111 | **WARNING**: Accessing the private key and signing certificate directly like this is fine during development, but doing so in production may be insecure. Instead use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key; for example as show in the [C2PA Python Example](https://github.com/contentauth/c2pa-python-example). 112 | 113 | ### Specifying a parent file 114 | 115 | A _parent file_ represents the state of the image before the current edits were made. 116 | 117 | Specify a parent file as the argument to the `--parent` / `-p` option; for example: 118 | 119 | ```shell 120 | c2patool sample/image.jpg -m sample/test.json -p sample/c.jpg -o signed_image.jpg 121 | ``` 122 | 123 | You can pass an ingredient generated with the `--ingredient` option by giving the folder or ingredient JSON file. 124 | 125 | ```shell 126 | c2patool sample/C.jpg --ingredient --output ./ingredient 127 | 128 | c2patool sample/image.jpg -m sample/test.json -p ./ingredient -o signed_image.jpg 129 | ``` 130 | 131 | ### Forced overwrite 132 | 133 | The tool will return an error if the output file already exists. Use the `--force` / `-f` option to force overwriting the output file. For example: 134 | 135 | ```shell 136 | c2patool sample/image.jpg -m sample/test.json -f -o signed_image.jpg 137 | ``` 138 | 139 | ## Generating an external manifest 140 | 141 | Use the `--sidecar` / `-s` option to put the manifest in an external sidecar file in the same location as the output file. The manifest will have the same output filename but with a `.c2pa` extension. The tool will copy the output file but the original will be untouched. 142 | 143 | ```shell 144 | c2patool sample/image.jpg -s -m sample/test.json -o signed_image.jpg 145 | ``` 146 | ## Generating a remote manifest 147 | 148 | Use the `--remote` / `-r` option to place an HTTP reference to the manifest in the output file. The manifest is returned as an external sidecar file in the same location as the output file with the same filename but with a `.c2pa` extension. Place the manifest at the location specified by the `-r` option. When using remote manifests the remote URL should be publicly accessible to be most useful to users. When verifying an asset, remote manifests are automatically fetched. 149 | 150 | ```shell 151 | c2patool sample/image.jpg -r http://my_server/myasset.c2pa -m sample/test.json -o signed_image.jpg 152 | ``` 153 | 154 | In the example above, the tool will embed the URL `http://my_server/myasset.c2pa` in `signed_image.jpg` then fetch the manifest from that URL and save it to `signed_image.c2pa`. 155 | 156 | If you use both the `-s` and `-r` options, the tool embeds a manifest in the output file and also adds the remote reference. 157 | 158 | ## Signing claim bytes with your own signer 159 | 160 | When generating a manifest, if the private key is not accessible on the system on which you are running the tool, use the `--signer-path` argument to specify the path to an executable that performs signing. 161 | This executable receives the claim bytes (the bytes to be signed) from standard input (`stdin`) and outputs the signature bytes to standard output (`stdout`). 162 | 163 | For example, the following command signs the asset's claim bytes by using an executable named `custom-signer`: 164 | 165 | ```shell 166 | c2patool sample/image.jpg \ 167 | --manifest sample/test.json \ 168 | --output sample/signed-image.jpg \ 169 | --signer-path ./custom-signer \ 170 | --reserve-size 20248 \ 171 | -f 172 | ``` 173 | 174 | For information on calculating the value of the `--reserve-size` argument, see `c2patool --help`. 175 | 176 | ## Providing a manifest definition on the command line 177 | 178 | To provide the manifest definition in a command line argument instead of a file, use the `--config` / `-c` option. 179 | 180 | For example, the following command adds a custom assertion called "org.contentauth.test". 181 | 182 | ```shell 183 | c2patool sample/image.jpg \ 184 | -c '{"assertions": \ 185 | [{"label": "org.contentauth.test", \ 186 | "data": {"my_key": "whatever I want"}}]}' 187 | ``` 188 | 189 | ## Speeding up signing 190 | 191 | By default, `c2patool` validates the signature immediately after signing a manifest. To disable this and speed up the validation process, use the `--no_signing_verify` option. 192 | 193 | ## Configuring trust support 194 | 195 | Enable trust support by using the `trust` subcommand, as follows: 196 | 197 | ``` 198 | c2patool [path] trust [OPTIONS] 199 | ``` 200 | 201 | When the `trust` subcommand is supplied, should c2patool encounter a problem with validating any of the claims in the asset, its JSON output will contain a `validation_status` field whose value is an array of objects, each describing a validation problem. 202 | 203 | ### Additional options 204 | 205 | Several additional CLI options are available with the `trust` sub-command to specify the location of files containing the trust anchors list or known certificate list, as described in the following table. You can also use environment variables to specify these values. 206 | 207 |
208 | 209 | | Option | Environment variable | Description | 210 | | ------ | -------------------- | ----------- | 211 | | `--trust_anchors` | `C2PATOOL_TRUST_ANCHORS` | URL or relative path to a file containing a list of trust anchors (in PEM format) used to validate the manifest certificate chain. To be valid, the manifest certificate chain must lead to a certificate on the trust list. All certificates in the trust anchor list must have the [Basic Constraints extension](https://docs.digicert.com/en/iot-trust-manager/certificate-templates/create-json-formatted-certificate-templates/extensions/basic-constraints.html) and the CA attribute of this extension must be `True`. | 212 | | `--allowed_list` | `C2PATOOL_ALLOWED_LIST` | URL or relative path to a file containing a list of end-entity certificates (in PEM format) to trust. These certificates are used to sign the manifest. Supersedes the `trust_anchors` setting. The list must NOT contain certificates with the [Basic Constraints extension](https://docs.digicert.com/en/iot-trust-manager/certificate-templates/create-json-formatted-certificate-templates/extensions/basic-constraints.html) with the CA attribute `True`. | 213 | | `--trust_config` | `C2PATOOL_TRUST_CONFIG` | URL or relative path to a file containing the allowed set of custom certificate extended key usages (EKUs). Each entry in the list is an object identifiers in [OID dot notation](http://www.oid-info.com/#oid) format. | 214 | 215 |
216 | 217 | For example: 218 | 219 | ```shell 220 | c2patool sample/C.jpg trust \ 221 | --allowed_list sample/allowed_list.pem \ 222 | --trust_config sample/store.cfg 223 | ``` 224 | 225 | Another example with URL argument values: 226 | 227 | ```shell 228 | c2patool sample/C.jpg trust \ 229 | --trust_anchors https://server.com/anchors.pem \ 230 | --trust_config https://server.com/store.cfg 231 | ``` 232 | 233 | ### Using the Verify known certificate list 234 | 235 | **IMPORTANT:** The C2PA intends to publish an official trust list. Until that time, the [C2PA Verify tool uses a temporary known certificate list](https://opensource.contentauthenticity.org/docs/verify-known-cert-list). These lists are subject to change, and will be deprecated when C2PA publishes its trust list. 236 | 237 | To configure C2PA tool to use the Verify temporary known certificate list, set the following environment variables on your system: 238 | 239 | ```shell 240 | export C2PATOOL_TRUST_ANCHORS='https://contentcredentials.org/trust/anchors.pem' 241 | export C2PATOOL_ALLOWED_LIST='https://contentcredentials.org/trust/allowed.sha256.txt' 242 | export C2PATOOL_TRUST_CONFIG='https://contentcredentials.org/trust/store.cfg' 243 | ``` 244 | 245 | **Note:** When these environment variables are set, C2PA Tool will make several HTTP requests each time it runs. Since these lists may change without notice (and the allowed list may change quite often), check these lists frequently to stay in sync with the Verify site. However, when performing bulk operations, you may want to cache these files locally to avoid a large number of network calls that might affect performance. 246 | 247 | You can then run: 248 | 249 | ```shell 250 | c2patool sample/C.jpg trust 251 | ``` 252 | 253 | You can also specify these values as CLI arguments instead: 254 | 255 | ```shell 256 | c2patool sample/C.jpg trust \ 257 | --trust_anchors='https://contentcredentials.org/trust/anchors.pem' \ 258 | --allowed_list='https://contentcredentials.org/trust/allowed.sha256.txt' \ 259 | --trust_config='https://contentcredentials.org/trust/store.cfg' 260 | ``` 261 | 262 | **Note:** This sample image should show a `signingCredential.untrusted` validation status since the test signing certificate used to sign them is not contained on the trust lists above. 263 | 264 | ## Adding a manifest to fragmented BMFF content 265 | 266 | The ISO base media file format (BMFF) is a container file format that defines a structure for files that contain time-based multimedia data such as video and audio. 267 | 268 | Add a manifest to a fragmented BMFF file by using the `fragment` subcommand, as follows: 269 | 270 | ``` 271 | c2patool fragment [--fragments_glob] 272 | ``` 273 | 274 | Where `` is a [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)). 275 | 276 | For example, to add manifest to a video file: 277 | 278 | ``` 279 | c2patool -m test2.json -o /1080p_out \ 280 | /Downloads/1080p/avc1/init.mp4 \ 281 | fragment --fragments_glob "seg-*[0-9].m4s" 282 | ``` 283 | 284 | Or to verify a manifest and fragments: 285 | ``` 286 | c2patool /Downloads/1080p_out/avc1/init.mp4 \ 287 | fragment --fragments_glob "seg-*[0-9].m4s" 288 | ``` 289 | 290 | ### Additional option 291 | 292 | The `--fragments_glob` option is only available with the `fragment` subcommand and specifies the glob pattern to find the fragments of the asset. The path is automatically set to be the same as the "init" segment, so the pattern must match only segment file names, not full paths. -------------------------------------------------------------------------------- /tests/fixtures/sample1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Adobe. All rights reserved. 2 | // This file is licensed to you under the Apache License, 3 | // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 | // or the MIT license (http://opensource.org/licenses/MIT), 5 | // at your option. 6 | // Unless required by applicable law or agreed to in writing, 7 | // this software is distributed on an "AS IS" BASIS, WITHOUT 8 | // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 9 | // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 10 | // specific language governing permissions and limitations under 11 | // each license. 12 | 13 | #![doc = include_str!("../README.md")] 14 | 15 | /// Tool to display and create C2PA manifests 16 | /// 17 | /// A file path to an asset must be provided 18 | /// If only the path is given, this will generate a summary report of any claims in that file 19 | /// If a manifest definition json file is specified, the claim will be added to any existing claims 20 | use std::{ 21 | fs::{create_dir_all, remove_dir_all, File}, 22 | io::Write, 23 | path::{Path, PathBuf}, 24 | str::FromStr, 25 | }; 26 | 27 | use anyhow::{anyhow, bail, Context, Result}; 28 | use c2pa::{Builder, ClaimGeneratorInfo, Error, Ingredient, ManifestDefinition, Reader, Signer}; 29 | use clap::{Parser, Subcommand}; 30 | use log::debug; 31 | use serde::Deserialize; 32 | use signer::SignConfig; 33 | use url::Url; 34 | 35 | use crate::{ 36 | callback_signer::{CallbackSigner, CallbackSignerConfig, ExternalProcessRunner}, 37 | info::info, 38 | }; 39 | 40 | mod info; 41 | mod tree; 42 | 43 | mod callback_signer; 44 | mod signer; 45 | 46 | /// Tool for displaying and creating C2PA manifests. 47 | #[derive(Parser, Debug)] 48 | #[command(author, version, about, long_about = None, arg_required_else_help(true))] 49 | struct CliArgs { 50 | /// Path to manifest definition JSON file. 51 | #[clap(short, long, requires = "output")] 52 | manifest: Option, 53 | 54 | /// Path to output file or folder. 55 | #[clap(short, long)] 56 | output: Option, 57 | 58 | /// Path to a parent file. 59 | #[clap(short, long)] 60 | parent: Option, 61 | 62 | /// Manifest definition passed as a JSON string. 63 | #[clap(short, long, conflicts_with = "manifest")] 64 | config: Option, 65 | 66 | /// Display detailed C2PA-formatted manifest data. 67 | #[clap(short, long)] 68 | detailed: bool, 69 | 70 | /// Force overwrite of output if it already exists. 71 | #[clap(short, long)] 72 | force: bool, 73 | 74 | /// The path to an asset to examine or embed a manifest into. 75 | path: PathBuf, 76 | 77 | /// Embed remote URL manifest reference. 78 | #[clap(short, long)] 79 | remote: Option, 80 | 81 | /// Generate a sidecar (.c2pa) manifest 82 | #[clap(short, long)] 83 | sidecar: bool, 84 | 85 | /// Write ingredient report and assets to a folder. 86 | #[clap(short, long)] 87 | ingredient: bool, 88 | 89 | /// Create a tree diagram of the manifest store. 90 | #[clap(long)] 91 | tree: bool, 92 | 93 | /// Extract certificate chain. 94 | #[clap(long = "certs")] 95 | cert_chain: bool, 96 | 97 | /// Do not perform validation of signature after signing 98 | #[clap(long = "no_signing_verify")] 99 | no_signing_verify: bool, 100 | 101 | #[command(subcommand)] 102 | command: Option, 103 | 104 | /// Show manifest size, XMP url and other stats. 105 | #[clap(long)] 106 | info: bool, 107 | 108 | /// Path to an executable that will sign the claim bytes. 109 | #[clap(long)] 110 | signer_path: Option, 111 | 112 | /// To be used with the [callback_signer] argument. This value should at least: size of CoseSign1 CBOR + 113 | /// the size of certificate chain provided in the manifest definition's `sign_cert` field + the size of the 114 | /// signature of the Time Stamp Authority response. A typical size of CoseSign1 CBOR is in the 1-2K range. If 115 | /// the reserve size is too small an error will be returned during signing. 116 | /// For example: 117 | /// 118 | /// The reserve-size can be calculated like this if you aren't including a `tsa_url` key in 119 | /// your manifest description: 120 | /// 121 | /// 1024 + sign_cert.len() 122 | /// 123 | /// Or, if you are including a `tsa_url` in your manifest definition, you will calculate the 124 | /// reserve size like this: 125 | /// 126 | /// 1024 + sign_cert.len() + tsa_signature_response.len() 127 | /// 128 | /// Note: 129 | /// We'll default the `reserve-size` to a value of 20_000, if no value is provided. This 130 | /// will probably leave extra `0`s of unused space. Please specify a reserve-size if possible. 131 | #[clap(long, default_value("20000"))] 132 | reserve_size: usize, 133 | } 134 | 135 | #[derive(Clone, Debug)] 136 | enum TrustResource { 137 | File(PathBuf), 138 | Url(Url), 139 | } 140 | 141 | fn parse_resource_string(s: &str) -> Result { 142 | if let Ok(url) = s.parse::() { 143 | Ok(TrustResource::Url(url)) 144 | } else { 145 | let p = PathBuf::from_str(s)?; 146 | 147 | Ok(TrustResource::File(p)) 148 | } 149 | } 150 | 151 | // We only construct one per invocation, not worth shrinking this. 152 | #[allow(clippy::large_enum_variant)] 153 | #[derive(Debug, Subcommand)] 154 | #[allow(clippy::large_enum_variant)] 155 | enum Commands { 156 | /// Sub-command to configure trust store options, "trust --help for more details" 157 | Trust { 158 | /// URL or path to file containing list of trust anchors in PEM format 159 | #[arg(long = "trust_anchors", env="C2PATOOL_TRUST_ANCHORS", value_parser = parse_resource_string)] 160 | trust_anchors: Option, 161 | 162 | /// URL or path to file containing specific manifest signing certificates in PEM format to implicitly trust 163 | #[arg(long = "allowed_list", env="C2PATOOL_ALLOWED_LIST", value_parser = parse_resource_string)] 164 | allowed_list: Option, 165 | 166 | /// URL or path to file containing configured EKUs in Oid dot notation 167 | #[arg(long = "trust_config", env="C2PATOOL_TRUST_CONFIG", value_parser = parse_resource_string)] 168 | trust_config: Option, 169 | }, 170 | /// Sub-command to add manifest to fragmented BMFF content 171 | /// 172 | /// The init path can be a glob to process entire directories of content, for example: 173 | /// 174 | /// c2patool -m test2.json -o /my_output_folder "/my_renditions/**/my_init.mp4" fragment --fragments_glob "myfile_abc*[0-9].m4s" 175 | /// 176 | /// Note: the glob patterns are quoted to prevent shell expansion. 177 | Fragment { 178 | /// Glob pattern to find the fragments of the asset. The path is automatically set to be the same as 179 | /// the init segment. 180 | /// 181 | /// The fragments_glob pattern should only match fragment file names not the full paths (e.g. "myfile_abc*[0-9].m4s" 182 | /// to match [myfile_abc1.m4s, myfile_abc2180.m4s, ...] ) 183 | #[arg(long = "fragments_glob", verbatim_doc_comment)] 184 | fragments_glob: Option, 185 | }, 186 | } 187 | 188 | #[derive(Debug, Default, Deserialize)] 189 | // Add fields that are not part of the standard Manifest 190 | struct ManifestDef { 191 | #[serde(flatten)] 192 | manifest: ManifestDefinition, 193 | // allows adding ingredients with file paths 194 | ingredient_paths: Option>, 195 | } 196 | 197 | // convert certain errors to output messages 198 | fn special_errs(e: c2pa::Error) -> anyhow::Error { 199 | match e { 200 | Error::JumbfNotFound => anyhow!("No claim found"), 201 | Error::FileNotFound(name) => anyhow!("File not found: {}", name), 202 | Error::UnsupportedType => anyhow!("Unsupported file type"), 203 | Error::PrereleaseError => anyhow!("Prerelease claim found"), 204 | _ => e.into(), 205 | } 206 | } 207 | 208 | // normalize extensions so we can compare them 209 | fn ext_normal(path: &Path) -> String { 210 | let ext = path 211 | .extension() 212 | .unwrap_or_default() 213 | .to_str() 214 | .unwrap_or_default() 215 | .to_lowercase(); 216 | match ext.as_str() { 217 | "jpeg" => "jpg".to_string(), 218 | "tiff" => "tif".to_string(), 219 | _ => ext, 220 | } 221 | } 222 | 223 | // loads an ingredient, allowing for a folder or json ingredient 224 | fn load_ingredient(path: &Path) -> Result { 225 | // if the path is a folder, look for ingredient.json 226 | let mut path_buf = PathBuf::from(path); 227 | let path = if path.is_dir() { 228 | path_buf = path_buf.join("ingredient.json"); 229 | path_buf.as_path() 230 | } else { 231 | path 232 | }; 233 | if path.extension() == Some(std::ffi::OsStr::new("json")) { 234 | let json = std::fs::read_to_string(path)?; 235 | let mut ingredient: Ingredient = serde_json::from_slice(json.as_bytes())?; 236 | if let Some(base) = path.parent() { 237 | ingredient.resources_mut().set_base_path(base); 238 | } 239 | Ok(ingredient) 240 | } else { 241 | Ok(Ingredient::from_file(path)?) 242 | } 243 | } 244 | 245 | fn load_trust_resource(resource: &TrustResource) -> Result { 246 | match resource { 247 | TrustResource::File(path) => { 248 | let data = std::fs::read_to_string(path) 249 | .with_context(|| format!("Failed to read trust resource from path: {:?}", path))?; 250 | 251 | Ok(data) 252 | } 253 | TrustResource::Url(url) => { 254 | let data = reqwest::blocking::get(url.to_string())? 255 | .text() 256 | .with_context(|| format!("Failed to read trust resource from URL: {}", url))?; 257 | 258 | Ok(data) 259 | } 260 | } 261 | } 262 | 263 | fn configure_sdk(args: &CliArgs) -> Result<()> { 264 | const TA: &str = r#"{"trust": { "trust_anchors": replacement_val } }"#; 265 | const AL: &str = r#"{"trust": { "allowed_list": replacement_val } }"#; 266 | const TC: &str = r#"{"trust": { "trust_config": replacement_val } }"#; 267 | const VS: &str = r#"{"verify": { "verify_after_sign": replacement_val } }"#; 268 | 269 | let mut enable_trust_checks = false; 270 | 271 | if let Some(Commands::Trust { 272 | trust_anchors, 273 | allowed_list, 274 | trust_config, 275 | }) = &args.command 276 | { 277 | if let Some(trust_list) = &trust_anchors { 278 | let data = load_trust_resource(trust_list)?; 279 | debug!("Using trust anchors from {:?}", trust_list); 280 | let replacement_val = serde_json::Value::String(data).to_string(); // escape string 281 | let setting = TA.replace("replacement_val", &replacement_val); 282 | 283 | c2pa::settings::load_settings_from_str(&setting, "json")?; 284 | 285 | enable_trust_checks = true; 286 | } 287 | 288 | if let Some(allowed_list) = &allowed_list { 289 | let data = load_trust_resource(allowed_list)?; 290 | debug!("Using allowed list from {:?}", allowed_list); 291 | let replacement_val = serde_json::Value::String(data).to_string(); // escape string 292 | let setting = AL.replace("replacement_val", &replacement_val); 293 | 294 | c2pa::settings::load_settings_from_str(&setting, "json")?; 295 | 296 | enable_trust_checks = true; 297 | } 298 | 299 | if let Some(trust_config) = &trust_config { 300 | let data = load_trust_resource(trust_config)?; 301 | debug!("Using trust config from {:?}", trust_config); 302 | let replacement_val = serde_json::Value::String(data).to_string(); // escape string 303 | let setting = TC.replace("replacement_val", &replacement_val); 304 | 305 | c2pa::settings::load_settings_from_str(&setting, "json")?; 306 | 307 | enable_trust_checks = true; 308 | } 309 | } 310 | 311 | // if any trust setting is provided enable the trust checks 312 | if enable_trust_checks { 313 | c2pa::settings::load_settings_from_str(r#"{"verify": { "verify_trust": true} }"#, "json")?; 314 | } else { 315 | c2pa::settings::load_settings_from_str(r#"{"verify": { "verify_trust": false} }"#, "json")?; 316 | } 317 | 318 | // enable or disable verification after signing 319 | { 320 | let replacement_val = serde_json::Value::Bool(!args.no_signing_verify).to_string(); 321 | let setting = VS.replace("replacement_val", &replacement_val); 322 | 323 | c2pa::settings::load_settings_from_str(&setting, "json")?; 324 | } 325 | 326 | Ok(()) 327 | } 328 | 329 | fn sign_fragmented( 330 | builder: &mut Builder, 331 | signer: &dyn Signer, 332 | init_pattern: &Path, 333 | frag_pattern: &PathBuf, 334 | output_path: &Path, 335 | ) -> Result<()> { 336 | // search folders for init segments 337 | let ip = init_pattern.to_str().ok_or(c2pa::Error::OtherError( 338 | "could not parse source pattern".into(), 339 | ))?; 340 | let inits = glob::glob(ip).context("could not process glob pattern")?; 341 | let mut count = 0; 342 | for init in inits { 343 | match init { 344 | Ok(p) => { 345 | let mut fragments = Vec::new(); 346 | let init_dir = p.parent().context("init segment had no parent dir")?; 347 | let seg_glob = init_dir.join(frag_pattern); // segment match pattern 348 | 349 | // grab the fragments that go with this init segment 350 | let seg_glob_str = seg_glob.to_str().context("fragment path not valid")?; 351 | let seg_paths = glob::glob(seg_glob_str).context("fragment glob not valid")?; 352 | for seg in seg_paths { 353 | match seg { 354 | Ok(f) => fragments.push(f), 355 | Err(_) => return Err(anyhow!("fragment path not valid")), 356 | } 357 | } 358 | 359 | println!("Adding manifest to: {:?}", p); 360 | let new_output_path = 361 | output_path.join(init_dir.file_name().context("invalid file name")?); 362 | builder.sign_fragmented_files(signer, &p, &fragments, &new_output_path)?; 363 | 364 | count += 1; 365 | } 366 | Err(_) => bail!("bad path to init segment"), 367 | } 368 | } 369 | if count == 0 { 370 | println!("No files matching pattern: {}", ip); 371 | } 372 | Ok(()) 373 | } 374 | 375 | fn verify_fragmented(init_pattern: &Path, frag_pattern: &Path) -> Result> { 376 | let mut readers = Vec::new(); 377 | 378 | let ip = init_pattern 379 | .to_str() 380 | .context("could not parse source pattern")?; 381 | let inits = glob::glob(ip).context("could not process glob pattern")?; 382 | let mut count = 0; 383 | 384 | // search folders for init segments 385 | for init in inits { 386 | match init { 387 | Ok(p) => { 388 | let mut fragments = Vec::new(); 389 | let init_dir = p.parent().context("init segment had no parent dir")?; 390 | let seg_glob = init_dir.join(frag_pattern); // segment match pattern 391 | 392 | // grab the fragments that go with this init segment 393 | let seg_glob_str = seg_glob.to_str().context("fragment path not valid")?; 394 | let seg_paths = glob::glob(seg_glob_str).context("fragment glob not valid")?; 395 | for seg in seg_paths { 396 | match seg { 397 | Ok(f) => fragments.push(f), 398 | Err(_) => return Err(anyhow!("fragment path not valid")), 399 | } 400 | } 401 | 402 | println!("Verifying manifest: {:?}", p); 403 | let reader = Reader::from_fragmented_files(p, &fragments)?; 404 | if let Some(vs) = reader.validation_status() { 405 | if let Some(e) = vs.iter().find(|v| !v.passed()) { 406 | eprintln!("Error validating segments: {:?}", e); 407 | return Ok(readers); 408 | } 409 | } 410 | 411 | readers.push(reader); 412 | 413 | count += 1; 414 | } 415 | Err(_) => bail!("bad path to init segment"), 416 | } 417 | } 418 | 419 | if count == 0 { 420 | println!("No files matching pattern: {}", ip); 421 | } 422 | 423 | Ok(readers) 424 | } 425 | 426 | fn main() -> Result<()> { 427 | let args = CliArgs::parse(); 428 | 429 | // set RUST_LOG=debug to get detailed debug logging 430 | if std::env::var("RUST_LOG").is_err() { 431 | std::env::set_var("RUST_LOG", "error"); 432 | } 433 | env_logger::init(); 434 | 435 | let path = &args.path; 436 | 437 | if args.info { 438 | return info(path); 439 | } 440 | 441 | if args.cert_chain { 442 | let reader = Reader::from_file(path).map_err(special_errs)?; 443 | if let Some(manifest) = reader.active_manifest() { 444 | if let Some(si) = manifest.signature_info() { 445 | println!("{}", si.cert_chain()); 446 | // todo: add ocsp validation info 447 | return Ok(()); 448 | } 449 | } 450 | bail!("No certificate chain found"); 451 | } 452 | 453 | if args.tree { 454 | println!("{}", tree::tree(path)?); 455 | return Ok(()); 456 | } 457 | 458 | let is_fragment = matches!( 459 | &args.command, 460 | Some(Commands::Fragment { fragments_glob: _ }) 461 | ); 462 | 463 | // make sure path is not a glob when not fragmented 464 | if !args.path.is_file() && !is_fragment { 465 | bail!("glob patterns only allowed when using \"fragment\" command") 466 | } 467 | 468 | // configure the SDK 469 | configure_sdk(&args).context("Could not configure c2pa-rs")?; 470 | 471 | // Remove manifest needs to also remove XMP provenance 472 | // if args.remove_manifest { 473 | // match args.output { 474 | // Some(output) => { 475 | // if output.exists() && !args.force { 476 | // bail!("Output already exists, use -f/force to force write"); 477 | // } 478 | // if path != &output { 479 | // std::fs::copy(path, &output)?; 480 | // } 481 | // Manifest::remove_manifest(&output)? 482 | // }, 483 | // None => { 484 | // bail!("The -o/--output argument is required for this operation"); 485 | // } 486 | // } 487 | // return Ok(()); 488 | // } 489 | 490 | // if we have a manifest config, process it 491 | if args.manifest.is_some() || args.config.is_some() { 492 | // read the json from file or config, and get base path if from file 493 | let (json, base_path) = match args.manifest.as_deref() { 494 | Some(manifest_path) => { 495 | let base_path = std::fs::canonicalize(manifest_path)? 496 | .parent() 497 | .map(|p| p.to_path_buf()); 498 | (std::fs::read_to_string(manifest_path)?, base_path) 499 | } 500 | None => ( 501 | args.config.unwrap_or_default(), 502 | std::env::current_dir().ok(), 503 | ), 504 | }; 505 | 506 | // read the signing information from the manifest definition 507 | let mut sign_config = SignConfig::from_json(&json)?; 508 | 509 | // read the manifest information 510 | let manifest_def: ManifestDef = serde_json::from_slice(json.as_bytes())?; 511 | let mut builder = Builder::from_json(&json)?; 512 | let mut manifest = manifest_def.manifest; 513 | 514 | // add claim_tool generator so we know this was created using this tool 515 | let mut tool_generator = ClaimGeneratorInfo::new(env!("CARGO_PKG_NAME")); 516 | tool_generator.set_version(env!("CARGO_PKG_VERSION")); 517 | if !manifest.claim_generator_info.is_empty() 518 | || manifest.claim_generator_info[0].name == "c2pa-rs" 519 | { 520 | manifest.claim_generator_info = vec![tool_generator]; 521 | } else { 522 | manifest.claim_generator_info.insert(1, tool_generator); 523 | } 524 | println!("claim generator {:?}", manifest.claim_generator_info); 525 | // set manifest base path before ingredients so ingredients can override it 526 | if let Some(base) = base_path.as_ref() { 527 | builder.base_path = Some(base.clone()); 528 | sign_config.set_base_path(base); 529 | } 530 | 531 | // Add any ingredients specified as file paths 532 | if let Some(paths) = manifest_def.ingredient_paths { 533 | for mut path in paths { 534 | // ingredient paths are relative to the manifest path 535 | if let Some(base) = &base_path { 536 | if !(path.is_absolute()) { 537 | path = base.join(&path) 538 | } 539 | } 540 | let ingredient = load_ingredient(&path)?; 541 | builder.add_ingredient(ingredient); 542 | } 543 | } 544 | 545 | if let Some(parent_path) = args.parent { 546 | let mut ingredient = load_ingredient(&parent_path)?; 547 | ingredient.set_is_parent(); 548 | builder.add_ingredient(ingredient); 549 | } 550 | 551 | // If the source file has a manifest store, and no parent is specified treat the source as a parent. 552 | // note: This could be treated as an update manifest eventually since the image is the same 553 | let has_parent = builder.definition.ingredients.iter().any(|i| i.is_parent()); 554 | if !has_parent && !is_fragment { 555 | let mut source_ingredient = Ingredient::from_file(&args.path)?; 556 | if source_ingredient.manifest_data().is_some() { 557 | source_ingredient.set_is_parent(); 558 | builder.add_ingredient(source_ingredient); 559 | } 560 | } 561 | 562 | if let Some(remote) = args.remote { 563 | if args.sidecar { 564 | builder.set_no_embed(true); 565 | builder.set_remote_url(remote); 566 | } else { 567 | builder.set_remote_url(remote); 568 | } 569 | } else if args.sidecar { 570 | builder.set_no_embed(true); 571 | } 572 | 573 | let signer = if let Some(signer_process_name) = args.signer_path { 574 | let cb_config = CallbackSignerConfig::new(&sign_config, args.reserve_size)?; 575 | 576 | let process_runner = Box::new(ExternalProcessRunner::new( 577 | cb_config.clone(), 578 | signer_process_name, 579 | )); 580 | let signer = CallbackSigner::new(process_runner, cb_config); 581 | 582 | Box::new(signer) 583 | } else { 584 | sign_config.signer()? 585 | }; 586 | 587 | if let Some(output) = args.output { 588 | // fragmented embedding 589 | if let Some(Commands::Fragment { fragments_glob }) = &args.command { 590 | if output.exists() && !output.is_dir() { 591 | bail!("Output cannot point to existing file, must be a directory"); 592 | } 593 | 594 | if let Some(fg) = &fragments_glob { 595 | return sign_fragmented(&mut builder, signer.as_ref(), &args.path, fg, &output); 596 | } else { 597 | bail!("fragments_glob must be set"); 598 | } 599 | } else { 600 | if ext_normal(&output) != ext_normal(&args.path) { 601 | bail!("Output type must match source type"); 602 | } 603 | if output.exists() && !args.force { 604 | bail!("Output already exists, use -f/force to force write"); 605 | } 606 | 607 | if output.file_name().is_none() { 608 | bail!("Missing filename on output"); 609 | } 610 | if output.extension().is_none() { 611 | bail!("Missing extension output"); 612 | } 613 | 614 | #[allow(deprecated)] // todo: remove when we can 615 | builder 616 | .sign_file(signer.as_ref(), &args.path, &output) 617 | .context("embedding manifest")?; 618 | 619 | // generate a report on the output file 620 | let reader = Reader::from_file(&output).map_err(special_errs)?; 621 | if args.detailed { 622 | println!("{:#?}", reader); 623 | } else { 624 | println!("{}", reader) 625 | } 626 | } 627 | } else { 628 | bail!("Output path required with manifest definition") 629 | } 630 | } else if args.parent.is_some() || args.sidecar || args.remote.is_some() { 631 | bail!("Manifest definition required with these options or flags") 632 | } else if let Some(output) = args.output { 633 | if output.is_file() || output.extension().is_some() { 634 | bail!("Output must be a folder for this option.") 635 | } 636 | if output.exists() { 637 | if args.force { 638 | remove_dir_all(&output)?; 639 | } else { 640 | bail!("Output already exists, use -f/force to force write"); 641 | } 642 | } 643 | create_dir_all(&output)?; 644 | if args.ingredient { 645 | let report = Ingredient::from_file_with_folder(&args.path, &output) 646 | .map_err(special_errs)? 647 | .to_string(); 648 | File::create(output.join("ingredient.json"))?.write_all(&report.into_bytes())?; 649 | println!("Ingredient report written to the directory {:?}", &output); 650 | } else { 651 | let reader = Reader::from_file(&args.path).map_err(special_errs)?; 652 | reader.to_folder(&output)?; 653 | let report = reader.to_string(); 654 | if args.detailed { 655 | // for a detailed report first call the above to generate the thumbnails 656 | // then call this to add the detailed report 657 | let detailed = format!( 658 | "{:#?}", 659 | Reader::from_file(&args.path).map_err(special_errs)? 660 | ); 661 | File::create(output.join("detailed.json"))?.write_all(&detailed.into_bytes())?; 662 | } 663 | File::create(output.join("manifest_store.json"))?.write_all(&report.into_bytes())?; 664 | println!("Manifest report written to the directory {:?}", &output); 665 | } 666 | } else if args.ingredient { 667 | println!( 668 | "{}", 669 | Ingredient::from_file(&args.path).map_err(special_errs)? 670 | ) 671 | } else if args.detailed { 672 | println!( 673 | "{:#?}", 674 | Reader::from_file(&args.path).map_err(special_errs)? 675 | ) 676 | } else if let Some(Commands::Fragment { 677 | fragments_glob: Some(fg), 678 | }) = &args.command 679 | { 680 | let stores = verify_fragmented(&args.path, fg)?; 681 | if stores.len() == 1 { 682 | println!("{}", stores[0]); 683 | } else { 684 | println!("{} Init manifests validated", stores.len()); 685 | } 686 | } else { 687 | println!("{}", Reader::from_file(&args.path).map_err(special_errs)?) 688 | } 689 | 690 | Ok(()) 691 | } 692 | 693 | #[cfg(test)] 694 | pub mod tests { 695 | #![allow(clippy::unwrap_used)] 696 | 697 | use super::*; 698 | 699 | const CONFIG: &str = r#"{ 700 | "alg": "es256", 701 | "private_key": "es256_private.key", 702 | "sign_cert": "es256_certs.pem", 703 | "ta_url": "http://timestamp.digicert.com", 704 | "assertions": [ 705 | { 706 | "label": "org.contentauth.test", 707 | "data": {"my_key": "whatever I want"} 708 | } 709 | ] 710 | }"#; 711 | 712 | #[test] 713 | fn test_manifest_config() { 714 | const SOURCE_PATH: &str = "tests/fixtures/earth_apollo17.jpg"; 715 | const OUTPUT_PATH: &str = "target/tmp/unit_out.jpg"; 716 | create_dir_all("target/tmp").expect("create_dir"); 717 | std::fs::remove_file(OUTPUT_PATH).ok(); // remove output file if it exists 718 | let mut builder = Builder::from_json(CONFIG).expect("from_json"); 719 | 720 | let signer = SignConfig::from_json(CONFIG) 721 | .unwrap() 722 | .set_base_path("sample") 723 | .signer() 724 | .expect("get_signer"); 725 | 726 | #[allow(deprecated)] // todo: remove when we can 727 | let _result = builder 728 | .sign_file(signer.as_ref(), SOURCE_PATH, OUTPUT_PATH) 729 | .expect("embed"); 730 | 731 | let ms = Reader::from_file(OUTPUT_PATH) 732 | .expect("from_file") 733 | .to_string(); 734 | println!("{}", ms); 735 | //let ms = report_from_path(&OUTPUT_PATH, false).expect("report_from_path"); 736 | assert!(ms.contains("my_key")); 737 | } 738 | } 739 | --------------------------------------------------------------------------------