commit 65e259bd8172cf60c578d209ab90aa78e94832fa
Author: Eric Scouten <scouten@adobe.com>
Date: Mon, 23 May 2022 14:21:30 -0700
Initial public release
Diffstat:
125 files changed, 24820 insertions(+), 0 deletions(-)
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,7 @@
+## Changes in This Pull Request
+_Give a narrative description of what has been changed._
+
+## Checklist
+- [ ] This PR represents a single feature, fix, or change.
+- [ ] All applicable changes have been documented.
+- [ ] Any `TO DO` items (or similar) have been entered as GitHub issues and the link to that issue has been included in a comment.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
@@ -0,0 +1,175 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches: main
+
+jobs:
+ tests:
+ name: Unit tests
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [windows-latest, macos-latest, ubuntu-latest]
+ rust_version: [stable]
+ # Temporarily reduce build matrix to save runtime cost.
+ # rust_version: [1.59.0, stable]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: ${{ matrix.rust_version }}
+ override: true
+
+ - name: Cache Rust dependencies
+ uses: Swatinem/rust-cache@v1
+
+ - name: Run self tests
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ # args: --all-targets --all-features --workspace (waiting on bug fix)
+ args: --all-features --workspace
+
+ wasm_tests:
+ name: Wasm tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+
+ - name: Install wasm-pack
+ run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+
+ - name: Run Wasm tests
+ run: wasm-pack test --chrome --headless
+ working-directory: ./sdk
+
+ clippy_check:
+ name: Clippy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v1
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+
+ - name: Install clippy
+ run: rustup component add clippy
+
+ - name: Cache Rust dependencies
+ uses: Swatinem/rust-cache@v1
+
+ - uses: actions-rs/clippy-check@v1
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ args: --all-features --all-targets -- -D warnings
+ env:
+ RUST_BACKTRACE: "1"
+
+ cargo_fmt:
+ name: Enforce Rust code format
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - name: Install stable toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ components: rustfmt
+ - uses: actions-rs/cargo@v1
+ with:
+ command: fmt
+ args: --all -- --check
+
+ doc_format:
+ name: Doc format
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v1
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+
+ - name: Cache Rust dependencies
+ uses: Swatinem/rust-cache@v1
+
+ - name: Run cargo docs
+ uses: actions-rs/cargo@v1
+ with:
+ command: doc
+ args: --no-deps
+
+ cargo-deny:
+ name: License / vulnerability audit
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ checks:
+ - advisories
+ - bans licenses sources
+
+ # Prevent sudden announcement of a new advisory from failing ci:
+ continue-on-error: ${{ matrix.checks == 'advisories' }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+
+ - name: Install cargo deny
+ run: cargo install cargo-deny
+
+ - name: Audit crate dependencies
+ uses: EmbarkStudios/cargo-deny-action@v1
+ with:
+ command: check ${{ matrix.checks }}
+
+ unused_deps:
+ name: Check for unused dependencies
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v1
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: nightly
+ override: true
+
+ - name: Run cargo-udeps
+ uses: aig787/cargo-udeps-action@v1
+ with:
+ version: latest
+ args: --all-targets
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,16 @@
+/target/
+
+Cargo.lock
+
+**/*.rs.bk
+
+.DS_Store
+.idea
+
+.x509
+.ec
+.rsa
+.ed
+.es509
+
+.vscode
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog for v0.x Series
+
+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.
+
+## v0.1.0
+_23 May 2022_
+
+* Initial public release.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
@@ -0,0 +1,74 @@
+# Adobe Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language.
+* Being respectful of differing viewpoints and experiences.
+* Gracefully accepting constructive criticism.
+* Focusing on what is best for the community.
+* Showing empathy towards other community members.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+advances.
+* Trolling, insulting/derogatory comments, and personal or political attacks.
+* Public or private harassment.
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission.
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting.
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at Grp-opensourceoffice@adobe.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at [https://contributor-covenant.org/version/1/4][version].
+
+[homepage]: https://contributor-covenant.org
+[version]: https://contributor-covenant.org/version/1/4/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
@@ -0,0 +1,51 @@
+# Contributing
+
+We welcome contributions to this project!
+
+Before you start, we ask that you understand the following guidelines.
+
+## Code of Conduct
+
+This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating,
+you are expected to uphold this code. Please report unacceptable behavior to
+[Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com).
+
+## Have a Question?
+
+Start by filing an issue. The existing committers on this project work to reach
+consensus around project direction and issue solutions within issue threads
+(when appropriate).
+
+## Contributor License Agreement
+
+All third-party contributions to this project must be accompanied by a signed contributor
+license agreement. This gives Adobe permission to redistribute your contributions
+as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You
+only need to submit an Adobe CLA one time, so if you have submitted one previously,
+you are good to go!
+
+## Code Reviews
+
+All submissions should come in the form of pull requests and need to be reviewed
+by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/)
+for more information on sending pull requests.
+
+Code submissions will need to pass all automated tests in place at the time of submission.
+These include such things as Rust code format, Clippy/lint checks, and unit test coverage.
+
+We encourage you to raise an issue in GitHub before starting work on a major addition to the crate.
+This will give us an opportunity to discuss API design and avoid duplicate efforts.
+
+## From Contributor to Committer
+
+We love contributions from our community! If you'd like to go a step beyond contributor
+and become a committer with full write access and a say in the project, you must
+be invited to the project. The existing committers employ an internal nomination
+process that must reach lazy consensus (silence is approval) before invitations
+are issued. If you feel you are qualified and want to get more deeply involved,
+feel free to reach out to existing committers to have a conversation about that.
+
+## Security Issues
+
+Security issues shouldn't be reported on this issue tracker. Instead,
+[file an issue to our security experts](https://helpx.adobe.com/security/alertus.html).
diff --git a/Cargo.toml b/Cargo.toml
@@ -0,0 +1,2 @@
+[workspace]
+members = ["sdk", "c2patool"]
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2020 Adobe
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/LICENSE-MIT b/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+© Copyright 2020 Adobe. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
@@ -0,0 +1,85 @@
+# C2PA Rust SDK and Command-Line Tool
+
+The **[Coalition for Content Provenance and Authenticity](https://c2pa.org)** (C2PA) addresses the prevalence of misleading information online through the development of technical standards for certifying the source and history (or provenance) of media content. C2PA is a Joint Development Foundation project, formed through an alliance between Adobe, Arm, Intel, Microsoft and Truepic.
+
+This Rust library and command-line tool for creating and inspecting C2PA data structures are created by Adobe and other contributors as part of our work on the [Content Authenticity Initiative](https://contentauthenticity.org).
+
+## Key Features
+
+* Creation and signing of C2PA [claims](https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_claims) and [manifests](https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_manifests).
+* Embedding manifests in certain file formats.
+* Parsing and validation of manifests found in certain file formats.
+* Support for several common C2PA [assertions](https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_c2pa_standard_assertions).
+* [Hard binding](https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_hard_bindings) support.
+
+## State of the Project
+
+This project is in a "soft launch" state as of May 2022.
+
+We have been using this crate as the foundation of our Content Authenticity Initiative-related products and services since late 2020, so we have considerable experience with this code ourselves.
+
+That said, we spent most of that time focused on our own internal requirements. As we shift toward making this crate available for open usage, we're aware that there is quite a bit of work to do to create what we'd feel comfortable calling a 1.0 release. We've decided to err on the side of releasing earlier so that people can experiment with it and give us feedback.
+
+We expect to do work on a number of areas in the next few months while we remain in prerelease (0.x) versions. Some broad categories of work (and thus things you might expect to change) are:
+
+* We'll be reviewing and refining our APIs for ease of use and comprehension. We'd appreciate feedback on areas that you find confusing or unnecessarily difficult.
+* We'll also be reviewing our APIs for compliance with Rust community best practices. There are some areas (for example, use of public fields and how we take ownership vs references) where we know some work is required.
+* Our documentation is incomplete. We'll be working on refining the documentation.
+* Our testing infrastructure is incomplete. We'll be working on improving test coverage, memory efficiency, and performance benchmarks.
+
+While in prerelease form, we'll increment the minor version number (0.x.0) when we make breaking API changes and we expect that this will happen with some frequency.
+
+## What's Implemented and Not Implemented?
+
+* This crate implements a subset of the [C2PA 1.0 technical specification](https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html).
+* This crate currently only supports still image formats (JPEG and PNG).
+* We haven't yet implemented parsing of identity structures (verifiable credentials).
+
+## Usage
+
+Add this to your `Cargo.toml`:
+
+```toml
+[dependencies]
+c2pa = "0.1"
+```
+
+## Crate Features
+
+* `async_signer` enables signing via asynchronous services which require `async` support.
+* `file_io` enables manifest generation, signing via OpenSSL, and embedding manifests in various file formats.
+
+## Rust Version Requirements
+
+This crate requires **Rust version 1.58.0** or newer.
+
+## Supported Platforms
+
+We have tested it on recent versions of the following operating systems:
+
+* Windows
+* MacOS (Intel and Apple Silicon)
+* Ubuntu Linux
+* WASM (note that claim _generation_ is not available on WASM)
+
+## What Feedback Do We Seek?
+
+We would welcome feedback on:
+
+* API design
+* prioritization of upcoming development, especially:
+ * file format support
+ * assertion support
+* optimizations and performance concerns
+* bugs or non-compliance with the C2PA spec
+* additional platform support
+
+If you would like to contribute to this crate, please read our [code of conduct](./CODE_OF_CONDUCT.md) and [contribution guidelines](./CONTRIBUTING.md).
+
+## License
+
+The `c2pa` crate is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
+
+See [LICENSE-APACHE](./LICENSE-APACHE) and [LICENSE-MIT](./LICENSE-MIT).
+
+Note that some components and dependent crates are licensed under different terms; please check the license terms for each crate and component for details.
diff --git a/c2patool/Cargo.toml b/c2patool/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "c2patool"
+version = "0.1.0"
+authors = ["Gavin Peacock <gpeacock@adobe.com>", "Maurice Fisher <mfisher@adobe.com>"]
+license = "MIT OR Apache-2.0"
+edition = "2018"
+rust-version = "1.58.0"
+
+[dependencies]
+anyhow = "1.0"
+c2pa = { path = "../sdk", features = ["file_io"] }
+dirs = "4.0"
+env_logger = "0.9"
+log = "0.4"
+serde = { version = "1.0", features = ["derive"] }
+serde_derive = "1.0"
+serde_json = "1.0"
+structopt = "0.3"
+tempfile = "3.3"
+
+[dev-dependencies]
+assert_cmd = "2.0"
+predicates = "2.1"
diff --git a/c2patool/sample/es256_certs.pem b/c2patool/sample/es256_certs.pem
@@ -0,0 +1,39 @@
+Bag Attributes
+ localKeyID: 21 9D 38 2E 7C 25 38 78 94 3F CA DA 5A A7 BC BA 3F 7F 24 21
+subject=/O=Media Publisher Company/CN=Bob
+issuer=/O=Media Publisher Company/CN=Media Publisher Company Intermediate CA
+-----BEGIN CERTIFICATE-----
+MIICQDCCAaGgAwIBAgIUXsGqKw4Bw9PJBv1BcL3SLGiyCT4wCgYIKoZIzj0EAwIw
+VDEgMB4GA1UECgwXTWVkaWEgUHVibGlzaGVyIENvbXBhbnkxMDAuBgNVBAMMJ01l
+ZGlhIFB1Ymxpc2hlciBDb21wYW55IEludGVybWVkaWF0ZSBDQTAeFw0yMjA0MDQx
+NDE1NDhaFw0yMzA0MDQxNDE1NDhaMDAxIDAeBgNVBAoMF01lZGlhIFB1Ymxpc2hl
+ciBDb21wYW55MQwwCgYDVQQDDANCb2IwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AAQv+8qHEYohEJwVLKQIqJX6se0E+CKk89bwRYRRfCsmHeZuFLdBcJ082I67mpSM
+zzFN1eSX82Mdphb7+t/uIKPko3UwczAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/
+BAwwCgYIKwYBBQUHAwQwCQYDVR0TBAIwADAdBgNVHQ4EFgQUTqgwjgSAknNN8T+e
+bNXm+mbtlfIwHwYDVR0jBBgwFoAUXF8IatqTvlwmGnVmM2L6+v1IQMcwCgYIKoZI
+zj0EAwIDgYwAMIGIAkIAkZ0LAaJ209QLyiSn/hIMfbBReg+d61gX8U+9OqBWYiD2
+i6u59mJrKdwCuj8po8jh7ntkcXHc1v+3ztHWCHCI9R0CQgErPKUhrxei5mbKU0Xx
+NUsTBB6oHMZccZCn1FS0R7YaCFume2mscC1rGGNXqu/Skgsq6FPkFHJqyFTZhtcW
+pJWKaw==
+-----END CERTIFICATE-----
+Bag Attributes: <No Attributes>
+subject=/O=Media Publisher Company/CN=Media Publisher Company Intermediate CA
+issuer=/CN=Media Provenance Intermediate CA 1
+-----BEGIN CERTIFICATE-----
+MIICbTCCAc+gAwIBAgIUA7qQpsd9jsBL7dahNfBx+ftJ5VQwCgYIKoZIzj0EAwQw
+LTErMCkGA1UEAwwiTWVkaWEgUHJvdmVuYW5jZSBJbnRlcm1lZGlhdGUgQ0EgMTAe
+Fw0yMjA0MDQxNDE1MDRaFw0zMjAzMzExNDE1MDRaMFQxIDAeBgNVBAoMF01lZGlh
+IFB1Ymxpc2hlciBDb21wYW55MTAwLgYDVQQDDCdNZWRpYSBQdWJsaXNoZXIgQ29t
+cGFueSBJbnRlcm1lZGlhdGUgQ0EwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABABo
+g4jSfIvYPwpADEOiQjWOSD5KXTJl9k/gz0vVpE1D3gdn5TK2UiuEiKiiZvND45pi
+U/TW0jVs6Rfns7mBTKEAygFGbcjEVqMZTfXcWYIi2AvLARe/HCeVMO3x5g4AmDmr
+CgshTHWNwrit6u/ae9YdOv5QdqBLKW6NRdvv4jvpZpK/QKNjMGEwHQYDVR0OBBYE
+FFxfCGrak75cJhp1ZjNi+vr9SEDHMB8GA1UdIwQYMBaAFP59tM4KPiaHG81hkWz1
+1CoFfPYiMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49
+BAMEA4GLADCBhwJCAeYlt/gDEOc0Y5NFLTcf3tOrwYR+m4Gn+bbgrdKznU2ygm0P
+PqKHjCT+bMZmO7NJHHcwRg245AeKp1tCJ/nM/3+FAkFMQq5CaD6l7meYE+8wFtlD
+vXlpFWg3ryuroc/DDBFMyrTZKWCz3wl5bLfx4GoWvdsrFBdClrhTO2srJmmQKH6t
+QA==
+-----END CERTIFICATE-----
+
diff --git a/c2patool/sample/es256_private.key b/c2patool/sample/es256_private.key
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIL2yf62xPPFBKbDcBEKU5HIickmG2DEDcZt0lD3N4aOLoAoGCCqGSM49
+AwEHoUQDQgAEL/vKhxGKIRCcFSykCKiV+rHtBPgipPPW8EWEUXwrJh3mbhS3QXCd
+PNiOu5qUjM8xTdXkl/NjHaYW+/rf7iCj5A==
+-----END EC PRIVATE KEY-----
diff --git a/c2patool/src/README.md b/c2patool/src/README.md
@@ -0,0 +1,160 @@
+# c2paTool
+
+Command line tool for displaying and adding C2PA manifests
+A file path to a JPEG or a claim definition JSON file must be provided
+If a JPEG path is given, this will generate a summary report of any manifests in that file
+If a manifest definition JSON file is specified, the manifest will be created and displayed in a JSON report
+
+## Displaying Manifest data
+
+Invoking the tool with a path to an image file will output a JSON report of the Manifests in the file
+File formats supported are jpeg and png.
+
+```c2patool image.jpg```
+
+## Displaying detailed Manifest data
+
+The -d option will output a detailed JSON report of the internal C2PA structure
+
+```c2patool image.jpg -d```
+
+## Previewing a Manifest
+
+If a path to a manifest def json file is given,
+the tool will generate a new manifest using the values given in definition
+this will display the results but not save anything unless an output (-o) is specified
+
+```c2patool claim.json```
+
+The manifest definition json can also be passed on the command line as string using the -c --create option
+
+```c2patool -c '{"vendor": "myvendor", "claim_generator": "MyApplication", "assertions": [{"label": "myvendor.assertion", "data": {"name": "Jane Doe"}}]}'```
+
+## Creating a new output image
+
+A file path for creating an output file with any added claim data
+If the output file already exists, any C2PA data in that file will be replaced and the image maintained
+If the output file doesn't exist, a parent file must be available for a source image
+If you are not changing an image and just adding C2PA data, use an existing output file and no parent
+If you have edited an image and want to add C2PA data to it, pass the original as the parent
+and put the edited file at the output location to have the C2PA data added.
+
+```c2patool claim.json -o output.jpg```
+## Overriding the parent file
+
+When using a json file, the parent file can be specified by passing -p or --parent with the path to the file
+This allows adding the same manifest data to different source images
+
+## Working with .c2pa manifest files
+
+If the extension of the output file is '.c2pa' a standalone manifest store will be written
+
+```c2patool claim_image.jpg -o manifest.c2pa```
+
+These .c2pa manifest files can be read by claim tool and will generate reports.
+
+```c2patool manifest.c2pa```
+## Setup
+
+Before you can add a manifest, you need to create an SSL certificate
+By default, c2patool expects to find temp_key.pem and temp_key in the user's ".cai" folder.
+The location of this folder can be changed by setting the CAI_KEY_PATH environment variable.
+This expects RSA/RSA_PSS certificates and private key. It will create signatures as PS256.
+
+```set CAI_KEY_PATH="~/mykeys"```
+
+The key and cert can also be placed in the environment variables CAI_PRIVATE_KEY and CAI_PUB_CERT
+These two variable are used to set the private key and public certificates. When using these variables
+the CAI_SIGNING_ALGORITHM must also be set to one of [ ps256 | ps384 | ps512 | es256 | es384 | es512 | ed25519] and
+must be compatible with values of CAI_PRIVATE_KEY and CAI_PUB_CERT. For example to sign with es256 signatures
+using the content of a private key file and certificate file:
+
+```set CAI_SIGNING_ALGORITHM=es256```
+```set CAI_PRIVATE_KEY=$(cat my_es256_private_key)```
+```set CAI_PUB_CERT=$(cat my_es256_certs)```
+
+The both CAI_PRIVATE_KEY and CAI_PUB_CERT should be in PEM format. CAI_PUB_CERT should contain a certificate
+chain PEMs starting for the end-entity certificate used to sign the claim ending with intermediate certificate
+before the root CA certificate. See ```sample`` folder for example certificates.
+
+To create temporary files for testing you can execute the following command
+
+```
+mkdir -p ~/.cai ; sudo openssl req -new -newkey rsa:4096 -sigopt rsa_padding_mode:pss -days 180 -extensions v3_ca -addext "keyUsage = digitalSignature" -addext "extendedKeyUsage = emailProtection" -nodes -x509 -keyout ~/.cai/temp_key.pem -out ~/.cai/temp_key.pub -sha256 ; sudo chmod 644 ~/.cai/temp_key.pem
+```
+
+Note you may have need to update your openssl version if the above command does not work.
+
+c2patool can also timestamp the signature data that is embedded. This is useful for validating an asset when the embedded
+certificates have expired. If c2patool finds the CAI_TA_URL environment variable set, c2patool will attempt to timestamp the signature using the TA service at the provided URL. The TA must be RFC3161 compliant. Example TSA setting:
+
+```set CAI_TA_URL=http://timestamp.digicert.com```
+
+## Manifest definition file format
+
+The manifest definition file is a JSON formatted file with a .json extension:
+
+The schema for this type is as follows:
+```json
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ns.adobe.com/cai/claim-definition/v1",
+ "type": "object",
+ "description": "Definition format for claim created with c2patool",
+ "examples": [
+ {
+ "vendor": "myvendor",
+ "claim_generator": "My Application",
+ "title" : "My Title",
+ "parent": "image.jpg",
+ "ingredients": [],
+ "assertions": [
+ {
+ "label": "my.assertion",
+ "data": {
+ "any_tag": "whatever I want"
+ }
+ }
+ ]
+ }
+ ],
+ "required": [
+ "vendor",
+ "claim_generator",
+ "assertions",
+ ],
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "description": "typically Internet domain name (without the TLD) for the vendor (i.e. `adobe`, `nytimes`)"
+ },
+ "claim_generator": {
+ "type": "string",
+ "description": "a UserAgent string that will let a user know what software/hardware/system produced this Manifest - names should not contain spaces"
+ },
+ "title": {
+ "type": "string",
+ "description": "a human-readable string to be displayed as the tile for this Manifest (defaults to embedded file name)"
+ },
+ "credentials": {
+ "type": "object",
+ "description": "array of W3C verifiable credentials objects defined in the c2pa assertion specification. Section 7"
+ },
+ "parent": {
+ "type": "string",
+ "format": "local file system path",
+ "description": "a file path to the source image that was modified by this Manifest (if any)"
+ },
+ "Ingredients": {
+ "type": "array of string",
+ "format": "array of local file system paths",
+ "description": "file paths to images that were used to modify the image referenced by this Manifest (if any)"
+ },
+ "assertions": {
+ "type": "object",
+ "description": "object with label, and data - an object with any value as defined in the c2pa assertion specification"
+ },
+ },
+ "additionalProperties": false
+}
+```
diff --git a/c2patool/src/claim_def.rs b/c2patool/src/claim_def.rs
@@ -0,0 +1,38 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+use c2pa::ManifestAssertion;
+
+use serde::Deserialize;
+use serde_json::Value;
+use std::path::PathBuf;
+
+/// A `ClaimDef` defines the components used to build a claim
+#[derive(Debug, Deserialize)]
+pub struct ClaimDef {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub vendor: Option<String>,
+ #[serde(alias = "recorder")]
+ pub claim_generator: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub title: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub parent: Option<PathBuf>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub credentials: Option<Vec<Value>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub ingredients: Option<Vec<PathBuf>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub assertions: Vec<ManifestAssertion>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub alg: Option<String>,
+}
diff --git a/c2patool/src/main.rs b/c2patool/src/main.rs
@@ -0,0 +1,281 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+/// Tool to display and create C2PA manifests
+///
+/// A file path to a jpeg must be provided
+/// If only the path is given, this will generate a summary report of any claims in that file
+/// If a claim def json file is specified, the claim will be added to any existing claims
+/// If the claim def includes an asset_path, the claims in that file will be used instead
+///
+use anyhow::Result;
+use c2pa::{Error, Ingredient, Manifest, ManifestStore, ManifestStoreReport};
+
+use std::{
+ fs,
+ path::{Path, PathBuf},
+ process::exit,
+};
+use structopt::StructOpt;
+use tempfile::tempdir;
+
+mod claim_def;
+use claim_def::ClaimDef;
+mod signer;
+use signer::get_test_signer;
+
+// define the command line options
+#[derive(Debug, StructOpt)]
+#[structopt(author = "Adobe", about = "Tool for displaying and creating C2PA manifests",setting = structopt::clap::AppSettings::ColoredHelp)]
+struct CliArgs {
+ #[structopt(parse(from_os_str))]
+ #[structopt(short = "o", long = "output", help = "path to output file")]
+ output: Option<std::path::PathBuf>,
+
+ #[structopt(parse(from_os_str))]
+ #[structopt(short = "p", long = "parent", help = "path to parent file")]
+ parent: Option<std::path::PathBuf>,
+
+ #[structopt(
+ short = "c",
+ long = "claimdef",
+ help = "claim definition passed as json string"
+ )]
+ claim_def: Option<String>,
+
+ #[structopt(
+ short = "d",
+ long = "detailed",
+ help = "display detailed internal manifest data"
+ )]
+ detailed: bool,
+
+ /// The path to the file to read (jpg or json for adding claims)
+ #[structopt(parse(from_os_str))]
+ path: Option<std::path::PathBuf>,
+}
+
+// converts any relative paths to absolute from base_path
+fn fix_relative_path(path: &Path, base_path: &Path) -> PathBuf {
+ let mut p = PathBuf::from(base_path);
+ p.push(path);
+ p
+}
+
+fn handle_claim_def(
+ json: &str,
+ base_dir: &Path,
+ parent: Option<&Path>,
+ output_opt: Option<&Path>,
+ is_detailed: bool,
+) -> Result<()> {
+ let claim_def: ClaimDef = serde_json::from_str(json)?;
+
+ let mut manifest = Manifest::new(claim_def.claim_generator);
+
+ if let Some(vendor) = claim_def.vendor {
+ manifest.set_vendor(vendor);
+ }
+
+ if let Some(credentials) = claim_def.credentials.as_ref() {
+ for credential in credentials {
+ manifest.add_verifiable_credential(credential)?;
+ }
+ }
+
+ // if claim_def has a parent, set the parent asset
+ let parent = match parent {
+ Some(parent) => Some(PathBuf::from(parent)),
+ None => claim_def
+ .parent
+ .as_deref()
+ .map(|parent| fix_relative_path(parent, base_dir)),
+ };
+ if let Some(parent) = parent.as_ref() {
+ if !parent.exists() {
+ eprintln!("Parent file not found {:#?}", parent);
+ exit(1);
+ }
+ manifest.set_parent(Ingredient::from_file(parent)?)?;
+ }
+
+ // add all the ingredients (claim def ingredients do not include the parent)
+ if let Some(ingredients) = claim_def.ingredients.as_ref() {
+ for ingredient in ingredients {
+ let path = fix_relative_path(ingredient, base_dir);
+ if !path.exists() {
+ eprintln!("Ingredient file not found {:#?}", path);
+ exit(1);
+ }
+ let ingredient = Ingredient::from_file(&path).unwrap_or_else(|e| {
+ eprintln!("error loading ingredient {:?} {:?}", &path, e);
+ exit(1);
+ });
+ manifest.add_ingredient(ingredient);
+ }
+ }
+
+ // add any assertions
+ for assertion in claim_def.assertions {
+ manifest.add_labeled_assertion(&assertion.label, &assertion.data)?;
+ }
+
+ // if we have an output option, then we must have a source image to add a claim to
+ // we need to determine the source file and copy it a temporary location where we will update it
+ // once successfully written we can copy the temp back to the output location, possibly overwriting
+ // The source can be an existing file at the output_path, or the parent file if we have one.
+ if let Some(output) = output_opt {
+ let file_name = match output.file_name().and_then(|s| s.to_str()) {
+ Some(name) => name,
+ None => {
+ eprintln!("Missing or invalid filename on output");
+ exit(1);
+ }
+ };
+ // check for valid extension and do special extension handling
+ let _extension = match output.extension().and_then(|s| s.to_str()) {
+ Some(ext) => ext,
+ None => {
+ eprintln!("Missing or invalid extension on output");
+ exit(1);
+ }
+ };
+
+ // get asset info from the output path (even it it doesn't exist yet)
+ let mut asset = Ingredient::from_file_info(output);
+ if let Some(t) = claim_def.title.as_ref() {
+ asset.set_title(t.to_owned());
+ };
+ manifest.set_asset(asset);
+
+ // select source from output or fallback to parent
+ let source_path = match output.exists() {
+ true => output,
+ false => {
+ parent.as_deref().filter(|p| p.exists()).or_else(||{
+ eprintln!("A valid parent path or existing output file is required for claim embedding");
+ exit(1);
+ }).unwrap()
+ }
+ };
+
+ // embed to a temporary file and then rename or copy back to the output
+ // so we never have a half written manifest
+ let dir = tempdir()?;
+ // temp file_name must match output file name, it is used as the claim title
+ let temp_path = dir.path().join(&file_name);
+
+ let signer = get_test_signer()?;
+
+ manifest
+ .embed(source_path, &temp_path, signer.as_ref())
+ .unwrap_or_else(|e| {
+ eprintln!("error embedding manifest: {:?}", e);
+ exit(1);
+ });
+
+ // embed completed successfully, now rename to the target path
+ std::fs::rename(&temp_path, &output)
+ // if rename fails, try to copy in case we are on different volumes
+ .or_else(|_| std::fs::copy(&temp_path, &output).and(Ok(())))
+ .map_err(Error::IoError)?;
+
+ // print a report on the output file
+ report_from_path(&output, is_detailed);
+
+ Ok(())
+ } else {
+ if is_detailed {
+ eprintln!("detailed report not supported for preview")
+ } else {
+ println!("{}", ManifestStore::from_manifest(&manifest)?);
+ }
+ Ok(())
+ }
+}
+
+// prints the requested kind of report or exits with error
+fn report_from_path<P: AsRef<Path>>(path: &P, is_detailed: bool) {
+ let report = match is_detailed {
+ true => ManifestStoreReport::from_file(path).map(|r| r.to_string()),
+ false => ManifestStore::from_file(path).map(|r| r.to_string()),
+ };
+ match report {
+ Ok(report) => {
+ println!("{}", report);
+ }
+ Err(Error::JumbfNotFound) | Err(Error::LogStop) => {
+ println!("No claim found");
+ exit(1)
+ }
+ Err(Error::PrereleaseError) => {
+ eprintln!("Prerelease claim found");
+ exit(1)
+ }
+ Err(e) => {
+ println!("Error Loading {:?} {:?}", &path.as_ref(), e);
+ exit(1);
+ }
+ }
+}
+
+fn main() -> Result<()> {
+ let args = CliArgs::from_args();
+
+ // set RUST_LOG=debug to get detailed debug logging
+ if std::env::var("RUST_LOG").is_err() {
+ std::env::set_var("RUST_LOG", "error");
+ }
+ env_logger::init();
+
+ let mut claim_def = args.claim_def;
+ let mut base_dir = PathBuf::from(".");
+
+ if let Some(path) = args.path.clone() {
+ if !path.exists() {
+ println!("File not found {:?}", path);
+ exit(1);
+ }
+
+ base_dir = PathBuf::from(&path);
+ let extension = path.extension().and_then(|p| p.to_str()).unwrap_or("");
+ // path can be a jpeg source file or a json working claim description
+ match extension {
+ "jpg" | "jpeg" | "png" | "c2pa" => {
+ report_from_path(&path, args.detailed);
+ }
+ "json" => {
+ // file paths in ClaimDef are relative to the json file
+ base_dir = PathBuf::from(&path);
+ base_dir.pop();
+
+ claim_def = Some(fs::read_to_string(&path)?);
+ }
+ _ => {
+ println!("Unsupported file type {}", extension);
+ exit(1);
+ }
+ };
+ }
+
+ if let Some(json) = claim_def {
+ handle_claim_def(
+ &json,
+ &base_dir,
+ args.parent.as_deref(),
+ args.output.as_deref(),
+ args.detailed,
+ )?;
+ }
+ Ok(())
+}
diff --git a/c2patool/src/signer.rs b/c2patool/src/signer.rs
@@ -0,0 +1,148 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use anyhow::Result;
+use c2pa::{
+ openssl::{EcSigner, EdSigner, RsaSigner},
+ signer::ConfigurableSigner,
+ signer::Signer,
+};
+use std::{env, path::PathBuf, process::exit};
+
+pub fn get_ta_url() -> Option<String> {
+ //const TA_URL: &str = "http://timestamp.digicert.com";
+ match std::env::var("CAI_TA_URL") {
+ Ok(url) => Some(url),
+ Err(_) => None,
+ }
+}
+
+/// Generates a temporary signature from local keys specified by the environment
+/// keys can be directly in environment variables
+/// or in a folder referenced by CAI_KEY_PATH
+/// also supports default dev environment keys
+pub fn get_test_signer() -> Result<Box<dyn Signer>> {
+ // Keys can be passed in separate environment variables
+ if let Ok(private_key) = env::var("CAI_PRIVATE_KEY") {
+ let private_key = private_key.as_bytes().to_vec();
+ if let Ok(sign_cert) = env::var("CAI_PUB_CERT") {
+ let sign_cert = sign_cert.as_bytes().to_vec();
+ let alg = env::var("CAI_SIGNING_ALGORITHM").ok();
+
+ let signer: Box<dyn Signer> = match alg {
+ Some(a) => match a.to_lowercase().as_str() {
+ "ps256" | "ps384" | "ps512" => Box::new(RsaSigner::from_signcert_and_pkey(
+ &sign_cert,
+ &private_key,
+ a.to_lowercase(),
+ get_ta_url(),
+ )?),
+ "es256" | "es384" | "es512" => Box::new(EcSigner::from_signcert_and_pkey(
+ &sign_cert,
+ &private_key,
+ a.to_lowercase(),
+ get_ta_url(),
+ )?),
+ "ed25519" => Box::new(EdSigner::from_signcert_and_pkey(
+ &sign_cert,
+ &private_key,
+ a.to_lowercase(),
+ get_ta_url(),
+ )?),
+ _ => {
+ eprintln!("Unsupported CAI_SIGNING_ALGORITHM, must be one of [ ps256 | ps384 | ps512 | es256 | es384 | es512 | ed25519 ]");
+ exit(2);
+ }
+ },
+ None => {
+ eprintln!("Must have CAI_SIGNING_ALGORITHM set");
+ exit(1);
+ }
+ };
+
+ return Ok(signer);
+ }
+ }
+
+ // or an environment variable can specify where to find the keys
+ let key_path = match std::env::var("CAI_KEY_PATH") {
+ Ok(keys_path) => PathBuf::from(keys_path),
+ Err(_) => {
+ // defaults to dev environment
+ match env::var("CARGO_MANIFEST_DIR") {
+ Ok(dir) => {
+ let mut path = PathBuf::from(dir);
+ path.push("..");
+ path.push(".x509");
+ path
+ }
+ Err(_) => {
+ let mut dir = dirs::home_dir().expect("home_dir");
+ dir.push(".cai");
+ dir
+ }
+ }
+ }
+ };
+ // we expect the key files to be named temp_key.pem and temp_key.pub
+ let mut pem_path = key_path.clone();
+ pem_path.push("temp_key.pem");
+ let mut pub_path = key_path;
+ pub_path.push("temp_key.pub");
+ if !pem_path.is_file() || !pub_path.is_file() {
+ eprintln!(
+ "\n\n-----------\n\n\
+ Claim creation requires key files {:?} and {:?}\n\
+ \n\
+ You can generate a throwaway RSAPSS SSH private key for testing by \n\
+ pasting the following line into a terminal and hitting enter\n\
+ mkdir -p ~/.x509 ; openssl req -new -newkey rsa:4096 -sigopt rsa_padding_mode:pss -days 180 -extensions v3_ca -addext \"keyUsage = digitalSignature\" -addext \"extendedKeyUsage = emailProtection\" -nodes -x509 -keyout ~/.x509/temp_key.pem -out ~/.x509/temp_key.pub -sha256 ; sudo chmod 644 ~/.x509/temp_key.pem\n\
+ \n\
+ You should only need to do this once. \n\
+ Set the environment var CAI_SIGNING_ALGORITHM=ps256 to set the signature algorithm
+ The environment variable CAI_KEY_PATH can specify an alternate key folder.\n\n\
+ -----------\n\n"
+ ,pem_path, pub_path);
+ exit(1);
+ }
+
+ let alg = env::var("CAI_SIGNING_ALGORITHM")
+ .unwrap_or_else(|_| "ps256".to_string())
+ .to_lowercase();
+ let signer: Box<dyn Signer> = match alg.as_str() {
+ "ps256" | "ps384" | "ps512" => Box::new(RsaSigner::from_files(
+ &pub_path,
+ &pem_path,
+ alg,
+ get_ta_url(),
+ )?),
+ "es256" | "es384" | "es512" => Box::new(EcSigner::from_files(
+ &pub_path,
+ &pem_path,
+ alg,
+ get_ta_url(),
+ )?),
+ "ed25519" => Box::new(EdSigner::from_files(
+ &pub_path,
+ &pem_path,
+ alg,
+ get_ta_url(),
+ )?),
+ _ => {
+ eprintln!("Unsupported CAI_SIGNING_ALGORITHM, must be one of [ ps256 | ps384 | ps512 | es256 | es384 | es512 | ed25519 ]");
+ exit(1);
+ }
+ };
+
+ Ok(signer)
+}
diff --git a/c2patool/tests/integration.rs b/c2patool/tests/integration.rs
@@ -0,0 +1,123 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+// isolate from wasm by wrapping in module
+#[cfg(not(target_arch = "wasm32"))]
+mod integration {
+ use assert_cmd::prelude::*; // Add methods on commands
+ use predicates::prelude::*;
+ use std::path::PathBuf;
+ use std::process::Command;
+
+ const TEST_IMAGE: &str = "earth_apollo17.jpg";
+ //const TEST_IMAGE: &str = "libpng-test.png"; // save for png testing
+ //const TEST_IMAGE_WITH_MANIFEST: &str = "C.jpg"; // save for manifest tests
+
+ fn fixture_path(name: &str) -> PathBuf {
+ let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ path.push("../sdk/tests/fixtures");
+ path.push(name);
+ std::fs::canonicalize(path).expect("canonicalize")
+ }
+
+ fn temp_path(name: &str) -> PathBuf {
+ let mut path = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
+ std::fs::create_dir_all(&path).ok();
+ path.push(name);
+ path
+ }
+
+ #[test]
+ fn tool_not_found() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = Command::cargo_bin("c2patool")?;
+ cmd.arg("test/file/not.found");
+ cmd.assert()
+ .failure()
+ .stdout(predicate::str::contains("File not found"));
+ Ok(())
+ }
+
+ #[test]
+ fn tool_version_check() {
+ // ensure c2patool version matches the toolkit version
+ assert_eq!(c2pa::VERSION, env!("CARGO_PKG_VERSION"));
+ }
+
+ #[test]
+ fn tool_jpeg_no_report() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = Command::cargo_bin("c2patool")?;
+ cmd.arg(fixture_path(TEST_IMAGE));
+ cmd.assert()
+ .failure()
+ .stdout(predicate::str::contains("No claim found"));
+ Ok(())
+ }
+
+ #[test]
+ fn tool_embed_jpeg_report() -> Result<(), Box<dyn std::error::Error>> {
+ generate_x509_temp_keys();
+
+ Command::cargo_bin("c2patool")?
+ .arg(fixture_path("claim.json"))
+ .arg("-p")
+ .arg(fixture_path(TEST_IMAGE))
+ .arg("-o")
+ .arg(temp_path("out.jpg"))
+ .assert()
+ .success() // should this be a failure?
+ .stdout(predicate::str::contains("My Title"));
+ Ok(())
+ }
+
+ /* remove this until the c2patool supports .c2pa write again
+ #[test]
+ fn tool_manifest_report() -> Result<(), Box<dyn std::error::Error>> {
+ generate_x509_temp_keys();
+
+ // first export a c2pa file
+ Command::cargo_bin("c2patool")?
+ .arg(fixture_path(TEST_IMAGE_WITH_MANIFEST))
+ .arg("-o")
+ .arg(temp_path("manifest.c2pa"))
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("C2PA Testing"));
+ // then read it back in
+ Command::cargo_bin("c2patool")?
+ .arg(temp_path("manifest.c2pa"))
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("C2PA Testing"));
+ Ok(())
+ }
+ */
+
+ fn generate_x509_temp_keys() {
+ let mut x509_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ x509_path.pop();
+ x509_path.push(".x509");
+
+ std::fs::create_dir_all(&x509_path).expect("Can't create .x509 dir in repo");
+
+ // Test for existence of x509_path.temp_key.pub and .pem.
+
+ let priv_key_path = x509_path.join("temp_key.pem");
+ let sign_cert_path = x509_path.join("temp_key.pub");
+
+ if !(priv_key_path.exists() && sign_cert_path.exists()) {
+ // Creating the signer (which we don't use) has the side effect of
+ // creating temporary private key and signing certificate.
+ c2pa::openssl::temp_signer::get_signer(&x509_path);
+ }
+ }
+}
diff --git a/deny.toml b/deny.toml
@@ -0,0 +1,57 @@
+# Configuration used for dependency checking with cargo-deny.
+#
+# For further details on all configuration options see:
+# https://embarkstudios.github.io/cargo-deny/checks/cfg.html
+
+targets = [
+ { triple = "x86_64-unknown-linux-gnu" },
+ { triple = "x86_64-apple-darwin" },
+ { triple = "x86_64-pc-windows-msvc" },
+ { triple = "aarch64-apple-darwin" },
+ { triple = "wasm32-unknown-unknown" },
+]
+
+# Deny all advisories unless explicitly ignored.
+[advisories]
+vulnerability = "allow" # "deny" # TODO: Re-enable when possible.
+unmaintained = "allow" # "deny" # TODO: Re-enable when possible.
+yanked = "allow" # "deny" # TODO: Re-enable when possible.
+notice = "allow" # "deny" # TODO: Re-enable when possible.
+ignore = [
+ "RUSTSEC-2021-0127" # serde_cbor
+]
+
+# Deny multiple versions unless explicitly skipped.
+[bans]
+multiple-versions = "allow" # "deny" # TODO: Re-enable when possible.
+wildcards = "allow"
+
+# List of allowed licenses.
+[licenses]
+allow = [
+ "Apache-2.0",
+ "BSD-2-Clause",
+ "BSD-3-Clause",
+ "CC0-1.0",
+ "ISC",
+ "LicenseRef-ring",
+ "MIT",
+ "MPL-2.0",
+ "Zlib",
+]
+copyleft = "deny"
+unlicensed = "deny"
+confidence-threshold = 0.8
+
+[[licenses.clarify]]
+name = "ring"
+expression = "LicenseRef-ring"
+license-files = [
+ { path = "LICENSE", hash = 0xbd0eed23 }
+]
+
+[sources]
+unknown-registry = "deny"
+unknown-git = "deny"
+allow-registry = ["https://github.com/rust-lang/crates.io-index"]
+allow-git = []
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -0,0 +1,82 @@
+[package]
+name = "c2pa"
+version = "0.1.0"
+authors = ["Maurice Fisher <mfisher@adobe.com>", "Gavin Peacock <gpeacock@adobe.com>", "Eric Scouten <scouten@adobe.com>", "Leonard Rosenthol <lrosenth@adobe.com>", "Dave Kozma <dkozma@adobe.com>"]
+license = "MIT OR Apache-2.0"
+edition = "2018"
+rust-version = "1.58.0"
+
+[features]
+async_signer = ["async-trait"]
+file_io = ["openssl"]
+
+# The diagnostics feature is unsupported and might be removed.
+# It enables some low-overhead timing features used in our development cycle.
+diagnostics = []
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+async-trait = { version = "0.1.48", optional = true }
+base64 = "0.12.2"
+bcder = "0.6.0"
+blake3 = "1.0.0"
+bytes = "1.1.0"
+byteorder = "1.3.4"
+chrono = { version = "0.4.19", features = ["wasmbind"] }
+ciborium = "0.2.0"
+conv = "0.3.3"
+coset = "0.3.1"
+extfmt = "0.1.1"
+hex = "0.4.3"
+image = "0.23.10"
+img-parts = "0.2.3"
+log = "0.4.8"
+multibase = "0.9.0"
+multihash = "0.11.4"
+nom = "6.0"
+png_pong = "0.8.2"
+quick-xml = "0.20.0"
+range-set = "0.0.7"
+serde = { version = "1.0", features = ["derive"] }
+serde_bytes = "0.11.5"
+serde_cbor = "0.11.1"
+serde_derive = "1.0.127"
+serde_json = "1.0.66"
+serde-transcode = "1.1.1"
+sha2 = "0.9.5"
+tempfile = "3.1.0"
+thiserror = ">= 1.0.20, < 1.0.26"
+time = ">= 0.2.23"
+twoway = "0.2.1"
+uuid = { version = "0.8.1", features = ["serde", "v4", "wasm-bindgen"] }
+x509-parser = "0.11.0"
+x509-certificate = "0.12.0"
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+ring = "0.16.20"
+url = "2.2.2"
+ureq = "2.4.0"
+instant = "0.1.0"
+openssl = { version = "0.10.31", features = ["vendored"], optional = true }
+xmp_toolkit = "0.3.4"
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+console_log = { version = "0.2", features = ["color"] }
+getrandom = { version = "0.2.2", features = ["js"] }
+# We need to use the `inaccurate` flag here to ensure usage of the JavaScript Date API
+# to handle certificate timestamp checking correctly.
+instant = { version = "0.1.0", features = ["wasm-bindgen", "inaccurate"] }
+js-sys = "0.3.54"
+serde-wasm-bindgen = "0.4.1"
+wasm-bindgen = "0.2.77"
+wasm-bindgen-futures = "0.4.27"
+web-sys = { version = "0.3.54", features = ["Crypto", "SubtleCrypto", "CryptoKey", "Window", "WorkerGlobalScope"] }
+
+[dev-dependencies]
+anyhow = "1.0.40"
+env_logger = "0.7.1"
+
+[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
+wasm-bindgen-test = "0.3.0"
diff --git a/sdk/examples/client/client.rs b/sdk/examples/client/client.rs
@@ -0,0 +1,134 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! Example C2PA client application
+
+use anyhow::Result;
+
+use c2pa::{
+ assertions::labels,
+ assertions::{c2pa_action, Action, Actions, CreativeWork},
+ openssl::temp_signer::get_signer,
+ Ingredient, Manifest, ManifestStore,
+};
+use std::path::PathBuf;
+use tempfile::tempdir;
+
+const GENERATOR: &str = "test_app/0.1";
+const CREATIVE_WORK_URL: &str = r#"{"@type":"CreativeWork","@context":"https://schema.org","url":"http://contentauthenticity.org"}"#;
+
+const INDENT_SPACE: usize = 2;
+
+// Example for reading the contents of a manifest store, recursively showing nested manifests
+fn show_manifest(manifest_store: &ManifestStore, manifest_label: &str, level: usize) -> Result<()> {
+ let indent = " ".repeat(level * INDENT_SPACE);
+
+ println!("{}manifest_label: {}", indent, manifest_label);
+ if let Some(manifest) = manifest_store.get(manifest_label) {
+ if let Some(asset) = manifest.asset().as_ref() {
+ println!(
+ "{}title: {} , format: {}, instance_id: {}",
+ indent,
+ asset.title(),
+ asset.format(),
+ asset.instance_id()
+ );
+ }
+
+ for assertion in manifest.assertions().iter() {
+ match assertion.label.as_str() {
+ labels::ACTIONS => {
+ let actions: Actions = assertion.to_assertion()?;
+ for action in actions.actions {
+ println!(
+ "{}{:?}, {:?}",
+ indent,
+ action.label,
+ action.parameters.unwrap_or_default()
+ );
+ }
+ }
+ labels::CREATIVE_WORK => {
+ let creative_work: CreativeWork = assertion.to_assertion()?;
+ if let Some(authors) = creative_work.author() {
+ for author in authors {
+ if let Some(name) = author.name() {
+ println!("{}author = {} ", indent, name);
+ }
+ }
+ }
+ if let Some(url) = creative_work.get::<String>("url") {
+ println!("{}url = {} ", indent, url);
+ }
+ }
+ _ => {}
+ }
+ }
+
+ for ingredient in manifest.ingredients().iter() {
+ println!("{}Ingredient title:{}", indent, ingredient.title());
+ if let Some(label) = ingredient.active_manifest() {
+ show_manifest(manifest_store, label, level + 1)?;
+ }
+ }
+ }
+ Ok(())
+}
+
+pub fn main() -> Result<()> {
+ let args: Vec<String> = std::env::args().collect();
+ if args.len() != 3 {
+ println!("This requires a path to a source image and a path to an output file. Both must be jpg or png files.");
+ return Ok(());
+ }
+ let source = PathBuf::from(&args[1]);
+ let dest = PathBuf::from(&args[2]);
+
+ // create a new Manifest
+ let mut manifest = Manifest::new(GENERATOR.to_owned());
+
+ // if a filepath was provided on the command line, read it as a parent file
+ let parent = Ingredient::from_file(source)?;
+ let source = PathBuf::from(&args[1]);
+
+ // create an action assertion stating that we imported this file
+ let mut actions = Actions::new();
+ actions.add_action(
+ Action::new(c2pa_action::PLACED)
+ .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?,
+ );
+ manifest.add_assertion(&actions)?;
+
+ // set the parent ingredient
+ manifest.set_parent(parent)?;
+
+ let creative_work = CreativeWork::from_json_str(CREATIVE_WORK_URL)?;
+ manifest.add_assertion(&creative_work)?;
+
+ // sign and embed into the target file
+ let temp_dir = tempdir()?;
+ let (signer, _) = get_signer(&temp_dir.path());
+ manifest.embed(&source, &dest, &signer)?;
+
+ let manifest_store = ManifestStore::from_file(&dest)?;
+
+ // example of how to print out the whole manifest as json
+ println!("{}\n", manifest_store);
+
+ // walk through the manifest and access data.
+ if let Some(manifest_label) = manifest_store.active_label() {
+ show_manifest(&manifest_store, manifest_label, 0)?;
+ }
+
+ Ok(())
+}
diff --git a/sdk/examples/client/main.rs b/sdk/examples/client/main.rs
@@ -0,0 +1,24 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use anyhow::Result;
+// This example is not designed to work with a wasm build
+// so we provide this shell to avoid testing errors
+
+#[cfg(not(target_arch = "wasm32"))]
+mod client;
+fn main() -> Result<()> {
+ #[cfg(not(target_arch = "wasm32"))]
+ client::main()?;
+ Ok(())
+}
diff --git a/sdk/examples/custom_assertion.rs b/sdk/examples/custom_assertion.rs
@@ -0,0 +1,90 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! Example: Creating a custom assertion
+//!
+use c2pa::{Assertion, AssertionBase, AssertionCbor, AssertionDecodeResult, Manifest, Result};
+use serde::{Deserialize, Serialize};
+
+/// Defines a Custom assertion
+/// This can be any Rust structure
+/// It must support serde Serialize and Deserialize
+/// In this example the assertion contains a version of this sdk
+#[derive(Serialize, Deserialize)]
+pub struct Custom {
+ /// Records the version of this c2pa library
+ pub version: String,
+}
+
+impl Custom {
+ pub fn new() -> Self {
+ Self {
+ version: c2pa::VERSION.to_owned(),
+ }
+ }
+}
+
+// Implementing default is a good idea
+impl Default for Custom {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+// Implement either AssertionCbor or AssertionJson
+impl AssertionCbor for Custom {}
+
+// Always implement AssertionBase by copying this template
+// If you chose AssertionJson, use to_json_assertion and from_json_assertion instead
+impl AssertionBase for Custom {
+ // A label for our assertion, use reverse domain name syntax
+ const LABEL: &'static str = "org.contentauth.custom";
+
+ fn to_assertion(&self) -> c2pa::Result<Assertion> {
+ Self::to_cbor_assertion(self)
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ Self::from_cbor_assertion(assertion)
+ }
+}
+
+fn main() -> Result<()> {
+ let mut manifest = Manifest::new("c2pa-rs".to_owned());
+ let original = Custom::new();
+ manifest.add_assertion(&original)?;
+ let result: Custom = manifest.find_assertion(Custom::LABEL)?;
+ println!("{}\n", manifest);
+ println!("c2pa sdk version = {}", result.version);
+
+ Ok(())
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+
+ use super::*;
+
+ #[test]
+ fn assertion_custom() {
+ let mut manifest = Manifest::new("my_app".to_owned());
+ let original = Custom::new();
+ manifest.add_assertion(&original).expect("adding assertion");
+ println!("{}", manifest);
+ let result: Custom = manifest
+ .find_assertion(Custom::LABEL)
+ .expect("find_assertion");
+ assert_eq!(original.version, result.version);
+ }
+}
diff --git a/sdk/examples/make_tests/main.rs b/sdk/examples/make_tests/main.rs
@@ -0,0 +1,43 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! This generates a set of test images with a wide variety of configurations
+//! To run this, use the following command in a terminal
+//! cargo run --release --example make_tests
+//!
+use anyhow::Result;
+use std::path::PathBuf;
+// The make_tests sample is not designed to work with a wasm build
+// so we provide a wasm stub here and only include the module for non wasm
+#[cfg(not(target_arch = "wasm32"))]
+mod make_tests;
+#[cfg(not(target_arch = "wasm32"))]
+use crate::make_tests::make_tests;
+
+#[cfg(target_arch = "wasm32")]
+fn make_tests(_output_folder: &std::path::Path, _alg: &str, _tsa: Option<String>) -> Result<()> {
+ panic!("Not implemented for wasm");
+}
+
+const TARGET_FOLDER: &str = "target/test_images";
+fn main() -> Result<()> {
+ // set RUST_LOG=debug to get detailed debug logging
+ env_logger::init();
+
+ // choose a timestamp service authority
+ let tsa = Some("http://timestamp.digicert.com".to_string());
+
+ make_tests(&PathBuf::from(TARGET_FOLDER), "ps256", tsa)?;
+
+ Ok(())
+}
diff --git a/sdk/examples/make_tests/make_tests.rs b/sdk/examples/make_tests/make_tests.rs
@@ -0,0 +1,361 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use anyhow::{Context, Result};
+
+use c2pa::{
+ assertions::{c2pa_action, Action, Actions, CreativeWork, SchemaDotOrgPerson},
+ jumbf_io,
+ openssl::temp_signer::get_signer_by_alg,
+ Error, Ingredient, IngredientOptions, Manifest, ManifestStore,
+};
+
+use image::GenericImageView;
+use nom::AsBytes;
+use tempfile::tempdir;
+use twoway::find_bytes;
+
+use std::{
+ fs,
+ path::{Path, PathBuf},
+};
+
+const GENERATOR: &str = "make_tests";
+const USER: &str = "Joe Bloggs";
+
+const IMAGE_WIDTH: u32 = 2048;
+const IMAGE_HEIGHT: u32 = 1365;
+
+/**
+Patch new content into a file
+path - path to file to be patched
+search_bytes - bytes to be replaced
+replace_bytes - replacement bytes
+*/
+pub fn patch_file(path: &std::path::Path, search_bytes: &[u8], replace_bytes: &[u8]) -> Result<()> {
+ let mut buf = fs::read(path)?;
+
+ if let Some(splice_start) = find_bytes(&buf, search_bytes) {
+ buf.splice(
+ splice_start..splice_start + search_bytes.len(),
+ replace_bytes.iter().cloned(),
+ );
+ } else {
+ return Err(Error::NotFound.into());
+ }
+
+ fs::write(path, &buf)?;
+
+ Ok(())
+}
+
+pub struct MakeTests {
+ output_dir: PathBuf,
+}
+
+impl MakeTests {
+ pub fn new(path: &Path) -> Self {
+ Self {
+ output_dir: PathBuf::from(path),
+ }
+ }
+
+ pub fn output_dir(&self) -> PathBuf {
+ self.output_dir.to_owned()
+ }
+
+ fn make_path(&self, s: &str) -> PathBuf {
+ //let output_dir = unsafe { OUTPUT_FOLDER.as_ref().unwrap().lock().unwrap().to_string() };
+ let mut path_buf = PathBuf::from(&self.output_dir);
+ path_buf.push(s);
+ if path_buf.extension().is_none() {
+ path_buf.set_extension("jpg");
+ }
+ path_buf
+ }
+
+ // create a test image with optional source and ingredients, out to dest
+ pub fn make_image(
+ &self,
+ src: Option<&str>,
+ ing: Option<&Vec<&str>>,
+ dst: &str,
+ alg: &str,
+ tsa: Option<String>,
+ ) -> Result<()> {
+ let dst_path = &self.make_path(dst);
+ println!("creating {:?}", dst_path);
+ // keep track of all actions here
+ let mut actions = Actions::new();
+
+ let options = IngredientOptions {
+ make_hash: true,
+ title: None,
+ };
+
+ let mut manifest = Manifest::new(GENERATOR.to_string());
+ manifest.set_vendor("contentauth".to_owned()); // needed for generating error cases below
+
+ let creative_work =
+ CreativeWork::new().add_author(SchemaDotOrgPerson::new().set_name(USER.to_owned())?)?;
+
+ manifest.add_assertion(&creative_work)?;
+
+ // process parent first
+ let mut img = match src {
+ Some(src) => {
+ let src_path = &self.make_path(src);
+
+ let parent = Ingredient::from_file_with_options(src_path, &options)?;
+ actions.add_action(
+ Action::new(c2pa_action::OPENED)
+ .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?,
+ );
+ manifest.set_parent(parent)?;
+
+ // load the image for editing
+ let mut img =
+ image::open(&src_path).context(format!("opening image A {:?}", src_path))?;
+
+ // adjust brightness to show we made an edit
+ img = img.brighten(30);
+ actions.add_action(
+ Action::new(c2pa_action::COLOR_ADJUSTMENTS)
+ .set_parameter("name".to_owned(), "brightnesscontrast")?,
+ );
+ img
+ }
+ None => {
+ // create a default image with a gradient
+ let mut img = image::DynamicImage::new_rgb8(IMAGE_WIDTH, IMAGE_HEIGHT);
+ if let Some(img_ref) = img.as_mut_rgb8() {
+ // fill image with a gradient
+ for (x, y, pixel) in img_ref.enumerate_pixels_mut() {
+ let r = (0.3 * x as f32) as u8;
+ let b = (0.3 * y as f32) as u8;
+ *pixel = image::Rgb([r, 100, b]);
+ }
+ }
+ actions
+ .add_action(Action::new(c2pa_action::CREATED))
+ .add_action(
+ Action::new(c2pa_action::DRAWING)
+ .set_parameter("name".to_owned(), "gradient")?,
+ );
+
+ img
+ }
+ };
+
+ // then add all ingredients
+ if let Some(ing_vec) = ing {
+ // scale ingredients to paste in top row of the image
+ let width = match ing_vec.len() as u32 {
+ 0 | 1 => img.width() / 2,
+ _ => img.width() / ing_vec.len() as u32,
+ };
+ let height = img.height() as u32 / 2;
+
+ let mut x = 0;
+ for ing in ing_vec {
+ let ing_path = &self.make_path(ing);
+
+ // get the bits of the ingredient, resize it and overlay it on the base image
+ let img_ingredient =
+ image::open(&ing_path).context(format!("opening image I {:?}", ing_path))?;
+ let img_small = img_ingredient.thumbnail(width, height);
+ image::imageops::overlay(&mut img, &img_small, x, 0);
+
+ // create and add the ingredient
+ let ingredient = Ingredient::from_file_with_options(ing_path, &options)?;
+ actions.add_action(
+ Action::new(c2pa_action::PLACED).set_parameter(
+ "identifier".to_owned(),
+ ingredient.instance_id().to_owned(),
+ )?,
+ );
+ manifest.add_ingredient(ingredient);
+
+ x += width;
+ }
+ // record what we did as an action (only need to record this once)
+ actions.add_action(Action::new(c2pa_action::RESIZED));
+ }
+
+ // save the changes to the image as our target file
+ img.save(dst_path)?;
+
+ // add all our actions as an assertion now.
+ manifest.add_assertion(&actions)?; // extra get required here, since actions is an array
+
+ // now create store; sign claim and embed in target
+ let temp_dir = tempdir()?;
+ let (signer, _) = get_signer_by_alg(&temp_dir.path(), alg, tsa);
+
+ manifest.embed(dst_path, dst_path, signer.as_ref())?;
+
+ println!("{}", ManifestStore::from_file(dst_path)?);
+
+ Ok(())
+ }
+
+ // make an off the golden path image from an existing image with a claim
+ fn make_ogp(&self, src: &str, dst: &str) -> Result<()> {
+ println!("creating OGP {}", dst);
+ let src_path = &self.make_path(src);
+ let dst_path = &self.make_path(dst);
+ let jumbf = jumbf_io::load_jumbf_from_file(&PathBuf::from(src_path))
+ .context(format!("loading OGP {:?}", src_path))?;
+ // save the edited image to our destination file
+ let mut img = image::open(&Path::new(src_path))
+ .context(format!("loading OGP image{:?}", src_path))?;
+ img = img.grayscale();
+ img.save(dst_path)
+ .context(format!("saving OGP image{:?}", dst_path))?;
+ // write the original claim data to the edited image
+ jumbf_io::save_jumbf_to_file(
+ &jumbf,
+ &PathBuf::from(dst_path),
+ Some(&PathBuf::from(dst_path)),
+ )
+ .context(format!("OGP save_jumbf_to_file {:?}", dst_path))?;
+ // The image library does not preserve any metadata so we have to write it ourselves.
+ // todo: should preserve all metadata and update instanceId.
+ Ok(())
+ }
+
+ fn make_err(&self, src: &str, err: &str) -> Result<()> {
+ let (search_bytes, replace_bytes) = match err {
+ // modify the XMP (change xmp magic id value) - this should cause a data hash mismatch (OTGP)
+ "dat" => (
+ b"W5M0MpCehiHzreSzNTczkc9d".as_bytes(),
+ b"W5M0MpCehiHzreSzdeadbeef".as_bytes(),
+ ),
+ // modify the claim_generator value inside the claim, the claim hash will no longer match the signature
+ "sig" => (b"make_tests".as_bytes(), b"make_xxxxx".as_bytes()),
+ // modify a value inside an actions assertion, the assertion hash will fail
+ "uri" => (
+ b"brightnesscontrast".as_bytes(),
+ b"brightnessdeadbeef".as_bytes(),
+ ),
+ // modify a uri to a manifest so the manifest cannot be found (missing manifest)
+ "clm" => (
+ b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth".as_bytes(),
+ b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth".as_bytes(),
+ ),
+ // modify the provenance uri so that is references a non-existing manifest
+ "prv" => (
+ b"dcterms:provenance=\"self#jumbf=/c2pa/contentauth".as_bytes(),
+ b"dcterms:provenance=\"self#jumbf=/c2pa/contentauth".as_bytes(),
+ ),
+ _ => panic!("bad parameter"),
+ };
+
+ let dst = format!("E-{}-{}", err, src);
+ std::fs::copy(&self.make_path(src), &self.make_path(&dst))
+ .context("copying for make_err")?;
+ patch_file(&self.make_path(&dst), search_bytes, replace_bytes)
+ .context(format!("patching {}", err))?;
+
+ Ok(())
+ }
+}
+
+pub fn make_tests(output_folder: &Path, alg: &str, tsa: Option<String>) -> Result<()> {
+ // destination folder for this content
+ let mt = MakeTests::new(output_folder);
+ let data_dir = mt.output_dir();
+
+ if !data_dir.exists() {
+ std::fs::create_dir_all(&data_dir).expect("Can't create C2PA data directory");
+ };
+
+ // copy A and I source images into destination folder
+ // these images should have no claims
+ std::fs::copy("sdk/tests/fixtures/IMG_0003.jpg", &mt.make_path("A.jpg"))
+ .context("error copying A")?;
+ std::fs::copy("sdk/tests/fixtures/P1000827.jpg", &mt.make_path("I.jpg"))
+ .context("error copying I")?;
+
+ // --------------------------------------------------------------------
+
+ //make_cai(None, Some(&vec!["PS.svg"]), "CIPS")?;
+ mt.make_image(None, None, "C", alg, tsa.clone())?;
+ mt.make_image(Some("A"), None, "CA", alg, tsa.clone())?;
+ mt.make_image(Some("CA"), None, "CACA", alg, tsa.clone())?;
+ mt.make_image(None, Some(&vec!["I"]), "CI", alg, tsa.clone())?;
+ mt.make_image(None, Some(&vec!["I", "I"]), "CII", alg, tsa.clone())?;
+ mt.make_image(
+ None,
+ Some(&vec!["I", "I", "I", "I", "I"]),
+ "CIIIII",
+ alg,
+ tsa.clone(),
+ )?;
+ mt.make_image(Some("A"), Some(&vec!["I"]), "CAI", alg, tsa.clone())?;
+ mt.make_image(Some("A"), Some(&vec!["CA"]), "CAICA", alg, tsa.clone())?;
+ mt.make_image(None, Some(&vec!["CA"]), "CICA", alg, tsa.clone())?;
+ mt.make_image(Some("CA"), Some(&vec!["CAI"]), "CAICAI", alg, tsa.clone())?;
+ mt.make_image(None, Some(&vec!["CA"]), "CICA", alg, tsa.clone())?;
+ mt.make_image(
+ Some("CAICA"),
+ Some(&vec!["CICA"]),
+ "CACAICAICICA",
+ alg,
+ tsa.clone(),
+ )?;
+ mt.make_image(
+ None,
+ Some(&vec!["CA", "CA", "CA"]),
+ "CICACACA",
+ alg,
+ tsa.clone(),
+ )?;
+
+ mt.make_ogp("CA", "XCA")?;
+ mt.make_ogp("CI", "XCI")?;
+ mt.make_image(Some("CA"), Some(&vec!["XCI"]), "CAIXCI", alg, tsa.clone())?;
+ mt.make_image(
+ Some("XCA"),
+ Some(&vec!["XCI"]),
+ "CAXCAIXCI",
+ alg,
+ tsa.clone(),
+ )?;
+
+ mt.make_err("CA", "dat")?;
+ mt.make_err("CA", "sig")?;
+ mt.make_err("CA", "uri")?;
+ mt.make_err("CAICAI", "clm")?;
+ mt.make_err("CA", "prv")?;
+ mt.make_image(
+ None,
+ Some(&vec!["E-sig-CA"]),
+ "CIE-sig-CA",
+ alg,
+ tsa.clone(),
+ )?;
+ // inject an assertion error into a claim that has an accepted error
+ mt.make_err("CIE-sig-CA", "uri")?;
+
+ mt.make_image(
+ Some("A"),
+ Some(&vec!["C", "A", "I", "CA", "CI", "CAI", "CICA"]),
+ "CAIAIIICAICIICAIICICA",
+ alg,
+ tsa,
+ )?;
+ // // save the changes to the image and add the claim
+ println!("done");
+ Ok(())
+}
diff --git a/sdk/src/asn1/mod.rs b/sdk/src/asn1/mod.rs
@@ -0,0 +1,12 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+/*! Holds Rust struct definitions for various ASN.1 primitives. */
+
+// https://github.com/indygreg/PyOxidizer/tree/main/cryptographic-message-syntax/src/asn1
+
+pub mod rfc3161;
+pub mod rfc3281;
+pub mod rfc4210;
+pub mod rfc5652;
diff --git a/sdk/src/asn1/rfc3161.rs b/sdk/src/asn1/rfc3161.rs
@@ -0,0 +1,482 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! ASN.1 types defined by RFC 3161.
+
+use {
+ crate::asn1::{rfc4210::PkiFreeText, rfc5652::ContentInfo},
+ bcder::{
+ decode::{Constructed, Malformed, Primitive, Source},
+ encode::{self, PrimitiveContent, Values},
+ ConstOid, Integer, OctetString, Oid, Tag,
+ },
+ x509_certificate::{
+ asn1time::GeneralizedTime,
+ rfc3280::GeneralName,
+ rfc5280::{AlgorithmIdentifier, Extensions},
+ },
+};
+
+/// Content-Type for Time-Stamp Token Info.
+///
+/// 1.2.840.113549.1.9.16.1.4
+pub const OID_CONTENT_TYPE_TST_INFO: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 9, 16, 1, 4]);
+
+/// id-aa-timeStampToken
+///
+/// 1.2.840.113549.1.9.16.2.14
+pub const OID_TIME_STAMP_TOKEN: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 9, 16, 2, 14]);
+
+/// A time-stamp request.
+///
+/// ```ASN.1
+/// TimeStampReq ::= SEQUENCE {
+/// version INTEGER { v1(1) },
+/// messageImprint MessageImprint,
+/// --a hash algorithm OID and the hash value of the data to be
+/// --time-stamped
+/// reqPolicy TSAPolicyId OPTIONAL,
+/// nonce INTEGER OPTIONAL,
+/// certReq BOOLEAN DEFAULT FALSE,
+/// extensions [0] IMPLICIT Extensions OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct TimeStampReq {
+ pub version: Integer,
+ pub message_imprint: MessageImprint,
+ pub req_policy: Option<TsaPolicyId>,
+ pub nonce: Option<Integer>,
+ pub cert_req: Option<bool>,
+ pub extensions: Option<Extensions>,
+}
+
+impl TimeStampReq {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let version = Integer::take_from(cons)?;
+ let message_imprint = MessageImprint::take_from(cons)?;
+ let req_policy = TsaPolicyId::take_opt_from(cons)?;
+ let nonce =
+ cons.take_opt_primitive_if(Tag::INTEGER, |prim| Integer::from_primitive(prim))?;
+ let cert_req = cons.take_opt_bool()?;
+ let extensions =
+ cons.take_opt_constructed_if(Tag::CTX_0, |cons| Extensions::take_from(cons))?;
+
+ Ok(Self {
+ version,
+ message_imprint,
+ req_policy,
+ nonce,
+ cert_req,
+ extensions,
+ })
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((
+ (&self.version).encode(),
+ self.message_imprint.encode_ref(),
+ self.req_policy
+ .as_ref()
+ .map(|req_policy| req_policy.encode_ref()),
+ self.nonce.as_ref().map(|nonce| nonce.encode()),
+ self.cert_req.as_ref().map(|cert_req| cert_req.encode_ref()),
+ self.extensions
+ .as_ref()
+ .map(|extensions| extensions.encode_ref_as(Tag::CTX_0)),
+ ))
+ }
+}
+
+/// Message imprint.
+///
+/// ```ASN.1
+/// MessageImprint ::= SEQUENCE {
+/// hashAlgorithm AlgorithmIdentifier,
+/// hashedMessage OCTET STRING }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct MessageImprint {
+ pub hash_algorithm: AlgorithmIdentifier,
+ pub hashed_message: OctetString,
+}
+
+impl MessageImprint {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let hash_algorithm = AlgorithmIdentifier::take_from(cons)?;
+ let hashed_message = OctetString::take_from(cons)?;
+
+ Ok(Self {
+ hash_algorithm,
+ hashed_message,
+ })
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((&self.hash_algorithm, self.hashed_message.encode_ref()))
+ }
+}
+
+pub type TsaPolicyId = Oid;
+
+/// Time stamp response.
+///
+/// ```ASN.1
+/// TimeStampResp ::= SEQUENCE {
+/// status PKIStatusInfo,
+/// timeStampToken TimeStampToken OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct TimeStampResp {
+ pub status: PkiStatusInfo,
+ pub time_stamp_token: Option<TimeStampToken>,
+}
+
+impl TimeStampResp {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let status = PkiStatusInfo::take_from(cons)?;
+ let time_stamp_token = TimeStampToken::take_opt_from(cons)?;
+
+ Ok(Self {
+ status,
+ time_stamp_token,
+ })
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((
+ self.status.encode_ref(),
+ if let Some(time_stamp_token) = &self.time_stamp_token {
+ Some(time_stamp_token)
+ } else {
+ None
+ },
+ ))
+ }
+}
+
+/// PKI status info
+///
+/// ```ASN.1
+/// PKIStatusInfo ::= SEQUENCE {
+/// status PKIStatus,
+/// statusString PKIFreeText OPTIONAL,
+/// failInfo PKIFailureInfo OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PkiStatusInfo {
+ pub status: PkiStatus,
+ pub status_string: Option<PkiFreeText>,
+ pub fail_info: Option<PkiFailureInfo>,
+}
+
+impl PkiStatusInfo {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let status = PkiStatus::take_from(cons)?;
+ let status_string = PkiFreeText::take_opt_from(cons)?;
+ let fail_info = PkiFailureInfo::take_opt_from(cons)?;
+
+ Ok(Self {
+ status,
+ status_string,
+ fail_info,
+ })
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((
+ (&self.status).encode(),
+ self.status_string
+ .as_ref()
+ .map(|status_string| status_string.encode_ref()),
+ self.fail_info.as_ref().map(|fail_info| fail_info.encode()),
+ ))
+ }
+}
+
+/// PKI status.
+///
+/// ```ASN.1
+/// PKIStatus ::= INTEGER {
+/// granted (0),
+/// -- when the PKIStatus contains the value zero a TimeStampToken, as
+/// requested, is present.
+/// grantedWithMods (1),
+/// -- when the PKIStatus contains the value one a TimeStampToken,
+/// with modifications, is present.
+/// rejection (2),
+/// waiting (3),
+/// revocationWarning (4),
+/// -- this message contains a warning that a revocation is
+/// -- imminent
+/// revocationNotification (5)
+/// -- notification that a revocation has occurred }
+///
+/// -- When the TimeStampToken is not present
+/// -- failInfo indicates the reason why the
+/// -- time-stamp request was rejected and
+/// -- may be one of the following values.
+/// ```
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum PkiStatus {
+ Granted = 0,
+ GrantedWithMods = 1,
+ Rejection = 2,
+ Waiting = 3,
+ RevocationWarning = 4,
+ RevocationNotification = 5,
+}
+
+impl PkiStatus {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ match cons.take_primitive_if(Tag::INTEGER, Integer::i8_from_primitive)? {
+ 0 => Ok(Self::Granted),
+ 1 => Ok(Self::GrantedWithMods),
+ 2 => Ok(Self::Rejection),
+ 3 => Ok(Self::Waiting),
+ 4 => Ok(Self::RevocationWarning),
+ 5 => Ok(Self::RevocationNotification),
+ _ => Err(Malformed.into()),
+ }
+ }
+
+ pub fn encode(self) -> impl Values {
+ u8::from(self).encode()
+ }
+}
+
+impl From<PkiStatus> for u8 {
+ fn from(v: PkiStatus) -> u8 {
+ match v {
+ PkiStatus::Granted => 0,
+ PkiStatus::GrantedWithMods => 1,
+ PkiStatus::Rejection => 2,
+ PkiStatus::Waiting => 3,
+ PkiStatus::RevocationWarning => 4,
+ PkiStatus::RevocationNotification => 5,
+ }
+ }
+}
+
+/// PKI failure info.
+///
+/// ```ASN.1
+/// PKIFailureInfo ::= BIT STRING {
+/// badAlg (0),
+/// -- unrecognized or unsupported Algorithm Identifier
+/// badRequest (2),
+/// -- transaction not permitted or supported
+/// badDataFormat (5),
+/// -- the data submitted has the wrong format
+/// timeNotAvailable (14),
+/// -- the TSA's time source is not available
+/// unacceptedPolicy (15),
+/// -- the requested TSA policy is not supported by the TSA.
+/// unacceptedExtension (16),
+/// -- the requested extension is not supported by the TSA.
+/// addInfoNotAvailable (17)
+/// -- the additional information requested could not be understood
+/// -- or is not available
+/// systemFailure (25)
+/// -- the request cannot be handled due to system failure }
+/// ```
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum PkiFailureInfo {
+ BadAlg = 0,
+ BadRequest = 1,
+ BadDataFormat = 5,
+ TimeNotAvailable = 14,
+ UnacceptedPolicy = 15,
+ UnacceptedExtension = 16,
+ AddInfoNotAvailable = 17,
+ SystemFailure = 25,
+}
+
+impl PkiFailureInfo {
+ pub fn take_opt_from<S: Source>(cons: &mut Constructed<S>) -> Result<Option<Self>, S::Err> {
+ cons.take_opt_primitive_if(Tag::INTEGER, Self::from_primitive)
+ }
+
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_primitive_if(Tag::INTEGER, Self::from_primitive)
+ }
+
+ pub fn from_primitive<S: Source>(prim: &mut Primitive<S>) -> Result<Self, S::Err> {
+ match Integer::i8_from_primitive(prim)? {
+ 0 => Ok(Self::BadAlg),
+ 1 => Ok(Self::BadRequest),
+ 5 => Ok(Self::BadDataFormat),
+ 14 => Ok(Self::TimeNotAvailable),
+ 15 => Ok(Self::UnacceptedPolicy),
+ 16 => Ok(Self::UnacceptedExtension),
+ 17 => Ok(Self::AddInfoNotAvailable),
+ 25 => Ok(Self::SystemFailure),
+ _ => Err(Malformed.into()),
+ }
+ }
+
+ pub fn encode(self) -> impl Values {
+ u8::from(self).encode()
+ }
+}
+
+impl From<PkiFailureInfo> for u8 {
+ fn from(v: PkiFailureInfo) -> u8 {
+ match v {
+ PkiFailureInfo::BadAlg => 0,
+ PkiFailureInfo::BadRequest => 1,
+ PkiFailureInfo::BadDataFormat => 5,
+ PkiFailureInfo::TimeNotAvailable => 14,
+ PkiFailureInfo::UnacceptedPolicy => 15,
+ PkiFailureInfo::UnacceptedExtension => 16,
+ PkiFailureInfo::AddInfoNotAvailable => 17,
+ PkiFailureInfo::SystemFailure => 25,
+ }
+ }
+}
+
+/// Time stamp token.
+///
+/// ```ASN.1
+/// TimeStampToken ::= ContentInfo
+/// ```
+pub type TimeStampToken = ContentInfo;
+
+/// Time stamp token info.
+///
+/// ```ASN.1
+/// TSTInfo ::= SEQUENCE {
+/// version INTEGER { v1(1) },
+/// policy TSAPolicyId,
+/// messageImprint MessageImprint,
+/// -- MUST have the same value as the similar field in
+/// -- TimeStampReq
+/// serialNumber INTEGER,
+/// -- Time-Stamping users MUST be ready to accommodate integers
+/// -- up to 160 bits.
+/// genTime GeneralizedTime,
+/// accuracy Accuracy OPTIONAL,
+/// ordering BOOLEAN DEFAULT FALSE,
+/// nonce INTEGER OPTIONAL,
+/// -- MUST be present if the similar field was present
+/// -- in TimeStampReq. In that case it MUST have the same value.
+/// tsa [0] GeneralName OPTIONAL,
+/// extensions [1] IMPLICIT Extensions OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct TstInfo {
+ pub version: Integer,
+ pub policy: TsaPolicyId,
+ pub message_imprint: MessageImprint,
+ pub serial_number: Integer,
+ pub gen_time: GeneralizedTime,
+ pub accuracy: Option<Accuracy>,
+ pub ordering: Option<bool>,
+ pub nonce: Option<Integer>,
+ pub tsa: Option<GeneralName>,
+ pub extensions: Option<Extensions>,
+}
+
+impl TstInfo {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let version = Integer::take_from(cons)?;
+ let policy = TsaPolicyId::take_from(cons)?;
+ let message_imprint = MessageImprint::take_from(cons)?;
+ let serial_number = Integer::take_from(cons)?;
+ let gen_time = GeneralizedTime::take_from_allow_fractional_z(cons)?;
+ let accuracy = Accuracy::take_opt_from(cons)?;
+ let ordering = cons.take_opt_bool()?;
+ let nonce =
+ cons.take_opt_primitive_if(Tag::INTEGER, |prim| Integer::from_primitive(prim))?;
+ let tsa =
+ cons.take_opt_constructed_if(Tag::CTX_0, |cons| GeneralName::take_from(cons))?;
+ let extensions =
+ cons.take_opt_constructed_if(Tag::CTX_1, |cons| Extensions::take_from(cons))?;
+
+ Ok(Self {
+ version,
+ policy,
+ message_imprint,
+ serial_number,
+ gen_time,
+ accuracy,
+ ordering,
+ nonce,
+ tsa,
+ extensions,
+ })
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((
+ (&self.version).encode(),
+ self.policy.encode_ref(),
+ self.message_imprint.encode_ref(),
+ (&self.serial_number).encode(),
+ self.gen_time.encode_ref(),
+ self.accuracy.as_ref().map(|accuracy| accuracy.encode_ref()),
+ self.ordering.as_ref().map(|ordering| ordering.encode_ref()),
+ self.nonce.as_ref().map(|nonce| nonce.encode()),
+ self.tsa
+ .as_ref()
+ .map(|tsa| tsa.encode_ref().explicit(Tag::CTX_0)),
+ self.extensions
+ .as_ref()
+ .map(|extensions| extensions.encode_ref_as(Tag::CTX_1)),
+ ))
+ }
+}
+
+/// Accuracy
+///
+/// ```ASN.1
+/// Accuracy ::= SEQUENCE {
+/// seconds INTEGER OPTIONAL,
+/// millis [0] INTEGER (1..999) OPTIONAL,
+/// micros [1] INTEGER (1..999) OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Accuracy {
+ pub seconds: Option<Integer>,
+ pub millis: Option<Integer>,
+ pub micros: Option<Integer>,
+}
+
+impl Accuracy {
+ pub fn take_opt_from<S: Source>(cons: &mut Constructed<S>) -> Result<Option<Self>, S::Err> {
+ cons.take_opt_sequence(|cons| Self::from_sequence(cons))
+ }
+
+ pub fn from_sequence<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ let seconds =
+ cons.take_opt_primitive_if(Tag::INTEGER, |prim| Integer::from_primitive(prim))?;
+ let millis =
+ cons.take_opt_primitive_if(Tag::CTX_0, |prim| Integer::from_primitive(prim))?;
+ let micros =
+ cons.take_opt_primitive_if(Tag::CTX_1, |prim| Integer::from_primitive(prim))?;
+
+ Ok(Self {
+ seconds,
+ millis,
+ micros,
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((
+ self.seconds.as_ref().map(|seconds| seconds.encode()),
+ self.millis.as_ref().map(|millis| millis.encode()),
+ self.micros.as_ref().map(|micros| micros.encode()),
+ ))
+ }
+}
diff --git a/sdk/src/asn1/rfc3281.rs b/sdk/src/asn1/rfc3281.rs
@@ -0,0 +1,212 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use {
+ bcder::{
+ decode::{Constructed, Source, Unimplemented},
+ BitString, Oid,
+ },
+ x509_certificate::{asn1time::*, rfc3280::*, rfc5280::*},
+};
+
+/// Attribute certificate.
+///
+/// ```ASN.1
+/// AttributeCertificate ::= SEQUENCE {
+/// acinfo AttributeCertificateInfo,
+/// signatureAlgorithm AlgorithmIdentifier,
+/// signatureValue BIT STRING
+/// }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AttributeCertificate {
+ pub ac_info: AttributeCertificateInfo,
+ pub signature_algorithm: AlgorithmIdentifier,
+ pub signature_value: BitString,
+}
+
+impl AttributeCertificate {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let ac_info = AttributeCertificateInfo::take_from(cons)?;
+ let signature_algorithm = AlgorithmIdentifier::take_from(cons)?;
+ let signature_value = BitString::take_from(cons)?;
+
+ Ok(Self {
+ ac_info,
+ signature_algorithm,
+ signature_value,
+ })
+ })
+ }
+}
+
+/// Attribute certificate info.
+///
+/// ```ASN.1
+/// AttributeCertificateInfo ::= SEQUENCE {
+/// version AttCertVersion -- version is v2,
+/// holder Holder,
+/// issuer AttCertIssuer,
+/// signature AlgorithmIdentifier,
+/// serialNumber CertificateSerialNumber,
+/// attrCertValidityPeriod AttCertValidityPeriod,
+/// attributes SEQUENCE OF Attribute,
+/// issuerUniqueID UniqueIdentifier OPTIONAL,
+/// extensions Extensions OPTIONAL
+/// }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AttributeCertificateInfo {
+ pub version: AttCertVersion,
+ pub holder: Holder,
+ pub issuer: AttCertIssuer,
+ pub signature: AlgorithmIdentifier,
+ pub serial_number: CertificateSerialNumber,
+ pub attr_cert_validity_period: AttCertValidityPeriod,
+ pub attributes: Vec<Attribute>,
+ pub issuer_unique_ud: Option<UniqueIdentifier>,
+ pub extensions: Option<Extensions>,
+}
+
+impl AttributeCertificateInfo {
+ pub fn take_from<S: Source>(_cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ Err(Unimplemented.into())
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum AttCertVersion {
+ V2 = 1,
+}
+
+/// Holder
+///
+/// ```ASN.1
+/// Holder ::= SEQUENCE {
+/// baseCertificateID [0] IssuerSerial OPTIONAL,
+/// -- the issuer and serial number of
+/// -- the holder's Public Key Certificate
+/// entityName [1] GeneralNames OPTIONAL,
+/// -- the name of the claimant or role
+/// objectDigestInfo [2] ObjectDigestInfo OPTIONAL
+/// -- used to directly authenticate the holder,
+/// -- for example, an executable
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Holder {
+ pub base_certificate_id: Option<IssuerSerial>,
+ pub entity_name: Option<GeneralNames>,
+ pub object_digest_info: Option<ObjectDigestInfo>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DigestedObjectType {
+ PublicKey = 0,
+ PublicKeyCert = 1,
+ OtherObjectTypes = 2,
+}
+
+/// Object digest info.
+///
+/// ```ASN.1
+/// ObjectDigestInfo ::= SEQUENCE {
+/// digestedObjectType ENUMERATED {
+/// publicKey (0),
+/// publicKeyCert (1),
+/// otherObjectTypes (2) },
+/// -- otherObjectTypes MUST NOT
+/// -- be used in this profile
+/// otherObjectTypeID OBJECT IDENTIFIER OPTIONAL,
+/// digestAlgorithm AlgorithmIdentifier,
+/// objectDigest BIT STRING
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ObjectDigestInfo {
+ pub digested_object_type: DigestedObjectType,
+ pub other_object_type_id: Oid,
+ pub digest_algorithm: AlgorithmIdentifier,
+ pub object_digest: BitString,
+}
+
+/// Att cert issuer
+///
+/// ```ASN.1
+/// AttCertIssuer ::= CHOICE {
+/// v1Form GeneralNames, -- MUST NOT be used in this
+/// -- profile
+/// v2Form [0] V2Form -- v2 only
+/// }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum AttCertIssuer {
+ V1Form(GeneralNames),
+ V2Form(Box<V2Form>),
+}
+
+/// V2 Form
+///
+/// ```ASN.1
+/// V2Form ::= SEQUENCE {
+/// issuerName GeneralNames OPTIONAL,
+/// baseCertificateID [0] IssuerSerial OPTIONAL,
+/// objectDigestInfo [1] ObjectDigestInfo OPTIONAL
+/// -- issuerName MUST be present in this profile
+/// -- baseCertificateID and objectDigestInfo MUST NOT
+/// -- be present in this profile
+/// }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct V2Form {
+ pub issuer_name: Option<GeneralNames>,
+ pub base_certificate_id: Option<IssuerSerial>,
+ pub object_digest_info: Option<ObjectDigestInfo>,
+}
+
+/// Issuer serial.
+///
+/// IssuerSerial ::= SEQUENCE {
+/// issuer GeneralNames,
+/// serial CertificateSerialNumber,
+/// issuerUID UniqueIdentifier OPTIONAL
+/// }
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct IssuerSerial {
+ pub issuer: GeneralNames,
+ pub serial: CertificateSerialNumber,
+ pub issuer_uid: Option<UniqueIdentifier>,
+}
+
+/// Att cert validity period
+///
+/// ```ASN.1
+/// AttCertValidityPeriod ::= SEQUENCE {
+/// notBeforeTime GeneralizedTime,
+/// notAfterTime GeneralizedTime
+/// }
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AttCertValidityPeriod {
+ pub not_before_time: GeneralizedTime,
+ pub not_after_time: GeneralizedTime,
+}
+
+/// Attribute
+///
+/// ```ASN.1
+/// Attribute ::= SEQUENCE {
+/// type AttributeType,
+/// values SET OF AttributeValue
+/// -- at least one value is required
+/// }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Attribute {
+ pub typ: AttributeType,
+ pub values: Vec<AttributeValue>,
+}
+
+pub type AttributeType = Oid;
+
+// TODO Any.
+pub type AttributeValue = Option<()>;
diff --git a/sdk/src/asn1/rfc4210.rs b/sdk/src/asn1/rfc4210.rs
@@ -0,0 +1,45 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! ASN.1 types defined by RFC 4210.
+
+use bcder::{
+ decode::{Constructed, Source},
+ encode::{self, Values},
+ Tag, Utf8String,
+};
+
+/// PKI free text.
+///
+/// ```ASN.1
+/// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PkiFreeText(Vec<Utf8String>);
+
+impl PkiFreeText {
+ pub fn take_opt_from<S: Source>(cons: &mut Constructed<S>) -> Result<Option<Self>, S::Err> {
+ cons.take_opt_sequence(|cons| Self::from_sequence(cons))
+ }
+
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| Self::from_sequence(cons))
+ }
+
+ pub fn from_sequence<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ let mut res = vec![];
+
+ while let Some(s) = cons.take_opt_value_if(Tag::UTF8_STRING, |content| {
+ Utf8String::from_content(content)
+ })? {
+ res.push(s);
+ }
+
+ Ok(Self(res))
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence(encode::slice(&self.0, |x| x.clone().encode()))
+ }
+}
diff --git a/sdk/src/asn1/rfc5652.rs b/sdk/src/asn1/rfc5652.rs
@@ -0,0 +1,1387 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+/*! ASN.1 data structures defined by RFC 5652.
+
+The types defined in this module are intended to be extremely low-level
+and only to be used for (de)serialization. See types outside the
+`asn1` module tree for higher-level functionality.
+
+Some RFC 5652 types are defined in the `x509-certificate` crate, which
+this crate relies on for certificate parsing functionality.
+*/
+
+use {
+ crate::asn1::rfc3281::AttributeCertificate,
+ bcder::{
+ decode::{Constructed, Malformed, Source, Unimplemented},
+ encode,
+ encode::{PrimitiveContent, Values},
+ BitString, Captured, ConstOid, Integer, Mode, OctetString, Oid, Tag,
+ },
+ std::{
+ fmt::{Debug, Formatter},
+ io::Write,
+ ops::{Deref, DerefMut},
+ },
+ x509_certificate::{asn1time::*, rfc3280::*, rfc5280::*, rfc5652::*},
+};
+
+/// The data content type.
+///
+/// `id-data` in the specification.
+///
+/// 1.2.840.113549.1.7.1
+pub const OID_ID_DATA: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 7, 1]);
+
+/// The signed-data content type.
+///
+/// 1.2.840.113549.1.7.2
+pub const OID_ID_SIGNED_DATA: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 7, 2]);
+
+/// Enveloped data content type.
+///
+/// 1.2.840.113549.1.7.3
+pub const OID_ENVELOPE_DATA: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 7, 3]);
+
+/// Digested-data content type.
+///
+/// 1.2.840.113549.1.7.5
+pub const OID_DIGESTED_DATA: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 7, 5]);
+
+/// Encrypted-data content type.
+///
+/// 1.2.840.113549.1.7.6
+pub const OID_ENCRYPTED_DATA: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 7, 6]);
+
+/// Authenticated-data content type.
+///
+/// 1.2.840.113549.1.9.16.1.2
+pub const OID_AUTHENTICATED_DATA: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 9, 16, 1, 2]);
+
+/// Identifies the content-type attribute.
+///
+/// 1.2.840.113549.1.9.3
+pub const OID_CONTENT_TYPE: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 9, 3]);
+
+/// Identifies the message-digest attribute.
+///
+/// 1.2.840.113549.1.9.4
+pub const OID_MESSAGE_DIGEST: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 9, 4]);
+
+/// Identifies the signing-time attribute.
+///
+/// 1.2.840.113549.1.9.5
+pub const OID_SIGNING_TIME: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 9, 5]);
+
+/// Identifies the countersignature attribute.
+///
+/// 1.2.840.113549.1.9.6
+pub const OID_COUNTER_SIGNATURE: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 9, 6]);
+
+/// Content info.
+///
+/// ```ASN.1
+/// ContentInfo ::= SEQUENCE {
+/// contentType ContentType,
+/// content [0] EXPLICIT ANY DEFINED BY contentType }
+/// ```
+#[derive(Clone, Debug)]
+pub struct ContentInfo {
+ pub content_type: ContentType,
+ pub content: Captured,
+}
+
+impl PartialEq for ContentInfo {
+ fn eq(&self, other: &Self) -> bool {
+ self.content_type == other.content_type
+ && self.content.as_slice() == other.content.as_slice()
+ }
+}
+
+impl Eq for ContentInfo {}
+
+impl ContentInfo {
+ pub fn take_opt_from<S: Source>(cons: &mut Constructed<S>) -> Result<Option<Self>, S::Err> {
+ cons.take_opt_sequence(|cons| Self::from_sequence(cons))
+ }
+
+ pub fn from_sequence<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ let content_type = ContentType::take_from(cons)?;
+ let content = cons.take_constructed_if(Tag::CTX_0, |cons| cons.capture_all())?;
+
+ Ok(Self {
+ content_type,
+ content,
+ })
+ }
+}
+
+impl Values for ContentInfo {
+ fn encoded_len(&self, mode: Mode) -> usize {
+ encode::sequence((self.content_type.encode_ref(), &self.content)).encoded_len(mode)
+ }
+
+ fn write_encoded<W: Write>(&self, mode: Mode, target: &mut W) -> Result<(), std::io::Error> {
+ encode::sequence((self.content_type.encode_ref(), &self.content))
+ .write_encoded(mode, target)
+ }
+}
+
+/// Represents signed data.
+///
+/// ASN.1 type specification:
+///
+/// ```ASN.1
+/// SignedData ::= SEQUENCE {
+/// version CMSVersion,
+/// digestAlgorithms DigestAlgorithmIdentifiers,
+/// encapContentInfo EncapsulatedContentInfo,
+/// certificates [0] IMPLICIT CertificateSet OPTIONAL,
+/// crls [1] IMPLICIT RevocationInfoChoices OPTIONAL,
+/// signerInfos SignerInfos }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct SignedData {
+ pub version: CmsVersion,
+ pub digest_algorithms: DigestAlgorithmIdentifiers,
+ pub content_info: EncapsulatedContentInfo,
+ pub certificates: Option<CertificateSet>,
+ pub crls: Option<RevocationInfoChoices>,
+ pub signer_infos: SignerInfos,
+}
+
+impl SignedData {
+ /// Attempt to decode BER encoded bytes to a parsed data structure.
+ pub fn decode_ber(data: &[u8]) -> Result<Self, bcder::decode::Error> {
+ Constructed::decode(data, bcder::Mode::Ber, |cons| Self::decode(cons))
+ }
+
+ pub fn decode<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let oid = Oid::take_from(cons)?;
+
+ if oid != OID_ID_SIGNED_DATA {
+ return Err(Malformed.into());
+ }
+
+ cons.take_constructed_if(Tag::CTX_0, Self::take_from)
+ })
+ }
+
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let version = CmsVersion::take_from(cons)?;
+ let digest_algorithms = DigestAlgorithmIdentifiers::take_from(cons)?;
+ let content_info = EncapsulatedContentInfo::take_from(cons)?;
+ let certificates =
+ cons.take_opt_constructed_if(Tag::CTX_0, |cons| CertificateSet::take_from(cons))?;
+ let crls = cons.take_opt_constructed_if(Tag::CTX_1, |cons| {
+ RevocationInfoChoices::take_from(cons)
+ })?;
+ let signer_infos = SignerInfos::take_from(cons)?;
+
+ Ok(Self {
+ version,
+ digest_algorithms,
+ content_info,
+ certificates,
+ crls,
+ signer_infos,
+ })
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((
+ OID_ID_SIGNED_DATA.encode_ref(),
+ encode::sequence_as(
+ Tag::CTX_0,
+ encode::sequence((
+ self.version.encode(),
+ self.digest_algorithms.encode_ref(),
+ self.content_info.encode_ref(),
+ self.certificates
+ .as_ref()
+ .map(|certs| certs.encode_ref_as(Tag::CTX_0)),
+ // TODO crls.
+ self.signer_infos.encode_ref(),
+ )),
+ ),
+ ))
+ }
+}
+
+/// Digest algorithm identifiers.
+///
+/// ```ASN.1
+/// DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier
+/// ```
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct DigestAlgorithmIdentifiers(Vec<DigestAlgorithmIdentifier>);
+
+impl Deref for DigestAlgorithmIdentifiers {
+ type Target = Vec<DigestAlgorithmIdentifier>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for DigestAlgorithmIdentifiers {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl DigestAlgorithmIdentifiers {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_set(|cons| {
+ let mut identifiers = Vec::new();
+
+ while let Some(identifier) = AlgorithmIdentifier::take_opt_from(cons)? {
+ identifiers.push(identifier);
+ }
+
+ Ok(Self(identifiers))
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::set(&self.0)
+ }
+}
+
+pub type DigestAlgorithmIdentifier = AlgorithmIdentifier;
+
+/// Signer infos.
+///
+/// ```ASN.1
+/// SignerInfos ::= SET OF SignerInfo
+/// ```
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct SignerInfos(Vec<SignerInfo>);
+
+impl Deref for SignerInfos {
+ type Target = Vec<SignerInfo>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for SignerInfos {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl SignerInfos {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_set(|cons| {
+ let mut infos = Vec::new();
+
+ while let Some(info) = SignerInfo::take_opt_from(cons)? {
+ infos.push(info);
+ }
+
+ Ok(Self(infos))
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::set(&self.0)
+ }
+}
+
+/// Encapsulated content info.
+///
+/// ```ASN.1
+/// EncapsulatedContentInfo ::= SEQUENCE {
+/// eContentType ContentType,
+/// eContent [0] EXPLICIT OCTET STRING OPTIONAL }
+/// ```
+#[derive(Clone, Eq, PartialEq)]
+pub struct EncapsulatedContentInfo {
+ pub content_type: ContentType,
+ pub content: Option<OctetString>,
+}
+
+impl Debug for EncapsulatedContentInfo {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ let mut s = f.debug_struct("EncapsulatedContentInfo");
+ s.field("content_type", &format_args!("{}", self.content_type));
+ s.field(
+ "content",
+ &format_args!(
+ "{:?}",
+ self.content
+ .as_ref()
+ .map(|x| hex::encode(x.clone().to_bytes().as_ref()))
+ ),
+ );
+ s.finish()
+ }
+}
+
+impl EncapsulatedContentInfo {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let content_type = ContentType::take_from(cons)?;
+ let content =
+ cons.take_opt_constructed_if(Tag::CTX_0, |cons| OctetString::take_from(cons))?;
+
+ Ok(Self {
+ content_type,
+ content,
+ })
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((
+ self.content_type.encode_ref(),
+ self.content
+ .as_ref()
+ .map(|content| encode::sequence_as(Tag::CTX_0, content.encode_ref())),
+ ))
+ }
+}
+
+/// Per-signer information.
+///
+/// ```ASN.1
+/// SignerInfo ::= SEQUENCE {
+/// version CMSVersion,
+/// sid SignerIdentifier,
+/// digestAlgorithm DigestAlgorithmIdentifier,
+/// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
+/// signatureAlgorithm SignatureAlgorithmIdentifier,
+/// signature SignatureValue,
+/// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL }
+/// ```
+#[derive(Clone, Eq, PartialEq)]
+pub struct SignerInfo {
+ pub version: CmsVersion,
+ pub sid: SignerIdentifier,
+ pub digest_algorithm: DigestAlgorithmIdentifier,
+ pub signed_attributes: Option<SignedAttributes>,
+ pub signature_algorithm: SignatureAlgorithmIdentifier,
+ pub signature: SignatureValue,
+ pub unsigned_attributes: Option<UnsignedAttributes>,
+
+ /// Raw bytes backing signed attributes data.
+ ///
+ /// Does not include constructed tag or length bytes.
+ pub signed_attributes_data: Option<Vec<u8>>,
+}
+
+impl SignerInfo {
+ pub fn take_opt_from<S: Source>(cons: &mut Constructed<S>) -> Result<Option<Self>, S::Err> {
+ cons.take_opt_sequence(|cons| Self::from_sequence(cons))
+ }
+
+ pub fn from_sequence<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ let version = CmsVersion::take_from(cons)?;
+ let sid = SignerIdentifier::take_from(cons)?;
+ let digest_algorithm = DigestAlgorithmIdentifier::take_from(cons)?;
+ let signed_attributes = cons.take_opt_constructed_if(Tag::CTX_0, |cons| {
+ // RFC 5652 Section 5.3: SignedAttributes MUST be DER encoded, even if the
+ // rest of the structure is BER encoded. So buffer all data so we can
+ // feed into a new decoder.
+ let der = cons.capture_all()?;
+
+ // But wait there's more! The raw data constituting the signed
+ // attributes is also digested and used for content/signature
+ // verification. Because our DER serialization may not roundtrip
+ // losslessly, we stash away a copy of these bytes so they may be
+ // referenced as part of verification.
+ let der_data = der.as_slice().to_vec();
+
+ Ok((
+ Constructed::decode(der.as_slice(), bcder::Mode::Der, |cons| {
+ SignedAttributes::take_from_set(cons)
+ })?,
+ der_data,
+ ))
+ })?;
+
+ let (signed_attributes, signed_attributes_data) = if let Some((x, y)) = signed_attributes {
+ (Some(x), Some(y))
+ } else {
+ (None, None)
+ };
+
+ let signature_algorithm = SignatureAlgorithmIdentifier::take_from(cons)?;
+ let signature = SignatureValue::take_from(cons)?;
+ let unsigned_attributes = cons
+ .take_opt_constructed_if(Tag::CTX_1, |cons| UnsignedAttributes::take_from_set(cons))?;
+
+ Ok(Self {
+ version,
+ sid,
+ digest_algorithm,
+ signed_attributes,
+ signature_algorithm,
+ signature,
+ unsigned_attributes,
+ signed_attributes_data,
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((
+ u8::from(self.version).encode(),
+ &self.sid,
+ &self.digest_algorithm,
+ // Always write signed attributes with DER encoding per RFC 5652.
+ self.signed_attributes
+ .as_ref()
+ .map(|attrs| SignedAttributesDer::new(attrs.clone(), Some(Tag::CTX_0))),
+ &self.signature_algorithm,
+ self.signature.encode_ref(),
+ self.unsigned_attributes
+ .as_ref()
+ .map(|attrs| attrs.encode_ref_as(Tag::CTX_1)),
+ ))
+ }
+
+ /// Obtain content representing the signed attributes data to be digested.
+ ///
+ /// Computing the content to go into the digest calculation is nuanced.
+ /// From RFC 5652:
+ ///
+ /// The result of the message digest calculation process depends on
+ /// whether the signedAttrs field is present. When the field is absent,
+ /// the result is just the message digest of the content as described
+ /// above. When the field is present, however, the result is the message
+ /// digest of the complete DER encoding of the SignedAttrs value
+ /// contained in the signedAttrs field. Since the SignedAttrs value,
+ /// when present, must contain the content-type and the message-digest
+ /// attributes, those values are indirectly included in the result. The
+ /// content-type attribute MUST NOT be included in a countersignature
+ /// unsigned attribute as defined in Section 11.4. A separate encoding
+ /// of the signedAttrs field is performed for message digest calculation.
+ /// The `IMPLICIT [0]` tag in the signedAttrs is not used for the DER
+ /// encoding, rather an EXPLICIT SET OF tag is used. That is, the DER
+ /// encoding of the EXPLICIT SET OF tag, rather than of the `IMPLICIT [0]`
+ /// tag, MUST be included in the message digest calculation along with
+ /// the length and content octets of the SignedAttributes value.
+ ///
+ /// A few things to note here:
+ ///
+ /// * We must ensure DER (not BER) encoding of the entire SignedAttrs values.
+ /// * The SignedAttr tag must use `EXPLICIT SET OF` instead of `IMPLICIT [0]`,
+ /// so default encoding is not appropriate.
+ /// * If this instance came into existence via a parse, we stashed away the
+ /// raw bytes constituting SignedAttributes to ensure we can do a lossless
+ /// copy.
+ pub fn signed_attributes_digested_content(&self) -> Result<Option<Vec<u8>>, std::io::Error> {
+ if let Some(signed_attributes) = &self.signed_attributes {
+ if let Some(existing_data) = &self.signed_attributes_data {
+ // +8 should be enough for tag + length.
+ let mut buffer = Vec::with_capacity(existing_data.len() + 8);
+ // EXPLICIT SET OF.
+ buffer.write_all(&[0x31])?;
+
+ // Length isn't exported by bcder :/ So do length encoding manually.
+ if existing_data.len() < 0x80 {
+ buffer.write_all(&[existing_data.len() as u8])?;
+ } else if existing_data.len() < 0x100 {
+ buffer.write_all(&[0x81, existing_data.len() as u8])?;
+ } else if existing_data.len() < 0x10000 {
+ buffer.write_all(&[
+ 0x82,
+ (existing_data.len() >> 8) as u8,
+ existing_data.len() as u8,
+ ])?;
+ } else if existing_data.len() < 0x1000000 {
+ buffer.write_all(&[
+ 0x83,
+ (existing_data.len() >> 16) as u8,
+ (existing_data.len() >> 8) as u8,
+ existing_data.len() as u8,
+ ])?;
+ } else {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "signed attributes length too long",
+ ));
+ }
+
+ buffer.write_all(existing_data)?;
+
+ Ok(Some(buffer))
+ } else {
+ // No existing copy present. Serialize from raw data structures.
+ // But we obtain a sorted instance of those attributes first, because
+ // bcder doesn't appear to follow DER encoding rules for sets.
+ let signed_attributes = signed_attributes.as_sorted()?;
+ let mut der = Vec::new();
+ // The mode argument here is actually ignored.
+ signed_attributes.write_encoded(Mode::Der, &mut der)?;
+
+ Ok(Some(der))
+ }
+ } else {
+ Ok(None)
+ }
+ }
+}
+
+impl Debug for SignerInfo {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ let mut s = f.debug_struct("SignerInfo");
+
+ s.field("version", &self.version);
+ s.field("sid", &self.sid);
+ s.field("digest_algorithm", &self.digest_algorithm);
+ s.field("signed_attributes", &self.signed_attributes);
+ s.field("signature_algorithm", &self.signature_algorithm);
+ s.field(
+ "signature",
+ &format_args!(
+ "{}",
+ hex::encode(self.signature.clone().into_bytes().as_ref())
+ ),
+ );
+ s.field("unsigned_attributes", &self.unsigned_attributes);
+ s.field(
+ "signed_attributes_data",
+ &format_args!(
+ "{:?}",
+ self.signed_attributes_data.as_ref().map(hex::encode)
+ ),
+ );
+ s.finish()
+ }
+}
+
+impl Values for SignerInfo {
+ fn encoded_len(&self, mode: Mode) -> usize {
+ self.encode_ref().encoded_len(mode)
+ }
+
+ fn write_encoded<W: Write>(&self, mode: Mode, target: &mut W) -> Result<(), std::io::Error> {
+ self.encode_ref().write_encoded(mode, target)
+ }
+}
+
+/// Identifies the signer.
+///
+/// ```ASN.1
+/// SignerIdentifier ::= CHOICE {
+/// issuerAndSerialNumber IssuerAndSerialNumber,
+/// subjectKeyIdentifier [0] SubjectKeyIdentifier }
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum SignerIdentifier {
+ IssuerAndSerialNumber(IssuerAndSerialNumber),
+ SubjectKeyIdentifier(SubjectKeyIdentifier),
+}
+
+impl SignerIdentifier {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ if let Some(identifier) =
+ cons.take_opt_constructed_if(Tag::CTX_0, |cons| SubjectKeyIdentifier::take_from(cons))?
+ {
+ Ok(Self::SubjectKeyIdentifier(identifier))
+ } else {
+ Ok(Self::IssuerAndSerialNumber(
+ IssuerAndSerialNumber::take_from(cons)?,
+ ))
+ }
+ }
+}
+
+impl Values for SignerIdentifier {
+ fn encoded_len(&self, mode: Mode) -> usize {
+ match self {
+ Self::IssuerAndSerialNumber(v) => v.encode_ref().encoded_len(mode),
+ Self::SubjectKeyIdentifier(v) => v.encode_ref_as(Tag::CTX_0).encoded_len(mode),
+ }
+ }
+
+ fn write_encoded<W: Write>(&self, mode: Mode, target: &mut W) -> Result<(), std::io::Error> {
+ match self {
+ Self::IssuerAndSerialNumber(v) => v.encode_ref().write_encoded(mode, target),
+ Self::SubjectKeyIdentifier(v) => {
+ v.encode_ref_as(Tag::CTX_0).write_encoded(mode, target)
+ }
+ }
+ }
+}
+
+/// Signed attributes.
+///
+/// ```ASN.1
+/// SignedAttributes ::= SET SIZE (1..MAX) OF Attribute
+/// ```
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct SignedAttributes(Vec<Attribute>);
+
+impl Deref for SignedAttributes {
+ type Target = Vec<Attribute>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for SignedAttributes {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl SignedAttributes {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_set(|cons| Self::take_from_set(cons))
+ }
+
+ pub fn take_from_set<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ let mut attributes = Vec::new();
+
+ while let Some(attribute) = Attribute::take_opt_from(cons)? {
+ attributes.push(attribute);
+ }
+
+ Ok(Self(attributes))
+ }
+
+ /// Obtain an instance where the attributes are sorted according to DER
+ /// rules. See the comment in [SignerInfo::signed_attributes_digested_content].
+ pub fn as_sorted(&self) -> Result<Self, std::io::Error> {
+ // The official rules say you sort by the first element in the container.
+ // That's an Oid, which implements comparisons. So our job is easy.
+ let mut res = self.0.clone();
+ res.sort_by(|a, b| a.typ.as_ref().cmp(b.typ.as_ref()));
+
+ Ok(Self(res))
+ }
+
+ fn encode_ref(&self) -> impl Values + '_ {
+ encode::set(encode::slice(&self.0, |x| x.clone().encode()))
+ }
+
+ fn encode_ref_as(&self, tag: Tag) -> impl Values + '_ {
+ encode::set_as(tag, encode::slice(&self.0, |x| x.clone().encode()))
+ }
+}
+
+impl Values for SignedAttributes {
+ // SignedAttributes are always written as DER encoded.
+ fn encoded_len(&self, _: Mode) -> usize {
+ self.encode_ref().encoded_len(Mode::Der)
+ }
+
+ fn write_encoded<W: Write>(&self, _: Mode, target: &mut W) -> Result<(), std::io::Error> {
+ self.encode_ref().write_encoded(Mode::Der, target)
+ }
+}
+
+pub struct SignedAttributesDer(SignedAttributes, Option<Tag>);
+
+impl SignedAttributesDer {
+ pub fn new(sa: SignedAttributes, tag: Option<Tag>) -> Self {
+ Self(sa, tag)
+ }
+}
+
+impl Values for SignedAttributesDer {
+ fn encoded_len(&self, _: Mode) -> usize {
+ if let Some(tag) = &self.1 {
+ self.0.encode_ref_as(*tag).encoded_len(Mode::Der)
+ } else {
+ self.0.encode_ref().encoded_len(Mode::Der)
+ }
+ }
+
+ fn write_encoded<W: Write>(&self, _: Mode, target: &mut W) -> Result<(), std::io::Error> {
+ if let Some(tag) = &self.1 {
+ self.0.encode_ref_as(*tag).write_encoded(Mode::Der, target)
+ } else {
+ self.0.encode_ref().write_encoded(Mode::Der, target)
+ }
+ }
+}
+
+/// Unsigned attributes.
+///
+/// ```ASN.1
+/// UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute
+/// ```
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct UnsignedAttributes(Vec<Attribute>);
+
+impl Deref for UnsignedAttributes {
+ type Target = Vec<Attribute>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for UnsignedAttributes {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl UnsignedAttributes {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_set(|cons| Self::take_from_set(cons))
+ }
+
+ pub fn take_from_set<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ let mut attributes = Vec::new();
+
+ while let Some(attribute) = Attribute::take_opt_from(cons)? {
+ attributes.push(attribute);
+ }
+
+ Ok(Self(attributes))
+ }
+
+ pub fn encode_ref_as(&self, tag: Tag) -> impl Values + '_ {
+ encode::set_as(tag, encode::slice(&self.0, |x| x.clone().encode()))
+ }
+}
+
+pub type SignatureValue = OctetString;
+
+/// Enveloped-data content type.
+///
+/// ```ASN.1
+/// EnvelopedData ::= SEQUENCE {
+/// version CMSVersion,
+/// originatorInfo [0] IMPLICIT OriginatorInfo OPTIONAL,
+/// recipientInfos RecipientInfos,
+/// encryptedContentInfo EncryptedContentInfo,
+/// unprotectedAttrs [1] IMPLICIT UnprotectedAttributes OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct EnvelopedData {
+ pub version: CmsVersion,
+ pub originator_info: Option<OriginatorInfo>,
+ pub recipient_infos: RecipientInfos,
+ pub encrypted_content_info: EncryptedContentInfo,
+ pub unprotected_attributes: Option<UnprotectedAttributes>,
+}
+
+/// Originator info.
+///
+/// ```ASN.1
+/// OriginatorInfo ::= SEQUENCE {
+/// certs [0] IMPLICIT CertificateSet OPTIONAL,
+/// crls [1] IMPLICIT RevocationInfoChoices OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct OriginatorInfo {
+ pub certs: Option<CertificateSet>,
+ pub crls: Option<RevocationInfoChoices>,
+}
+
+pub type RecipientInfos = Vec<RecipientInfo>;
+
+/// Encrypted content info.
+///
+/// ```ASN.1
+/// EncryptedContentInfo ::= SEQUENCE {
+/// contentType ContentType,
+/// contentEncryptionAlgorithm ContentEncryptionAlgorithmIdentifier,
+/// encryptedContent [0] IMPLICIT EncryptedContent OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct EncryptedContentInfo {
+ pub content_type: ContentType,
+ pub content_encryption_algorithms: ContentEncryptionAlgorithmIdentifier,
+ pub encrypted_content: Option<EncryptedContent>,
+}
+
+pub type EncryptedContent = OctetString;
+
+pub type UnprotectedAttributes = Vec<Attribute>;
+
+/// Recipient info.
+///
+/// ```ASN.1
+/// RecipientInfo ::= CHOICE {
+/// ktri KeyTransRecipientInfo,
+/// kari [1] KeyAgreeRecipientInfo,
+/// kekri [2] KEKRecipientInfo,
+/// pwri [3] PasswordRecipientinfo,
+/// ori [4] OtherRecipientInfo }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum RecipientInfo {
+ KeyTransRecipientInfo(KeyTransRecipientInfo),
+ KeyAgreeRecipientInfo(KeyAgreeRecipientInfo),
+ KekRecipientInfo(KekRecipientInfo),
+ PasswordRecipientInfo(PasswordRecipientInfo),
+ OtherRecipientInfo(OtherRecipientInfo),
+}
+
+pub type EncryptedKey = OctetString;
+
+/// Key trans recipient info.
+///
+/// ```ASN.1
+/// KeyTransRecipientInfo ::= SEQUENCE {
+/// version CMSVersion, -- always set to 0 or 2
+/// rid RecipientIdentifier,
+/// keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier,
+/// encryptedKey EncryptedKey }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct KeyTransRecipientInfo {
+ pub version: CmsVersion,
+ pub rid: RecipientIdentifier,
+ pub key_encryption_algorithm: KeyEncryptionAlgorithmIdentifier,
+ pub encrypted_key: EncryptedKey,
+}
+
+/// Recipient identifier.
+///
+/// ```ASN.1
+/// RecipientIdentifier ::= CHOICE {
+/// issuerAndSerialNumber IssuerAndSerialNumber,
+/// subjectKeyIdentifier [0] SubjectKeyIdentifier }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum RecipientIdentifier {
+ IssuerAndSerialNumber(IssuerAndSerialNumber),
+ SubjectKeyIdentifier(SubjectKeyIdentifier),
+}
+
+/// Key agreement recipient info.
+///
+/// ```ASN.1
+/// KeyAgreeRecipientInfo ::= SEQUENCE {
+/// version CMSVersion, -- always set to 3
+/// originator [0] EXPLICIT OriginatorIdentifierOrKey,
+/// ukm [1] EXPLICIT UserKeyingMaterial OPTIONAL,
+/// keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier,
+/// recipientEncryptedKeys RecipientEncryptedKeys }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct KeyAgreeRecipientInfo {
+ pub version: CmsVersion,
+ pub originator: OriginatorIdentifierOrKey,
+ pub ukm: Option<UserKeyingMaterial>,
+ pub key_encryption_algorithm: KeyEncryptionAlgorithmIdentifier,
+ pub recipient_encrypted_keys: RecipientEncryptedKeys,
+}
+
+/// Originator identifier or key.
+///
+/// ```ASN.1
+/// OriginatorIdentifierOrKey ::= CHOICE {
+/// issuerAndSerialNumber IssuerAndSerialNumber,
+/// subjectKeyIdentifier [0] SubjectKeyIdentifier,
+/// originatorKey [1] OriginatorPublicKey }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum OriginatorIdentifierOrKey {
+ IssuerAndSerialNumber(IssuerAndSerialNumber),
+ SubjectKeyIdentifier(SubjectKeyIdentifier),
+ OriginatorKey(OriginatorPublicKey),
+}
+
+/// Originator public key.
+///
+/// ```ASN.1
+/// OriginatorPublicKey ::= SEQUENCE {
+/// algorithm AlgorithmIdentifier,
+/// publicKey BIT STRING }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct OriginatorPublicKey {
+ pub algorithm: AlgorithmIdentifier,
+ pub public_key: BitString,
+}
+
+/// SEQUENCE of RecipientEncryptedKey.
+type RecipientEncryptedKeys = Vec<RecipientEncryptedKey>;
+
+/// Recipient encrypted key.
+///
+/// ```ASN.1
+/// RecipientEncryptedKey ::= SEQUENCE {
+/// rid KeyAgreeRecipientIdentifier,
+/// encryptedKey EncryptedKey }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct RecipientEncryptedKey {
+ pub rid: KeyAgreeRecipientInfo,
+ pub encrypted_key: EncryptedKey,
+}
+
+/// Key agreement recipient identifier.
+///
+/// ```ASN.1
+/// KeyAgreeRecipientIdentifier ::= CHOICE {
+/// issuerAndSerialNumber IssuerAndSerialNumber,
+/// rKeyId [0] IMPLICIT RecipientKeyIdentifier }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum KeyAgreeRecipientIdentifier {
+ IssuerAndSerialNumber(IssuerAndSerialNumber),
+ RKeyId(RecipientKeyIdentifier),
+}
+
+/// Recipient key identifier.
+///
+/// ```ASN.1
+/// RecipientKeyIdentifier ::= SEQUENCE {
+/// subjectKeyIdentifier SubjectKeyIdentifier,
+/// date GeneralizedTime OPTIONAL,
+/// other OtherKeyAttribute OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct RecipientKeyIdentifier {
+ pub subject_key_identifier: SubjectKeyIdentifier,
+ pub date: Option<GeneralizedTime>,
+ pub other: Option<OtherKeyAttribute>,
+}
+
+type SubjectKeyIdentifier = OctetString;
+
+/// Key encryption key recipient info.
+///
+/// ```ASN.1
+/// KEKRecipientInfo ::= SEQUENCE {
+/// version CMSVersion, -- always set to 4
+/// kekid KEKIdentifier,
+/// keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier,
+/// encryptedKey EncryptedKey }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct KekRecipientInfo {
+ pub version: CmsVersion,
+ pub kek_id: KekIdentifier,
+ pub kek_encryption_algorithm: KeyEncryptionAlgorithmIdentifier,
+ pub encrypted_key: EncryptedKey,
+}
+
+/// Key encryption key identifier.
+///
+/// ```ASN.1
+/// KEKIdentifier ::= SEQUENCE {
+/// keyIdentifier OCTET STRING,
+/// date GeneralizedTime OPTIONAL,
+/// other OtherKeyAttribute OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct KekIdentifier {
+ pub key_identifier: OctetString,
+ pub date: Option<GeneralizedTime>,
+ pub other: Option<OtherKeyAttribute>,
+}
+
+/// Password recipient info.
+///
+/// ```ASN.1
+/// PasswordRecipientInfo ::= SEQUENCE {
+/// version CMSVersion, -- Always set to 0
+/// keyDerivationAlgorithm [0] KeyDerivationAlgorithmIdentifier
+/// OPTIONAL,
+/// keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier,
+/// encryptedKey EncryptedKey }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PasswordRecipientInfo {
+ pub version: CmsVersion,
+ pub key_derivation_algorithm: Option<KeyDerivationAlgorithmIdentifier>,
+ pub key_encryption_algorithm: KeyEncryptionAlgorithmIdentifier,
+ pub encrypted_key: EncryptedKey,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct OtherRecipientInfo {
+ pub ori_type: Oid,
+ // TODO Any
+ pub ori_value: Option<()>,
+}
+
+/// Digested data.
+///
+/// ```ASN.1
+/// DigestedData ::= SEQUENCE {
+/// version CMSVersion,
+/// digestAlgorithm DigestAlgorithmIdentifier,
+/// encapContentInfo EncapsulatedContentInfo,
+/// digest Digest }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct DigestedData {
+ pub version: CmsVersion,
+ pub digest_algorithm: DigestAlgorithmIdentifier,
+ pub content_type: EncapsulatedContentInfo,
+ pub digest: Digest,
+}
+
+pub type Digest = OctetString;
+
+/// Encrypted data.
+///
+/// ```ASN.1
+/// EncryptedData ::= SEQUENCE {
+/// version CMSVersion,
+/// encryptedContentInfo EncryptedContentInfo,
+/// unprotectedAttrs [1] IMPLICIT UnprotectedAttributes OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct EncryptedData {
+ pub version: CmsVersion,
+ pub encrypted_content_info: EncryptedContentInfo,
+ pub unprotected_attributes: Option<UnprotectedAttributes>,
+}
+
+/// Authenticated data.
+///
+/// ```ASN.1
+/// AuthenticatedData ::= SEQUENCE {
+/// version CMSVersion,
+/// originatorInfo [0] IMPLICIT OriginatorInfo OPTIONAL,
+/// recipientInfos RecipientInfos,
+/// macAlgorithm MessageAuthenticationCodeAlgorithm,
+/// digestAlgorithm [1] DigestAlgorithmIdentifier OPTIONAL,
+/// encapContentInfo EncapsulatedContentInfo,
+/// authAttrs [2] IMPLICIT AuthAttributes OPTIONAL,
+/// mac MessageAuthenticationCode,
+/// unauthAttrs [3] IMPLICIT UnauthAttributes OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AuthenticatedData {
+ pub version: CmsVersion,
+ pub originator_info: Option<OriginatorInfo>,
+ pub recipient_infos: RecipientInfos,
+ pub mac_algorithm: MessageAuthenticationCodeAlgorithm,
+ pub digest_algorithm: Option<DigestAlgorithmIdentifier>,
+ pub content_info: EncapsulatedContentInfo,
+ pub authenticated_attributes: Option<AuthAttributes>,
+ pub mac: MessageAuthenticationCode,
+ pub unauthenticated_attributes: Option<UnauthAttributes>,
+}
+
+pub type AuthAttributes = Vec<Attribute>;
+
+pub type UnauthAttributes = Vec<Attribute>;
+
+pub type MessageAuthenticationCode = OctetString;
+
+pub type SignatureAlgorithmIdentifier = AlgorithmIdentifier;
+
+pub type KeyEncryptionAlgorithmIdentifier = AlgorithmIdentifier;
+
+pub type ContentEncryptionAlgorithmIdentifier = AlgorithmIdentifier;
+
+pub type MessageAuthenticationCodeAlgorithm = AlgorithmIdentifier;
+
+pub type KeyDerivationAlgorithmIdentifier = AlgorithmIdentifier;
+
+/// Revocation info choices.
+///
+/// ```ASN.1
+/// RevocationInfoChoices ::= SET OF RevocationInfoChoice
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct RevocationInfoChoices(Vec<RevocationInfoChoice>);
+
+impl RevocationInfoChoices {
+ pub fn take_from<S: Source>(_cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ Err(Unimplemented.into())
+ }
+}
+
+/// Revocation info choice.
+///
+/// ```ASN.1
+/// RevocationInfoChoice ::= CHOICE {
+/// crl CertificateList,
+/// other [1] IMPLICIT OtherRevocationInfoFormat }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum RevocationInfoChoice {
+ Crl(Box<CertificateList>),
+ Other(OtherRevocationInfoFormat),
+}
+
+/// Other revocation info format.
+///
+/// ```ASN.1
+/// OtherRevocationInfoFormat ::= SEQUENCE {
+/// otherRevInfoFormat OBJECT IDENTIFIER,
+/// otherRevInfo ANY DEFINED BY otherRevInfoFormat }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct OtherRevocationInfoFormat {
+ pub other_rev_info_info_format: Oid,
+ // TODO Any
+ pub other_rev_info: Option<()>,
+}
+
+/// Certificate choices.
+///
+/// ```ASN.1
+/// CertificateChoices ::= CHOICE {
+/// certificate Certificate,
+/// extendedCertificate [0] IMPLICIT ExtendedCertificate, -- Obsolete
+/// v1AttrCert [1] IMPLICIT AttributeCertificateV1, -- Obsolete
+/// v2AttrCert [2] IMPLICIT AttributeCertificateV2,
+/// other [3] IMPLICIT OtherCertificateFormat }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum CertificateChoices {
+ Certificate(Box<Certificate>),
+ // ExtendedCertificate(ExtendedCertificate),
+ // AttributeCertificateV1(AttributeCertificateV1),
+ AttributeCertificateV2(Box<AttributeCertificateV2>),
+ Other(Box<OtherCertificateFormat>),
+}
+
+impl CertificateChoices {
+ pub fn take_opt_from<S: Source>(cons: &mut Constructed<S>) -> Result<Option<Self>, S::Err> {
+ cons.take_opt_constructed_if(Tag::CTX_0, |_cons| -> Result<(), S::Err> {
+ Err(Unimplemented.into())
+ })?;
+ cons.take_opt_constructed_if(Tag::CTX_1, |_cons| -> Result<(), S::Err> {
+ Err(Unimplemented.into())
+ })?;
+
+ // TODO these first 2 need methods that parse an already entered SEQUENCE.
+ if let Some(certificate) = cons
+ .take_opt_constructed_if(Tag::CTX_2, |cons| AttributeCertificateV2::take_from(cons))?
+ {
+ Ok(Some(Self::AttributeCertificateV2(Box::new(certificate))))
+ } else if let Some(certificate) = cons
+ .take_opt_constructed_if(Tag::CTX_3, |cons| OtherCertificateFormat::take_from(cons))?
+ {
+ Ok(Some(Self::Other(Box::new(certificate))))
+ } else if let Some(certificate) =
+ cons.take_opt_constructed(|_, cons| Certificate::from_sequence(cons))?
+ {
+ Ok(Some(Self::Certificate(Box::new(certificate))))
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ match self {
+ Self::Certificate(cert) => cert.encode_ref(),
+ Self::AttributeCertificateV2(_) => unimplemented!(),
+ Self::Other(_) => unimplemented!(),
+ }
+ }
+}
+
+impl Values for CertificateChoices {
+ fn encoded_len(&self, mode: Mode) -> usize {
+ self.encode_ref().encoded_len(mode)
+ }
+
+ fn write_encoded<W: Write>(&self, mode: Mode, target: &mut W) -> Result<(), std::io::Error> {
+ self.encode_ref().write_encoded(mode, target)
+ }
+}
+
+/// Other certificate format.
+///
+/// ```ASN.1
+/// OtherCertificateFormat ::= SEQUENCE {
+/// otherCertFormat OBJECT IDENTIFIER,
+/// otherCert ANY DEFINED BY otherCertFormat }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct OtherCertificateFormat {
+ pub other_cert_format: Oid,
+ // TODO Any
+ pub other_cert: Option<()>,
+}
+
+impl OtherCertificateFormat {
+ pub fn take_from<S: Source>(_cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ Err(Unimplemented.into())
+ }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct CertificateSet(Vec<CertificateChoices>);
+
+impl Deref for CertificateSet {
+ type Target = Vec<CertificateChoices>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for CertificateSet {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl CertificateSet {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ let mut certs = Vec::new();
+
+ while let Some(cert) = CertificateChoices::take_opt_from(cons)? {
+ certs.push(cert);
+ }
+
+ Ok(Self(certs))
+ }
+
+ pub fn encode_ref_as(&self, tag: Tag) -> impl Values + '_ {
+ encode::set_as(tag, &self.0)
+ }
+}
+
+/// Issuer and serial number.
+///
+/// ```ASN.1
+/// IssuerAndSerialNumber ::= SEQUENCE {
+/// issuer Name,
+/// serialNumber CertificateSerialNumber }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct IssuerAndSerialNumber {
+ pub issuer: Name,
+ pub serial_number: CertificateSerialNumber,
+}
+
+impl IssuerAndSerialNumber {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ cons.take_sequence(|cons| {
+ let issuer = Name::take_from(cons)?;
+ let serial_number = Integer::take_from(cons)?;
+
+ Ok(Self {
+ issuer,
+ serial_number,
+ })
+ })
+ }
+
+ pub fn encode_ref(&self) -> impl Values + '_ {
+ encode::sequence((self.issuer.encode_ref(), (&self.serial_number).encode()))
+ }
+}
+
+pub type CertificateSerialNumber = Integer;
+
+/// Version number.
+///
+/// ```ASN.1
+/// CMSVersion ::= INTEGER
+/// { v0(0), v1(1), v2(2), v3(3), v4(4), v5(5) }
+/// ```
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum CmsVersion {
+ V0 = 0,
+ V1 = 1,
+ V2 = 2,
+ V3 = 3,
+ V4 = 4,
+ V5 = 5,
+}
+
+impl CmsVersion {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ match cons.take_primitive_if(Tag::INTEGER, Integer::i8_from_primitive)? {
+ 0 => Ok(Self::V0),
+ 1 => Ok(Self::V1),
+ 2 => Ok(Self::V2),
+ 3 => Ok(Self::V3),
+ 4 => Ok(Self::V4),
+ 5 => Ok(Self::V5),
+ _ => Err(Malformed.into()),
+ }
+ }
+
+ pub fn encode(self) -> impl Values {
+ u8::from(self).encode()
+ }
+}
+
+impl From<CmsVersion> for u8 {
+ fn from(v: CmsVersion) -> u8 {
+ match v {
+ CmsVersion::V0 => 0,
+ CmsVersion::V1 => 1,
+ CmsVersion::V2 => 2,
+ CmsVersion::V3 => 3,
+ CmsVersion::V4 => 4,
+ CmsVersion::V5 => 5,
+ }
+ }
+}
+
+pub type UserKeyingMaterial = OctetString;
+
+/// Other key attribute.
+///
+/// ```ASN.1
+/// OtherKeyAttribute ::= SEQUENCE {
+/// keyAttrId OBJECT IDENTIFIER,
+/// keyAttr ANY DEFINED BY keyAttrId OPTIONAL }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct OtherKeyAttribute {
+ pub key_attribute_id: Oid,
+ // TODO Any
+ pub key_attribute: Option<()>,
+}
+
+pub type ContentType = Oid;
+
+pub type MessageDigest = OctetString;
+
+pub type SigningTime = Time;
+
+/// Time variant.
+///
+/// ```ASN.1
+/// Time ::= CHOICE {
+/// utcTime UTCTime,
+/// generalizedTime GeneralizedTime }
+/// ```
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum Time {
+ UtcTime(UtcTime),
+ GeneralizedTime(GeneralizedTime),
+}
+
+impl Time {
+ pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, S::Err> {
+ if let Some(utc) =
+ cons.take_opt_primitive_if(Tag::UTC_TIME, |prim| UtcTime::from_primitive(prim))?
+ {
+ Ok(Self::UtcTime(utc))
+ } else if let Some(generalized) = cons
+ .take_opt_primitive_if(Tag::GENERALIZED_TIME, |prim| {
+ GeneralizedTime::from_primitive_no_fractional_or_timezone_offsets(prim)
+ })?
+ {
+ Ok(Self::GeneralizedTime(generalized))
+ } else {
+ Err(Malformed.into())
+ }
+ }
+}
+
+impl From<Time> for chrono::DateTime<chrono::Utc> {
+ fn from(t: Time) -> Self {
+ match t {
+ Time::UtcTime(utc) => *utc,
+ Time::GeneralizedTime(gt) => gt.into(),
+ }
+ }
+}
+
+pub type CounterSignature = SignerInfo;
+
+pub type AttributeCertificateV2 = AttributeCertificate;
diff --git a/sdk/src/assertion.rs b/sdk/src/assertion.rs
@@ -0,0 +1,658 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ assertions::labels,
+ error::{Error, Result},
+};
+use std::fmt;
+
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+use serde_bytes::ByteBuf;
+use serde_json::Value;
+use thiserror::Error;
+
+/// Check to see if this a label whose string can vary, if so return the root of the label and version if available
+fn get_mutable_label(var_label: &str) -> (String, Option<usize>) {
+ if var_label.starts_with(labels::SCHEMA_ORG) {
+ (var_label.to_string(), None)
+ } else {
+ // is it a type of thumbnail
+ let tn = get_thumbnail_type(var_label);
+
+ if tn == "none" {
+ let components: Vec<&str> = var_label.split('.').collect();
+ match components.last() {
+ Some(last) => {
+ // check for a valid version number
+ if last.len() > 1 {
+ let (ver, ver_inst_str) = last.split_at(1);
+ if ver == "v" {
+ if let Ok(ver_inst) = ver_inst_str.parse::<usize>() {
+ let ver_trim = format!(".{}", last);
+ let root_label = var_label.trim_end_matches(&ver_trim);
+ return (root_label.to_string(), Some(ver_inst));
+ }
+ }
+ }
+ (var_label.to_string(), None)
+ }
+ None => (var_label.to_string(), None),
+ }
+ } else {
+ (tn, None)
+ }
+ }
+}
+
+pub fn get_thumbnail_type(thumbnail_label: &str) -> String {
+ if thumbnail_label.starts_with(labels::CLAIM_THUMBNAIL) {
+ return labels::CLAIM_THUMBNAIL.to_string();
+ }
+ if thumbnail_label.starts_with(labels::INGREDIENT_THUMBNAIL) {
+ return labels::INGREDIENT_THUMBNAIL.to_string();
+ }
+ "none".to_string()
+}
+
+pub fn get_thumbnail_image_type(thumbnail_label: &str) -> String {
+ let components: Vec<&str> = thumbnail_label.split('.').collect();
+
+ if thumbnail_label.contains("thumbnail") && components.len() >= 4 {
+ let image_type: Vec<&str> = components[3].split('_').collect(); // strip and other label adornments
+ image_type[0].to_ascii_lowercase()
+ } else {
+ "none".to_string()
+ }
+}
+
+pub fn get_thumbnail_instance(label: &str) -> Option<usize> {
+ let label_type = get_thumbnail_type(label);
+ // only ingredients thumbs store ids in the label, so use placeholder ids for the others
+ match label_type.as_ref() {
+ labels::INGREDIENT_THUMBNAIL => {
+ // extract id from underscore separated part of the full label
+ let components: Vec<&str> = label.split("__").collect();
+ if components.len() == 2 {
+ let subparts: Vec<&str> = components[1].split('.').collect();
+ match subparts[0].parse::<usize>() {
+ Ok(i) => Some(i),
+ Err(_e) => None,
+ }
+ } else {
+ Some(0)
+ }
+ }
+ _ => None,
+ }
+}
+
+/// The core required trait for all assertions
+///
+/// This defines the label and version for the assertion
+/// and supplies the to/from converters for c2pa assertion format
+pub trait AssertionBase
+where
+ Self: Sized,
+{
+ const LABEL: &'static str = "unknown";
+
+ const VERSION: Option<usize> = None;
+
+ /// Return a label for this assertion (can be overridden )
+ fn label(&self) -> &str {
+ Self::LABEL
+ }
+
+ /// Returns an Assertion upon success or Error otherwise.
+ fn to_assertion(&self) -> Result<Assertion>;
+
+ /// Returns Self or AssertionDecode Result from an assertion
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self>;
+}
+
+/// Trait to handle default Cbor encoding/decoding of Assertions
+pub trait AssertionCbor: Serialize + DeserializeOwned + AssertionBase {
+ fn to_cbor_assertion(&self) -> Result<Assertion> {
+ let data =
+ AssertionData::Cbor(serde_cbor::to_vec(self).map_err(|_err| Error::AssertionEncoding)?);
+ Ok(Assertion::new(self.label(), Self::VERSION, data))
+ }
+
+ fn from_cbor_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ assertion.check_max_version(Self::VERSION)?;
+
+ match assertion.decode_data() {
+ AssertionData::Cbor(data) => Ok(serde_cbor::from_slice(data)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_cbor_err(assertion, e))?),
+
+ data => Err(AssertionDecodeError::from_assertion_unexpected_data_type(
+ assertion, data, "cbor",
+ )),
+ }
+ }
+}
+
+/// Trait to handle default Json encoding/decoding of Assertions
+pub trait AssertionJson: Serialize + DeserializeOwned + AssertionBase {
+ fn to_json_assertion(&self) -> Result<Assertion> {
+ let data = AssertionData::Json(
+ serde_json::to_string(self).map_err(|_err| Error::AssertionEncoding)?,
+ );
+ Ok(Assertion::new(self.label(), Self::VERSION, data).set_content_type("application/json"))
+ }
+
+ fn from_json_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ assertion.check_max_version(Self::VERSION)?;
+
+ match assertion.decode_data() {
+ AssertionData::Json(data) => Ok(serde_json::from_str(data)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(assertion, e))?),
+ data => Err(AssertionDecodeError::from_assertion_unexpected_data_type(
+ assertion, data, "json",
+ )),
+ }
+ }
+}
+
+/// Assertion data as binary cbor or json depending upon
+/// the Assertion type (see spec).
+/// for Json assertions the data is a Json string and Vec<u8> for
+/// binary data and json data to be cbor encoded.
+#[derive(Deserialize, Serialize, PartialEq, Clone)]
+pub enum AssertionData {
+ Json(String), // json encoded data
+ Binary(Vec<u8>), // binary data
+ Cbor(Vec<u8>), // binary cbor encoded data
+ Uuid(String, Vec<u8>), // user defined content (uuid, data)
+}
+
+impl fmt::Debug for AssertionData {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Self::Json(s) => write!(f, "{:?}", s), // json encoded data
+ Self::Binary(_) => write!(f, "<omitted>"),
+ Self::Uuid(uuid, _) => {
+ write!(f, "uuid: {}, <omitted>", uuid)
+ }
+ Self::Cbor(s) => {
+ let buf: Vec<u8> = Vec::new();
+ let mut from = serde_cbor::Deserializer::from_slice(s);
+ let mut to = serde_json::Serializer::pretty(buf);
+
+ serde_transcode::transcode(&mut from, &mut to).map_err(|_err| fmt::Error)?;
+ let buf2 = to.into_inner();
+
+ let decoded: Value = serde_json::from_slice(&buf2).map_err(|_err| fmt::Error)?;
+
+ write!(f, "{:?}", decoded.to_string())
+ }
+ }
+ }
+}
+
+/// Standard Assertion data types. Each assertion type will
+/// contain its AssertionData. For the User Assertion type we
+/// allow a String to set the label. The AssertionData contains
+/// the data payload for the assertion and the version number for its schema (if supported).
+#[derive(Clone, Debug, PartialEq)]
+pub struct Assertion {
+ label: String,
+ version: Option<usize>,
+ data: AssertionData,
+ content_type: String,
+}
+
+impl Assertion {
+ pub fn new(label: &str, version: Option<usize>, data: AssertionData) -> Self {
+ Self {
+ label: label.to_owned(),
+ version,
+ content_type: "application/cbor".to_owned(),
+ data,
+ }
+ }
+
+ pub fn set_content_type(mut self, content_type: &str) -> Self {
+ self.content_type = content_type.to_owned();
+ self
+ }
+
+ /// return content_type for the the data enclosed in the Assertion
+ pub fn content_type(&self) -> String {
+ self.content_type.clone()
+ }
+
+ pub fn set_data(mut self, data: &AssertionData) -> Self {
+ self.data = data.to_owned();
+ self
+ }
+
+ // Return version string of known assertion if available
+ pub fn get_ver(&self) -> Option<usize> {
+ self.version
+ }
+
+ pub fn check_version(&self, max_version: usize) -> AssertionDecodeResult<()> {
+ match self.version {
+ Some(version) if version > max_version => Err(AssertionDecodeError {
+ label: self.label.clone(),
+ version: self.version,
+ content_type: self.content_type.clone(),
+ source: AssertionDecodeErrorCause::AssertionTooNew {
+ max: max_version,
+ found: version,
+ },
+ }),
+ _ => Ok(()),
+ }
+ }
+
+ /// Return a reference to the AssertionData bound to this Assertion
+ pub fn decode_data(&self) -> &AssertionData {
+ &self.data
+ }
+
+ /// return mimetype for the the data enclosed in the Assertion
+ pub fn mime_type(&self) -> String {
+ self.content_type.clone()
+ }
+
+ /// Test to see if the Assertions are of the same variant
+ pub fn assertions_eq(a: &Assertion, b: &Assertion) -> bool {
+ a.label_root() == b.label_root()
+ }
+
+ /// Return the CAI label for this Assertion (no version)
+ pub fn label_root(&self) -> String {
+ let label = get_mutable_label(&self.label).0;
+ // thumbnails need the image_type added
+ match get_thumbnail_image_type(&self.label).as_str() {
+ "none" => label,
+ image_type => format!("{}.{}", label, image_type),
+ }
+ }
+
+ /// Return the CAI label for this Assertion with version string if available
+ pub fn label(&self) -> String {
+ let base_label = self.label_root();
+ match self.get_ver() {
+ Some(v) => {
+ if v > 1 {
+ // c2pa does not include v1 labels
+ format!("{}.v{}", base_label, v)
+ } else {
+ base_label
+ }
+ }
+ None => base_label,
+ }
+ }
+
+ /// Return a reference to the data as a byte array
+ pub fn data(&self) -> &[u8] {
+ // return bytes of the assertion data
+ match self.decode_data() {
+ AssertionData::Json(x) => x.as_bytes(), // json encoded data
+ AssertionData::Binary(x) | AssertionData::Uuid(_, x) => x, // binary data
+ AssertionData::Cbor(x) => x,
+ }
+ }
+
+ /// Return assertion as serde_json Object
+ /// this may have loss of cbor structure if unsupported in conversion to json
+ pub fn as_json_object(&self) -> AssertionDecodeResult<Value> {
+ match self.decode_data() {
+ AssertionData::Json(x) => serde_json::from_str(x)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e)),
+
+ AssertionData::Cbor(x) => {
+ let buf: Vec<u8> = Vec::new();
+ let mut from = serde_cbor::Deserializer::from_slice(x);
+ let mut to = serde_json::Serializer::new(buf);
+
+ serde_transcode::transcode(&mut from, &mut to)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))?;
+
+ let buf2 = to.into_inner();
+ serde_json::from_slice(&buf2)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))
+ }
+
+ AssertionData::Binary(x) => {
+ let binary_bytes = ByteBuf::from(x.clone());
+ let binary_str = serde_json::to_string(&binary_bytes)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))?;
+
+ serde_json::from_str(&binary_str)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))
+ }
+ AssertionData::Uuid(uuid, x) => {
+ #[derive(Serialize)]
+ struct TmpObj<'a> {
+ uuid: &'a str,
+ data: ByteBuf,
+ }
+
+ let v = TmpObj {
+ uuid,
+ data: ByteBuf::from(x.clone()),
+ };
+
+ let binary_str = serde_json::to_string(&v)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))?;
+
+ serde_json::from_str(&binary_str)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(self, e))
+ }
+ }
+ }
+
+ fn from_assertion_data(label: &str, content_type: &str, data: AssertionData) -> Assertion {
+ use crate::claim::Claim;
+ let version = labels::version(label);
+ let (label, instance) = Claim::assertion_label_from_link(label);
+ let label = Claim::label_with_instance(&label, instance);
+
+ Self {
+ label,
+ version,
+ data,
+ content_type: content_type.to_owned(),
+ }
+ }
+
+ /// create an assertion from binary data
+ pub fn from_data_binary(label: &str, mime_type: &str, binary_data: &[u8]) -> Assertion {
+ Self::from_assertion_data(
+ label,
+ mime_type,
+ AssertionData::Binary(binary_data.to_vec()),
+ )
+ }
+
+ /// create an assertion from user binary data
+ pub fn from_data_uuid(label: &str, uuid_str: &str, binary_data: &[u8]) -> Assertion {
+ Self::from_assertion_data(
+ label,
+ "application/octet-stream",
+ AssertionData::Uuid(uuid_str.to_owned(), binary_data.to_vec()),
+ )
+ }
+
+ pub fn from_data_cbor(label: &str, binary_data: &[u8]) -> Assertion {
+ Self::from_assertion_data(
+ label,
+ "application/cbor",
+ AssertionData::Cbor(binary_data.to_vec()),
+ )
+ }
+
+ pub fn from_data_json(label: &str, binary_data: &[u8]) -> AssertionDecodeResult<Assertion> {
+ let json = String::from_utf8(binary_data.to_vec()).map_err(|_| AssertionDecodeError {
+ label: label.to_string(),
+ version: None, // TODO: Can we get this info?
+ content_type: "json".to_string(),
+ source: AssertionDecodeErrorCause::BinaryDataNotUtf8,
+ })?;
+
+ Ok(Self::from_assertion_data(
+ label,
+ "application/json",
+ AssertionData::Json(json),
+ ))
+ }
+
+ // Check assertion label against a target label.
+ pub fn check_version_from_label(&self, desired_version: usize) -> AssertionDecodeResult<()> {
+ if let Some(base_version) = labels::version(&self.label) {
+ if desired_version > base_version {
+ return Err(AssertionDecodeError {
+ label: self.label.clone(),
+ version: self.version,
+ content_type: self.content_type.clone(),
+ source: AssertionDecodeErrorCause::AssertionTooNew {
+ max: desired_version,
+ found: base_version,
+ },
+ });
+ }
+ }
+
+ Ok(())
+ }
+
+ fn check_max_version(&self, max_version: Option<usize>) -> AssertionDecodeResult<()> {
+ if let Some(data_version) = self.version {
+ if let Some(max_version) = max_version {
+ if data_version > max_version {
+ return Err(AssertionDecodeError {
+ label: self.label.clone(),
+ version: self.version,
+ content_type: self.content_type.clone(),
+ source: AssertionDecodeErrorCause::AssertionTooNew {
+ max: max_version,
+ found: data_version,
+ },
+ });
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct JsonAssertionData {
+ label: String,
+ data: Value,
+ is_cbor: bool,
+}
+
+/// This error type is returned when an assertion can not be decoded.
+#[non_exhaustive]
+pub struct AssertionDecodeError {
+ pub label: String,
+ pub version: Option<usize>,
+ pub content_type: String,
+ pub source: AssertionDecodeErrorCause,
+}
+
+impl AssertionDecodeError {
+ fn fmt_internal(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "could not decode assertion {} (version {}, content type {}): {}",
+ self.label,
+ self.version
+ .map_or("(no version)".to_string(), |v| v.to_string()),
+ self.content_type,
+ self.source
+ )
+ }
+
+ pub(crate) fn from_assertion_and_cbor_err(
+ assertion: &Assertion,
+ source: serde_cbor::error::Error,
+ ) -> Self {
+ Self {
+ label: assertion.label.clone(),
+ version: assertion.version,
+ content_type: assertion.content_type.clone(),
+ source: source.into(),
+ }
+ }
+
+ pub(crate) fn from_assertion_and_json_err(
+ assertion: &Assertion,
+ source: serde_json::error::Error,
+ ) -> Self {
+ Self {
+ label: assertion.label.clone(),
+ version: assertion.version,
+ content_type: assertion.content_type.clone(),
+ source: source.into(),
+ }
+ }
+
+ pub(crate) fn from_assertion_unexpected_data_type(
+ assertion: &Assertion,
+ assertion_data: &AssertionData,
+ expected: &str,
+ ) -> Self {
+ Self {
+ label: assertion.label.clone(),
+ version: assertion.version,
+ content_type: assertion.content_type.clone(),
+ source: AssertionDecodeErrorCause::UnexpectedDataType {
+ expected: expected.to_string(),
+ found: Self::data_type_from_assertion_data(assertion_data),
+ },
+ }
+ }
+
+ fn data_type_from_assertion_data(assertion_data: &AssertionData) -> String {
+ match assertion_data {
+ AssertionData::Json(_) => "json".to_string(),
+ AssertionData::Binary(_) => "binary".to_string(),
+ AssertionData::Cbor(_) => "cbor".to_string(),
+ AssertionData::Uuid(_, _) => "uuid".to_string(),
+ }
+ }
+
+ pub(crate) fn from_json_err(
+ label: String,
+ version: Option<usize>,
+ content_type: String,
+ source: serde_json::error::Error,
+ ) -> Self {
+ Self {
+ label,
+ version,
+ content_type,
+ source: source.into(),
+ }
+ }
+}
+
+impl std::fmt::Debug for AssertionDecodeError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.fmt_internal(f)
+ }
+}
+
+impl std::fmt::Display for AssertionDecodeError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.fmt_internal(f)
+ }
+}
+
+impl std::error::Error for AssertionDecodeError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ Some(&self.source)
+ }
+}
+
+/// This error type is used inside `AssertionDecodeError` to describe the
+/// root cause for the decoding error.
+#[derive(Debug, Error)]
+#[non_exhaustive]
+pub enum AssertionDecodeErrorCause {
+ /// The assertion had an unexpected data type.
+ #[error("the assertion had an unexpected data type: expected {expected}, found {found}")]
+ UnexpectedDataType { expected: String, found: String },
+
+ /// The assertion has a version that is newer that this toolkit can understand.
+ #[error("the assertion version is too new: expected no later than {max}, found {found}")]
+ AssertionTooNew { max: usize, found: usize },
+
+ /// Binary data could not be interepreted as UTF-8.
+ #[error("binary data could not be interpreted as UTF-8")]
+ BinaryDataNotUtf8,
+
+ /// Assertion data did not match hash link.
+ #[error("the assertion data did not match the hash embedded in the link")]
+ AssertionDataIncorrect,
+
+ #[error(transparent)]
+ JsonError(#[from] serde_json::Error),
+
+ #[error(transparent)]
+ CborError(#[from] serde_cbor::Error),
+}
+
+pub type AssertionDecodeResult<T> = std::result::Result<T, AssertionDecodeError>;
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use crate::assertions::{Action, Actions};
+
+ #[test]
+ fn test_version_label() {
+ let test_json = r#"{
+ "left": 0,
+ "right": 2000,
+ "top": 1000,
+ "botton": 4000
+ }"#;
+ let json = AssertionData::Json(test_json.to_string());
+ let json2 = AssertionData::Json(test_json.to_string());
+
+ let a = Assertion::new(Actions::LABEL, Some(2), json);
+ let a_no_ver = Assertion::new(Actions::LABEL, None, json2);
+
+ assert_eq!(a.get_ver().unwrap(), 2);
+ assert_eq!(a_no_ver.get_ver(), None);
+ assert_eq!(a.label(), format!("{}.{}", Actions::LABEL, "v2"));
+ assert_eq!(a.label_root(), Actions::LABEL);
+ assert_eq!(a_no_ver.label(), Actions::LABEL);
+ }
+
+ #[test]
+ fn test_cbor_conversion() {
+ let action = Actions::new()
+ .add_action(
+ Action::new("c2pa.cropped")
+ .set_parameter(
+ "coordinate".to_owned(),
+ r#"{"left": 0,"right": 2000,"top": 1000,"botton": 4000}"#,
+ )
+ .unwrap(),
+ )
+ .add_action(
+ Action::new("c2pa.filtered")
+ .set_parameter("name".to_owned(), "gaussian blur")
+ .unwrap()
+ .set_software_agent("Photoshop")
+ .set_when("2015-06-26T16:43:23+0200"),
+ )
+ .to_assertion()
+ .unwrap();
+
+ let action_cbor = action.data();
+
+ let action_restored = Assertion::from_data_cbor(&action.label(), action_cbor);
+
+ assert!(Assertion::assertions_eq(&action, &action_restored));
+
+ let action_obj = action.as_json_object().unwrap();
+ let action_restored_obj = action_restored.as_json_object().unwrap();
+
+ assert_eq!(action_obj, action_restored_obj);
+ }
+}
diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs
@@ -0,0 +1,410 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ assertion::{Assertion, AssertionBase, AssertionCbor, AssertionDecodeResult},
+ assertions::{labels, Actor, Metadata},
+ error::Result,
+ Error,
+};
+
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::collections::HashMap;
+
+/// Specification defined C2PA actions
+pub mod c2pa_action {
+ /// Changes to tone, saturation, etc.
+ pub const COLOR_ADJUSTMENTS: &str = "c2pa.color_adjustments";
+ /// The format of the asset was changed.
+ pub const CONVERTED: &str = "c2pa.converted";
+ /// The asset was first created, usually the asset's origin.
+ pub const CREATED: &str = "c2pa.created";
+ /// Areas of the asset's "editorial" content were cropped out.
+ pub const CROPPED: &str = "c2pa.cropped";
+ /// Changes using drawing tools including brushes or eraser.
+ pub const DRAWING: &str = "c2pa.drawing";
+ /// Generalized actions that affect the "editorial" meaning of the content.
+ pub const EDITED: &str = "c2pa.edited";
+ /// Changes to appearance with applied filters, styles, etc.
+ pub const FILTERED: &str = "c2pa.filtered";
+ /// An existing asset was opened and is being set as the `parentOf` ingredient.
+ pub const OPENED: &str = "c2pa.opened";
+ /// Changes to the direction and position of content.
+ pub const ORIENTATION: &str = "c2pa.orientation";
+ /// Added/Placed a `componentOf` ingredient into the asset.
+ pub const PLACED: &str = "c2pa.placed";
+ /// Asset is released to a wider audience.
+ pub const PUBLISHED: &str = "c2pa.published";
+ /// A conversion of one packaging or container format to another. Content may be repackaged without transcoding.
+ /// Does not include any adjustments that would affect the "editorial" meaning of the content.
+ pub const REPACKAGED: &str = "c2pa.repackaged";
+ /// Changes to content dimensions and/or file size
+ pub const RESIZED: &str = "c2pa.resized";
+ /// A direct conversion of one encoding to another, including resolution scaling, bitrate adjustment and encoding format change.
+ /// Does not include any adjustments that would affect the "editorial" meaning of the content.
+ pub const TRANSCODED: &str = "c2pa.transcoded";
+ /// Something happened, but the claim_generator cannot specify what.
+ pub const UNKNOWN: &str = "c2pa.unknown";
+}
+
+/// Defines an action taken on an image
+#[derive(Deserialize, Serialize, Debug, PartialEq)]
+pub struct Action {
+ #[serde(rename = "action")]
+ pub label: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub when: Option<String>,
+ #[serde(rename = "softwareAgent", skip_serializing_if = "Option::is_none")]
+ pub software_agent: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub changed: Option<String>,
+ #[serde(rename = "InstanceId", skip_serializing_if = "Option::is_none")]
+ pub instance_id: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub parameters: Option<HashMap<String, Value>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub actors: Option<Vec<Actor>>,
+}
+
+impl Action {
+ pub fn new(label: &str) -> Self {
+ Self {
+ label: label.to_owned(),
+ when: None,
+ software_agent: None,
+ changed: None,
+ instance_id: None,
+ parameters: None,
+ actors: None,
+ }
+ }
+
+ /// set Timestamp of when the action occurred
+ pub fn set_when(mut self, when: &str) -> Self {
+ self.when = Some(when.to_owned());
+ self
+ }
+
+ /// Set the software agent that performed the action.
+ pub fn set_software_agent(mut self, software_agent: &str) -> Self {
+ self.software_agent = Some(software_agent.to_owned());
+ self
+ }
+
+ /// Set a list of the parts of the resource that were changed since the previous event history.
+ pub fn set_changed(mut self, changed: Option<&Vec<&str>>) -> Self {
+ self.changed = changed.map(|v| v.join(";"));
+ self
+ }
+
+ /// The value of the xmpMM:InstanceID property for the modified (output) resource
+ pub fn set_instance_id(mut self, id: &str) -> Self {
+ self.instance_id = Some(id.to_owned());
+ self
+ }
+
+ /// Set additional parameters of the action. These will often vary by the type of action
+ pub fn set_parameter<T: Serialize>(mut self, key: String, value: T) -> Result<Self> {
+ let value = serde_json::to_value(value).map_err(|_| Error::AssertionEncoding)?;
+ self.parameters = Some(match self.parameters {
+ Some(mut parameters) => {
+ parameters.insert(key, value);
+ parameters
+ }
+ None => {
+ let mut p = HashMap::new();
+ p.insert(key, value);
+ p
+ }
+ });
+ Ok(self)
+ }
+
+ /// An array of the creators that undertook this action
+ pub fn set_actors(mut self, actors: Option<&Vec<Actor>>) -> Self {
+ self.actors = actors.cloned();
+ self
+ }
+}
+
+/// A list of actions as an assertion
+#[derive(Deserialize, Serialize, Debug, PartialEq)]
+pub struct Actions {
+ pub actions: Vec<Action>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub metadata: Option<Metadata>,
+}
+
+impl Actions {
+ /// Label prefix for an actions assertion.
+ ///
+ /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>.
+ pub const LABEL: &'static str = labels::ACTIONS;
+
+ /// creates a new Actions object
+ pub fn new() -> Self {
+ Self {
+ actions: Vec::new(),
+ metadata: None,
+ }
+ }
+
+ /// Adds an action
+ pub fn add_action(&mut self, action: Action) -> &mut Self {
+ self.actions.push(action);
+ self
+ }
+
+ /// Adds a metadata structure to the action
+ pub fn add_metadata(&mut self, metadata: Metadata) -> &Self {
+ self.metadata = Some(metadata);
+ self
+ }
+
+ /// creates an actions assertion from a compatible JSON Value
+ pub fn from_json_value(json: &serde_json::Value) -> Result<Self> {
+ let actions: Actions = serde_json::from_value(json.clone())?;
+ Ok(actions)
+ }
+}
+
+impl AssertionCbor for Actions {}
+
+impl AssertionBase for Actions {
+ const LABEL: &'static str = labels::ACTIONS;
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ Self::to_cbor_assertion(self)
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ Self::from_cbor_assertion(assertion)
+ }
+}
+
+impl Default for Actions {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::panic)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use crate::assertion::{Assertion, AssertionData};
+ use crate::assertions::metadata::{DataSource, ReviewRating, C2PA_SOURCE_GENERATOR_REE};
+ use crate::hashed_uri::HashedUri;
+
+ fn make_hashed_uri1() -> HashedUri {
+ HashedUri::new(
+ "self#jumbf=verified_credentials/1234".to_string(),
+ None,
+ b"hashed",
+ )
+ }
+
+ fn make_action1() -> Action {
+ Action::new(c2pa_action::CROPPED)
+ .set_software_agent("test")
+ .set_when("2015-06-26T16:43:23+0200")
+ .set_parameter(
+ "foo".to_owned(),
+ &r#"{
+ "left": 0,
+ "right": 2000,
+ "top": 1000,
+ "bottom": 4000
+ }"#
+ .to_owned(),
+ )
+ .unwrap()
+ .set_parameter("ingredient".to_owned(), &make_hashed_uri1())
+ .unwrap()
+ .set_changed(Some(&["this", "that"].to_vec()))
+ .set_instance_id("xmp.iid:cb9f5498-bb58-4572-8043-8c369e6bfb9b")
+ .set_actors(Some(
+ &[Actor::new(
+ Some("Somebody"),
+ Some(&[make_hashed_uri1()].to_vec()),
+ )]
+ .to_vec(),
+ ))
+ }
+
+ #[test]
+ fn assertion_actions() {
+ let mut original = Actions::new();
+ original
+ .add_action(make_action1())
+ .add_action(
+ Action::new("c2pa.filtered")
+ .set_parameter("name".to_owned(), &"gaussian blur")
+ .unwrap()
+ .set_when("2015-06-26T16:43:23+0200"),
+ )
+ .add_metadata(
+ Metadata::new()
+ .add_review(ReviewRating::new("foo", Some("bar".to_owned()), 3))
+ .set_reference(Some(make_hashed_uri1()))
+ .set_data_source(Some(DataSource::new(C2PA_SOURCE_GENERATOR_REE))),
+ );
+
+ dbg!(&original);
+ assert_eq!(original.actions.len(), 2);
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/cbor");
+ assert_eq!(assertion.label(), Actions::LABEL);
+
+ let result = Actions::from_assertion(&assertion).expect("extract_assertion");
+ assert_eq!(result.actions.len(), 2);
+ assert_eq!(result.actions[0].label, original.actions[0].label);
+ assert_eq!(
+ result.actions[0].parameters.as_ref().unwrap().get("name"),
+ original.actions[0].parameters.as_ref().unwrap().get("name")
+ );
+ assert_eq!(result.actions[1].label, original.actions[1].label);
+ assert_eq!(
+ result.actions[1].parameters.as_ref().unwrap().get("name"),
+ original.actions[1].parameters.as_ref().unwrap().get("name")
+ );
+ assert_eq!(result.actions[1].when, original.actions[1].when);
+ assert_eq!(
+ result.metadata.unwrap().date_time,
+ original.metadata.unwrap().date_time
+ );
+ }
+
+ #[test]
+ fn test_build_assertion() {
+ let assertion = Actions::new()
+ .add_action(
+ Action::new("c2pa.cropped")
+ .set_parameter(
+ "coordinate".to_owned(),
+ r#"{
+ "left": 0,
+ "right": 2000,
+ "top": 1000,
+ "bottom": 4000
+ }"#,
+ )
+ .unwrap(),
+ )
+ .add_action(
+ Action::new("c2pa.filtered")
+ .set_parameter("name".to_owned(), "gaussian blur")
+ .unwrap()
+ .set_when("2015-06-26T16:43:23+0200"),
+ )
+ .to_assertion()
+ .unwrap();
+
+ println!("assertion label: {}", assertion.label());
+
+ let j = assertion.data();
+ //println!("assertion as json {:#?}", j);
+
+ let from_j = Assertion::from_data_cbor(&assertion.label(), j);
+ let ad_ref = from_j.decode_data();
+
+ if let AssertionData::Cbor(ref ad_cbor) = ad_ref {
+ // compare results
+ let orig_d = assertion.decode_data();
+ if let AssertionData::Cbor(ref orig_cbor) = orig_d {
+ assert_eq!(orig_cbor, ad_cbor);
+ } else {
+ panic!("Couldn't decode orig_d");
+ }
+ } else {
+ panic!("Couldn't decode ad_ref");
+ }
+ }
+
+ #[test]
+ fn test_binary_round_trip() {
+ let assertion = Actions::new()
+ // .set_dictionary("http://testdictionary")
+ .add_action(
+ Action::new("c2pa.cropped")
+ .set_parameter(
+ "name".to_owned(),
+ r#"{
+ "left": 0,
+ "right": 2000,
+ "top": 1000,
+ "bottom": 4000
+ }"#,
+ )
+ .unwrap(),
+ )
+ .add_action(
+ Action::new("c2pa.filtered")
+ .set_parameter("name".to_owned(), "gaussian blur")
+ .unwrap()
+ .set_when("2015-06-26T16:43:23+0200"),
+ )
+ .to_assertion()
+ .unwrap();
+
+ let orig_bytes = assertion.data();
+
+ let assertion_from_binary = Assertion::from_data_cbor(&assertion.label(), orig_bytes);
+
+ println!(
+ "Label Match Test {} = {}",
+ assertion.label(),
+ assertion_from_binary.label()
+ );
+
+ assert_eq!(assertion.label(), assertion_from_binary.label());
+
+ // compare the data as bytes
+ assert_eq!(orig_bytes, assertion_from_binary.data());
+ println!("Decoded binary matches")
+ }
+
+ #[test]
+ fn test_json_round_trip() {
+ let json = serde_json::json!({
+ "actions": [
+ {
+ "action": "c2pa.edited",
+ "parameters": {
+ "description": "gradient",
+ "name": "any value"
+ }
+ },
+ {
+ "action": "c2pa.edited",
+ "parameters": {
+ "description": "import"
+ }
+ },
+ ],
+ "metadata": {
+ "mytag": "myvalue"
+ }
+ });
+ let original = Actions::from_json_value(&json).expect("from json");
+ let assertion = original.to_assertion().expect("build_assertion");
+ let result = Actions::from_assertion(&assertion).expect("extract_assertion");
+ println!("{:?}", serde_json::to_string(&result));
+ assert_eq!(original.actions, result.actions);
+ }
+}
diff --git a/sdk/src/assertions/creative_work.rs b/sdk/src/assertions/creative_work.rs
@@ -0,0 +1,219 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+
+use crate::{
+ assertion::{Assertion, AssertionBase, AssertionDecodeResult, AssertionJson},
+ assertions::{labels, SchemaDotOrg, SchemaDotOrgPerson},
+ error::Result,
+};
+use serde_json::json;
+use std::ops::Deref;
+
+const ASSERTION_CREATION_VERSION: usize = 1;
+const CW_AUTHOR: &str = "author";
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct CreativeWork(SchemaDotOrg);
+
+impl CreativeWork {
+ /// Label prefix for a creative work assertion.
+ ///
+ /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_creative_work>.
+ pub const LABEL: &'static str = labels::CREATIVE_WORK;
+
+ pub fn new() -> CreativeWork {
+ Self(
+ SchemaDotOrg::new("CreativeWork".to_owned()).set_context(json!("http://schema.org/")),
+ // todo: this should reflect the c2pa extensions in some way to be correct
+ //.set_context(json!(["http://schema.org/",{"credential": {"@id": "c2pa:Credential"},"alg": {"@id": "c2pa:Alg"},"hash": {"@id": "c2pa:hash"}}]))
+ )
+ }
+
+ /// get values by key
+ pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
+ self.0.get(key)
+ }
+
+ /// insert key / value pair
+ pub fn insert<T: Serialize>(self, key: String, value: T) -> Result<Self> {
+ self.0.insert(key, value).map(Self)
+ }
+
+ /// get creative work from json string
+ pub fn from_json_str(json: &str) -> Result<Self> {
+ SchemaDotOrg::from_json_str(json).map(Self)
+ }
+
+ // get author field if it exists
+ pub fn author(&self) -> Option<Vec<SchemaDotOrgPerson>> {
+ self.get(CW_AUTHOR)
+ }
+
+ pub fn set_author(self, author: &[SchemaDotOrgPerson]) -> Result<Self> {
+ self.insert(CW_AUTHOR.to_owned(), &author)
+ }
+
+ pub fn add_author(self, author: SchemaDotOrgPerson) -> Result<Self> {
+ let mut v = self.author().unwrap_or_default();
+ v.push(author);
+ self.insert(CW_AUTHOR.to_owned(), &v)
+ }
+}
+
+impl Default for CreativeWork {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Deref for CreativeWork {
+ type Target = SchemaDotOrg;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl AssertionJson for CreativeWork {}
+
+impl AssertionBase for CreativeWork {
+ const LABEL: &'static str = Self::LABEL;
+ const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ Self::to_json_assertion(self)
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ Self::from_json_assertion(assertion)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ use crate::hashed_uri::HashedUri;
+
+ const USER: &str = "Joe Bloggs";
+ const USER_ID: &str = "1234567890";
+ const IDENTITY_URI: &str = "https://some_identity/service/";
+
+ // example CreativeWork from
+ // https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_claim_review
+ const SAMPLE_CREATIVE_WORK: &str = r#"{
+ "@context": [
+ "http://schema.org/",
+ {
+ "credential": null
+ }
+ ],
+ "@type": "CreativeWork",
+ "datePublished": "2021-05-20T23:02:36+00:00",
+ "publisher": {
+ "name": "BBC News",
+ "publishingPrinciples": "https://www.bbc.co.uk/news/help-41670342",
+ "logo": "https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/5.1.0/bbc_news_logo.png",
+ "parentOrganization": {
+ "name": "BBC",
+ "legalName": "British Broadcasting Corporation"
+ }
+ },
+ "url": "https://www.bbc.co.uk/news/av/world-europe-57194011",
+ "identifier": "p09j7vzv",
+ "producer": {
+ "identifier": "https://en.wikipedia.org/wiki/Joe_Bloggs",
+ "name": "Joe Bloggs",
+ "credential": [
+ {
+ "url": "self#jumbf=c2pa/urn:uuid:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.credentials/Joe_Bloggs",
+ "alg": "sha256",
+ "hash": "Auxjtmax46cC2N3Y9aFmBO9Jfay8LEwJWzBUtZ0sUM8gA"
+ }
+ ]
+ },
+ "copyrightHolder": {
+ "name": "BBC",
+ "legalName": "British Broadcasting Corporation"
+ },
+ "copyrightYear": 2021,
+ "copyrightNotice": "Copyright © 2021 BBC."
+ }"#;
+
+ const STOCK_CREATIVE_WORK: &str = r#"{"@type":"CreativeWork","@context":"https://schema.org","url":"https://stock.adobe.com/295991044"}"#;
+
+ #[test]
+ fn assertion_creative_work() {
+ let uri = HashedUri::new(USER_ID.to_string(), None, b"abcde");
+ let cw_person = SchemaDotOrgPerson::new()
+ .set_name(USER.to_owned())
+ .unwrap()
+ .set_identifier(IDENTITY_URI.to_owned())
+ .unwrap()
+ .insert(
+ "@id".to_owned(),
+ ["https://www.twitter.com/joebloggs".to_owned()].to_vec(),
+ )
+ .unwrap()
+ .add_credential(uri)
+ .unwrap();
+ let original = CreativeWork::new()
+ .add_author(cw_person.clone())
+ .expect("add_author")
+ // example of adding a different kind of person field
+ .insert("creator".to_owned(), cw_person)
+ .expect("insert");
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/json");
+ assert_eq!(assertion.label(), CreativeWork::LABEL);
+ let result = CreativeWork::from_assertion(&assertion).expect("extract_assertion");
+ dbg!(serde_json::to_string(&result).unwrap());
+ assert_eq!(
+ original.author().unwrap()[0].name(),
+ result.author().unwrap()[0].name()
+ );
+ }
+
+ #[test]
+ fn from_creative_work_sample() {
+ let original = CreativeWork::from_json_str(SAMPLE_CREATIVE_WORK).expect("from_json_str");
+ dbg!(&original);
+ let original_publisher: SchemaDotOrgPerson = original.get("publisher").unwrap();
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/json");
+ assert_eq!(assertion.label(), CreativeWork::LABEL);
+ let result = CreativeWork::from_assertion(&assertion).expect("extract_assertion");
+ assert_eq!(original.object_type(), result.object_type());
+ let result_publisher: SchemaDotOrgPerson = result.get("publisher").unwrap();
+ assert_eq!(result_publisher.name().unwrap(), "BBC News");
+ assert_eq!(original_publisher.name(), result_publisher.name());
+ }
+
+ #[test]
+ fn from_creative_work_stock() {
+ let original = CreativeWork::from_json_str(STOCK_CREATIVE_WORK).expect("from_json_str");
+ dbg!(&original);
+ let original_url: String = original.get("url").unwrap();
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/json");
+ assert_eq!(assertion.label(), CreativeWork::LABEL);
+ let result = CreativeWork::from_assertion(&assertion).expect("extract_assertion");
+ assert_eq!(original.object_type(), result.object_type());
+ let result_url: String = result.get("url").unwrap();
+ assert_eq!(original_url, result_url);
+ }
+}
diff --git a/sdk/src/assertions/data_hash.rs b/sdk/src/assertions/data_hash.rs
@@ -0,0 +1,326 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::{fs, path::*};
+
+use serde::{Deserialize, Serialize};
+use serde_bytes::ByteBuf;
+
+use crate::{
+ assertion::{Assertion, AssertionBase, AssertionCbor, AssertionDecodeResult},
+ assertions::labels,
+ cbor_types::UriT,
+ error::{wrap_io_err, Error, Result},
+ utils::hash_utils::{hash_by_alg, verify_by_alg, Exclusion},
+};
+
+const ASSERTION_CREATION_VERSION: usize = 1;
+
+/// Helper class to create DataHash assertion
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+pub struct DataHash {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub exclusions: Option<Vec<Exclusion>>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub name: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub alg: Option<String>,
+
+ #[serde(with = "serde_bytes")]
+ pub hash: Vec<u8>,
+ #[serde(with = "serde_bytes")]
+ pub pad: Vec<u8>,
+
+ // must use explicit ByteBuf here because #[serde(with = "serde_bytes")] does not working if Option<Vec<u8>>
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub pad2: Option<serde_bytes::ByteBuf>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub url: Option<UriT>,
+
+ #[serde(skip_deserializing, skip_serializing)]
+ pub path: PathBuf,
+}
+
+impl DataHash {
+ /// Label prefix for a data hash assertion.
+ ///
+ /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_data_hash>.
+ pub const LABEL: &'static str = labels::DATA_HASH;
+
+ /// Create new DataHash instance
+ pub fn new(name: &str, alg: &str, url: Option<UriT>) -> Self {
+ DataHash {
+ exclusions: None,
+ name: Some(name.to_string()),
+ alg: Some(alg.to_string()),
+ hash: Vec::new(),
+ pad: Vec::new(),
+ pad2: None,
+ url,
+ path: PathBuf::new(),
+ }
+ }
+
+ pub fn add_exclusion(&mut self, exclusion: Exclusion) {
+ if self.exclusions.is_none() {
+ self.exclusions = Some(Vec::new());
+ }
+
+ if let Some(ref mut e) = self.exclusions {
+ e.push(exclusion);
+ }
+ }
+
+ pub fn set_hash(&mut self, hash: Vec<u8>) {
+ self.hash = hash;
+ }
+
+ pub fn exclusions(&self) -> Option<&Vec<Exclusion>> {
+ self.exclusions.as_ref()
+ }
+
+ pub fn add_padding(&mut self, padding: Vec<u8>) {
+ self.pad = padding;
+ }
+
+ /// Checks if this is a remote hash
+ pub fn is_remote_hash(&self) -> bool {
+ self.url.is_some()
+ }
+
+ /// generate the hash value for the Asset using the range from the DataHash
+ pub fn gen_hash(&mut self, asset_path: &Path) -> Result<()> {
+ self.hash = self.hash_from_asset(asset_path)?;
+ self.path = PathBuf::from(asset_path);
+ Ok(())
+ }
+
+ // generate the hash again
+ pub fn regen_hash(&mut self) -> Result<()> {
+ let p = self.path.clone();
+ self.hash = self.hash_from_asset(p.as_path())?;
+ Ok(())
+ }
+
+ // add padding to match size
+ pub fn pad_to_size(&mut self, desired_size: usize) -> Result<()> {
+ let mut curr_size = self.to_assertion()?.data().len();
+
+ // this should not happen
+ if curr_size > desired_size {
+ return Err(Error::JumbfCreationError);
+ }
+
+ let mut last_pad = 0;
+ loop {
+ if curr_size == desired_size {
+ break;
+ }
+
+ if desired_size > curr_size {
+ self.pad.push(0x0);
+ curr_size = self.to_assertion()?.data().len();
+ last_pad += 1;
+ } else {
+ match &self.pad2 {
+ Some(_pad2) => return Err(Error::JumbfCreationError),
+ None => {
+ // if we reach here we need a new second padding object to hit exact size
+ self.pad.clear();
+ let pad2_size = last_pad / 2; // spit across two pads
+ self.pad2 = Some(ByteBuf::from(vec![0u8; pad2_size]));
+ return self.pad_to_size(desired_size);
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// generate the asset hash from a file asset using the constructed
+ /// start and length values
+ fn hash_from_asset(&mut self, asset_path: &Path) -> Result<Vec<u8>> {
+ if self.is_remote_hash() {
+ return Err(Error::BadParam(
+ "asset hash is remote, not yet supported".to_owned(),
+ ));
+ }
+
+ let data = fs::read(asset_path).map_err(wrap_io_err)?;
+
+ let alg = match self.alg {
+ Some(ref a) => a.clone(),
+ None => "sha256".to_string(),
+ };
+
+ // sort the exclusions
+ let hash = match self.exclusions {
+ Some(ref e) => hash_by_alg(&alg, &data, Some(e.clone())),
+ None => hash_by_alg(&alg, &data, None),
+ };
+
+ if hash.is_empty() {
+ Err(Error::BadParam("could not generate data hash".to_string()))
+ } else {
+ Ok(hash)
+ }
+ }
+
+ // verify data using currently set algorithm or default alg is none currently set
+ pub fn verify_in_memory_hash(&self, data: &[u8], alg: Option<String>) -> Result<()> {
+ if self.is_remote_hash() {
+ return Err(Error::BadParam("asset hash is remote".to_owned()));
+ }
+
+ let curr_alg = match alg {
+ Some(a) => a,
+ None => match self.alg {
+ Some(ref a) => a.clone(),
+ None => "sha256".to_string(),
+ },
+ };
+
+ let exclusions = self.exclusions.as_ref().cloned();
+
+ if verify_by_alg(&curr_alg, &self.hash, data, exclusions) {
+ Ok(())
+ } else {
+ Err(Error::HashMismatch("Hashes do not match".to_owned()))
+ }
+ }
+
+ /// Used to verify a DataHash against an asset.
+ pub fn verify_hash(&self, asset_path: &Path) -> Result<()> {
+ let buf = fs::read(asset_path).map_err(wrap_io_err)?;
+ self.verify_in_memory_hash(&buf, self.alg.clone())
+ }
+
+ /// Create a new instance from Assertion
+ pub fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ assertion.check_version_from_label(ASSERTION_CREATION_VERSION)?;
+ Self::from_cbor_assertion(assertion)
+ }
+}
+
+impl AssertionCbor for DataHash {}
+
+impl AssertionBase for DataHash {
+ const LABEL: &'static str = Self::LABEL;
+ const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ if self.hash.is_empty() {
+ return Err(Error::BadParam(
+ "no hash found, gen_hash must be called".to_string(),
+ ));
+ }
+ Self::to_cbor_assertion(self)
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ Self::from_cbor_assertion(assertion)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::panic)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use crate::{
+ assertion::{Assertion, AssertionData},
+ utils::test::fixture_path,
+ };
+
+ #[test]
+ fn test_build_assertion() {
+ // try json based assertion
+ let mut data_hash = DataHash::new("Some data", "sha256", None);
+ data_hash.add_exclusion(Exclusion::new(0, 1234));
+ data_hash.hash = vec![1, 2, 3];
+
+ let assertion = data_hash.to_assertion().unwrap();
+
+ println!("assertion label: {}", assertion.label());
+
+ let j = assertion.data();
+
+ let from_j = Assertion::from_data_cbor(&assertion.label(), j);
+ let ad_ref = from_j.decode_data();
+
+ let _assertion_type = match ad_ref {
+ AssertionData::Cbor(ref _ad_cbor) => "cbor",
+ AssertionData::Json(ref _ad_json) => "json",
+ AssertionData::Binary(ref _ad_bin) => "binary",
+ AssertionData::Uuid(_, _) => "uuid",
+ };
+
+ if let AssertionData::Cbor(ref ad_cbor) = ad_ref {
+ // compare results
+ let orig_d = assertion.decode_data();
+ if let AssertionData::Cbor(ref orig_cbor) = orig_d {
+ // TO DISCUSS: Maurice, I'm not quite sure what we were testing
+ // in the original test. LMK if I've lost too much in translation
+ // here.
+ let orig_as_value: DataHash = serde_cbor::from_slice(orig_cbor).unwrap();
+ let ad_as_value: DataHash = serde_cbor::from_slice(ad_cbor).unwrap();
+
+ assert_eq!(orig_as_value, ad_as_value);
+ } else {
+ panic!("Couldn't decode orig_d");
+ }
+ } else {
+ panic!("Couldn't decode ad_ref");
+ }
+ }
+
+ #[test]
+ fn test_binary_round_trip() {
+ let mut data_hash = DataHash::new("Some data", "sha256", None);
+ data_hash.add_exclusion(Exclusion::new(0x2000, 0x1000));
+ data_hash.add_exclusion(Exclusion::new(0x4000, 0x1000));
+
+ // add some data to hash
+ let ap = fixture_path("earth_apollo17.jpg");
+
+ // generate the hash
+ data_hash.gen_hash(&ap).unwrap();
+
+ // verify
+ data_hash.verify_hash(&ap).unwrap();
+
+ let assertion = data_hash.to_assertion().unwrap();
+
+ let orig_bytes = assertion.data();
+
+ let assertion_from_binary = Assertion::from_data_cbor(&assertion.label(), orig_bytes);
+
+ println!(
+ "Label Match Test {} = {}",
+ assertion.label(),
+ assertion_from_binary.label()
+ );
+
+ assert_eq!(assertion.label(), assertion_from_binary.label());
+
+ // compare the data as bytes
+ assert_eq!(orig_bytes, assertion_from_binary.data());
+ println!("Decoded binary matches");
+ }
+}
diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs
@@ -0,0 +1,282 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ assertion::{Assertion, AssertionBase, AssertionCbor, AssertionDecodeResult},
+ assertions::{labels, Metadata, ReviewRating},
+ error::Result,
+ hashed_uri::HashedUri,
+ validation_status::ValidationStatus,
+};
+
+const ASSERTION_CREATION_VERSION: usize = 1;
+
+// Used to differentiate a parent from a component
+#[derive(Serialize, Deserialize, Debug, PartialEq)]
+pub enum Relationship {
+ #[serde(rename = "parentOf")]
+ ParentOf,
+ #[serde(rename = "componentOf")]
+ ComponentOf,
+}
+
+impl Default for Relationship {
+ fn default() -> Self {
+ Relationship::ComponentOf
+ }
+}
+
+/// An ingredient assertion
+#[derive(Serialize, Deserialize, Debug, Default)]
+pub struct Ingredient {
+ #[serde(rename = "dc:title")]
+ pub title: String,
+ #[serde(rename = "dc:format")]
+ pub format: String,
+ #[serde(rename = "documentID", skip_serializing_if = "Option::is_none")]
+ pub document_id: Option<String>,
+ #[serde(rename = "instanceID")]
+ pub instance_id: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub c2pa_manifest: Option<HashedUri>,
+ #[serde(rename = "validationStatus", skip_serializing_if = "Option::is_none")]
+ pub validation_status: Option<Vec<ValidationStatus>>,
+ pub relationship: Relationship,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub thumbnail: Option<HashedUri>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub metadata: Option<Metadata>,
+}
+
+impl Ingredient {
+ /// Label prefix for an ingredient assertion.
+ ///
+ /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_ingredient>.
+ pub const LABEL: &'static str = labels::INGREDIENT;
+
+ pub fn new(title: &str, format: &str, instance_id: &str, document_id: Option<&str>) -> Self {
+ Self {
+ title: title.to_owned(),
+ format: format.to_owned(),
+ document_id: document_id.map(|id| id.to_owned()),
+ instance_id: instance_id.to_owned(),
+ c2pa_manifest: None,
+ validation_status: None,
+ relationship: Relationship::ComponentOf,
+ thumbnail: None,
+ metadata: None,
+ }
+ }
+
+ pub fn set_parent(mut self) -> Self {
+ self.relationship = Relationship::ParentOf;
+ self
+ }
+
+ pub fn set_c2pa_manifest_from_hashed_uri(mut self, provenance: Option<HashedUri>) -> Self {
+ self.c2pa_manifest = provenance;
+ self
+ }
+
+ pub fn set_thumbnail_hash_link(mut self, thumbnail: Option<&str>) -> Self {
+ self.thumbnail =
+ thumbnail.map(|thumb| HashedUri::new(thumb.to_owned(), None, "Hash".as_bytes()));
+ self
+ }
+
+ pub fn set_thumbnail(mut self, hashed_uri: Option<&HashedUri>) -> Self {
+ self.thumbnail = hashed_uri.map(|h| h.to_owned());
+ self
+ }
+
+ pub fn add_review(mut self, review: ReviewRating) -> Self {
+ if self.metadata.is_none() {
+ self.metadata = Some(Metadata::new())
+ }
+ if let Some(metadata) = &mut self.metadata {
+ match &mut metadata.reviews {
+ None => metadata.reviews = Some(vec![review]),
+ Some(reviews) => reviews.push(review),
+ }
+ }
+ self
+ }
+
+ pub fn add_reviews(mut self, reviews: Option<Vec<ReviewRating>>) -> Self {
+ if let Some(reviews) = reviews {
+ let mut metadata = Metadata::new();
+ metadata.reviews = Some(reviews);
+ self.metadata = Some(metadata);
+ };
+ self
+ }
+
+ pub fn add_validation_status(mut self, status: ValidationStatus) {
+ match &mut self.validation_status {
+ None => self.validation_status = Some(vec![status]),
+ Some(validation_status) => validation_status.push(status),
+ }
+ }
+}
+
+impl AssertionCbor for Ingredient {}
+
+impl AssertionBase for Ingredient {
+ const LABEL: &'static str = Self::LABEL;
+ const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ Self::to_cbor_assertion(self)
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ Self::from_cbor_assertion(assertion)
+ }
+}
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::panic)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use crate::assertion::{AssertionCbor, AssertionData};
+
+ #[test]
+ fn assertion_ingredient() {
+ let original = Ingredient::new(
+ "image 1.jpg",
+ "image/jpeg",
+ "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
+ Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
+ )
+ .set_thumbnail_hash_link(Some("#c2pa.ingredient.thumbnail.jpeg"));
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/cbor");
+ assert_eq!(assertion.label(), Ingredient::LABEL);
+ let result = Ingredient::from_cbor_assertion(&assertion).expect("from_assertion");
+ assert_eq!(original.title, result.title);
+ assert_eq!(original.format, result.format);
+ assert_eq!(original.document_id, result.document_id);
+ assert_eq!(original.instance_id, result.instance_id);
+ assert_eq!(original.thumbnail, result.thumbnail);
+ }
+
+ #[test]
+ fn test_build_assertion() {
+ let assertion = Ingredient::new(
+ "image 1.jpg",
+ "image/jpeg",
+ "xmp.did:87d51599-286e-43b2-9478-88c79f49c347",
+ Some("xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d"),
+ )
+ .set_thumbnail_hash_link(Some("#c2pa.ingredient.thumbnail.jpeg"))
+ .to_assertion()
+ .unwrap();
+
+ println!("assertion label: {}", assertion.label());
+
+ let j = assertion.data();
+
+ let from_j = Assertion::from_data_cbor(&assertion.label(), j);
+ let ad_ref = from_j.decode_data();
+
+ if let AssertionData::Cbor(ref ad_cbor) = ad_ref {
+ // compare results
+ let orig_d = assertion.decode_data();
+ if let AssertionData::Cbor(ref orig_cbor) = orig_d {
+ assert_eq!(orig_cbor, ad_cbor);
+ } else {
+ panic!("Couldn't decode orig_d");
+ }
+ } else {
+ panic!("Couldn't decode ad_ref");
+ }
+ }
+
+ #[test]
+ fn test_binary_round_trip() {
+ let assertion = Ingredient::new(
+ "image 1.jpg",
+ "image/jpeg",
+ "xmp.did:87d51599-286e-43b2-9478-88c79f49c347",
+ Some("xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d"),
+ )
+ //.set_provenance("")
+ .set_thumbnail_hash_link(Some("#c2pa.ingredient.thumbnail.jpeg"))
+ .to_assertion()
+ .unwrap();
+
+ let orig_bytes = assertion.data();
+
+ let assertion_from_binary = Assertion::from_data_cbor(&assertion.label(), orig_bytes);
+
+ println!(
+ "Label Match Test {} = {}",
+ assertion.label(),
+ assertion_from_binary.label()
+ );
+ assert_eq!(assertion.label(), assertion_from_binary.label());
+
+ // compare the data as bytes
+ assert_eq!(orig_bytes, assertion_from_binary.data());
+ println!("Decoded binary matches")
+ }
+
+ #[test]
+ fn test_assertion_with_reviews() {
+ let review = ReviewRating::new(
+ "a 3rd party plugin was used",
+ Some("actions.unknownActionsPerformed".to_string()),
+ 1,
+ );
+ let original = Ingredient::new(
+ "image 1.jpg",
+ "image/jpeg",
+ "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
+ Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
+ )
+ .add_review(review);
+
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/cbor");
+ assert_eq!(assertion.label(), Ingredient::LABEL);
+ let restored = Ingredient::from_cbor_assertion(&assertion).expect("from_assertion");
+ assert_eq!(original.title, restored.title);
+ assert_eq!(original.format, restored.format);
+ assert_eq!(original.document_id, restored.document_id);
+ assert_eq!(original.instance_id, restored.instance_id);
+ assert_eq!(original.thumbnail, restored.thumbnail);
+
+ assert!(restored.metadata.is_some());
+ let metadata = restored.metadata.unwrap();
+ let date_time = metadata.date_time.unwrap();
+ let date_time_parsed = chrono::DateTime::parse_from_rfc3339(&date_time);
+
+ assert!(metadata.reviews.is_some());
+ assert!(date_time_parsed.is_ok());
+
+ let reviews = metadata.reviews.unwrap();
+
+ assert_eq!(reviews.len(), 1);
+ assert_eq!(
+ reviews[0].code.as_ref().unwrap(),
+ "actions.unknownActionsPerformed"
+ );
+ assert_eq!(reviews[0].explanation, "a 3rd party plugin was used");
+ assert_eq!(reviews[0].value, 1);
+ }
+}
diff --git a/sdk/src/assertions/labels.rs b/sdk/src/assertions/labels.rs
@@ -0,0 +1,201 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#![deny(missing_docs)]
+
+//! Labels for assertion types as defined in C2PA 1.0 Specification.
+//!
+//! These constants do not include version suffixes.
+//!
+//! See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_c2pa_standard_assertions>.
+
+/// Label prefix for a claim assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_overview_4>.
+pub const CLAIM: &str = "c2pa.claim";
+
+/// Label prefix for an assertion metadata assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_metadata_about_assertions>.
+pub const ASSERTION_METADATA: &str = "c2pa.assertion.metadata";
+
+/// Label prefix for a data hash assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_data_hash>.
+pub const DATA_HASH: &str = "c2pa.hash.data";
+
+/// Label prefix for a BMFF-based hash assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_bmff_based_hash>.
+pub const BMFF_HASH: &str = "c2pa.hash.bmff";
+
+/// Label prefix for a soft binding assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_soft_binding_2>.
+pub const SOFT_BINDING: &str = "c2pa.soft-binding";
+
+/// Label prefix for a cloud data assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_cloud_data>.
+pub const CLOUD_DATA: &str = "c2pa.cloud-data";
+
+/// Label prefix for a thumbnail assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_thumbnail>.
+pub const THUMBNAIL: &str = "c2pa.thumbnail";
+
+/// Label prefix for a claim thumbnail assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_thumbnail>.
+pub const CLAIM_THUMBNAIL: &str = "c2pa.thumbnail.claim";
+
+/// Label prefix for an ingredient thumbnail assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_thumbnail>.
+pub const INGREDIENT_THUMBNAIL: &str = "c2pa.thumbnail.ingredient";
+
+/// Label prefix for a JPEG claim thumbnail assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_thumbnail>.
+pub const JPEG_CLAIM_THUMBNAIL: &str = "c2pa.thumbnail.claim.jpeg";
+
+/// Label prefix for a JPEG ingredient thumbnail assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_thumbnail>.
+pub const JPEG_INGREDIENT_THUMBNAIL: &str = "c2pa.thumbnail.ingredient.jpeg";
+
+/// Label prefix for a PNG claim thumbnail assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_thumbnail>.
+pub const PNG_CLAIM_THUMBNAIL: &str = "c2pa.thumbnail.claim.png";
+
+/// Label prefix for a PNG ingredient thumbnail assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_thumbnail>.
+pub const PNG_INGREDIENT_THUMBNAIL: &str = "c2pa.thumbnail.ingredient.png";
+
+/// Label prefix for an actions assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>.
+pub const ACTIONS: &str = "c2pa.actions";
+
+/// Label prefix for an ingredient assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_ingredient>.
+pub const INGREDIENT: &str = "c2pa.ingredient";
+
+/// Label prefix for a depthmap assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_depthmap>.
+pub const DEPTHMAP: &str = "c2pa.depthmap";
+
+/// Label prefix for a GDepth depthmap assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_gdepth_depthmap>.
+pub const DEPTHMAP_GDEPTH: &str = "c2pa.depthmap.GDepth";
+
+/// Label prefix for an EXIF information assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_exif_information>.
+pub const EXIF: &str = "stds.exif";
+
+/// Label prefix for an IPTC photo metadata assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_iptc_photo_metadata>.
+pub const IPTC_PHOTO_METADATA: &str = "stds.iptc.photo-metadata";
+
+/// Label prefix for any assertion based on a schema.org grammar.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_use_of_schema_org>.
+pub const SCHEMA_ORG: &str = "schema.org";
+
+/// Label prefix for a claim review assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_claim_review>.
+pub const CLAIM_REVIEW: &str = "stds.schema-org.ClaimReview";
+
+/// Label prefix for a creative work assertion.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_creative_work>.
+pub const CREATIVE_WORK: &str = "stds.schema-org.CreativeWork";
+
+/// Return the version suffix from an assertion label if it exists.
+///
+/// When an assertion's schema is changed in a backwards-compatible manner,
+/// the label would consist of an incremented version number, for example
+/// moving from `c2pa.ingredient` to `c2pa.ingredient.v2`.
+///
+/// If such a suffix exists (`.v(integer)`), that will be returned; otherwise,
+/// `None` will be returned.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_versioning>.
+///
+/// # Examples
+///
+/// ```
+/// use c2pa::assertions::labels;
+///
+/// assert_eq!(labels::version("c2pa.ingredient"), None);
+/// assert_eq!(labels::version("c2pa.ingredient.v2"), Some(2));
+/// assert_eq!(labels::version("c2pa.ingredient.V2"), None);
+/// assert_eq!(labels::version("c2pa.ingredient.x2"), None);
+/// assert_eq!(labels::version("c2pa.ingredient.v-2"), None);
+/// ```
+pub fn version(label: &str) -> Option<usize> {
+ let components: Vec<&str> = label.split('.').collect();
+ if let Some(last) = components.last() {
+ if last.len() > 1 {
+ let (ver, ver_inst_str) = last.split_at(1);
+ if ver == "v" {
+ if let Ok(ver) = ver_inst_str.parse::<usize>() {
+ return Some(ver);
+ }
+ }
+ }
+ }
+
+ None
+}
+
+/// Given a thumbnail label prefix such as `CLAIM_THUMBNAIL` and a file
+/// format (such as `png`), create a suitable label for an assertion.
+///
+/// # Examples
+///
+/// ```
+/// use c2pa::assertions::labels;
+///
+/// assert_eq!(
+/// labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, "image/jpeg"),
+/// labels::JPEG_CLAIM_THUMBNAIL
+/// );
+///
+/// assert_eq!(
+/// labels::add_thumbnail_format(labels::INGREDIENT_THUMBNAIL, "image/png"),
+/// labels::PNG_INGREDIENT_THUMBNAIL
+/// );
+/// ```
+pub fn add_thumbnail_format(label: &str, format: &str) -> String {
+ match format {
+ "image/jpeg" | "jpeg" | "jpg" => format!("{}.jpeg", label),
+ "image/png" | "png" => format!("{}.png", label),
+ _ => {
+ let p: Vec<&str> = format.split('/').collect();
+ if p.len() == 2 && p[0] == "image" {
+ format!("{}/{}", label, p[1]) // try to parse other image types
+ } else {
+ format!("{}/{}", label, format)
+ }
+ }
+ }
+}
diff --git a/sdk/src/assertions/metadata.rs b/sdk/src/assertions/metadata.rs
@@ -0,0 +1,248 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ assertion::{Assertion, AssertionBase, AssertionCbor, AssertionDecodeResult},
+ assertions::labels,
+ error::Result,
+ hashed_uri::HashedUri,
+};
+
+use chrono::{SecondsFormat, Utc};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::collections::HashMap;
+
+const ASSERTION_CREATION_VERSION: usize = 1;
+
+/// The Metadata structure can be used as part of other assertions or on its own to reference others
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+pub struct Metadata {
+ #[serde(rename = "reviewRatings", skip_serializing_if = "Option::is_none")]
+ pub reviews: Option<Vec<ReviewRating>>,
+ #[serde(rename = "dateTime", skip_serializing_if = "Option::is_none")]
+ pub date_time: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub reference: Option<HashedUri>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub data_source: Option<DataSource>,
+ #[serde(flatten)]
+ other: HashMap<String, Value>,
+}
+
+impl Metadata {
+ /// Label prefix for an assertion metadata assertion.
+ ///
+ /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_metadata_about_assertions>.
+ pub const LABEL: &'static str = labels::ASSERTION_METADATA;
+
+ pub fn new() -> Self {
+ Self {
+ reviews: None,
+ date_time: Some(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)),
+ reference: None,
+ data_source: None,
+ other: HashMap::new(),
+ }
+ }
+
+ /// add a review rating associated with the assertion
+ pub fn add_review(mut self, review: ReviewRating) -> Self {
+ match &mut self.reviews {
+ None => self.reviews = Some(vec![review]),
+ Some(reviews) => reviews.push(review),
+ }
+ self
+ }
+
+ /// Set review ratings associated with the assertion
+ pub fn set_reviews(mut self, reviews: Option<Vec<ReviewRating>>) -> Self {
+ self.reviews = reviews;
+ self
+ }
+
+ /// Set hashed_uri reference to another assertion to which this metadata applies
+ pub fn set_reference(mut self, reference: Option<HashedUri>) -> Self {
+ self.reference = reference;
+ self
+ }
+
+ /// set a description of the source of the assertion data, selected from a predefined list
+ pub fn set_data_source(mut self, data_source: Option<DataSource>) -> Self {
+ self.data_source = data_source;
+ self
+ }
+
+ /// add additional key / value pair
+ pub fn insert(&mut self, key: &str, value: &Value) -> &mut Self {
+ self.other.insert(key.to_string(), value.clone());
+ self
+ }
+
+ /// get additional values by key
+ pub fn get(self, key: &str) -> Option<Value> {
+ self.other.get(key).cloned()
+ }
+}
+
+impl Default for Metadata {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl AssertionCbor for Metadata {}
+
+impl AssertionBase for Metadata {
+ const LABEL: &'static str = Self::LABEL;
+ const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ Self::to_cbor_assertion(self)
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ Self::from_cbor_assertion(assertion)
+ }
+}
+
+/// DATA_SOURCE Type values
+pub const C2PA_SOURCE_SIGNER: &str = "signer";
+pub const C2PA_SOURCE_GENERATOR_REE: &str = "claimGenerator.REE";
+pub const C2PA_SOURCE_GENERATOR_TEE: &str = "claimGenerator.TEE";
+pub const C2PA_SOURCE_LOCAL_REE: &str = "localProvider.REE";
+pub const C2PA_SOURCE_LOCAL_TEE: &str = "localProvider.TEE";
+pub const C2PA_SOURCE_REMOTE_REE: &str = "remoteProvider.1stParty";
+pub const C2PA_SOURCE_REMOTE_TEE: &str = "remoteProvider.3rdParty";
+pub const C2PA_SOURCE_HUMAN_ANONYMOUS: &str = "humanEntry.anonymous";
+pub const C2PA_SOURCE_HUMAN_IDENTIFIED: &str = "humanEntry.identified";
+
+/// A description of the source for assertion data
+#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)]
+pub struct DataSource {
+ #[serde(rename = "type")]
+ pub source_type: String, // A value from among the enumerated list indicating the source of the assertion
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub details: Option<String>, // A human readable string giving details about the source of the assertion data
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub actors: Option<Vec<Actor>>, // array of hashed_uri references to W3C Verifiable Credentials
+}
+
+impl DataSource {
+ pub fn new(source_type: &str) -> Self {
+ Self {
+ source_type: source_type.to_owned(),
+ details: None,
+ actors: None,
+ }
+ }
+
+ /// Set a human readable string giving details about the source of the assertion data
+ pub fn set_details(mut self, details: Option<&str>) -> Self {
+ self.details = details.map(|s| s.to_owned());
+ self
+ }
+
+ /// Set list of actors associated with this source
+ pub fn set_actors(mut self, actors: Option<&Vec<Actor>>) -> Self {
+ self.actors = actors.cloned();
+ self
+ }
+}
+/// identifies a person responsible for an action
+#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)]
+pub struct Actor {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub identifier: Option<String>, // An identifier for a human actor, used when the "type" is humanEntry.identified
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub credentials: Option<Vec<HashedUri>>, // array of hashed_uri references to W3C Verifiable Credentials
+}
+
+impl Actor {
+ pub fn new(identifier: Option<&str>, credentials: Option<&Vec<HashedUri>>) -> Self {
+ Self {
+ identifier: identifier.map(|id| id.to_owned()),
+ credentials: credentials.cloned(),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+pub enum ReviewCode {
+ #[serde(rename(serialize = "actions.unknownActionsPerformed"))]
+ ActionsUnknown,
+ #[serde(rename(serialize = "actions.missing"))]
+ ActionsMissing,
+ #[serde(rename(serialize = "actions.possiblyMissing"))]
+ ActionsPossiblyMissing,
+ #[serde(rename(serialize = "depthMap.sceneMismatch"))]
+ DepthMapSceneMismatch,
+ #[serde(rename(serialize = "ingredient.modified"))]
+ IngredientModified,
+ #[serde(rename(serialize = "ingredient.possiblyModified"))]
+ IngredientPossiblyModified,
+ #[serde(rename(serialize = "thumbnail.primaryMismatch"))]
+ ThumbnailPrimaryMismatch,
+ #[serde(rename(serialize = "stds.iptc.location.inaccurate"))]
+ IptcLocationInaccurate,
+ #[serde(rename(serialize = "stds.schema-org.CreativeWork.misattributed"))]
+ CreativeWorkMisAttributed,
+ #[serde(rename(serialize = "stds.schema-org.CreativeWork.missingAttribution"))]
+ CreativeWorkMissingAttribution,
+ Other(String),
+}
+
+/// A rating on an assertion
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct ReviewRating {
+ pub explanation: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub code: Option<String>,
+ pub value: u8,
+}
+
+impl ReviewRating {
+ pub fn new(explanation: &str, code: Option<String>, value: u8) -> Self {
+ Self {
+ explanation: explanation.to_owned(),
+ value, // should be in range 1 to 5
+ code,
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ #[test]
+ fn assertion_metadata() {
+ let review = ReviewRating::new("foo", Some("bar".to_owned()), 3);
+ let test_value = Value::from("test");
+ let mut original = Metadata::new().add_review(review);
+ original.insert("foo", &test_value);
+ println!("{:?}", &original);
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/cbor");
+ assert_eq!(assertion.label(), Metadata::LABEL);
+ let result = Metadata::from_assertion(&assertion).expect("extract_assertion");
+ println!("{:?}", serde_json::to_string(&result));
+ assert_eq!(original.date_time, result.date_time);
+ assert_eq!(original.reviews, result.reviews);
+ assert_eq!(original.get("foo").unwrap(), "test".to_string());
+ //assert_eq!(original.reviews.unwrap().len(), 1);
+ }
+}
diff --git a/sdk/src/assertions/mod.rs b/sdk/src/assertions/mod.rs
@@ -0,0 +1,44 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+mod actions;
+pub use actions::*;
+
+mod data_hash;
+pub use data_hash::DataHash;
+
+mod creative_work;
+pub use creative_work::CreativeWork;
+
+mod ingredient;
+pub use ingredient::{Ingredient, Relationship};
+
+pub mod labels;
+
+mod metadata;
+pub use metadata::{Actor, DataSource, Metadata, ReviewRating, *};
+
+mod schema_org;
+pub use schema_org::{SchemaDotOrg, SchemaDotOrgPerson};
+
+mod thumbnail;
+pub use thumbnail::Thumbnail;
+
+mod user;
+pub use user::User;
+
+mod user_cbor;
+pub use user_cbor::UserCbor;
+
+mod uuid_assertion;
+pub use uuid_assertion::Uuid;
diff --git a/sdk/src/assertions/schema_org.rs b/sdk/src/assertions/schema_org.rs
@@ -0,0 +1,274 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+
+use crate::{
+ assertion::{Assertion, AssertionBase, AssertionDecodeResult, AssertionJson},
+ assertions::labels,
+ error::{Error, Result},
+ hashed_uri::HashedUri,
+};
+use serde_json::{json, Value};
+use std::collections::HashMap;
+
+const ASSERTION_CREATION_VERSION: usize = 1;
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct SchemaDotOrg {
+ #[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
+ object_context: Option<Value>,
+ #[serde(rename = "@type", default = "default_type")]
+ object_type: String,
+ #[serde(flatten)]
+ value: HashMap<String, Value>,
+}
+
+// used to set the default @type if it is missing
+fn default_type() -> String {
+ "Thing".to_string()
+}
+
+impl SchemaDotOrg {
+ /// constructs an empty Schema.org object of the specified @type with @context
+ pub fn new(object_type: String) -> Self {
+ Self {
+ object_context: None,
+ object_type,
+ value: HashMap::new(),
+ }
+ }
+
+ /// sets the @context field for Schema dot org.
+ pub fn set_default_context(mut self) -> Self {
+ self.object_context = Some(json!("https://schema.org"));
+ self
+ }
+
+ /// sets the @context field for Schema dot org.
+ pub fn set_context(mut self, context: Value) -> Self {
+ self.object_context = Some(context);
+ self
+ }
+
+ /// return the @type value from the object
+ pub fn object_type(&self) -> &str {
+ self.object_type.as_str()
+ }
+
+ /// get values by key as an instance of type `T`.
+ /// This return T is owned, not a reference
+ /// # Errors
+ ///
+ /// This conversion can fail if the structure of the field at key does not match the
+ /// structure expected by `T`
+ pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
+ self.value
+ .get(key)
+ .and_then(|v| serde_json::from_value(v.clone()).ok())
+ }
+
+ /// insert key / value pair of instance of type `T`
+ /// # Errors
+ ///
+ /// This conversion can fail if `T`'s implementation of `Serialize` decides to
+ /// fail, or if `T` contains a map with non-string keys.
+ pub fn insert<T: Serialize>(mut self, key: String, value: T) -> Result<Self> {
+ self.value.insert(key, serde_json::to_value(value)?);
+ Ok(self)
+ }
+
+ // add a value to a Vec stored at key
+ pub fn insert_push<T: Serialize + DeserializeOwned>(
+ self,
+ key: String,
+ value: T,
+ ) -> Result<Self> {
+ Ok(match self.get(&key) as Option<Vec<T>> {
+ Some(mut v) => {
+ v.push(value);
+ self
+ }
+ None => self.insert(key, &Vec::from([value]))?,
+ })
+ }
+
+ /// creates the struct from a correctly formatted JSON string
+ pub fn from_json_str(json: &str) -> Result<Self> {
+ serde_json::from_slice(json.as_bytes()).map_err(Error::JsonError)
+ }
+}
+
+impl Default for SchemaDotOrg {
+ fn default() -> Self {
+ Self::new(default_type())
+ }
+}
+
+impl AssertionJson for SchemaDotOrg {}
+
+impl AssertionBase for SchemaDotOrg {
+ const LABEL: &'static str = labels::SCHEMA_ORG;
+ const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ Self::to_json_assertion(self)
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ Self::from_json_assertion(assertion)
+ }
+}
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct SchemaDotOrgPerson(SchemaDotOrg);
+
+impl SchemaDotOrgPerson {
+ pub const PERSON: &'static str = "Person";
+ pub const NAME: &'static str = "name";
+ pub const IDENTIFIER: &'static str = "identifier";
+ pub const CREDENTIAL: &'static str = "credential";
+
+ pub fn new() -> Self {
+ Self(SchemaDotOrg::new(Self::PERSON.to_owned()))
+ }
+
+ pub fn new_person(name: String, identifier: String) -> Result<Self> {
+ Self(SchemaDotOrg::new(Self::PERSON.to_owned()))
+ .set_name(name)?
+ .set_identifier(identifier)
+ }
+
+ /// get values by key
+ pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
+ self.0.get(key)
+ }
+
+ /// insert key / value pair
+ pub fn insert<T: Serialize>(self, key: String, value: T) -> Result<Self> {
+ self.0.insert(key, value).map(Self)
+ }
+
+ // add a value to a Vec stored at key
+ pub fn insert_push<T>(self, key: String, value: T) -> Result<Self>
+ where
+ T: Serialize + DeserializeOwned,
+ {
+ self.0.insert_push(key, value).map(Self)
+ }
+
+ // get name field if it exists
+ pub fn name(&self) -> Option<String> {
+ self.get(Self::NAME)
+ }
+
+ pub fn set_name(self, author: String) -> Result<Self> {
+ self.insert(Self::NAME.to_owned(), author)
+ }
+
+ // get identifier field if it exists
+ pub fn identifier(&self) -> Option<String> {
+ self.get(Self::IDENTIFIER)
+ }
+
+ pub fn set_identifier(self, identifier: String) -> Result<Self> {
+ self.insert(Self::IDENTIFIER.to_owned(), identifier)
+ }
+
+ pub fn add_credential(self, credential: HashedUri) -> Result<Self> {
+ self.insert_push(Self::CREDENTIAL.to_owned(), credential)
+ }
+}
+
+impl Default for SchemaDotOrgPerson {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl std::ops::Deref for SchemaDotOrgPerson {
+ type Target = SchemaDotOrg;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ const USER: &str = "Joe Bloggs";
+ const USER_ID: &str = "1234567890";
+ const IDENTITY_URI: &str = "https://some_identity/service/";
+
+ // example review rating from
+ // https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_claim_review
+ const RATING: &str = r#"{
+ "@context": "http://schema.org",
+ "@type": "ClaimReview",
+ "claimReviewed": "The world is flat",
+ "reviewRating": {
+ "@type": "Rating",
+ "ratingValue": "1",
+ "bestRating": "5",
+ "worstRating": "1",
+ "ratingExplanation": "The world is not flat",
+ "alternateName": "False"
+ },
+ "itemReviewed": {
+ "@type": "CreativeWork",
+ "author": {
+ "@type": "Person",
+ "name": "A N Other"
+ },
+ "headline": "Earth: Flat."
+ }
+ }"#;
+
+ #[test]
+ fn assertion_creative_work() {
+ let uri = HashedUri::new(USER_ID.to_string(), None, b"abcde");
+ let original_person = SchemaDotOrgPerson::new()
+ .set_name(USER.to_owned())
+ .unwrap()
+ .set_identifier(IDENTITY_URI.to_owned())
+ .unwrap()
+ .add_credential(uri)
+ .unwrap();
+ let original = SchemaDotOrg::new("CreativeWork".to_owned())
+ .insert("author".to_owned(), original_person.clone())
+ .expect("insert");
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/json");
+ assert_eq!(assertion.label(), SchemaDotOrg::LABEL);
+ let result = SchemaDotOrg::from_assertion(&assertion).expect("extract_assertion");
+ assert_eq!(original.object_type(), result.object_type());
+ let result_person = result.get::<SchemaDotOrgPerson>("author").unwrap();
+ assert_eq!(original_person.name(), result_person.name());
+ }
+
+ #[test]
+ fn from_rating() {
+ let original = SchemaDotOrg::from_json_str(RATING).expect("from_json");
+ let original_claim_reviewed: String = original.get("claimReviewed").unwrap();
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/json");
+ assert_eq!(assertion.label(), SchemaDotOrg::LABEL);
+ let result = SchemaDotOrg::from_assertion(&assertion).expect("extract_assertion");
+ assert_eq!(original.object_type(), result.object_type());
+ let result_claim_reviewed: String = result.get("claimReviewed").unwrap();
+ assert_eq!(original_claim_reviewed, result_claim_reviewed);
+ }
+}
diff --git a/sdk/src/assertions/thumbnail.rs b/sdk/src/assertions/thumbnail.rs
@@ -0,0 +1,131 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ assertion::{
+ get_thumbnail_image_type, Assertion, AssertionBase, AssertionData, AssertionDecodeError,
+ AssertionDecodeResult,
+ },
+ assertions::labels,
+ error::Result,
+};
+
+use serde::Serialize;
+
+/// A Thumbnail assertion
+#[derive(Serialize)]
+pub struct Thumbnail {
+ pub data: Vec<u8>,
+ pub label: String,
+ pub content_type: String,
+}
+
+impl Thumbnail {
+ pub fn new(label: &str, data: Vec<u8>) -> Self {
+ let image_type = get_thumbnail_image_type(label);
+ let content_type = match image_type.as_str() {
+ "jpeg" | "jpk2" => "image/jpeg",
+ "png" => "image/png",
+ "bmp" => "image/bmp",
+ "gif" => "image/gif",
+ "tiff" => "image/tiff",
+ "ico" => "image/x-icon",
+ "webp" => "image/webp",
+ _ => "octet-stream",
+ }
+ .to_string();
+
+ Thumbnail {
+ data,
+ label: label.to_owned(),
+ content_type,
+ }
+ }
+}
+
+impl AssertionBase for Thumbnail {
+ /// returns the base label type for this thumbnail
+ fn label(&self) -> &str {
+ if self.label.starts_with(labels::CLAIM_THUMBNAIL) {
+ labels::CLAIM_THUMBNAIL
+ } else {
+ labels::INGREDIENT_THUMBNAIL
+ }
+ }
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ let data = AssertionData::Binary(self.data.to_owned());
+ Ok(Assertion::new(&self.label, None, data).set_content_type(&self.content_type))
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Thumbnail> {
+ match assertion.decode_data() {
+ AssertionData::Binary(data) => Ok(Self {
+ data: data.to_owned(),
+ label: assertion.label(),
+ content_type: assertion.content_type(),
+ }),
+ ad => Err(AssertionDecodeError::from_assertion_unexpected_data_type(
+ assertion, ad, "binary",
+ )),
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use crate::assertions::labels;
+
+ // a binary assertion ('deadbeefadbeadbe')
+ fn some_binary_data() -> Vec<u8> {
+ vec![
+ 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
+ 0x0b, 0x0e,
+ ]
+ }
+
+ fn thumbnail_test(label: &str, content_type: &str) {
+ let original = Thumbnail::new(label, some_binary_data());
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.content_type(), content_type);
+ assert_eq!(assertion.label(), label);
+ let result = Thumbnail::from_assertion(&assertion).expect("from_assertion");
+ assert_eq!(original.label, result.label);
+ assert_eq!(original.content_type, result.content_type);
+ assert_eq!(original.data, result.data);
+ }
+
+ #[test]
+ fn assertion_thumbnail_valid() {
+ thumbnail_test(labels::JPEG_CLAIM_THUMBNAIL, "image/jpeg");
+ thumbnail_test(labels::PNG_CLAIM_THUMBNAIL, "image/png");
+ thumbnail_test(labels::JPEG_INGREDIENT_THUMBNAIL, "image/jpeg");
+ thumbnail_test(labels::PNG_INGREDIENT_THUMBNAIL, "image/png");
+ // unrecognized labels will be formatted as octet_streams
+ thumbnail_test("foo", "octet-stream");
+ }
+
+ #[test]
+ fn assertion_thumbnail_invalid_from() {
+ // only current error is if the assertion data is the wrong type, so use JSON
+ let data = AssertionData::Json("foo".to_owned());
+ let assertion = Assertion::new(labels::JPEG_CLAIM_THUMBNAIL, None, data);
+ let result = Thumbnail::from_assertion(&assertion);
+ assert!(result.is_err())
+ }
+}
diff --git a/sdk/src/assertions/user.rs b/sdk/src/assertions/user.rs
@@ -0,0 +1,105 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ assertion::{
+ Assertion, AssertionBase, AssertionData, AssertionDecodeError, AssertionDecodeResult,
+ },
+ error::{Error, Result},
+};
+use serde::Serialize;
+
+/// Helper class to create User assertion
+#[derive(Debug, Default, Serialize)]
+pub struct User {
+ label: String,
+ data: String,
+}
+
+impl User {
+ /// Create new Identity instance
+ pub fn new(label: &str, data: &str) -> User {
+ User {
+ label: label.to_owned(),
+ data: data.to_owned(),
+ }
+ }
+}
+
+impl AssertionBase for User {
+ /// returns the label for this instance
+ fn label(&self) -> &str {
+ &self.label
+ }
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ // validate that the string is valid json, but don't modify it
+ let _json_value: serde_json::Value =
+ serde_json::from_str(&self.data).map_err(|_err| Error::AssertionEncoding)?;
+ //let data = AssertionData::AssertionJson(json_value.to_string());
+ let data = AssertionData::Json(self.data.to_owned());
+ Ok(Assertion::new(&self.label, None, data).set_content_type("application/json"))
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ match assertion.decode_data() {
+ AssertionData::Json(data) => {
+ // validate that the data is valid json, but do not modify it if valid
+ let _value: serde_json::Value = serde_json::from_str(data)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_json_err(assertion, e))?;
+
+ Ok(User::new(&assertion.label(), data))
+ }
+ ad => Err(AssertionDecodeError::from_assertion_unexpected_data_type(
+ assertion, ad, "json",
+ )),
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ const LABEL: &str = "user_test_assertion";
+ const DATA: &str = r#"{ "l1":"some data", "l2":"some other data" }"#;
+ const INVALID_JSON: &str = "={this isn't valid{";
+
+ #[test]
+ fn assertion_user() {
+ let original = User::new(LABEL, DATA);
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/json");
+ assert_eq!(assertion.label(), LABEL);
+ let result = User::from_assertion(&assertion).expect("from_assertion");
+ assert_eq!(original.data, result.data);
+ }
+
+ #[test]
+ fn assertion_user_invalid_json_to() {
+ let original = User::new(LABEL, INVALID_JSON);
+ original
+ .to_assertion()
+ .expect_err("Assertion encoding error expected");
+ }
+
+ #[test]
+ fn assertion_user_invalid_json_from() {
+ let data = AssertionData::Json(INVALID_JSON.to_owned());
+ let assertion = Assertion::new(LABEL, None, data);
+ let _result =
+ User::from_assertion(&assertion).expect_err("Assertion decoding error expected");
+ }
+}
diff --git a/sdk/src/assertions/user_cbor.rs b/sdk/src/assertions/user_cbor.rs
@@ -0,0 +1,111 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ assertion::{
+ Assertion, AssertionBase, AssertionData, AssertionDecodeError, AssertionDecodeResult,
+ },
+ error::{Error, Result},
+};
+
+/// Helper class to create Cbor User assertion
+#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
+pub struct UserCbor {
+ label: String,
+ cbor_data: Vec<u8>,
+}
+
+impl UserCbor {
+ /// Create new UserCbor instance
+ pub fn new(label: &str, data: Vec<u8>) -> UserCbor {
+ UserCbor {
+ label: label.to_owned(),
+ cbor_data: data,
+ }
+ }
+}
+
+impl AssertionBase for UserCbor {
+ /// returns the label for this instance
+ fn label(&self) -> &str {
+ &self.label
+ }
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ // validate cbor
+ let _value: serde_cbor::Value =
+ serde_cbor::from_slice(&self.cbor_data).map_err(|_err| Error::AssertionEncoding)?;
+ let data = AssertionData::Cbor(self.cbor_data.clone());
+ Ok(Assertion::new(&self.label, None, data))
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ match assertion.decode_data() {
+ AssertionData::Cbor(data) => {
+ // validate cbor
+ let _value: serde_cbor::Value = serde_cbor::from_slice(data)
+ .map_err(|e| AssertionDecodeError::from_assertion_and_cbor_err(assertion, e))?;
+
+ Ok(Self::new(&assertion.label(), data.clone()))
+ }
+ ad => Err(AssertionDecodeError::from_assertion_unexpected_data_type(
+ assertion, ad, "cbor",
+ )),
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use crate::assertion::Assertion;
+
+ const LABEL: &str = "user_test_assertion";
+ const DATA: &str = r#"{ "l1":"some data", "l2":"some other data" }"#;
+
+ #[test]
+ fn assertion_user_cbor() {
+ let json: serde_json::Value = serde_json::from_str(DATA).unwrap();
+ let data = serde_cbor::to_vec(&json).unwrap();
+ let original = UserCbor::new(LABEL, data);
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/cbor");
+ assert_eq!(assertion.label(), LABEL);
+ let result = UserCbor::from_assertion(&assertion).expect("from_assertion");
+ assert_eq!(original.cbor_data, result.cbor_data);
+ }
+
+ #[test]
+ fn assertion_user_cbor_invalid_to() {
+ let invalid_cbor = vec![0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f];
+ let original = UserCbor::new(LABEL, invalid_cbor);
+ original
+ .to_assertion()
+ .expect_err("Assertion encoding error expected");
+ }
+
+ #[test]
+ fn assertion_user_cbor_invalid_from() {
+ let invalid_cbor = vec![0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f];
+ let data = AssertionData::Cbor(invalid_cbor);
+ let assertion = Assertion::new(LABEL, None, data);
+ let _result =
+ UserCbor::from_assertion(&assertion).expect_err("Assertion decoding error expected");
+ }
+}
diff --git a/sdk/src/assertions/uuid_assertion.rs b/sdk/src/assertions/uuid_assertion.rs
@@ -0,0 +1,102 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ assertion::{
+ Assertion, AssertionBase, AssertionData, AssertionDecodeError, AssertionDecodeResult,
+ },
+ error::{Error, Result},
+};
+
+/// Helper class to create User assertion
+#[derive(Debug, Default)]
+pub struct Uuid {
+ label: String,
+ uuid: String,
+ data: Vec<u8>,
+}
+
+impl Uuid {
+ /// Create new Identity instance
+ pub fn new(label: &str, uuid: String, data: Vec<u8>) -> Uuid {
+ Uuid {
+ label: label.to_owned(),
+ uuid,
+ data,
+ }
+ }
+}
+
+impl AssertionBase for Uuid {
+ /// returns the label for this instance
+ fn label(&self) -> &str {
+ &self.label
+ }
+
+ // Build UUID assertion containing user defined data
+ // Uuid must be a hex string representing a uuid
+ fn to_assertion(&self) -> Result<Assertion> {
+ // validate that the string is 16 hex bytes
+ match hex::decode(&self.uuid) {
+ Ok(v) if v.len() == 16 => (),
+ _ => return Err(Error::BadParam("uuid must be 32 hex digits".to_string())),
+ }
+
+ let data = AssertionData::Uuid(self.uuid.to_owned(), self.data.to_owned());
+ Ok(Assertion::new(&self.label, None, data).set_content_type("application/octet-stream"))
+ }
+
+ fn from_assertion(assertion: &Assertion) -> AssertionDecodeResult<Self> {
+ match assertion.decode_data() {
+ AssertionData::Uuid(s, data) => {
+ Ok(Uuid::new(&assertion.label(), s.clone(), data.clone()))
+ }
+ ad => Err(AssertionDecodeError::from_assertion_unexpected_data_type(
+ assertion, ad, "uuid",
+ )),
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ const LABEL: &str = "uuid_test_assertion";
+ const UUID: &str = "ABCDABCDABCDABCDABCDABCDABCDABCD";
+ const INVALID_UUID: &str = "I am bad";
+ const DATA: [u8; 16] = [
+ 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
+ 0x0e,
+ ];
+
+ #[test]
+ fn assertion_uuid() {
+ let original = Uuid::new(LABEL, UUID.to_string(), DATA.to_vec());
+ let assertion = original.to_assertion().expect("build_assertion");
+ assert_eq!(assertion.mime_type(), "application/octet-stream");
+ assert_eq!(assertion.label(), LABEL);
+ let result = Uuid::from_assertion(&assertion).expect("from_assertion");
+ assert_eq!(original.data, result.data);
+ }
+
+ #[test]
+ fn assertion_bad_uuid() {
+ let original = Uuid::new(LABEL, INVALID_UUID.to_string(), DATA.to_vec());
+ original
+ .to_assertion()
+ .expect_err("Assertion encoding error expected");
+ }
+}
diff --git a/sdk/src/asset_handlers/c2pa_io.rs b/sdk/src/asset_handlers/c2pa_io.rs
@@ -0,0 +1,96 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::asset_io::{AssetIO, CAILoader, CAIRead, HashObjectPositions};
+use crate::error::{Error, Result};
+use std::fs::File;
+use std::path::Path;
+
+/// Supports working with ".c2pa" files containing only manifest store data
+pub struct C2paIO {}
+
+impl CAILoader for C2paIO {
+ fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> {
+ let mut cai_data = Vec::new();
+ // read the whole file
+ asset_reader.read_to_end(&mut cai_data)?;
+ Ok(cai_data)
+ }
+
+ // C2PA files have no xmp data
+ fn read_xmp(&self, _asset_reader: &mut dyn CAIRead) -> Option<String> {
+ None
+ }
+}
+
+impl AssetIO for C2paIO {
+ fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
+ let mut f = File::open(asset_path)?;
+ self.read_cai(&mut f)
+ }
+
+ fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
+ // just save the data in a file
+ std::fs::write(asset_path, &store_bytes)
+ .map_err(|_err| Error::BadParam("C2PA write error".to_owned()))?;
+
+ Ok(())
+ }
+
+ fn get_object_locations(
+ &self,
+ _asset_path: &std::path::Path,
+ ) -> Result<Vec<HashObjectPositions>> {
+ Ok(Vec::new())
+ }
+}
+
+#[cfg(test)]
+#[cfg(feature = "file_io")]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::{AssetIO, C2paIO};
+
+ use tempfile::tempdir;
+
+ use crate::{
+ openssl::temp_signer::get_signer,
+ status_tracker::OneShotStatusTracker,
+ store::Store,
+ utils::test::{fixture_path, temp_dir_path},
+ };
+
+ #[test]
+ fn c2pa_io_parse() {
+ let path = fixture_path("C.jpg");
+
+ let temp_dir = tempdir().expect("temp dir");
+ let temp_path = temp_dir_path(&temp_dir, "test.c2pa");
+
+ let c2pa_io = C2paIO {};
+ let manifest = crate::jumbf_io::load_jumbf_from_file(&path).expect("read_cai_store");
+ c2pa_io
+ .save_cai_store(&temp_path, &manifest)
+ .expect("save cai store");
+
+ let store = Store::load_from_asset(&temp_path, false, &mut OneShotStatusTracker::new())
+ .expect("loading store");
+
+ let (signer, _) = get_signer(&temp_dir.path());
+
+ let manifest2 = store.to_jumbf(&signer).expect("to_jumbf");
+ assert_eq!(&manifest, &manifest2);
+ }
+}
diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs
@@ -0,0 +1,420 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::fs::{read, File};
+use std::io::Cursor;
+use std::path::*;
+
+use byteorder::{BigEndian, ReadBytesExt};
+
+use img_parts::jpeg::{markers, Jpeg, JpegSegment};
+use img_parts::Bytes;
+use img_parts::DynImage;
+
+use crate::asset_io::{AssetIO, CAILoader, CAIRead, HashBlockObjectType, HashObjectPositions};
+use crate::error::{wrap_io_err, Error, Result};
+
+const XMP_SIGNATURE: &[u8] = b"http://ns.adobe.com/xap/1.0/";
+const XMP_SIGNATURE_BUFFER_SIZE: usize = XMP_SIGNATURE.len() + 1; // skip null or space char at end
+
+const MAX_JPEG_MARKER_SIZE: usize = 64000; // technically it's 64K but a bit smaller is fine
+
+const C2PA_MARKER: [u8; 4] = [0x63, 0x32, 0x70, 0x61];
+
+fn vec_compare(va: &[u8], vb: &[u8]) -> bool {
+ (va.len() == vb.len()) && // zip stops at the shortest
+ va.iter()
+ .zip(vb)
+ .all(|(a,b)| a == b)
+}
+
+// todo decide if want to keep this just for in-memory use cases
+fn extract_xmp(seg: &JpegSegment) -> Option<String> {
+ let contents = seg.contents();
+ if contents.starts_with(XMP_SIGNATURE) {
+ let rest = contents.slice(XMP_SIGNATURE_BUFFER_SIZE..);
+ if let Ok(c) = String::from_utf8(rest.to_vec()) {
+ Some(c)
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+}
+
+fn xmp_from_bytes(asset_bytes: &[u8]) -> Option<String> {
+ if let Ok(jpeg) = Jpeg::from_bytes(Bytes::copy_from_slice(asset_bytes)) {
+ let segs = jpeg.segments_by_marker(markers::APP1);
+ let xmp: String = segs.filter_map(extract_xmp).collect();
+ Some(xmp)
+ } else {
+ None
+ }
+}
+
+fn add_required_segs(asset_path: &std::path::Path) -> Result<()> {
+ let buf = read(asset_path)?;
+ let dimg_opt = DynImage::from_bytes(buf.into())
+ .map_err(|_err| Error::BadParam("Could not parse input image".to_owned()))?;
+
+ if let Some(DynImage::Jpeg(jpeg)) = dimg_opt {
+ // check for JUMBF Seg
+ let app11 = jpeg.segment_by_marker(markers::APP11);
+ if app11.is_none() {
+ // create dummy JUMBF seg
+ let mut no_bytes: Vec<u8> = vec![0; 50]; // enough bytes to be valid
+ no_bytes.splice(16..20, C2PA_MARKER); // cai UUID signature
+ let aio = JpegIO {};
+ aio.save_cai_store(asset_path, &no_bytes)?;
+ }
+ } else {
+ return Err(Error::BadParam(
+ "Image type not supported by handler".to_owned(),
+ ));
+ }
+
+ Ok(())
+}
+
+// all cai specific segments
+fn get_cai_segments(jpeg: &img_parts::jpeg::Jpeg) -> Result<Vec<usize>> {
+ let segments = jpeg.segments();
+ let mut cai_segs: Vec<usize> = Vec::new();
+ let mut cai_en: Vec<u8> = Vec::new();
+ let mut cai_seg_cnt: u32 = 0;
+
+ for (i, segment) in segments.iter().enumerate() {
+ let raw_bytes = segment.contents();
+ let seg_type = segment.marker();
+
+ if raw_bytes.len() > 16 && seg_type == markers::APP11 {
+ // we need at least 16 bytes in each segment for CAI
+ let mut raw_vec = raw_bytes.to_vec();
+ let _ci = raw_vec.as_mut_slice()[0..2].to_vec();
+ let en = raw_vec.as_mut_slice()[2..4].to_vec();
+ let mut z_vec = Cursor::new(raw_vec.as_mut_slice()[4..8].to_vec());
+ let _z = z_vec.read_u32::<BigEndian>()?;
+
+ let is_cai_continuation = vec_compare(&cai_en, &en);
+
+ if cai_seg_cnt > 0 && is_cai_continuation {
+ cai_seg_cnt += 1;
+ cai_segs.push(i);
+ } else {
+ // check if this is a CAI JUMBF block
+ let jumb_type = raw_vec.as_mut_slice()[24..28].to_vec();
+ let is_cai = vec_compare(&C2PA_MARKER, &jumb_type);
+ if is_cai {
+ cai_segs.push(i);
+ cai_seg_cnt = 1;
+ cai_en = en.clone(); // store the identifier
+ }
+ }
+ }
+ }
+ Ok(cai_segs)
+}
+
+// delete cai segments
+fn delete_cai_segments(jpeg: &mut img_parts::jpeg::Jpeg) -> Result<()> {
+ let cai_segs = get_cai_segments(jpeg)?;
+ let jpeg_segs = jpeg.segments_mut();
+
+ // remove cai segments
+ for seg in cai_segs.iter().rev() {
+ jpeg_segs.remove(*seg);
+ }
+ Ok(())
+}
+pub struct JpegIO {}
+
+impl CAILoader for JpegIO {
+ fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> {
+ let mut buffer: Vec<u8> = Vec::new();
+
+ // load the bytes
+ let mut buf: Vec<u8> = Vec::new();
+ asset_reader.read_to_end(&mut buf).map_err(Error::IoError)?;
+
+ let dimg_opt = DynImage::from_bytes(buf.into())
+ .map_err(|_err| Error::BadParam("Could not parse input image".to_owned()))?;
+
+ if let Some(dimg) = dimg_opt {
+ match dimg {
+ DynImage::Jpeg(jpeg) => {
+ let app11 = jpeg.segments_by_marker(markers::APP11);
+ let mut cai_en: Vec<u8> = Vec::new();
+ let mut cai_seg_cnt: u32 = 0;
+ for (_i, segment) in app11.enumerate() {
+ let raw_bytes = segment.contents();
+ if raw_bytes.len() > 16 {
+ // we need at least 16 bytes in each segment for CAI
+ let mut raw_vec = raw_bytes.to_vec();
+ let _ci = raw_vec.as_mut_slice()[0..2].to_vec();
+ let en = raw_vec.as_mut_slice()[2..4].to_vec();
+ let mut z_vec = Cursor::new(raw_vec.as_mut_slice()[4..8].to_vec());
+ let z = z_vec.read_u32::<BigEndian>()?;
+
+ let is_cai_continuation = vec_compare(&cai_en, &en);
+
+ if cai_seg_cnt > 0 && is_cai_continuation {
+ // make sure this is a cai segment for additional segments,
+ if z <= cai_seg_cnt {
+ // this a non contiguous segment with same "en"" so a bad set of data
+ // reset and continue to search
+ cai_en = Vec::new();
+ continue;
+ }
+ // take out LBox & TBox
+ buffer.append(&mut raw_vec.as_mut_slice()[16..].to_vec());
+
+ cai_seg_cnt += 1;
+ } else {
+ // check if this is a CAI JUMBF block
+ let jumb_type = raw_vec.as_mut_slice()[24..28].to_vec();
+ let is_cai = vec_compare(&C2PA_MARKER, &jumb_type);
+ if is_cai {
+ buffer.append(&mut raw_vec.as_mut_slice()[8..].to_vec());
+ cai_seg_cnt = 1;
+ cai_en = en.clone(); // store the identifier
+ }
+ }
+ }
+ }
+ }
+ _ => return Err(Error::BadParam("Unknown image format".to_owned())),
+ };
+ } else {
+ return Err(Error::BadParam(
+ "Image type not supported by handler".to_owned(),
+ ));
+ }
+
+ if buffer.is_empty() {
+ return Err(Error::JumbfNotFound);
+ }
+
+ Ok(buffer)
+ }
+
+ // Get XMP block
+ fn read_xmp(&self, asset_reader: &mut dyn CAIRead) -> Option<String> {
+ // load the bytes
+ let mut buf: Vec<u8> = Vec::new();
+ match asset_reader.read_to_end(&mut buf) {
+ Ok(_) => xmp_from_bytes(&buf),
+ Err(_) => None,
+ }
+ }
+}
+
+impl AssetIO for JpegIO {
+ fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
+ let mut f = File::open(asset_path)?;
+
+ self.read_cai(&mut f)
+ }
+
+ fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
+ let input = read(asset_path).map_err(wrap_io_err)?;
+
+ let mut jpeg = Jpeg::from_bytes(input.into()).map_err(|_err| Error::EmbeddingError)?;
+
+ // remove existing CAI segments
+ delete_cai_segments(&mut jpeg)?;
+
+ let jumbf_len = store_bytes.len();
+ let num_segments = (jumbf_len / MAX_JPEG_MARKER_SIZE) + 1;
+ let mut seg_chucks = store_bytes.chunks(MAX_JPEG_MARKER_SIZE);
+
+ for seg in 1..num_segments + 1 {
+ /*
+ If the size of the box payload is less than 2^32-8 bytes,
+ then all fields except the XLBox field, that is: Le, CI, En, Z, LBox and TBox,
+ shall be present in all JPEG XT marker segment representing this box,
+ regardless of whether the marker segments starts this box,
+ or continues a box started by a former JPEG XT Marker segment.
+ */
+ // we need to prefix the JUMBF with the JPEG XT markers (ISO 19566-5)
+ // CI: JPEG extensions marker - JP
+ // En: Box Instance Number - 0x0001
+ // (NOTE: can be any unique ID, so we pick one that shouldn't conflict)
+ // Z: Packet sequence number - 0x00000001...
+ let ci = vec![0x4A, 0x50];
+ let en = vec![0x02, 0x11];
+ let z = seg.to_be_bytes();
+
+ let mut seg_data = Vec::new();
+ seg_data.extend(ci);
+ seg_data.extend(en);
+ seg_data.extend(&z[4..]);
+ if seg > 1 {
+ // the LBox and TBox are already in the JUMBF
+ // but we need to duplicate them in all other segments
+ let lbox_tbox = &store_bytes[..8];
+ seg_data.extend(lbox_tbox);
+ }
+ if seg_chucks.len() > 0 {
+ // make sure we have some...
+ if let Some(next_seg) = seg_chucks.next() {
+ seg_data.extend(next_seg);
+ }
+ } else {
+ seg_data.extend(store_bytes);
+ }
+ let seg_bytes = Bytes::from(seg_data);
+ let app11_segment = JpegSegment::new_with_contents(markers::APP11, seg_bytes);
+ jpeg.segments_mut().insert(seg, app11_segment); // we put this in the beginning...
+ }
+
+ let output = std::fs::OpenOptions::new()
+ .read(true)
+ .write(true)
+ .truncate(true)
+ .open(asset_path)
+ .map_err(Error::IoError)?;
+
+ jpeg.encoder()
+ .write_to(output)
+ .map_err(|_err| Error::BadParam("JPEG write error".to_owned()))?;
+
+ Ok(())
+ }
+
+ fn get_object_locations(
+ &self,
+ asset_path: &std::path::Path,
+ ) -> Result<Vec<HashObjectPositions>> {
+ // make sure the file has the required segments so we can generate all the required offsets
+ add_required_segs(asset_path)?;
+
+ let mut cai_en: Vec<u8> = Vec::new();
+ let mut cai_seg_cnt: u32 = 0;
+
+ let mut positions: Vec<HashObjectPositions> = Vec::new();
+ let mut curr_offset = 2; // start after JPEG marker
+
+ let buf = read(asset_path)?;
+ let dimg = DynImage::from_bytes(buf.into())
+ .map_err(|e| Error::OtherError(Box::new(e)))?
+ .ok_or(Error::UnsupportedType)?;
+
+ match dimg {
+ DynImage::Jpeg(jpeg) => {
+ for seg in jpeg.segments() {
+ match seg.marker() {
+ markers::APP11 => {
+ // JUMBF marker
+ let raw_bytes = seg.contents();
+
+ if raw_bytes.len() > 16 {
+ // we need at least 16 bytes in each segment for CAI
+ let mut raw_vec = raw_bytes.to_vec();
+ let _ci = raw_vec.as_mut_slice()[0..2].to_vec();
+ let en = raw_vec.as_mut_slice()[2..4].to_vec();
+
+ let is_cai_continuation = vec_compare(&cai_en, &en);
+
+ if cai_seg_cnt > 0 && is_cai_continuation {
+ cai_seg_cnt += 1;
+
+ let v = HashObjectPositions {
+ offset: curr_offset,
+ length: seg.len_with_entropy(),
+ htype: HashBlockObjectType::Cai,
+ };
+ positions.push(v);
+ } else {
+ // check if this is a CAI JUMBF block
+ let jumb_type = raw_vec.as_mut_slice()[24..28].to_vec();
+ let is_cai = vec_compare(&C2PA_MARKER, &jumb_type);
+ if is_cai {
+ cai_seg_cnt = 1;
+ cai_en = en.clone(); // store the identifier
+
+ let v = HashObjectPositions {
+ offset: curr_offset,
+ length: seg.len_with_entropy(),
+ htype: HashBlockObjectType::Cai,
+ };
+
+ positions.push(v);
+ } else {
+ // save other for completeness sake
+ let v = HashObjectPositions {
+ offset: curr_offset,
+ length: seg.len_with_entropy(),
+ htype: HashBlockObjectType::Other,
+ };
+ positions.push(v);
+ }
+ }
+ }
+ }
+ markers::APP1 => {
+ // XMP marker or EXIF or Extra XMP
+ let v = HashObjectPositions {
+ offset: curr_offset,
+ length: seg.len_with_entropy(),
+ htype: HashBlockObjectType::Xmp,
+ };
+ // todo: pick the app1 that is the xmp (not cruical as it gets hashed either way)
+ positions.push(v);
+ }
+ _ => {
+ // save other for completeness sake
+ let v = HashObjectPositions {
+ offset: curr_offset,
+ length: seg.len_with_entropy(),
+ htype: HashBlockObjectType::Other,
+ };
+
+ positions.push(v);
+ }
+ }
+ curr_offset += seg.len_with_entropy();
+ }
+ }
+ _ => return Err(Error::BadParam("Unknown image format".to_owned())),
+ }
+
+ Ok(positions)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ use img_parts::Bytes;
+
+ #[test]
+ fn test_extract_xmp() {
+ let contents = Bytes::from_static(b"http://ns.adobe.com/xap/1.0/\0stuff");
+ let seg = JpegSegment::new_with_contents(markers::APP1, contents);
+ let result = extract_xmp(&seg);
+ assert_eq!(result, Some("stuff".to_owned()));
+
+ let contents = Bytes::from_static(b"http://ns.adobe.com/xap/1.0/ stuff");
+ let seg = JpegSegment::new_with_contents(markers::APP1, contents);
+ let result = extract_xmp(&seg);
+ assert_eq!(result, Some("stuff".to_owned()));
+
+ let contents = Bytes::from_static(b"tiny");
+ let seg = JpegSegment::new_with_contents(markers::APP1, contents);
+ let result = extract_xmp(&seg);
+ assert_eq!(result, None);
+ }
+}
diff --git a/sdk/src/asset_handlers/mod.rs b/sdk/src/asset_handlers/mod.rs
@@ -0,0 +1,16 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+pub mod c2pa_io;
+pub mod jpeg_io;
+pub mod png_io;
diff --git a/sdk/src/asset_handlers/png_io.rs b/sdk/src/asset_handlers/png_io.rs
@@ -0,0 +1,309 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::fs::File;
+use std::io::{Cursor, SeekFrom};
+use std::path::*;
+
+use byteorder::{BigEndian, ReadBytesExt};
+use conv::ValueFrom;
+
+use crate::asset_io::{AssetIO, CAILoader, CAIRead, HashBlockObjectType, HashObjectPositions};
+use crate::error::{Error, Result};
+
+const PNG_ID: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
+const CAI_CHUNK: [u8; 4] = *b"caBX";
+const IMG_HDR: [u8; 4] = *b"IHDR";
+const XMP_KEY: &str = "XML:com.adobe.xmp";
+const PNG_END: [u8; 4] = *b"IEND";
+const PNG_HDR_LEN: u64 = 12;
+
+#[derive(Clone, Debug)]
+struct PngChunkPos {
+ pub start: u64,
+ pub length: u32,
+ pub name: [u8; 4],
+ #[allow(dead_code)]
+ pub name_str: String,
+}
+
+impl PngChunkPos {
+ pub fn end(&self) -> u64 {
+ self.start + self.length as u64 + PNG_HDR_LEN
+ }
+}
+
+fn get_png_chunk_positions(f: &mut dyn CAIRead) -> Result<Vec<PngChunkPos>> {
+ let current_len = f.seek(SeekFrom::End(0))?;
+ let mut chunk_positions: Vec<PngChunkPos> = Vec::new();
+
+ // move to beginning of file
+ f.seek(SeekFrom::Start(0))?;
+
+ let mut buf4 = [0; 4];
+ let mut hdr = [0; 8];
+
+ // check PNG signature
+ f.read_exact(&mut hdr)
+ .map_err(|_err| Error::BadParam("PNG invalid".to_string()))?;
+ if hdr != PNG_ID {
+ return Err(Error::BadParam("PNG invalid".to_string()));
+ }
+
+ loop {
+ let current_pos = f.stream_position()?;
+
+ // read the chunk length
+ let length = f
+ .read_u32::<BigEndian>()
+ .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+
+ // read the chunk type
+ f.read_exact(&mut buf4)
+ .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+ let name = buf4;
+
+ // seek past data
+ f.seek(SeekFrom::Current(length as i64))
+ .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+
+ // read crc
+ let _crc = f
+ .read_exact(&mut buf4)
+ .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+
+ let chunk_name = String::from_utf8(name.to_vec())
+ .map_err(|_err| Error::BadParam("PNG bad chunk name".to_string()))?;
+
+ let pcp = PngChunkPos {
+ start: current_pos,
+ length,
+ name,
+ name_str: chunk_name,
+ };
+
+ // add to list
+ chunk_positions.push(pcp);
+
+ // should we break the loop
+ if name == PNG_END || f.stream_position()? > current_len {
+ break;
+ }
+ }
+
+ Ok(chunk_positions)
+}
+
+fn get_cai_data(f: &mut dyn CAIRead) -> Result<Vec<u8>> {
+ let ps = get_png_chunk_positions(f)?;
+
+ let pcp = ps
+ .into_iter()
+ .find(|pcp| pcp.name == CAI_CHUNK)
+ .ok_or(Error::JumbfNotFound)?;
+
+ let length: usize = pcp.length as usize;
+
+ f.seek(SeekFrom::Start(pcp.start + 8))?; // skip ahead from chunk start + length(4) + name(4)
+
+ let mut data: Vec<u8> = vec![0; length];
+ f.read_exact(&mut data[..])
+ .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+
+ Ok(data)
+}
+
+fn add_required_chunks(asset_path: &std::path::Path) -> Result<()> {
+ let mut f = File::open(asset_path)?;
+ let aio = PngIO {};
+
+ match aio.read_cai(&mut f) {
+ Ok(_) => Ok(()),
+ Err(_) => {
+ let no_bytes: Vec<u8> = Vec::new();
+ aio.save_cai_store(asset_path, &no_bytes)
+ }
+ }
+}
+pub struct PngIO {}
+
+impl CAILoader for PngIO {
+ fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> {
+ let cai_data = get_cai_data(asset_reader)?;
+ Ok(cai_data)
+ }
+
+ // Get XMP block
+ fn read_xmp(&self, asset_reader: &mut dyn CAIRead) -> Option<String> {
+ let chunks = png_pong::Decoder::new(asset_reader).ok()?.into_chunks();
+ for chunk_r in chunks.flatten() {
+ if let png_pong::chunk::Chunk::InternationalText(c) = chunk_r {
+ if c.key == XMP_KEY {
+ return Some(c.val);
+ }
+ }
+ }
+ None
+ }
+}
+
+impl AssetIO for PngIO {
+ fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
+ let mut f = File::open(asset_path)?;
+ self.read_cai(&mut f)
+ }
+
+ fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
+ let mut cai_data = Vec::new();
+ let mut cai_encoder = png_pong::Encoder::new(&mut cai_data).into_chunk_enc();
+
+ // get png byte
+ let mut png_buf = std::fs::read(asset_path).map_err(|_err| Error::EmbeddingError)?;
+
+ let mut cursor = Cursor::new(png_buf);
+ let mut ps = get_png_chunk_positions(&mut cursor)?;
+
+ // get back buffer
+ png_buf = cursor.into_inner();
+
+ // add CAI chunk
+ let cai_unknown = png_pong::chunk::Unknown {
+ name: CAI_CHUNK,
+ data: store_bytes.to_vec(),
+ };
+
+ let mut cai_chunk = png_pong::chunk::Chunk::Unknown(cai_unknown);
+ cai_encoder
+ .encode(&mut cai_chunk)
+ .map_err(|_err| Error::EmbeddingError)?;
+
+ /* splice in new chunk. Each PNG chunk has the following format:
+ chunk data length (4 bytes big endian)
+ chunk identifier (4 byte character sequence)
+ chunk data (0 - n bytes of chunck data)
+ chunk crc (4 bytes in crc in format defined in PNG spec)
+ */
+
+ // erase existing
+ let empty_buf = Vec::new();
+ let mut iter = ps.into_iter();
+ if let Some(existing_cai) = iter.find(|pcp| pcp.name == CAI_CHUNK) {
+ // replace existing CAI
+ let start = usize::value_from(existing_cai.start)
+ .map_err(|_err| Error::BadParam("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label
+
+ let end = usize::value_from(existing_cai.end())
+ .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
+
+ png_buf.splice(start..end, empty_buf.iter().cloned());
+ }
+
+ // update positions and reset png_buf
+ cursor = Cursor::new(png_buf);
+ ps = get_png_chunk_positions(&mut cursor)?;
+ iter = ps.into_iter();
+ png_buf = cursor.into_inner();
+
+ // add new cai data after image header chunk
+ if let Some(img_hdr) = iter.find(|pcp| pcp.name == IMG_HDR) {
+ let end = usize::value_from(img_hdr.end())
+ .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
+
+ png_buf.splice(end..end, cai_data.iter().cloned());
+ } else {
+ return Err(Error::EmbeddingError);
+ }
+
+ // save png data
+ std::fs::write(asset_path, png_buf)
+ .map_err(|_err| Error::BadParam("PNG write error".to_owned()))?;
+
+ Ok(())
+ }
+
+ fn get_object_locations(
+ &self,
+ asset_path: &std::path::Path,
+ ) -> Result<Vec<HashObjectPositions>> {
+ add_required_chunks(asset_path)?;
+
+ let mut f = std::fs::File::open(asset_path).map_err(|_err| Error::EmbeddingError)?;
+ let ps = get_png_chunk_positions(&mut f)?;
+
+ let mut positions: Vec<HashObjectPositions> = Vec::new();
+
+ let pcp = ps
+ .into_iter()
+ .find(|pcp| pcp.name == CAI_CHUNK)
+ .ok_or(Error::JumbfNotFound)?;
+
+ positions.push(HashObjectPositions {
+ offset: pcp.start as usize,
+ length: pcp.length as usize + PNG_HDR_LEN as usize,
+ htype: HashBlockObjectType::Cai,
+ });
+
+ // add hash of chunks before cai
+ positions.push(HashObjectPositions {
+ offset: 0,
+ length: pcp.start as usize,
+ htype: HashBlockObjectType::Other,
+ });
+
+ // add position from cai to end
+ let end = pcp.end();
+ let file_end = f.metadata()?.len();
+ positions.push(HashObjectPositions {
+ offset: end as usize, // len of cai
+ length: (file_end - end) as usize,
+ htype: HashBlockObjectType::Other,
+ });
+
+ Ok(positions)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::panic)]
+ #![allow(clippy::unwrap_used)]
+
+ use twoway::find_bytes;
+
+ use super::*;
+
+ #[test]
+ fn test_png_parse() {
+ let ap = crate::utils::test::fixture_path("libpng-test.png");
+
+ let png_bytes = std::fs::read(&ap).unwrap();
+
+ // grab PNG chunks and positions
+ let mut f = std::fs::File::open(ap).unwrap();
+ let positions = get_png_chunk_positions(&mut f).unwrap();
+
+ for hop in positions {
+ if let Some(start) = find_bytes(&png_bytes, &hop.name) {
+ if hop.start != (start - 4) as u64 {
+ panic!("find_bytes found the wrong position");
+ // assert!(true);
+ }
+
+ println!(
+ "Chunk {} position matches, start: {}, length: {} ",
+ hop.name_str, hop.start, hop.length
+ );
+ }
+ }
+ }
+}
diff --git a/sdk/src/asset_io.rs b/sdk/src/asset_io.rs
@@ -0,0 +1,64 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::io::{Read, Seek};
+
+use crate::error::Result;
+use std::{fmt, path::Path};
+#[derive(Clone, Debug, PartialEq)]
+pub enum HashBlockObjectType {
+ Cai,
+ Xmp,
+ Other,
+}
+
+impl fmt::Display for HashBlockObjectType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+#[derive(Debug)]
+pub struct HashObjectPositions {
+ pub offset: usize, // offset from begining of file to the beginning of object
+ pub length: usize, // length of object
+ pub htype: HashBlockObjectType, // type of hash block object
+}
+/// CAIReader trait to insure CAILoader method support both Read & Seek
+pub trait CAIRead: Read + Seek {}
+
+impl CAIRead for std::fs::File {}
+impl CAIRead for std::io::Cursor<&[u8]> {}
+impl CAIRead for std::io::Cursor<Vec<u8>> {}
+
+// Interface for in memory CAI reading
+pub trait CAILoader {
+ // Return entire CAI block as Vec<u8>
+ fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>>;
+
+ // Get XMP block
+ fn read_xmp(&self, asset_reader: &mut dyn CAIRead) -> Option<String>;
+}
+
+pub trait AssetIO {
+ // Return entire CAI block as Vec<u8>
+ fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>>;
+
+ // Write the CAI block to an asset
+ fn save_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()>;
+
+ /// List of standard object offests
+ /// If the offsets exist return the start of those locations other it should
+ /// return the calculated location of when it should start. There may still be a
+ /// length if the format contains extra header information for example.
+ fn get_object_locations(&self, asset_path: &Path) -> Result<Vec<HashObjectPositions>>;
+}
diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs
@@ -0,0 +1,1512 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Map, Value};
+use std::collections::HashMap;
+use std::fmt;
+use uuid::Uuid;
+
+use crate::assertion::{
+ get_thumbnail_image_type, get_thumbnail_instance, get_thumbnail_type, Assertion, AssertionBase,
+ AssertionData,
+};
+use crate::assertions::{self, labels, DataHash};
+use crate::cose_validator::{get_signing_info, verify_cose, verify_cose_async};
+use crate::hashed_uri::HashedUri;
+use crate::jumbf::{
+ self,
+ boxes::{CAICBORAssertionBox, CAIJSONAssertionBox, CAIUUIDAssertionBox, JumbfEmbeddedFileBox},
+};
+use crate::salt::{SaltGenerator, NO_SALT};
+use crate::utils::hash_utils::{hash_by_alg, vec_compare, verify_by_alg};
+
+use crate::error::{Error, Result};
+use crate::status_tracker::{log_item, OneShotStatusTracker, StatusTracker};
+use crate::validation_status;
+use crate::validator::ValidationInfo;
+
+const BUILD_HASH_ALG: &str = "sha256";
+
+/// JSON structure representing an Assertion reference in a Claim's "assertions" list
+use HashedUri as C2PAAssertion;
+
+const GH_FULL_VERSION_LIST: &str = "Sec-CH-UA-Full-Version-List";
+const GH_UA: &str = "Sec-CH-UA";
+
+#[derive(PartialEq, Clone)]
+// helper struct to allow arbitrary order for assertions stored in jumbf. The instance is
+// stored separate from the Assertion to allow for late binding to the label. Also,
+// we can load assertions in any order and know the position without re-parsing label. We also
+// save on parsing the cbor assertion each time we need its contents
+pub struct ClaimAssertion {
+ assertion: Assertion,
+ instance: usize,
+ hash_val: Vec<u8>,
+ hash_alg: String,
+ salt: Option<Vec<u8>>,
+}
+
+impl ClaimAssertion {
+ pub fn new(
+ assertion: Assertion,
+ instance: usize,
+ hashval: &[u8],
+ alg: &str,
+ salt: Option<Vec<u8>>,
+ ) -> ClaimAssertion {
+ ClaimAssertion {
+ assertion,
+ instance,
+ hash_val: hashval.to_vec(),
+ hash_alg: alg.to_string(),
+ salt,
+ }
+ }
+
+ pub fn update_assertion(&mut self, assertion: Assertion, hash: Vec<u8>) -> Result<()> {
+ self.hash_val = hash;
+ self.assertion = assertion;
+ Ok(())
+ }
+
+ pub fn label(&self) -> String {
+ let al_ref = self.assertion.label();
+ if self.instance > 0 {
+ if get_thumbnail_type(&al_ref) == labels::INGREDIENT_THUMBNAIL {
+ format!(
+ "{}__{}.{}",
+ get_thumbnail_type(&al_ref),
+ self.instance,
+ get_thumbnail_image_type(&al_ref)
+ )
+ } else {
+ format!("{}__{}", al_ref, self.instance)
+ }
+ } else {
+ self.assertion.label()
+ }
+ }
+
+ pub fn instance(&self) -> usize {
+ self.instance
+ }
+
+ pub fn instance_string(&self) -> String {
+ format!("{}", self.instance)
+ }
+
+ pub fn label_raw(&self) -> String {
+ self.assertion.label()
+ }
+
+ pub fn assertion(&self) -> &Assertion {
+ &self.assertion
+ }
+
+ pub fn hash(&self) -> &[u8] {
+ &self.hash_val
+ }
+
+ pub fn salt(&self) -> &Option<Vec<u8>> {
+ &self.salt
+ }
+
+ pub fn hash_alg(&self) -> &str {
+ &self.hash_alg
+ }
+
+ /// returns true if assertions are of the same enum variant
+ pub fn is_same_type(&self, input_assertion: &Assertion) -> bool {
+ Assertion::assertions_eq(&self.assertion, input_assertion)
+ }
+}
+
+impl fmt::Debug for ClaimAssertion {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}, instance: {}", self.assertion, self.instance)
+ }
+}
+/// A `Claim` gathers together all the `Assertion`s about an asset
+/// from an actor at a given time, and may also include one or more
+/// hashes of the asset itself, and a reference to the previous `Claim`.
+///
+/// It has all the same properties as an `Assertion` including being
+/// assigned a label (`c2pa.claim.v1`) and being either embedded into the
+/// asset or in the cloud. The claim is cryptographically hashed and
+/// that hash is signed to produce the claim signature.
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+pub struct Claim {
+ // root of CAI store
+ #[serde(skip_deserializing, skip_serializing)]
+ update_manifest: bool,
+
+ #[serde(skip_serializing_if = "Option::is_none", rename = "dc:title")]
+ pub title: Option<String>, // title for this claim, generally the name of the containing asset
+
+ #[serde(rename = "dc:format")]
+ pub format: String, // mime format of document containing this claim
+
+ #[serde(rename = "instanceID")]
+ pub instance_id: String, // instance Id of document containing this claim
+
+ // Internal list of ingredients
+ #[serde(skip_deserializing, skip_serializing)]
+ ingredients_store: HashMap<String, Vec<Claim>>,
+
+ // internal scratch objects
+ #[serde(skip_deserializing, skip_serializing)]
+ box_prefix: String, // where in JUMBF heirachy should this claim exist
+
+ #[serde(skip_deserializing, skip_serializing)]
+ signature_val: Vec<u8>, // the signature of the loaded/saved claim
+
+ // root of CAI store
+ #[serde(skip_deserializing, skip_serializing)]
+ root: String,
+
+ // internal scratch objects
+ #[serde(skip_deserializing, skip_serializing)]
+ label: String, // label of claim
+
+ // Internal list of assertions for claim.
+ // These are serialized manually based on need.
+ #[serde(skip_deserializing, skip_serializing)]
+ assertion_store: Vec<ClaimAssertion>,
+
+ // Internal list of verifiable credentials for claim.
+ // These are serialized manually based on need.
+ #[serde(skip_deserializing, skip_serializing)]
+ vc_store: Vec<AssertionData>,
+
+ claim_generator: String, // generator of this claim
+
+ signature: String, // link to signature box
+ assertions: Vec<C2PAAssertion>, // list of assertion hashed URIs
+
+ // original JSON bytes of claim; only present when reading from asset
+ #[serde(skip_deserializing, skip_serializing)]
+ original_bytes: Option<Vec<u8>>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ redacted_assertions: Option<Vec<String>>, // list of redacted assertions
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ alg: Option<String>, // hashing algorithm (default to Sha256)
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ alg_soft: Option<String>, // hashing algorithm for soft bindings
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ claim_generator_hints: Option<HashMap<String, Value>>,
+}
+
+/// Enum to define how assertions are are stored when output to json
+pub enum AssertionStoreJsonFormat {
+ None, // no assertion store
+ KeyValue, // key (uri), value (Assertion json object)
+ KeyValueNoBinary, // KeyValue omitting binary results
+ OrderedList, // list of Assertions as json objects
+ OrderedListNoBinary, // list of Assertions as json objects omitting binaries results
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct JsonOrderedAssertionData {
+ label: String,
+ data: Value,
+ hash: String,
+ is_binary: bool,
+ mime_type: String,
+}
+
+impl Claim {
+ /// Label prefix for a claim assertion.
+ ///
+ /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_overview_4>.
+ pub const LABEL: &'static str = assertions::labels::CLAIM;
+
+ /// Create a new claim.
+ /// vendor: name used to label the claim (unique instance number is automatically calculated)
+ /// claim_generator: User agent see c2pa spec for format
+ // #[cfg(not(target_arch = "wasm32"))]
+ pub fn new(claim_generator: &str, vendor: Option<&str>) -> Self {
+ let urn = Uuid::new_v4();
+ let l = match vendor {
+ Some(v) => format!(
+ "{}:{}",
+ v.to_lowercase(),
+ urn.to_urn().encode_lower(&mut Uuid::encode_buffer())
+ ),
+ None => urn
+ .to_urn()
+ .encode_lower(&mut Uuid::encode_buffer())
+ .to_string(),
+ };
+
+ Claim {
+ box_prefix: "self#jumbf".to_string(),
+ root: jumbf::labels::MANIFEST_STORE.to_string(),
+ signature_val: Vec::new(),
+ ingredients_store: HashMap::new(),
+ label: l,
+ signature: "".to_string(),
+
+ claim_generator: claim_generator.to_string(),
+ assertion_store: Vec::new(),
+ vc_store: Vec::new(),
+ assertions: Vec::new(),
+ original_bytes: None,
+ redacted_assertions: None,
+ alg: Some(BUILD_HASH_ALG.to_string()),
+ alg_soft: None,
+ claim_generator_hints: None,
+
+ title: None,
+ format: "".to_string(),
+ instance_id: "".to_string(),
+
+ update_manifest: false,
+ }
+ }
+
+ /// Build a claim and verify its integrity.
+ pub fn build(&mut self) -> Result<()> {
+ // A claim must have a signature box.
+ if self.signature.is_empty() {
+ self.add_signature_box_link();
+ }
+
+ Ok(())
+ }
+
+ /// return version this claim supports
+ pub fn build_version() -> &'static str {
+ Self::LABEL
+ }
+
+ /// Return the JUMBF label for this claim.
+ pub fn label(&self) -> &str {
+ &self.label
+ }
+
+ /// Return the JUMBF URI for this claim.
+ pub fn uri(&self) -> String {
+ jumbf::labels::to_manifest_uri(&self.label)
+ }
+
+ /// Return the JUMBF URI for an assertion on this claim.
+ pub fn assertion_uri(&self, assertion_label: &str) -> String {
+ jumbf::labels::to_assertion_uri(&self.label, assertion_label)
+ }
+
+ /// Return the JUMBF Signature URI for this claim.
+ pub fn signature_uri(&self) -> String {
+ jumbf::labels::to_signature_uri(&self.label)
+ }
+
+ // Add link to the signature box for this claim.
+ fn add_signature_box_link(&mut self) {
+ self.signature = format!("{}={}", self.box_prefix, jumbf::labels::SIGNATURE);
+ }
+
+ /// set signature of the claim
+ pub(crate) fn set_signature_val(&mut self, signature: Vec<u8>) {
+ self.signature_val = signature;
+ }
+
+ /// get signature of the claim
+ pub fn signature_val(&self) -> &Vec<u8> {
+ &self.signature_val
+ }
+
+ /// get claim generator
+ pub fn claim_generator(&self) -> &str {
+ &self.claim_generator
+ }
+
+ /// get format
+ pub fn format(&self) -> &str {
+ &self.format
+ }
+
+ /// get instance_id
+ pub fn instance_id(&self) -> &str {
+ &self.instance_id
+ }
+
+ /// set title
+ pub fn set_title(&mut self, title: Option<String>) {
+ self.title = title;
+ }
+
+ /// get title
+ pub fn title(&self) -> Option<&String> {
+ self.title.as_ref()
+ }
+
+ /// get algorithm
+ pub fn alg(&self) -> &str {
+ match self.alg.as_ref() {
+ Some(alg) => alg,
+ None => BUILD_HASH_ALG,
+ }
+ }
+
+ /// get soft algorithm
+ pub fn alg_soft(&self) -> Option<&String> {
+ self.alg_soft.as_ref()
+ }
+
+ /// Is this an update manifest
+ pub fn update_manifest(&self) -> bool {
+ self.update_manifest
+ }
+
+ pub(crate) fn set_update_manifest(&mut self, is_update_manifest: bool) {
+ self.update_manifest = is_update_manifest;
+ }
+ pub fn add_claim_generator_hint(&mut self, hint_key: &str, hint_value: Value) {
+ if self.claim_generator_hints.is_none() {
+ self.claim_generator_hints = Some(HashMap::new());
+ }
+
+ if let Some(map) = &mut self.claim_generator_hints {
+ // if the key is already there do we need to merge the new value, so get its value
+ let curr_val = match hint_key {
+ // keys where new values should be merges
+ GH_UA | GH_FULL_VERSION_LIST => {
+ if let Some(curr_ch_ua) = map.get(hint_key) {
+ curr_ch_ua.as_str().map(|curr_val| curr_val.to_owned())
+ } else {
+ None
+ }
+ }
+ _ => None,
+ };
+
+ // had an existing value so merge
+ if let Some(curr_val) = curr_val {
+ if let Some(append_val) = hint_value.as_str() {
+ map.insert(
+ hint_key.to_string(),
+ Value::String(format!("{}, {}", curr_val, append_val)),
+ );
+ }
+ return;
+ }
+
+ // all other keys treat as replacement
+ map.insert(hint_key.to_string(), hint_value);
+ }
+ }
+
+ pub fn get_claim_generator_hint_map(&self) -> Option<&HashMap<String, Value>> {
+ self.claim_generator_hints.as_ref()
+ }
+
+ pub fn calc_box_hash(
+ label: &str,
+ assertion: &Assertion,
+ salt: Option<Vec<u8>>,
+ alg: &str,
+ ) -> Result<Vec<u8>> {
+ // Grab assertion data object.
+ let d = assertion.decode_data();
+
+ let mut hash_bytes = Vec::with_capacity(2048);
+
+ match d {
+ AssertionData::Json(_) => {
+ let mut json_data = CAIJSONAssertionBox::new(label);
+ json_data.add_json(assertion.data().to_vec());
+ if let Some(salt) = salt {
+ json_data.set_salt(salt)?;
+ }
+ json_data.super_box().write_box_payload(&mut hash_bytes)?;
+ }
+ AssertionData::Binary(_) => {
+ // TODO: Handle other binary box types if needed.
+ let mut data = JumbfEmbeddedFileBox::new(label);
+ data.add_data(assertion.data().to_vec(), assertion.mime_type(), None);
+ if let Some(salt) = salt {
+ data.set_salt(salt)?;
+ }
+ data.super_box().write_box_payload(&mut hash_bytes)?;
+ }
+ AssertionData::Cbor(_) => {
+ let mut cbor_data = CAICBORAssertionBox::new(label);
+ cbor_data.add_cbor(assertion.data().to_vec());
+ if let Some(salt) = salt {
+ cbor_data.set_salt(salt)?;
+ }
+ cbor_data.super_box().write_box_payload(&mut hash_bytes)?;
+ }
+ AssertionData::Uuid(uuid_str, _) => {
+ let mut data = CAIUUIDAssertionBox::new(label);
+ data.add_uuid(uuid_str, assertion.data().to_vec())?;
+ if let Some(salt) = salt {
+ data.set_salt(salt)?;
+ }
+ data.super_box().write_box_payload(&mut hash_bytes)?;
+ }
+ }
+
+ Ok(hash_by_alg(alg, &hash_bytes, None))
+ }
+
+ /// Add an assertion to this claim and verify
+ pub fn add_assertion(
+ &mut self,
+ assertion_builder: &impl AssertionBase,
+ ) -> Result<C2PAAssertion> {
+ self.add_assertion_with_salt(assertion_builder, NO_SALT)
+ }
+
+ /// Add an assertion to this claim and verify with a salted assertion store
+ /// This version should be used if the assertion may be redacted for addition protection.
+ pub fn add_assertion_with_salt(
+ &mut self,
+ assertion_builder: &impl AssertionBase,
+ salt_generator: &impl SaltGenerator,
+ ) -> Result<C2PAAssertion> {
+ // make sure the assertion is valid
+ let assertion = assertion_builder.to_assertion()?;
+
+ // Update label if there are multiple instances of
+ // the same claim type.
+ let as_label = self.make_assertion_instance_label(assertion.label().as_ref());
+
+ // Get salted hash of the assertion's contents.
+ let salt = salt_generator.generate_salt();
+
+ let hash = Claim::calc_box_hash(&as_label, &assertion, salt.clone(), self.alg())?;
+
+ // Build hash link.
+ let link = jumbf::labels::to_assertion_uri(self.label(), &as_label);
+ let link_relative = jumbf::labels::to_relative_uri(&link);
+
+ let c2pa_assertion = C2PAAssertion::new(link_relative, None, &hash);
+
+ // Add to assertion store.
+ let (_l, instance) = Claim::assertion_label_from_link(&as_label);
+ let ca = ClaimAssertion::new(assertion, instance, &hash, self.alg(), salt);
+ self.assertion_store.push(ca);
+ self.assertions.push(c2pa_assertion.clone());
+
+ Ok(c2pa_assertion)
+ }
+
+ pub(crate) fn vc_id(vc_json: &str) -> Result<String> {
+ let vc: Value =
+ serde_json::from_str(vc_json).map_err(|_err| Error::VerifiableCredentialInvalid)?; // check for json validity
+
+ let credential_subject = vc
+ .get("credentialSubject")
+ .ok_or(Error::VerifiableCredentialInvalid)?;
+ let id = credential_subject
+ .get("id")
+ .ok_or(Error::VerifiableCredentialInvalid)?
+ .as_str()
+ .ok_or(Error::VerifiableCredentialInvalid)?;
+
+ Ok(id.to_string())
+ }
+
+ /// Add a verifiable credential to vc store and return a JUMBF URI
+ /// the credential json must contain "credentialsSubject" object like:
+ /// ```json
+ /// "credentialSubject": {
+ /// "id": "did:nppa:eb1bb9934d9896a374c384521410c7f14",
+ /// "name": "Bob Ross",
+ /// "memberOf": "https://nppa.org/"
+ /// },
+ /// ```
+ // the "id" value will be used as the label in the vcstore
+ pub fn add_verifiable_credential(&mut self, vc_json: &str) -> Result<HashedUri> {
+ let id = Claim::vc_id(vc_json)?;
+
+ let hash = hash_by_alg(self.alg(), vc_json.as_bytes(), None);
+
+ let link = jumbf::labels::to_verifiable_credential_uri(self.label(), &id);
+
+ let c2pa_assertion = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash);
+
+ // add credential to vcstore
+ let credential = AssertionData::Json(vc_json.to_string());
+ self.vc_store.push(credential);
+
+ Ok(c2pa_assertion)
+ }
+
+ pub fn get_verifiable_credentials(&self) -> &Vec<AssertionData> {
+ &self.vc_store
+ }
+
+ /// Add directly to store during a reload of a claim
+ pub(crate) fn put_assertion_store(&mut self, assertion: ClaimAssertion) {
+ self.assertion_store.push(assertion);
+ }
+
+ // crate private function to allow for patching a data hash with final contents
+ #[cfg(feature = "file_io")]
+ pub(crate) fn update_data_hash(&mut self, mut data_hash: DataHash) -> Result<()> {
+ let mut replacement_assertion = data_hash.to_assertion()?;
+
+ match self.assertion_store.iter_mut().find(|assertion| {
+ // is this a DataHash Assertion
+ if !Assertion::assertions_eq(&replacement_assertion, assertion.assertion()) {
+ return false;
+ }
+
+ if let Ok(dh) = DataHash::from_assertion(assertion.assertion()) {
+ dh.name == data_hash.name
+ } else {
+ false
+ }
+ }) {
+ Some(ref mut dh_assertion) => {
+ let original_hash = dh_assertion.hash().to_vec();
+ let original_len = dh_assertion.assertion().data().len();
+ data_hash.pad_to_size(original_len)?;
+ replacement_assertion = data_hash.to_assertion()?;
+
+ let replacement_hash = Claim::calc_box_hash(
+ &dh_assertion.label(),
+ &replacement_assertion,
+ dh_assertion.salt().clone(),
+ dh_assertion.hash_alg(),
+ )?;
+ dh_assertion.update_assertion(replacement_assertion, replacement_hash)?;
+
+ // fix up hashed uri
+ match self.assertions.iter_mut().find_map(|f| {
+ if f.url().contains(&dh_assertion.label())
+ && vec_compare(&f.hash(), &original_hash)
+ {
+ // replace with newly updated hash
+ f.update_hash(dh_assertion.hash().to_vec());
+ Some(f)
+ } else {
+ None
+ }
+ }) {
+ Some(_) => Ok(()),
+ None => Err(Error::NotFound),
+ }
+ }
+ None => Err(Error::NotFound),
+ }
+ }
+
+ /// Not ready for use!!!!!
+ /// Redact an assertion from a prior claim.
+ /// This will remove the assertion from the JUMBF
+ fn redact_assertion(&mut self, assertion_uri: &str) -> Result<()> {
+ // cannot redact action assertions per the spec
+ let (label, _instance) = Claim::assertion_label_from_link(assertion_uri);
+ if label == assertions::labels::ACTIONS {
+ return Err(Error::AssertionInvalidRedaction);
+ }
+
+ // delete assertion
+ if let Some(index) = self
+ .assertion_store
+ .iter()
+ .position(|x| assertion_uri.contains(&x.label()))
+ {
+ self.assertion_store.remove(index);
+ Ok(())
+ } else {
+ Err(Error::AssertionInvalidRedaction)
+ }
+ }
+
+ /// Return a hash of this claim.
+ pub fn hash(&self) -> Vec<u8> {
+ match self.data() {
+ Ok(claim_data) => hash_by_alg(self.alg(), &claim_data, None),
+ Err(_) => Vec::new(), // should never happen bug if it does just give no hash
+ }
+ }
+
+ /// Return the signing date and time for this claim, if there is one.
+ pub fn signing_time(&self) -> Option<DateTime<Utc>> {
+ if let Some(validation_data) = self.signature_info() {
+ validation_data.date
+ } else {
+ None
+ }
+ }
+
+ /// Return the signing date and time for this claim, if there is one.
+ pub fn signing_issuer(&self) -> Option<String> {
+ if let Some(validation_data) = self.signature_info() {
+ validation_data.issuer_org
+ } else {
+ None
+ }
+ }
+
+ /// Return information about the signature
+ pub fn signature_info(&self) -> Option<ValidationInfo> {
+ let sig = self.signature_val();
+ let data = self.data().ok()?;
+ let mut validation_log = OneShotStatusTracker::new();
+
+ Some(get_signing_info(sig, &data, &mut validation_log))
+ }
+
+ /// Verify claim signature, assertion store and asset hashes
+ /// claim - claim to be verified
+ /// asset_bytes - reference to bytes of the asset
+ pub async fn verify_claim_async(
+ claim: &Claim,
+ asset_bytes: &[u8],
+ is_provenance: bool,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<()> {
+ // Parse COSE signed data (signature) and validate it.
+ let sig = claim.signature_val().clone();
+ let additional_bytes: Vec<u8> = Vec::new();
+ let claim_data = claim.data()?;
+
+ // make sure signature manifest if present points to this manifest
+ let sig_box_err = match jumbf::labels::manifest_label_from_uri(&claim.signature) {
+ Some(signature_url) if signature_url != claim.label() => true,
+ _ => {
+ jumbf::labels::box_name_from_uri(&claim.signature).unwrap_or_else(|| "".to_string())
+ != jumbf::labels::SIGNATURE
+ } // relative signature box
+ };
+
+ if sig_box_err {
+ let log_item = log_item!(
+ claim.signature_uri(),
+ "signature missing",
+ "verify_claim_async"
+ )
+ .error(Error::ClaimMissingSignatureBox)
+ .validation_status(validation_status::CLAIM_SIGNATURE_MISSING);
+
+ validation_log.log(log_item, Some(Error::ClaimMissingSignatureBox))?;
+ }
+
+ let verified = verify_cose_async(
+ sig,
+ claim_data,
+ additional_bytes,
+ !is_provenance,
+ validation_log,
+ )
+ .await;
+ Claim::verify_internal(claim, asset_bytes, is_provenance, verified, validation_log)
+ }
+
+ /// Verify claim signature, assertion store and asset hashes
+ /// claim - claim to be verified
+ /// asset_bytes - reference to bytes of the asset
+ pub fn verify_claim(
+ claim: &Claim,
+ asset_bytes: &[u8],
+ is_provenance: bool,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<()> {
+ // Parse COSE signed data (signature) and validate it.
+ let sig = claim.signature_val();
+ let additional_bytes: Vec<u8> = Vec::new();
+
+ // make sure signature manifest if present points to this manifest
+ let sig_box_err = match jumbf::labels::manifest_label_from_uri(&claim.signature) {
+ Some(signature_url) if signature_url != claim.label() => true,
+ _ => {
+ jumbf::labels::box_name_from_uri(&claim.signature).unwrap_or_else(|| "".to_string())
+ != jumbf::labels::SIGNATURE
+ } // relative signature box
+ };
+
+ if sig_box_err {
+ let log_item = log_item!(claim.signature_uri(), "signature missing", "verify_claim")
+ .error(Error::ClaimMissingSignatureBox)
+ .validation_status(validation_status::CLAIM_SIGNATURE_MISSING);
+ validation_log.log(log_item, Some(Error::ClaimMissingSignatureBox))?;
+ }
+
+ let verified = verify_cose(
+ sig,
+ &claim.data()?,
+ &additional_bytes,
+ !is_provenance,
+ validation_log,
+ );
+
+ Claim::verify_internal(claim, asset_bytes, is_provenance, verified, validation_log)
+ }
+
+ fn verify_internal(
+ claim: &Claim,
+ asset_bytes: &[u8],
+ is_provenance: bool,
+ verified: Result<ValidationInfo>,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<()> {
+ let default_str = |s: &String| s.clone();
+
+ match verified {
+ Ok(vi) => {
+ if !vi.validated {
+ let log_item = log_item!(
+ claim.signature_uri(),
+ "claim signature is not valid",
+ "verify_internal"
+ )
+ .error(Error::CoseSignature)
+ .validation_status(validation_status::CLAIM_SIGNATURE_MISMATCH);
+ validation_log.log(log_item, Some(Error::CoseSignature))?;
+ } else {
+ let log_item = log_item!(
+ claim.signature_uri(),
+ "claim signature valid",
+ "verify_internal"
+ )
+ .validation_status(validation_status::CLAIM_SIGNATURE_VALIDATED);
+ validation_log.log_silent(log_item);
+ }
+ }
+ Err(parse_err) => {
+ let log_item = log_item!(
+ claim.signature_uri(),
+ "claim signature is not valid",
+ "verify_internal"
+ )
+ .error(parse_err)
+ .validation_status(validation_status::CLAIM_SIGNATURE_MISMATCH);
+ validation_log.log(log_item, Some(Error::CoseSignature))?;
+ }
+ };
+
+ // check for self redacted assertions and illegal readactions
+ if let Some(redactions) = claim.redactions() {
+ for r in redactions {
+ let r_manifest = jumbf::labels::manifest_label_from_uri(r)
+ .ok_or(Error::AssertionInvalidRedaction)?;
+ if claim.label().contains(&r_manifest) {
+ let log_item = log_item!(
+ claim.uri(),
+ "claim contains self redaction",
+ "verify_internal"
+ )
+ .error(Error::ClaimSelfRedact)
+ .validation_status(validation_status::ASSERTION_SELF_REDACTED);
+ validation_log.log(log_item, Some(Error::ClaimSelfRedact))?;
+ }
+
+ if r.contains(assertions::labels::ACTIONS) {
+ let log_item = log_item!(
+ claim.uri(),
+ "readaction of action assertions disallowed",
+ "verify_internal"
+ )
+ .error(Error::ClaimDisallowedRedaction)
+ .validation_status(validation_status::ACTION_ASSERTION_REDACTED);
+ validation_log.log(log_item, Some(Error::ClaimDisallowedRedaction))?;
+ }
+ }
+ }
+
+ // make sure UpdateManifests do not contain actions
+ if claim.update_manifest() && claim.label().contains(assertions::labels::ACTIONS) {
+ let log_item = log_item!(
+ claim.uri(),
+ "update manifests cannot contain actions",
+ "verify_internal"
+ )
+ .error(Error::UpdateManifestInvalid)
+ .validation_status(validation_status::MANIFEST_UPDATE_INVALID);
+ validation_log.log(log_item, Some(Error::UpdateManifestInvalid))?;
+ }
+ // verify assertion structure comparing hashes from assertion list to contents of assertion store
+ for assertion in claim.assertions() {
+ let (label, instance) = Claim::assertion_label_from_link(&assertion.url());
+ match claim.get_claim_assertion(&label, instance) {
+ // get the assertion if label and hash match
+ Some(ca) => {
+ if !vec_compare(ca.hash(), &assertion.hash()) {
+ let log_item = log_item!(
+ assertion.url(),
+ format!("hash does not match assertion data: {}", assertion.url()),
+ "verify_internal"
+ )
+ .error(Error::HashMismatch(format!(
+ "Assertion hash failure: {}",
+ assertion.url()
+ )))
+ .validation_status(validation_status::ASSERTION_HASHEDURI_MISMATCH);
+ validation_log.log(
+ log_item,
+ Some(Error::HashMismatch(format!(
+ "Assertion hash failure: {}",
+ assertion.url()
+ ))),
+ )?;
+ } else {
+ let log_item = log_item!(
+ assertion.url(),
+ format!("hashed uri matched: {}", assertion.url()),
+ "verify_internal"
+ )
+ .validation_status(validation_status::ASSERTION_HASHEDURI_MATCH);
+ validation_log.log_silent(log_item);
+ }
+ }
+ None => {
+ let log_item = log_item!(
+ assertion.url(),
+ format!("cannot find matching assertion: {}", assertion.url()),
+ "verify_internal"
+ )
+ .error(Error::AssertionMissing {
+ url: assertion.url(),
+ })
+ .validation_status(validation_status::ASSERTION_MISSING);
+ validation_log.log(
+ log_item,
+ Some(Error::AssertionMissing {
+ url: assertion.url(),
+ }),
+ )?;
+ }
+ }
+ }
+
+ // verify data hashes for provenance claims
+ if is_provenance {
+ // must have at least one hard binding for normal manifests
+ if claim.data_hash_assertions().is_empty() && !claim.update_manifest() {
+ let log_item = log_item!(
+ &claim.uri(),
+ "claim missing data binding",
+ "verify_internal"
+ )
+ .error(Error::ClaimMissingHardBinding)
+ .validation_status(validation_status::HARD_BINDINGS_MISSING);
+ validation_log.log(log_item, Some(Error::ClaimMissingHardBinding))?;
+ }
+
+ // update manifests cannot have data hashes
+ if !claim.data_hash_assertions().is_empty() && claim.update_manifest() {
+ let log_item = log_item!(
+ &claim.uri(),
+ "update manifests cannot contain data hash assertions",
+ "verify_internal"
+ )
+ .error(Error::UpdateManifestInvalid)
+ .validation_status(validation_status::MANIFEST_UPDATE_INVALID);
+ validation_log.log(log_item, Some(Error::UpdateManifestInvalid))?;
+ }
+
+ for dh_assertion in claim.data_hash_assertions() {
+ let dh = DataHash::from_assertion(&dh_assertion)?;
+ let name = dh.name.as_ref().map_or("unnamed".to_string(), default_str);
+ if !dh.is_remote_hash() {
+ // only verify local hashes here
+ match dh.verify_in_memory_hash(asset_bytes, Some(claim.alg().to_string())) {
+ Ok(_a) => {
+ let log_item = log_item!(
+ claim.assertion_uri(&dh_assertion.label()),
+ "data hash valid",
+ "verify_internal"
+ )
+ .validation_status(validation_status::ASSERTION_DATAHASH_MATCH);
+ validation_log.log_silent(log_item);
+
+ continue;
+ }
+ Err(e) => {
+ let log_item = log_item!(
+ claim.assertion_uri(&dh_assertion.label()),
+ format!("asset hash error, name: {}, error: {}", name, e),
+ "verify_internal"
+ )
+ .error(Error::HashMismatch(format!("Asset hash failure: {}", e)))
+ .validation_status(validation_status::ASSERTION_DATAHASH_MISMATCH);
+
+ validation_log.log(
+ log_item,
+ Some(Error::HashMismatch(format!("Asset hash failure: {}", e))),
+ )?;
+ }
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+
+ /// Verify hash against self. True if match,
+ /// false if no match or unsupported
+ pub fn verify_hash(&self, hash: &[u8]) -> bool {
+ // get hash of self for comparison
+ if let Some(ref original_bytes) = self.original_bytes {
+ verify_by_alg(self.alg(), hash, original_bytes, None)
+ } else if let Ok(claim_data) = self.data() {
+ verify_by_alg(self.alg(), hash, &claim_data, None)
+ } else {
+ false
+ }
+ }
+
+ /// Return list of data hash assertions
+ pub fn data_hash_assertions(&self) -> Vec<Assertion> {
+ let dummy_data = AssertionData::Cbor(Vec::new());
+ let dummy_hash = Assertion::new(DataHash::LABEL, None, dummy_data);
+ let mut data_hashes = self.assertions_by_type(&dummy_hash);
+
+ // add in an BMFF hashes
+ let dummy_bmff_data = AssertionData::Cbor(Vec::new());
+ let dummy_bmff_hash = Assertion::new(assertions::labels::BMFF_HASH, None, dummy_bmff_data);
+ data_hashes.append(&mut self.assertions_by_type(&dummy_bmff_hash));
+
+ data_hashes
+ }
+
+ /// Return list of ingredient assertions. This function
+ /// is only useful on commited or loaded claims since ingredients
+ /// are resolved at commit time.
+ pub fn ingredient_assertions(&self) -> Vec<Assertion> {
+ let dummy_data = AssertionData::Cbor(Vec::new());
+ let dummy_ingredient = Assertion::new(labels::INGREDIENT, None, dummy_data);
+ self.assertions_by_type(&dummy_ingredient)
+ }
+
+ /// Return reference to the internal claim assertion store.
+ pub fn claim_assertion_store(&self) -> &Vec<ClaimAssertion> {
+ &self.assertion_store
+ }
+
+ /// Return reference to the internal claim ingredient store.
+ /// Used during generation
+ pub fn claim_ingredient_store(&self) -> &HashMap<String, Vec<Claim>> {
+ &self.ingredients_store
+ }
+
+ /// Return reference to the internal claim ingredient store matching this guid.
+ /// Used during generation
+ pub fn claim_ingredient(&self, claim_guid: &str) -> Option<&Vec<Claim>> {
+ self.ingredients_store.get(claim_guid)
+ }
+
+ /// Adds ingredients, this data will be written out during commit of the Claim
+ pub(crate) fn add_ingredient_data(
+ &mut self,
+ provenance_label: &str,
+ mut ingredient: Vec<Claim>,
+ redactions_opt: Option<Vec<String>>,
+ ) -> Result<()> {
+ // redact assertion from incoming ingredients
+ if let Some(redactions) = &redactions_opt {
+ for redaction in redactions {
+ if let Some(claim) = ingredient
+ .iter_mut()
+ .find(|x| redaction.contains(&x.label()))
+ {
+ claim.redact_assertion(redaction)?;
+ } else {
+ return Err(Error::AssertionRedactionNotFound);
+ }
+ }
+ }
+
+ // all have been removed (if necessary) so replace redaction list
+ self.redacted_assertions = redactions_opt;
+
+ // add ingredients
+ self.ingredients_store
+ .insert(provenance_label.to_string(), ingredient);
+
+ Ok(())
+ }
+
+ /// List of redactions
+ pub fn redactions(&self) -> Option<&Vec<String>> {
+ self.redacted_assertions.as_ref()
+ }
+
+ /// Return snapshot clone of the claim's assertions.
+ pub fn assertion_store(&self) -> Vec<Assertion> {
+ self.assertion_store
+ .iter()
+ .map(|x| x.assertion.clone())
+ .collect()
+ }
+
+ pub fn assertions_by_type(&self, assertion_proto: &Assertion) -> Vec<Assertion> {
+ self.assertion_store
+ .iter()
+ .filter_map(|x| {
+ if Assertion::assertions_eq(assertion_proto, x.assertion()) {
+ Some(x.assertion.clone())
+ } else {
+ None
+ }
+ })
+ .collect()
+ }
+
+ /// Return reference to the assertions list.
+ ///
+ /// This list matches item-for-item with the `Assertion`s
+ /// stored in the assertion store.
+ pub fn assertions(&self) -> &Vec<C2PAAssertion> {
+ &self.assertions
+ }
+
+ /// Returns the cbor binary value of the claim data.
+ /// If this claim was read from a file, returns the exact byte
+ /// sequence that was read from the file. If this claim was
+ /// constructed locally, contains the claim data that was/will be
+ /// generated locally.
+ pub fn data(&self) -> Result<Vec<u8>> {
+ match self.original_bytes {
+ Some(ref ob) => Ok(ob.clone()),
+ None => Ok(serde_cbor::ser::to_vec(&self).map_err(|_err| Error::ClaimEncoding)?),
+ }
+ }
+
+ /// Create claim from binary data (not including assertions).
+ pub fn from_data(label: &str, data: &[u8]) -> Result<Claim> {
+ let mut claim: Claim = serde_cbor::from_slice(data).map_err(|_err| Error::ClaimDecoding)?;
+
+ claim.label = label.to_string();
+ claim.original_bytes = Some(data.to_owned());
+
+ Ok(claim)
+ }
+
+ /// Generate a JSON representation of the Claim
+ /// returns Result as a String
+ pub fn to_json(
+ &self,
+ assertion_store_format: AssertionStoreJsonFormat,
+ pretty: bool,
+ ) -> Result<String> {
+ let mut v = serde_json::to_value(self)?;
+
+ match assertion_store_format {
+ AssertionStoreJsonFormat::None => {}
+ AssertionStoreJsonFormat::KeyValue | AssertionStoreJsonFormat::KeyValueNoBinary => {
+ // add additional data if needed to the assertion store
+ if let Value::Object(ref mut map) = v {
+ // merge the label with the data
+ let mut json_map: Map<String, Value> = Map::new();
+ let iter = self.assertions.iter().zip(&self.assertion_store);
+
+ for (_key, claim_assertion) in iter {
+ let link = claim_assertion.label();
+ let (label, instance) = Self::assertion_label_from_link(&link);
+ let label = Self::label_with_instance(&label, instance);
+
+ match claim_assertion.assertion.decode_data() {
+ AssertionData::Json(x) => {
+ // json strings
+ let decoded = serde_json::from_str(x)?;
+ json_map.insert(label, decoded);
+ }
+ AssertionData::Cbor(x) => {
+ // some types are not translatable to json so explicitly convert
+ let buf: Vec<u8> = Vec::new();
+ let mut from = serde_cbor::Deserializer::from_slice(x);
+ let mut to = serde_json::Serializer::new(buf);
+
+ serde_transcode::transcode(&mut from, &mut to)
+ .map_err(|_err| Error::AssertionEncoding)?;
+ let buf2 = to.into_inner();
+
+ let decoded: Value = serde_json::from_slice(&buf2)
+ .map_err(|_err| Error::AssertionEncoding)?;
+
+ json_map.insert(label, decoded);
+ }
+ AssertionData::Binary(x) => {
+ // binary vecs
+ let d = match assertion_store_format {
+ AssertionStoreJsonFormat::KeyValue => {
+ Value::String(base64::encode(x))
+ }
+ AssertionStoreJsonFormat::KeyValueNoBinary => {
+ Value::String("omitted".to_owned())
+ }
+ _ => Value::String("".to_owned()),
+ };
+ json_map.insert(label, d);
+ continue;
+ }
+ AssertionData::Uuid(s, x) => {
+ // binary vecs
+ let d = match assertion_store_format {
+ AssertionStoreJsonFormat::KeyValue => {
+ Value::String(base64::encode(x))
+ }
+ AssertionStoreJsonFormat::KeyValueNoBinary => {
+ Value::String("omitted".to_owned())
+ }
+ _ => Value::String("".to_owned()),
+ };
+
+ let m = json!({
+ "uuid": s,
+ "data": d,
+ });
+
+ json_map.insert(label, m);
+ continue;
+ }
+ }
+ }
+ //let s = serde_json::to_string(&json_map)?;
+ //let as_val = serde_json::from_str(&s)?;
+ let as_val = serde_json::to_value(json_map)?;
+ map.insert("assertion_store".to_string(), as_val);
+
+ // add vcstore
+ map.insert(
+ "vc_store".to_string(),
+ serde_json::to_value(&self.vc_store)?,
+ );
+
+ // add claim label
+ map.insert("label".to_string(), Value::String(self.label.to_string()));
+ }
+ }
+ AssertionStoreJsonFormat::OrderedList
+ | AssertionStoreJsonFormat::OrderedListNoBinary => {
+ // add additional data if needed to the assertion store
+ if let Value::Object(ref mut map) = v {
+ let mut json_vec: Vec<Value> = Vec::new();
+
+ // assertion values
+ for claim_assertion in self.claim_assertion_store() {
+ match claim_assertion.assertion.decode_data() {
+ AssertionData::Json(x) => {
+ let d: Value = serde_json::from_str(x)
+ .map_err(|_err| Error::AssertionEncoding)?;
+
+ let j = JsonOrderedAssertionData {
+ label: claim_assertion.label().to_owned(),
+ hash: base64::encode(claim_assertion.hash()),
+ data: d,
+ is_binary: false,
+ mime_type: claim_assertion.assertion.mime_type(),
+ };
+
+ let new_val = serde_json::to_value(j)?;
+ json_vec.push(new_val);
+ }
+ AssertionData::Cbor(x) => {
+ // some types are not translatable to json so explicitly convert
+ let buf: Vec<u8> = Vec::new();
+ let mut from = serde_cbor::Deserializer::from_slice(x);
+ let mut to = serde_json::Serializer::new(buf);
+
+ serde_transcode::transcode(&mut from, &mut to)
+ .map_err(|_err| Error::AssertionEncoding)?;
+ let buf2 = to.into_inner();
+
+ let d: Value = serde_json::from_slice(&buf2)
+ .map_err(|_err| Error::AssertionEncoding)?;
+
+ let j = JsonOrderedAssertionData {
+ label: claim_assertion.label().to_owned(),
+ hash: base64::encode(claim_assertion.hash()),
+ data: d,
+ is_binary: false,
+ mime_type: claim_assertion.assertion.mime_type(),
+ };
+
+ let new_val = serde_json::to_value(j)?;
+ json_vec.push(new_val);
+ }
+ AssertionData::Binary(x) => {
+ // binary data
+ let d = match assertion_store_format {
+ AssertionStoreJsonFormat::OrderedList => {
+ Value::String(base64::encode(x))
+ }
+ AssertionStoreJsonFormat::OrderedListNoBinary => {
+ Value::String("omitted".to_owned())
+ }
+ _ => Value::String("".to_owned()),
+ };
+
+ let j = JsonOrderedAssertionData {
+ label: claim_assertion.label().to_owned(),
+ hash: base64::encode(claim_assertion.hash()),
+ data: d,
+ is_binary: true,
+ mime_type: claim_assertion.assertion.mime_type(),
+ };
+
+ let new_val = serde_json::to_value(j)?;
+ json_vec.push(new_val);
+ }
+ AssertionData::Uuid(s, x) => {
+ // binary data
+ let d = match assertion_store_format {
+ AssertionStoreJsonFormat::OrderedList => {
+ Value::String(base64::encode(x))
+ }
+ AssertionStoreJsonFormat::OrderedListNoBinary => {
+ Value::String("omitted".to_owned())
+ }
+ _ => Value::String("".to_owned()),
+ };
+
+ let m = json!({
+ "uuid": s,
+ "data": d,
+ });
+
+ let j = JsonOrderedAssertionData {
+ label: claim_assertion.label().to_owned(),
+ hash: base64::encode(claim_assertion.hash()),
+ data: m,
+ is_binary: true,
+ mime_type: claim_assertion.assertion.mime_type(),
+ };
+
+ let new_val = serde_json::to_value(j)?;
+ json_vec.push(new_val);
+ }
+ }
+ }
+
+ let as_val = serde_json::to_value(json_vec)?;
+ map.insert("assertion_store".to_string(), as_val);
+
+ // add claim label
+ map.insert("label".to_string(), Value::String(self.label.to_string()));
+ }
+ }
+ }
+
+ if pretty {
+ serde_json::to_string_pretty(&v).map_err(|e| e.into())
+ } else {
+ serde_json::to_string(&v).map_err(|e| e.into())
+ }
+ }
+
+ /// Return the label for this assertion given its link
+ pub fn assertion_label_from_link(assertion_link: &str) -> (String, usize) {
+ let v = jumbf::labels::to_normalized_uri(assertion_link);
+
+ let v2: Vec<&str> = v.split('/').collect();
+ if let Some(s) = v2.last() {
+ // treat ingredient thumbnails differently ingredient.thumbnail
+ if get_thumbnail_type(s) == labels::INGREDIENT_THUMBNAIL {
+ let instance = get_thumbnail_instance(s).unwrap_or(0);
+ let label = match get_thumbnail_image_type(s).as_str() {
+ "none" => get_thumbnail_type(s),
+ image_type => format!("{}.{}", get_thumbnail_type(s), image_type),
+ };
+ (label, instance)
+ } else {
+ let label_parts: Vec<&str> = s.split("__").collect();
+ let mut instance: usize = 0;
+
+ if label_parts.len() == 2 {
+ match label_parts[1].parse::<usize>() {
+ Ok(i) => instance = i,
+ _ => instance = 0,
+ }
+ }
+
+ (label_parts[0].to_owned(), instance)
+ }
+ } else {
+ (v2[0].to_owned(), 0)
+ }
+ }
+
+ /// generates label with instance if needed
+ pub fn label_with_instance(label: &str, instance: usize) -> String {
+ if instance == 0 {
+ label.to_string()
+ } else if get_thumbnail_type(label) == labels::INGREDIENT_THUMBNAIL {
+ let tn_type = get_thumbnail_image_type(label);
+ format!("{}__{}.{}", get_thumbnail_type(label), instance, tn_type)
+ } else {
+ format!("{}__{}", label, instance)
+ }
+ }
+
+ pub fn assertion_hashed_uri_from_label(&self, assertion_label: &str) -> Option<&C2PAAssertion> {
+ self.assertions()
+ .iter()
+ .find(|hashed_uri| hashed_uri.url().contains(assertion_label))
+ }
+
+ // Given a proposed label, make a new label that is unique within this
+ // assertion store. Typically this is done by adding `__{n}` where `n` is
+ // an integer starting from 1. Ingredient thumbnails have special handling.
+ fn make_assertion_instance_label(&self, assertion_label: &str) -> String {
+ let cnt = self.next_instance(assertion_label);
+
+ Claim::label_with_instance(assertion_label, cnt)
+ }
+
+ /// returns first instance of an assertion whose label and instance match
+ pub fn get_assertion(&self, assertion_label: &str, instance: usize) -> Option<&Assertion> {
+ let mut iter = self.claim_assertion_store().iter().filter_map(|ca| {
+ if ca.label_raw() == assertion_label && ca.instance() == instance {
+ Some(ca.assertion())
+ } else {
+ None
+ }
+ });
+
+ iter.next()
+ }
+
+ /// returns instance of an assertion whose label and instance match
+ pub fn get_claim_assertion(
+ &self,
+ assertion_label: &str,
+ instance: usize,
+ ) -> Option<&ClaimAssertion> {
+ self.claim_assertion_store()
+ .iter()
+ .find(|ca| ca.label_raw() == assertion_label && ca.instance() == instance)
+ }
+
+ /// returns hash of an assertion whose label and instance match
+ pub fn get_claim_assertion_hash(&self, assertion_label: &str) -> Option<Vec<u8>> {
+ let (l, i) = Claim::assertion_label_from_link(assertion_label);
+ self.get_claim_assertion(&l, i).map(|a| a.hash().to_vec())
+ }
+
+ /// Returns how many assertions of this assertion type exist?
+ pub fn count_instances(&self, in_label: &str) -> usize {
+ let (l, i) = Claim::assertion_label_from_link(in_label);
+ let label = Claim::label_with_instance(&l, i);
+ self.assertions
+ .iter()
+ .filter(|assertion| assertion.url().contains(&label))
+ .count()
+ }
+
+ // Get the next highest instance label
+ fn next_instance(&self, in_label: &str) -> usize {
+ let (label, _) = Claim::assertion_label_from_link(in_label);
+ match self
+ .assertion_store
+ .iter()
+ .filter(|&x| x.assertion.label().contains(&label))
+ .map(|x| {
+ let (_l, i) = Claim::assertion_label_from_link(&x.label());
+ i
+ })
+ .max()
+ {
+ Some(last_instance) => last_instance + 1,
+ None => 0,
+ }
+ }
+
+ // Do any assertions of this type exist?
+ pub fn has_assertion_type(&self, in_label: &str) -> bool {
+ let (label, _) = Claim::assertion_label_from_link(in_label);
+ let found = self
+ .assertion_store
+ .iter()
+ .find(|&x| x.assertion.label().starts_with(&label));
+
+ !matches!(found, None)
+ }
+
+ // Create a JUMBF URI from a claim label.
+ pub(crate) fn to_claim_uri(manifest_label: &str) -> String {
+ format!(
+ "{}/{}",
+ jumbf::labels::to_manifest_uri(manifest_label),
+ Self::LABEL
+ )
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ use crate::utils::test::create_test_claim;
+
+ #[test]
+ fn test_build_claim() {
+ // Create a new claim.
+ let mut claim = create_test_claim().expect("create test claim");
+
+ // Add a redaction.
+ // claim.redact_assertion("as_tp_1/c2pa.location.precise");
+
+ // Build claim checking rules.
+ claim.build().expect("bad claim");
+
+ // Test round-tripping of binary.
+ let orig_binary = claim.data().expect("failure returning data");
+ let restored_claim =
+ Claim::from_data("as_adbe_1", &orig_binary).expect("could not restore from binary");
+ let restored_binary = restored_claim.data().expect("failure returning data");
+
+ assert_eq!(orig_binary, restored_binary);
+ println!("Restored Claim: {:?}", restored_claim);
+
+ // NOTE: I added a separate mirror of original data because a third-party's
+ // JSON serialization could differ from our re-serialization of that same data.
+ // When reading claims from assets and verifying signatures of those claims,
+ // we need the exact original bytes of the signed JSON or the signature verification
+ // will fail.
+ assert_eq!(orig_binary, restored_claim.original_bytes.unwrap());
+
+ // JSON examples
+ let json_str = claim
+ .to_json(AssertionStoreJsonFormat::OrderedList, true)
+ .expect("could not generate json");
+
+ println!("Claim: {}", json_str);
+ }
+
+ #[test]
+ fn test_build_claim_generator_hints() {
+ // Create a new claim.
+ let mut claim = create_test_claim().expect("create test claim");
+
+ claim.add_claim_generator_hint(
+ GH_FULL_VERSION_LIST,
+ Value::String(r#""user app";v="2.3.4""#.to_string()),
+ );
+ claim.add_claim_generator_hint(
+ GH_FULL_VERSION_LIST,
+ Value::String(r#""some toolkit";v="1.0.0""#.to_string()),
+ );
+
+ let expected_value = r#""user app";v="2.3.4", "some toolkit";v="1.0.0""#;
+
+ let cg_map = claim.get_claim_generator_hint_map().unwrap();
+ let value = &cg_map[GH_FULL_VERSION_LIST];
+
+ assert_eq!(expected_value, value.as_str().unwrap());
+ }
+}
diff --git a/sdk/src/cose_sign.rs b/sdk/src/cose_sign.rs
@@ -0,0 +1,221 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::time_stamp::{cose_timestamp_countersign, make_cose_timestamp};
+use crate::{Error, Result, Signer}; // enable when TimeStamp Authority is ready
+
+use ciborium::value::Value;
+use coset::{iana, CoseSign1, CoseSign1Builder, HeaderBuilder, Label, TaggedCborSerializable};
+
+/// Returns signed Cose_Sign1 bytes for "data". The Cose_Sign1 will be signed with the algorithm from `Signer`.
+pub fn cose_sign(signer: &dyn Signer, data: &[u8], box_size: usize) -> Result<Vec<u8>> {
+ // 13.2.1. X.509 Certificates
+ //
+ // X.509 Certificates are stored in a header named x5chain draft-ietf-cose-x509.
+ // The value is a CBOR array of byte strings, each of which contains the certificate
+ // encoded as ASN.1 distinguished encoding rules (DER). This array must contain at
+ // least one element. The first element of the array must be the certificate of
+ // the signer, and the subjectPublicKeyInfo element of the certificate will be the
+ // public key used to validate the signature. The Validity member of the TBSCertificate
+ // sequence provides the time validity period of the certificate.
+
+ /*
+ This header parameter allows for a single X.509 certificate or a
+ chain of X.509 certificates to be carried in the message.
+
+ * If a single certificate is conveyed, it is placed in a CBOR
+ byte string.
+
+ * If multiple certificates are conveyed, a CBOR array of byte
+ strings is used, with each certificate being in its own byte
+ string.
+ */
+
+ let alg = signer.alg().ok_or(Error::UnsupportedType)?;
+
+ let alg_id = match alg.as_ref() {
+ "ps256" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::PS256)
+ .build(),
+ "ps384" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::PS384)
+ .build(),
+ "ps512" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::PS512)
+ .build(),
+ /* No longer supported by C2PA
+ "rs256" => {
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::RS256)
+ .build()
+ }
+ "rs384" => {
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::RS384)
+ .build()
+ }
+ "rs512" => {
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::RS512)
+ .build()
+ }
+ */
+ "es256" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .build(),
+ "es384" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES384)
+ .build(),
+ "es512" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES512)
+ .build(),
+ "ed25519" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::EdDSA)
+ .build(),
+ _ => return Err(Error::UnsupportedType),
+ };
+
+ // Get the public CAs for the Signer
+ let certs = signer.certs()?;
+ let sc_der_array_or_bytes = match certs.len() {
+ 1 => Value::Bytes(certs[0].clone()), // single cert
+ _ => {
+ let mut sc_der_array: Vec<Value> = Vec::new();
+ for cert in certs {
+ sc_der_array.push(Value::Bytes(cert));
+ }
+ Value::Array(sc_der_array) // provide vec of certs when required
+ }
+ };
+
+ let mut unprotected = match signer.time_authority_url() {
+ Some(url) => {
+ let cts = cose_timestamp_countersign(data, &alg, &url)?;
+ let sigtst_vec = serde_cbor::to_vec(&make_cose_timestamp(&cts))?;
+ let sigtst_cbor = serde_cbor::from_slice(&sigtst_vec)?;
+
+ HeaderBuilder::new()
+ .text_value("x5chain".to_string(), sc_der_array_or_bytes)
+ .text_value("sigTst".to_string(), sigtst_cbor)
+ }
+ None => {
+ let sign_time = chrono::Utc::now().to_rfc3339(); // todo: remove when switch to cose_timestamp
+ HeaderBuilder::new()
+ .text_value("x5chain".to_string(), sc_der_array_or_bytes)
+ .text_value("temp_signing_time".to_string(), Value::Text(sign_time))
+ }
+ };
+
+ // set the ocsp responder response if available
+ if let Some(ocsp) = signer.ocsp_val() {
+ let mut ocsp_vec: Vec<Value> = Vec::new();
+ let mut r_vals: Vec<(Value, Value)> = vec![];
+
+ ocsp_vec.push(Value::Bytes(ocsp));
+ r_vals.push((Value::Text("ocspVals".to_string()), Value::Array(ocsp_vec)));
+
+ unprotected = unprotected.text_value("rVals".to_string(), Value::Map(r_vals));
+ }
+
+ // build complete header
+ let unprotected_header = unprotected.build();
+
+ let aad = b""; // no additional data required here
+
+ let sign1_builder = CoseSign1Builder::new()
+ .protected(alg_id)
+ .unprotected(unprotected_header)
+ .payload(data.to_vec())
+ .try_create_signature(aad, |bytes| signer.sign(bytes))?;
+
+ let mut sign1 = sign1_builder.build();
+ sign1.payload = None; // clear the payload since it is known
+
+ let c2pa_sig_data = pad_cose_sig(&mut sign1, box_size)?;
+
+ // println!("sig: {}", Hexlify(&c2pa_sig_data));
+
+ Ok(c2pa_sig_data)
+}
+
+const PAD: &str = "pad";
+const PAD2: &str = "pad2";
+const PAD_OFFSET: usize = 7;
+
+// Pad the CoseSign1 structure with 0s to match the reserved box size.
+// There are some values lengths that are impossible to hit with a single padding so
+// when that happens a second padding is added to change the remaining needed padding.
+// The default initial guess works for almost all sizes, without the need for additional loops.
+fn pad_cose_sig(sign1: &mut CoseSign1, end_size: usize) -> Result<Vec<u8>> {
+ let mut sign1_clone = sign1.clone();
+ let cur_vec = sign1_clone
+ .to_tagged_vec()
+ .map_err(|_e| Error::CoseSignature)?;
+ let cur_size = cur_vec.len();
+
+ // check for box too small
+ match cur_size > end_size {
+ true => {
+ return Err(Error::CoseSigboxTooSmall);
+ }
+ false if cur_size == end_size => return Ok(cur_vec),
+ false => (),
+ }
+
+ let mut padding_found = false;
+ let mut last_pad = 0;
+ let mut target_guess = end_size - cur_size - PAD_OFFSET; // start close to desired end_size accounting for label
+ loop {
+ // clone to use
+ sign1_clone = sign1.clone();
+
+ // replace padding with new estimate
+ for header_pair in &mut sign1_clone.unprotected.rest {
+ if header_pair.0 == Label::Text("pad".to_string()) {
+ if let Value::Bytes(b) = &header_pair.1 {
+ last_pad = b.len();
+ }
+ header_pair.1 = Value::Bytes(vec![0u8; target_guess]);
+ padding_found = true;
+ break;
+ }
+ }
+
+ // if there was no padding add it and call again
+ if !padding_found {
+ sign1_clone.unprotected.rest.push((
+ Label::Text(PAD.to_string()),
+ Value::Bytes(vec![0u8; target_guess]),
+ ));
+ return pad_cose_sig(&mut sign1_clone, end_size);
+ }
+
+ // get current cbor vec to size if we reached target size
+ let new_cbor = sign1_clone
+ .to_tagged_vec()
+ .map_err(|_e| Error::CoseSignature)?;
+
+ match new_cbor.len() < end_size {
+ true => target_guess += 1,
+ false if new_cbor.len() == end_size => return Ok(new_cbor),
+ false => break, // we couuld not match end_size in a single pad so break and add a second
+ }
+ }
+
+ // if we reach here we need a new second padding object to hit exact size
+ sign1.unprotected.rest.push((
+ Label::Text(PAD2.to_string()),
+ Value::Bytes(vec![0u8; last_pad - 10]),
+ ));
+ pad_cose_sig(sign1, end_size)
+}
diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs
@@ -0,0 +1,1102 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::error::{Error, Result};
+use crate::status_tracker::{log_item, StatusTracker};
+use crate::time_stamp::gt_to_datetime;
+use crate::validation_status;
+#[cfg(not(target_arch = "wasm32"))]
+use crate::validator::get_validator;
+#[cfg(not(target_arch = "wasm32"))]
+use crate::validator::CoseValidator;
+use crate::validator::ValidationInfo;
+
+#[cfg(target_arch = "wasm32")]
+use crate::wasm::webcrypto_validator::validate_async;
+
+use crate::asn1::rfc3161::TstInfo;
+use ciborium::value::Value;
+use conv::*;
+use coset::{sig_structure_data, Label, TaggedCborSerializable};
+
+use std::str::FromStr;
+
+use x509_parser::der_parser::ber::parse_ber_sequence;
+use x509_parser::der_parser::oid;
+use x509_parser::oid_registry::Oid;
+use x509_parser::prelude::*;
+
+const RSA_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .1);
+const EC_PUBLICKEY_OID: Oid<'static> = oid!(1.2.840 .10045 .2 .1);
+const ECDSA_WITH_SHA256_OID: Oid<'static> = oid!(1.2.840 .10045 .4 .3 .2);
+const ECDSA_WITH_SHA384_OID: Oid<'static> = oid!(1.2.840 .10045 .4 .3 .3);
+const ECDSA_WITH_SHA512_OID: Oid<'static> = oid!(1.2.840 .10045 .4 .3 .4);
+const RSASSA_PSS_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .10);
+const SHA256_WITH_RSAENCRYPTION_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .11);
+const SHA384_WITH_RSAENCRYPTION_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .12);
+const SHA512_WITH_RSAENCRYPTION_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .13);
+const ED25519_OID: Oid<'static> = oid!(1.3.101 .112);
+const SHA256_OID: Oid<'static> = oid!(2.16.840 .1 .101 .3 .4 .2 .1);
+const SHA384_OID: Oid<'static> = oid!(2.16.840 .1 .101 .3 .4 .2 .2);
+const SHA512_OID: Oid<'static> = oid!(2.16.840 .1 .101 .3 .4 .2 .3);
+const SECP521R1_OID: Oid<'static> = oid!(1.3.132 .0 .35);
+const SECP384R1_OID: Oid<'static> = oid!(1.3.132 .0 .34);
+const PRIME256V1_OID: Oid<'static> = oid!(1.2.840 .10045 .3 .1 .7);
+
+/********************** Supported Valiators ***************************************
+ RS256 RSASSA-PKCS1-v1_5 using SHA-256 - not recommended
+ RS384 RSASSA-PKCS1-v1_5 using SHA-384 - not recommended
+ RS512 RSASSA-PKCS1-v1_5 using SHA-512 - not recommended
+ PS256 RSASSA-PSS using SHA-256 and MGF1 with SHA-256
+ PS384 RSASSA-PSS using SHA-384 and MGF1 with SHA-384
+ PS512 RSASSA-PSS using SHA-512 and MGF1 with SHA-512
+ ES256 ECDSA using P-256 and SHA-256
+ ES384 ECDSA using P-384 and SHA-384
+ ES512 ECDSA using P-521 and SHA-512
+ ED25519 Edwards Curve 25519
+**********************************************************************************/
+
+fn get_cose_sign1(
+ cose_bytes: &[u8],
+ data: &[u8],
+ validation_log: &mut impl StatusTracker,
+) -> Result<coset::CoseSign1> {
+ match <coset::CoseSign1 as TaggedCborSerializable>::from_tagged_slice(cose_bytes) {
+ Ok(mut sign1) => {
+ sign1.payload = Some(data.to_vec()); // restore payload for verification check
+
+ Ok(sign1)
+ }
+ Err(coset_error) => {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "could not deserialize signature",
+ "get_cose_sign1"
+ )
+ .error(Error::InvalidCoseSignature { coset_error })
+ .validation_status(validation_status::CLAIM_SIGNATURE_MISMATCH);
+
+ validation_log.log_silent(log_item);
+
+ Err(Error::CoseSignature)
+ }
+ }
+}
+fn check_cert(
+ _alg: &str,
+ ca_der_bytes: &[u8],
+ validation_log: &mut impl StatusTracker,
+ _tst_info_opt: Option<&TstInfo>,
+) -> Result<()> {
+ // get the cert in der format
+ let (_rem, signcert) = X509Certificate::from_der(ca_der_bytes).map_err(|_err| {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate could not be parsed",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+ Error::CoseInvalidCert
+ })?;
+
+ // cert version must be 3
+ if signcert.version() != X509Version::V3 {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate version incorrect",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+
+ // check for cert expiration
+ if let Some(tst_info) = _tst_info_opt {
+ // was there a time stamp associtation with this signature, is verify against that time
+ let signing_time = gt_to_datetime(tst_info.gen_time.clone());
+ if !signcert
+ .validity()
+ .is_valid_at(x509_parser::time::ASN1Time::from_timestamp(
+ signing_time.timestamp(),
+ ))
+ {
+ let log_item = log_item!("Cose_Sign1", "certificate expired", "check_cert_alg")
+ .error(Error::CoseCertExpiration)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_EXPIRED);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseCertExpiration);
+ }
+ } else {
+ // no timestamp so check against current time
+ // use instant to avoid wasm issues
+ let now_f64 = instant::now() / 1000.0;
+ let now: i64 = now_f64
+ .approx_as::<i64>()
+ .map_err(|_e| Error::BadParam("system time invalid".to_string()))?;
+
+ if !signcert
+ .validity()
+ .is_valid_at(x509_parser::time::ASN1Time::from_timestamp(now))
+ {
+ let log_item = log_item!("Cose_Sign1", "certificate expired", "check_cert_alg")
+ .error(Error::CoseCertExpiration)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_EXPIRED);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseCertExpiration);
+ }
+ }
+
+ let cert_alg = signcert.signature_algorithm.algorithm.clone();
+
+ // check algorithm needed from cert
+
+ // cert must be signed with one the following algorithm
+ if !(cert_alg == SHA256_WITH_RSAENCRYPTION_OID
+ || cert_alg == SHA384_WITH_RSAENCRYPTION_OID
+ || cert_alg == SHA512_WITH_RSAENCRYPTION_OID
+ || cert_alg == ECDSA_WITH_SHA256_OID
+ || cert_alg == ECDSA_WITH_SHA384_OID
+ || cert_alg == ECDSA_WITH_SHA512_OID
+ || cert_alg == RSASSA_PSS_OID
+ || cert_alg == ED25519_OID)
+ {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate algorithm not supported",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+
+ // verify rsassa_pss parameters
+ if cert_alg == RSASSA_PSS_OID {
+ if let Some(parameters) = &signcert.signature_algorithm.parameters {
+ let seq = parameters
+ .as_sequence()
+ .map_err(|_err| Error::CoseInvalidCert)?;
+ if seq.len() < 3 {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate incorrect rsapss algorithm",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+
+ // get hash algorithm
+ let (_b, ha_alg) = AlgorithmIdentifier::from_der(
+ seq[0]
+ .content
+ .as_slice()
+ .map_err(|_err| Error::CoseInvalidCert)?,
+ )
+ .map_err(|_err| Error::CoseInvalidCert)?;
+
+ let (_b, mgf_ai) = AlgorithmIdentifier::from_der(
+ seq[1]
+ .content
+ .as_slice()
+ .map_err(|_err| Error::CoseInvalidCert)?,
+ )
+ .map_err(|_err| Error::CoseInvalidCert)?;
+
+ let mgf_ai_parameters = mgf_ai.parameters.ok_or(Error::CoseInvalidCert)?;
+ let s = mgf_ai_parameters
+ .as_sequence()
+ .map_err(|_err| Error::CoseInvalidCert)?;
+ let t0 = &s[0];
+ //let _t1 = &s[1];
+ let mfg_ai_params_algorithm = t0.as_oid_val().map_err(|_err| Error::CoseInvalidCert)?;
+
+ // must be the same
+ if ha_alg.algorithm != mfg_ai_params_algorithm {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate algorithm error",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+
+ // check for one of the mandatory types
+ if !(ha_alg.algorithm == SHA256_OID
+ || ha_alg.algorithm == SHA384_OID
+ || ha_alg.algorithm == SHA512_OID)
+ {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate hash algorithm not supported",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+ } else {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate missing algorithm parameters",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+ }
+
+ // check curves for SPKI EC algorithms
+ let pk = signcert.public_key();
+ let skpi_alg = &pk.algorithm;
+
+ if skpi_alg.algorithm == EC_PUBLICKEY_OID {
+ if let Some(parameters) = &skpi_alg.parameters {
+ let named_curve_oid = parameters
+ .as_oid_val()
+ .map_err(|_err| Error::CoseInvalidCert)?;
+
+ // must be one of these named curves
+ if !(named_curve_oid == PRIME256V1_OID
+ || named_curve_oid == SECP384R1_OID
+ || named_curve_oid == SECP521R1_OID)
+ {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate unsupported EC curve",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+ } else {
+ return Err(Error::CoseInvalidCert);
+ }
+ }
+
+ // check modulus minumum length (for RSA & PSS algorithms)
+ if skpi_alg.algorithm == RSA_OID || skpi_alg.algorithm == RSASSA_PSS_OID {
+ let (_, skpi_ber) = parse_ber_sequence(pk.subject_public_key.data)
+ .map_err(|_err| Error::CoseInvalidCert)?;
+
+ let seq = skpi_ber
+ .as_sequence()
+ .map_err(|_err| Error::CoseInvalidCert)?;
+ if seq.len() < 2 {
+ return Err(Error::CoseInvalidCert);
+ }
+
+ let modulus = seq[0].as_bigint().ok_or(Error::CoseInvalidCert)?;
+
+ if modulus.bits() < 2048 {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate key length too short",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+ }
+
+ // check cert values
+ let tbscert = &signcert.tbs_certificate;
+
+ let is_self_signed = tbscert.is_ca() && tbscert.issuer_uid == tbscert.subject_uid;
+
+ // only allowable for self sigbed
+ if !is_self_signed && tbscert.issuer_uid.is_some() || tbscert.subject_uid.is_some() {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate issuer and subject cannot be the same",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+
+ // non self signed CA certs are not allowed, must be an end entity (leaf) cert
+ if tbscert.is_ca() && !is_self_signed {
+ return Err(Error::CoseInvalidCert);
+ }
+
+ let mut aki_good = false;
+ let mut ski_good = false;
+ let mut key_usage_good = false;
+ let mut handled_all_critical = true;
+ let extended_key_usage_good = match tbscert.extended_key_usage() {
+ Some((_critical, eku)) => {
+ if eku.any {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate 'any' EKU not allowed",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+
+ if !(eku.email_protection || eku.ocsp_signing || eku.time_stamping) {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate missing required EKU",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+
+ // one or the other || either of these two, and no others field
+ if (eku.ocsp_signing && eku.time_stamping)
+ || ((eku.ocsp_signing ^ eku.time_stamping)
+ && (eku.client_auth
+ | eku.code_signing
+ | eku.email_protection
+ | eku.server_auth))
+ {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate invalid set of EKUs",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+
+ true
+ }
+ None => tbscert.is_ca(), // if is not ca it must be present
+ };
+
+ // popluate needed extension info
+ for e in signcert.extensions() {
+ match e.parsed_extension() {
+ ParsedExtension::AuthorityKeyIdentifier(_aki) => {
+ aki_good = true;
+ }
+ ParsedExtension::SubjectKeyIdentifier(_spki) => {
+ ski_good = true;
+ }
+ ParsedExtension::KeyUsage(ku) => {
+ if ku.digital_signature() {
+ if ku.key_cert_sign() && !tbscert.is_ca() {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate missing digitalSignature EKU",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseInvalidCert);
+ }
+ key_usage_good = true;
+ }
+ if ku.key_cert_sign() {
+ key_usage_good = true;
+ }
+ // todo: warn if not marked critical
+ // if !e.critical { // warn here somehow}
+ }
+ ParsedExtension::CertificatePolicies(_) => (),
+ ParsedExtension::PolicyMappings(_) => (),
+ ParsedExtension::SubjectAlternativeName(_) => (),
+ ParsedExtension::BasicConstraints(_) => (),
+ ParsedExtension::NameConstraints(_) => (),
+ ParsedExtension::PolicyConstraints(_) => (),
+ ParsedExtension::ExtendedKeyUsage(_) => (),
+ ParsedExtension::CRLDistributionPoints(_) => (),
+ ParsedExtension::InhibitAnyPolicy(_) => (),
+ ParsedExtension::AuthorityInfoAccess(_) => (),
+ ParsedExtension::NSCertType(_) => (),
+ ParsedExtension::CRLNumber(_) => (),
+ ParsedExtension::ReasonCode(_) => (),
+ ParsedExtension::InvalidityDate(_) => (),
+ ParsedExtension::Unparsed => {
+ if e.critical {
+ // unhandled critical extension
+ handled_all_critical = false;
+ }
+ }
+ _ => {
+ if e.critical {
+ // unhandled critical extension
+ handled_all_critical = false;
+ }
+ }
+ }
+ }
+
+ // if cert is a CA must have valid SubjectKeyIdentifier
+ ski_good = if tbscert.is_ca() { ski_good } else { true };
+
+ // check all flags
+ if aki_good && ski_good && key_usage_good && extended_key_usage_good && handled_all_critical {
+ Ok(())
+ } else {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "certificate params incorrect",
+ "check_cert_alg"
+ )
+ .error(Error::CoseInvalidCert)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_INVALID);
+ validation_log.log_silent(log_item);
+
+ Err(Error::CoseInvalidCert)
+ }
+}
+
+pub(crate) fn get_validator_str(cs1: &coset::CoseSign1) -> Result<String> {
+ // find the supported handler for the algorithm
+ let validator_str = match cs1.protected.header.alg {
+ Some(ref alg) => {
+ let alg_str = match alg {
+ coset::RegisteredLabelWithPrivate::PrivateUse(a) => match a {
+ -39 => "ps512",
+ -38 => "ps384",
+ -37 => "ps256",
+ -36 => "es512",
+ -35 => "es384",
+ -7 => "es256",
+ // todo: deprecated figure out lecacy support for RS signatures
+ -259 => "rs512",
+ -258 => "rs384",
+ -257 => "rs256",
+
+ -8 => "ed25519",
+ _ => "unknown",
+ },
+ coset::RegisteredLabelWithPrivate::Assigned(a) => match a {
+ coset::iana::Algorithm::PS512 => "ps512",
+ coset::iana::Algorithm::PS384 => "ps384",
+ coset::iana::Algorithm::PS256 => "ps256",
+ coset::iana::Algorithm::ES512 => "es512",
+ coset::iana::Algorithm::ES384 => "es384",
+ coset::iana::Algorithm::ES256 => "es256",
+ // todo: deprecated figure out lecacy support for RS signatures
+ coset::iana::Algorithm::RS512 => "rs512",
+ coset::iana::Algorithm::RS384 => "rs384",
+ coset::iana::Algorithm::RS256 => "rs256",
+ coset::iana::Algorithm::EdDSA => "ed25519",
+ _ => "unknown",
+ },
+ coset::RegisteredLabelWithPrivate::Text(a) => a,
+ };
+
+ Some(alg_str.to_owned())
+ }
+ None => None,
+ }
+ .ok_or(Error::CoseSignatureAlgorithmNotSupported)?;
+
+ Ok(validator_str)
+}
+
+fn get_sign_cert(sign1: &coset::CoseSign1) -> Result<Vec<u8>> {
+ // element 0 is the signing cert
+ let certs = get_sign_certs(sign1)?;
+ Ok(certs[0].clone())
+}
+// get the public key der
+fn get_sign_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>> {
+ let mut certs: Vec<Vec<u8>> = Vec::new();
+
+ // get the public key der
+ if let Some(der) = sign1
+ .unprotected
+ .rest
+ .iter()
+ .find_map(|x: &(Label, Value)| {
+ if x.0 == Label::Text("x5chain".to_string()) {
+ Some(x.1.clone())
+ } else {
+ None
+ }
+ })
+ {
+ match der {
+ Value::Array(cert_chain) => {
+ // handle array of certs
+ for c in cert_chain {
+ if let Value::Bytes(der_bytes) = c {
+ certs.push(der_bytes.clone());
+ }
+ }
+ Ok(certs)
+ }
+ Value::Bytes(ref der_bytes) => {
+ // handle single cert case
+ certs.push(der_bytes.clone());
+ Ok(certs)
+ }
+ _ => Err(Error::CoseX5ChainMissing),
+ }
+ } else {
+ Err(Error::CoseX5ChainMissing)
+ }
+}
+
+// Note: this function is only used to get the display string and not for cert validation.
+fn get_signing_time(
+ sign1: &coset::CoseSign1,
+ data: &[u8],
+ validation_log: &mut impl StatusTracker,
+) -> Option<chrono::DateTime<chrono::Utc>> {
+ // get timestamp info if available
+
+ if let Ok(tst_info) = get_timestamp_info(sign1, data) {
+ Some(gt_to_datetime(tst_info.gen_time))
+ } else if let Some(t) = &sign1
+ .unprotected
+ .rest
+ .iter()
+ .find_map(|x: &(Label, Value)| {
+ if x.0 == Label::Text("temp_signing_time".to_string()) {
+ Some(x.1.clone())
+ } else {
+ None
+ }
+ })
+ {
+ let time_cbor = serde_cbor::to_vec(t).ok()?;
+ let dt_string: String = serde_cbor::from_slice(&time_cbor).ok()?;
+ chrono::DateTime::<chrono::Utc>::from_str(&dt_string).ok()
+ } else {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "invalid timestamp message imprint",
+ "get_signing_time"
+ )
+ .error(Error::CoseTimeStampMismatch)
+ .validation_status(validation_status::TIMESTAMP_MISMATCH);
+ validation_log
+ .log(log_item, Some(Error::CoseTimeStampMismatch))
+ .ok()?;
+
+ None
+ }
+}
+
+// return appropriate TstInfo if available
+fn get_timestamp_info(sign1: &coset::CoseSign1, data: &[u8]) -> Result<TstInfo> {
+ // parse the temp timestamp
+ if let Some(t) = &sign1
+ .unprotected
+ .rest
+ .iter()
+ .find_map(|x: &(Label, Value)| {
+ if x.0 == Label::Text("sigTst".to_string()) {
+ Some(x.1.clone())
+ } else {
+ None
+ }
+ })
+ {
+ let alg = get_validator_str(sign1)?;
+ let time_cbor = serde_cbor::to_vec(t)?;
+ let tst_infos = crate::time_stamp::cose_sigtst_to_tstinfos(&time_cbor, data, &alg)?;
+
+ // there should only be one but consider handling more in the future since it is technically ok
+ if !tst_infos.is_empty() {
+ return Ok(tst_infos[0].clone());
+ }
+ }
+ Err(Error::NotFound)
+}
+
+fn extract_subject_from_cert(cert: &X509Certificate) -> Result<String> {
+ cert.subject()
+ .iter_organization()
+ .map(|attr| attr.as_str())
+ .last()
+ .ok_or(Error::CoseX5ChainMissing)?
+ .map(|attr| attr.to_string())
+ .map_err(|_e| Error::CoseX5ChainMissing)
+}
+
+/// Asynchronously validate a COSE_SIGN1 byte vector and verify against expected data
+/// cose_bytes - byte array containing the raw COSE_SIGN1 data
+/// data: data that was used to create the cose_bytes, these must match
+/// addition_data: additional optional data that may have been used during signing
+/// returns - Ok on success
+pub async fn verify_cose_async(
+ cose_bytes: Vec<u8>,
+ data: Vec<u8>,
+ additional_data: Vec<u8>,
+ signature_only: bool,
+ validation_log: &mut impl StatusTracker,
+) -> Result<ValidationInfo> {
+ let mut sign1 = get_cose_sign1(&cose_bytes, &data, validation_log)?;
+
+ let validator_str = match get_validator_str(&sign1) {
+ Ok(s) => s,
+ Err(_) => {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "unsupported or missing Cose algorithhm",
+ "verify_cose_async"
+ )
+ .error(Error::CoseSignatureAlgorithmNotSupported)
+ .validation_status(validation_status::ALGORITHM_UNSUPPORTED);
+ validation_log.log(log_item, Some(Error::CoseSignatureAlgorithmNotSupported))?;
+
+ // one of these must exist
+ return Err(Error::CoseSignatureAlgorithmNotSupported);
+ }
+ };
+
+ // build result structure
+ let mut result = ValidationInfo::default();
+
+ // get the public key der
+ let der_bytes = get_sign_cert(&sign1)?;
+
+ // verify cert matches requested algorithm
+ if !signature_only {
+ // verify certs
+ match get_timestamp_info(&sign1, &data) {
+ Ok(tst_info) => {
+ check_cert(&validator_str, &der_bytes, validation_log, Some(&tst_info))?
+ }
+ Err(e) => {
+ // log timestamp errors
+ match e {
+ Error::NotFound => {
+ check_cert(&validator_str, &der_bytes, validation_log, None)?
+ }
+ Error::CoseTimeStampMismatch => {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "timestamp message imprint did not match",
+ "verify_cose"
+ )
+ .error(Error::CoseTimeStampMismatch)
+ .validation_status(validation_status::TIMESTAMP_MISMATCH);
+ validation_log.log(log_item, Some(Error::CoseTimeStampMismatch))?;
+ }
+ Error::CoseTimeStampValidity => {
+ let log_item =
+ log_item!("Cose_Sign1", "timestamp outside of validity", "verify_cose")
+ .error(Error::CoseTimeStampValidity)
+ .validation_status(validation_status::TIMESTAMP_OUTSIDE_VALIDITY);
+ validation_log.log(log_item, Some(Error::CoseTimeStampValidity))?;
+ }
+ _ => {
+ let log_item =
+ log_item!("Cose_Sign1", "error parsing timestamp", "verify_cose")
+ .error(Error::CoseInvalidTimeStamp);
+ validation_log.log(log_item, Some(Error::CoseInvalidTimeStamp))?;
+
+ return Err(Error::CoseInvalidTimeStamp);
+ }
+ }
+ }
+ }
+ }
+
+ // Check the signature, which needs to have the same `additional_data` provided, by
+ // providing a closure that can do the verify operation.
+ sign1.payload = Some(data.clone()); // restore payload
+
+ let p_header = sign1.protected.clone();
+
+ let tbs = sig_structure_data(
+ coset::SignatureContext::CoseSign1,
+ p_header,
+ None,
+ &additional_data,
+ sign1.payload.as_ref().unwrap_or(&vec![]),
+ ); // get "to be signed" bytes
+
+ if let Ok(issuer) =
+ validate_with_cert_async(&validator_str, &sign1.signature, &tbs, &der_bytes).await
+ {
+ result.issuer_org = Some(issuer);
+ result.validated = true;
+ result.alg = validator_str.to_owned();
+
+ // parse the temp time for now util we have TA
+ result.date = get_signing_time(&sign1, &data, validation_log);
+ }
+
+ Ok(result)
+}
+
+pub fn get_signing_info(
+ cose_bytes: &[u8],
+ data: &[u8],
+ validation_log: &mut impl StatusTracker,
+) -> ValidationInfo {
+ let mut date = None;
+ let mut issuer_org = None;
+ let mut alg = "".to_string();
+
+ let _ = get_cose_sign1(cose_bytes, data, validation_log).and_then(|sign1| {
+ // get the public key der
+ let der_bytes = get_sign_cert(&sign1)?;
+
+ let _ = X509Certificate::from_der(&der_bytes).map(|(_rem, signcert)| {
+ date = get_signing_time(&sign1, data, validation_log);
+ issuer_org = extract_subject_from_cert(&signcert).ok();
+ if let Ok(a) = get_validator_str(&sign1) {
+ alg = a;
+ }
+
+ (_rem, signcert)
+ });
+
+ Ok(sign1)
+ });
+
+ ValidationInfo {
+ issuer_org,
+ date,
+ alg,
+ validated: false,
+ }
+}
+
+/// Validate a COSE_SIGN1 byte vector and verify against expected data
+/// cose_bytes - byte array containing the raw COSE_SIGN1 data
+/// data: data that was used to create the cose_bytes, these must match
+/// addition_data: additional optional data that may have been used during signing
+/// returns - Ok on success
+#[cfg(not(target_arch = "wasm32"))]
+pub fn verify_cose(
+ cose_bytes: &[u8],
+ data: &[u8],
+ additional_data: &[u8],
+ signature_only: bool,
+ validation_log: &mut impl StatusTracker,
+) -> Result<ValidationInfo> {
+ let sign1 = get_cose_sign1(cose_bytes, data, validation_log)?;
+
+ let validator_str = match get_validator_str(&sign1) {
+ Ok(s) => s,
+ Err(_) => {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "unsupported or missing Cose algorithhm",
+ "verify_cose"
+ )
+ .error(Error::CoseSignatureAlgorithmNotSupported)
+ .validation_status(validation_status::ALGORITHM_UNSUPPORTED);
+
+ validation_log.log(log_item, Some(Error::CoseSignatureAlgorithmNotSupported))?;
+
+ return Err(Error::CoseSignatureAlgorithmNotSupported);
+ }
+ };
+
+ let validator =
+ get_validator(&validator_str).ok_or(Error::CoseSignatureAlgorithmNotSupported)?;
+
+ // build result structure
+ let mut result = ValidationInfo::default();
+
+ // get the cert chain
+ let certs = get_sign_certs(&sign1)?;
+
+ // get the public key der
+ let der_bytes = &certs[0];
+
+ if !signature_only {
+ // verify certs
+ match get_timestamp_info(&sign1, data) {
+ Ok(tst_info) => check_cert(&validator_str, der_bytes, validation_log, Some(&tst_info))?,
+ Err(e) => {
+ // log timestamp errors
+ match e {
+ Error::NotFound => check_cert(&validator_str, der_bytes, validation_log, None)?,
+ Error::CoseTimeStampMismatch => {
+ let log_item = log_item!(
+ "Cose_Sign1",
+ "timestamp message imprint did not match",
+ "verify_cose"
+ )
+ .error(Error::CoseTimeStampMismatch)
+ .validation_status(validation_status::TIMESTAMP_MISMATCH);
+ validation_log.log(log_item, Some(Error::CoseTimeStampMismatch))?;
+ }
+ Error::CoseTimeStampValidity => {
+ let log_item =
+ log_item!("Cose_Sign1", "timestamp outside of validity", "verify_cose")
+ .error(Error::CoseTimeStampValidity)
+ .validation_status(validation_status::TIMESTAMP_OUTSIDE_VALIDITY);
+ validation_log.log(log_item, Some(Error::CoseTimeStampValidity))?;
+ }
+ _ => {
+ let log_item =
+ log_item!("Cose_Sign1", "error parsing timestamp", "verify_cose")
+ .error(Error::CoseInvalidTimeStamp);
+ validation_log.log(log_item, Some(Error::CoseInvalidTimeStamp))?;
+
+ return Err(Error::CoseInvalidTimeStamp);
+ }
+ }
+ }
+ }
+ }
+
+ // Check the signature, which needs to have the same `additional_data` provided, by
+ // providing a closure that can do the verify operation.
+ sign1.verify_signature(additional_data, |sig, verify_data| -> Result<()> {
+ if let Ok(issuer) = validate_with_cert(validator, sig, verify_data, der_bytes) {
+ result.issuer_org = Some(issuer);
+ result.validated = true;
+ result.alg = validator_str.to_string();
+
+ // parse the temp time for now util we have TA
+ result.date = get_signing_time(&sign1, data, validation_log);
+ }
+ // Note: not adding validation_log entry here since caller will supply claim specific info to log
+ Ok(())
+ })?;
+
+ Ok(result)
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn verify_cose(
+ _cose_bytes: &[u8],
+ _data: &[u8],
+ _additional_data: &[u8],
+ _signature_only: bool,
+ _validation_log: &mut impl StatusTracker,
+) -> Result<ValidationInfo> {
+ Err(Error::CoseVerifier)
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn validate_with_cert(
+ validator: Box<dyn CoseValidator>,
+ sig: &[u8],
+ data: &[u8],
+ der_bytes: &[u8],
+) -> Result<String> {
+ // get the cert in der format
+ let (_rem, signcert) =
+ X509Certificate::from_der(der_bytes).map_err(|_err| Error::CoseInvalidCert)?;
+ let pk = signcert.public_key();
+ let pk_der = pk.raw;
+
+ if validator.validate(sig, data, pk_der)? {
+ Ok(extract_subject_from_cert(&signcert)?)
+ } else {
+ Err(Error::CoseSignature)
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+async fn validate_with_cert_async(
+ validator_str: &str,
+ sig: &[u8],
+ data: &[u8],
+ der_bytes: &[u8],
+) -> Result<String> {
+ let (_rem, signcert) =
+ X509Certificate::from_der(der_bytes).map_err(|_err| Error::CoseMissingKey)?;
+ let pk = signcert.public_key();
+ let pk_der = pk.raw;
+
+ if validate_async(validator_str, sig, data, pk_der).await? {
+ Ok(extract_subject_from_cert(&signcert)?)
+ } else {
+ Err(Error::CoseSignature)
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+async fn validate_with_cert_async(
+ _validator_str: &str,
+ _sig: &[u8],
+ _data: &[u8],
+ _der_bytes: &[u8],
+) -> Result<String> {
+ Err(Error::CoseSignatureAlgorithmNotSupported)
+}
+#[allow(unused_imports)]
+#[cfg(feature = "file_io")]
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use sha2::digest::generic_array::sequence::Shorten;
+
+ use crate::status_tracker::DetailedStatusTracker;
+
+ use super::*;
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_expired_cert() {
+ let mut validation_log = DetailedStatusTracker::new();
+
+ let mut cert_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ cert_path.push("tests/fixtures/rsa-pss256_key-expired.pub");
+
+ let expired_cert = std::fs::read(&cert_path).unwrap();
+
+ if let Ok(signcert) = openssl::x509::X509::from_pem(&expired_cert) {
+ let der_bytes = signcert.to_der().unwrap();
+ assert!(check_cert("ps256", &der_bytes, &mut validation_log, None).is_err());
+
+ assert!(!validation_log.get_log().is_empty());
+
+ assert_eq!(
+ validation_log.get_log()[0].validation_status,
+ Some(validation_status::SIGNING_CREDENTIAL_EXPIRED.to_string())
+ );
+ }
+ }
+
+ #[test]
+ fn test_verify_cose_good() {
+ let validator = get_validator("ps256").unwrap();
+
+ let sig_bytes = include_bytes!("../tests/fixtures/sig.data");
+ let data_bytes = include_bytes!("../tests/fixtures/data.data");
+ let key_bytes = include_bytes!("../tests/fixtures/key.data");
+
+ assert!(validator
+ .validate(sig_bytes, data_bytes, key_bytes)
+ .unwrap());
+ }
+
+ #[test]
+ fn test_verify_ec_good() {
+ // EC signatures
+ let mut validator = get_validator("es384").unwrap();
+
+ let sig_es384_bytes = include_bytes!("../tests/fixtures/sig_es384.data");
+ let data_es384_bytes = include_bytes!("../tests/fixtures/data_es384.data");
+ let key_es384_bytes = include_bytes!("../tests/fixtures/key_es384.data");
+
+ assert!(validator
+ .validate(sig_es384_bytes, data_es384_bytes, key_es384_bytes)
+ .unwrap());
+
+ validator = get_validator("es512").unwrap();
+
+ let sig_es512_bytes = include_bytes!("../tests/fixtures/sig_es512.data");
+ let data_es512_bytes = include_bytes!("../tests/fixtures/data_es512.data");
+ let key_es512_bytes = include_bytes!("../tests/fixtures/key_es512.data");
+
+ assert!(validator
+ .validate(sig_es512_bytes, data_es512_bytes, key_es512_bytes)
+ .unwrap());
+ }
+
+ #[test]
+ fn test_verify_cose_bad() {
+ let validator = get_validator("ps256").unwrap();
+
+ let sig_bytes = include_bytes!("../tests/fixtures/sig.data");
+ let data_bytes = include_bytes!("../tests/fixtures/data.data");
+ let key_bytes = include_bytes!("../tests/fixtures/key.data");
+
+ let mut bad_bytes = data_bytes.to_vec();
+ bad_bytes[0] = b'c';
+ bad_bytes[1] = b'2';
+ bad_bytes[2] = b'p';
+ bad_bytes[3] = b'a';
+
+ assert!(!validator
+ .validate(sig_bytes, &bad_bytes, key_bytes)
+ .unwrap());
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_cert_algorithms() {
+ use tempfile::tempdir;
+
+ use crate::openssl::temp_signer;
+
+ let mut validation_log = DetailedStatusTracker::new();
+
+ let temp_dir = tempdir().unwrap();
+ let (_, cert_path) = temp_signer::get_ec_signer(&temp_dir.path(), "es256", None);
+ let es256_cert = std::fs::read(&cert_path).unwrap();
+
+ let (_, cert_path) = temp_signer::get_ec_signer(&temp_dir.path(), "es384", None);
+ let es384_cert = std::fs::read(&cert_path).unwrap();
+
+ let (_, cert_path) = temp_signer::get_ec_signer(&temp_dir.path(), "es512", None);
+ let es512_cert = std::fs::read(&cert_path).unwrap();
+
+ let (_, cert_path) = temp_signer::get_rsa_signer(&temp_dir.path(), "ps256", None);
+ let rsa_pss256_cert = std::fs::read(&cert_path).unwrap();
+
+ if let Ok(signcert) = openssl::x509::X509::from_pem(&es256_cert) {
+ let der_bytes = signcert.to_der().unwrap();
+ assert!(check_cert("es256", &der_bytes, &mut validation_log, None).is_ok());
+ }
+
+ if let Ok(signcert) = openssl::x509::X509::from_pem(&es384_cert) {
+ let der_bytes = signcert.to_der().unwrap();
+ assert!(check_cert("es384", &der_bytes, &mut validation_log, None).is_ok());
+ }
+
+ if let Ok(signcert) = openssl::x509::X509::from_pem(&es512_cert) {
+ let der_bytes = signcert.to_der().unwrap();
+ assert!(check_cert("es512", &der_bytes, &mut validation_log, None).is_ok());
+ }
+
+ if let Ok(signcert) = openssl::x509::X509::from_pem(&rsa_pss256_cert) {
+ let der_bytes = signcert.to_der().unwrap();
+ assert!(check_cert("ps256", &der_bytes, &mut validation_log, None).is_ok());
+ }
+ }
+}
diff --git a/sdk/src/embedded_xmp.rs b/sdk/src/embedded_xmp.rs
@@ -0,0 +1,41 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::path::Path;
+
+use xmp_toolkit::{OpenFileOptions, XmpFile, XmpFileError, XmpMeta};
+
+/// Add the URI for the active manifest to the XMP packet for a file.
+///
+/// This will replace any existing `dc:provenance` term
+/// in the file's metadata, or create a new one if necessary.
+///
+/// This does not check the claim at all; it is presumed
+/// that the string that is passed is a valid signed claim.
+pub(crate) fn add_manifest_uri_to_file<P: AsRef<Path>>(
+ path: P,
+ manifest_uri: &str,
+) -> Result<(), XmpFileError> {
+ XmpMeta::register_namespace("http://purl.org/dc/terms/", "dcterms");
+
+ let mut f = XmpFile::new();
+
+ f.open_file(path, OpenFileOptions::OPEN_FOR_UPDATE)?;
+
+ let mut m = f.xmp().unwrap_or_else(XmpMeta::new);
+ m.set_property("http://purl.org/dc/terms/", "provenance", manifest_uri);
+ f.put_xmp(&m);
+ f.close();
+
+ Ok(())
+}
diff --git a/sdk/src/error.rs b/sdk/src/error.rs
@@ -0,0 +1,244 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+// #![deny(missing_docs)] (we'll turn this on once fully documented)
+
+use thiserror::Error;
+
+/// `Error` enumerates errors returned by most C2PA toolkit operations.
+#[derive(Debug, Error)]
+pub enum Error {
+ // --- c2pa errors ---
+ /// Could not find a claim with this label.
+ #[error("claim missing: label = {label}")]
+ ClaimMissing { label: String },
+
+ /// An assertion could not be found at the expected URL.
+ #[error("assertion missing: url = {url}")]
+ AssertionMissing { url: String },
+
+ /// The attempt to serialize the assertion (typically to JSON or CBOR) failed.
+ #[error("unable to encode assertion data")]
+ AssertionEncoding,
+
+ #[error(transparent)]
+ AssertionDecoding(#[from] crate::assertion::AssertionDecodeError),
+
+ #[error("assertion could not be redacted")]
+ AssertionInvalidRedaction,
+
+ #[error("could not find the assertion to redact")]
+ AssertionRedactionNotFound,
+
+ #[error("bad parameter: {0}")]
+ BadParam(String),
+
+ /// The attempt to serialize the claim to CBOR failed.
+ #[error("claim could not be converted to CBOR")]
+ ClaimEncoding,
+
+ /// The attempt to deserialize the claim from CBOR failed.
+ #[error("claim could not be converted from CBOR")]
+ ClaimDecoding,
+
+ #[error("claim already signed, no further changes allowed")]
+ ClaimAlreadySigned,
+
+ #[error("missing signature box link")]
+ ClaimMissingSignatureBox,
+
+ #[error("identity required required with copyright assertion")]
+ ClaimMissingIdentity,
+
+ #[error("incompatible claim version")]
+ ClaimVersion,
+
+ #[error("invalid claim content")]
+ ClaimInvalidContent,
+
+ #[error("claim missing hard binding")]
+ ClaimMissingHardBinding,
+
+ #[error("claim contains self redactions")]
+ ClaimSelfRedact,
+
+ #[error("claim contains disallowed redactions")]
+ ClaimDisallowedRedaction,
+
+ #[error("update manifest is invalid")]
+ UpdateManifestInvalid,
+
+ /// The COSE Sign1 structure can not be parsed.
+ #[error("COSE Sign1 structure can not be parsed: {coset_error}")]
+ InvalidCoseSignature {
+ coset_error: coset::CoseError, // NOTE: We can not use #[transparent] here because
+ // coset::CoseError does not implement std::Error::error
+ // and can't because coset is nostd.
+ },
+
+ /// The COSE signature uses an algorithm that is not supported by this crate.
+ #[error("COSE signature algorithm is not supported")]
+ CoseSignatureAlgorithmNotSupported,
+
+ #[error("COSE could not find verification key")]
+ CoseMissingKey,
+
+ /// The COSE signature did not contain a signing certificate.
+ #[error("could not find signing certificate chain in COSE signature")]
+ CoseX5ChainMissing,
+
+ #[error("COSE error parsing certificate")]
+ CoseInvalidCert,
+
+ #[error("COSE signature invalid")]
+ CoseSignature,
+
+ #[error("COSE verifier failure")]
+ CoseVerifier,
+
+ #[error("COSE certificate has expired")]
+ CoseCertExpiration,
+
+ #[error("COSE certificate has been revoked")]
+ CoseCertRevoked,
+
+ /// Unable to parse the time stamp from this signature.
+ #[error("COSE time stamp could not be parsed")]
+ CoseInvalidTimeStamp,
+
+ #[error("COSE time stamp had expired cert")]
+ CoseTimeStampValidity,
+
+ /// The time stamp in the signature did not match the signed data.
+ #[error("COSE time stamp does not match data")]
+ CoseTimeStampMismatch,
+
+ /// Unable to generate a trusted time stamp.
+ #[error("could not generate a trusted time stamp")]
+ CoseTimeStampGeneration,
+
+ #[error("COSE TimeStamp Authority failure")]
+ CoseTimeStampAuthority,
+
+ #[error("COSE Signature too big for JUMBF box")]
+ CoseSigboxTooSmall,
+
+ #[error("WASM verifier error")]
+ WasmVerifier,
+
+ #[error("WASM crypto key error")]
+ WasmKey,
+
+ #[error("WASM not called from window or worker global scope")]
+ WasmInvalidContext,
+
+ #[error("WASM could not load crypto library")]
+ WasmNoCrypto,
+
+ /// Unable to generate valid JUMBF for a claim.
+ #[error("could not create valid JUMBF for claim")]
+ JumbfCreationError,
+
+ /// No JUMBF data found.
+ /// TODO before merging PR: Does this error case need to be part of the public API?
+ #[error("no JUMBF data found")]
+ JumbfNotFound,
+
+ #[error("required JUMBF box not found")]
+ JumbfBoxNotFound,
+
+ #[error("stopped because of logged error")]
+ LogStop,
+
+ #[error("not found")]
+ NotFound,
+
+ #[error("type is unsupported")]
+ UnsupportedType,
+
+ #[error("embedding error")]
+ EmbeddingError,
+
+ // Working claim errors
+ #[error("ingredient file not found")]
+ IngredientNotFound,
+
+ #[error("file not found: {0}")]
+ FileNotFound(String),
+
+ #[error("XMP read error")]
+ XmpReadError,
+
+ #[error("XMP write error")]
+ XmpWriteError,
+
+ #[error("C2PA provenance not found in XMP")]
+ ProvenanceMissing,
+
+ #[error("hash verification( {0} )")]
+ HashMismatch(String),
+
+ #[error("claim verification failure: {0}")]
+ ClaimVerification(String),
+
+ #[error("PDF read error")]
+ PdfReadError,
+
+ #[error(transparent)]
+ InvalidClaim(#[from] crate::store::InvalidClaimError),
+
+ #[error(transparent)]
+ JumbfParseError(#[from] crate::jumbf::boxes::JumbfParseError),
+
+ #[error("The Verifiable Content structure is not valid")]
+ VerifiableCredentialInvalid,
+
+ /// Could not parse ECDSA signature. (Only appears when using WASM web crypto.)
+ #[error("could not parse ECDSA signature")]
+ InvalidEcdsaSignature,
+
+ // --- third-party errors ---
+ #[error(transparent)]
+ IoError(#[from] std::io::Error),
+
+ #[error(transparent)]
+ JsonError(#[from] serde_json::Error),
+
+ #[error(transparent)]
+ ImageError(#[from] image::ImageError),
+
+ #[error(transparent)]
+ CborError(#[from] serde_cbor::Error),
+
+ #[error(transparent)]
+ #[cfg(feature = "file_io")]
+ OpenSslError(#[from] openssl::error::ErrorStack),
+
+ #[error(transparent)]
+ OtherError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
+
+ #[error("prerelease content detected")]
+ PrereleaseError,
+}
+
+/// A specialized `Result` type for C2PA toolkit operations.
+pub type Result<T> = std::result::Result<T, Error>;
+
+pub(crate) fn wrap_io_err(err: std::io::Error) -> Error {
+ Error::IoError(err)
+}
+
+#[cfg(feature = "file_io")]
+pub(crate) fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error {
+ Error::OpenSslError(err)
+}
diff --git a/sdk/src/hashed_uri.rs b/sdk/src/hashed_uri.rs
@@ -0,0 +1,62 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use serde::{Deserialize, Serialize};
+use std::fmt;
+
+/// Hashed Uri stucture as defined by C2PA spec
+/// It is annotated to produce the correctly tagged cbor serialization
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct HashedUri {
+ url: String, // URI stored as tagged cbor
+ #[serde(skip_serializing_if = "Option::is_none")]
+ alg: Option<String>,
+ #[serde(with = "serde_bytes")]
+ hash: Vec<u8>, // hash stored as cbor byte string
+}
+
+impl HashedUri {
+ pub fn new(url: String, alg: Option<String>, hash_bytes: &[u8]) -> Self {
+ Self {
+ url,
+ alg,
+ hash: hash_bytes.to_vec(),
+ }
+ }
+
+ pub fn url(&self) -> String {
+ self.url.clone()
+ }
+ pub fn is_relative_url(&self) -> bool {
+ crate::jumbf::labels::manifest_label_from_uri(&self.url).is_none()
+ }
+
+ pub fn alg(&self) -> Option<String> {
+ self.alg.clone()
+ }
+
+ pub fn hash(&self) -> Vec<u8> {
+ self.hash.clone()
+ }
+
+ #[cfg(feature = "file_io")]
+ pub(crate) fn update_hash(&mut self, hash: Vec<u8>) {
+ self.hash = hash;
+ }
+}
+
+impl fmt::Display for HashedUri {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "url: {}, alg: {:?}, hash", self.url, self.alg)
+ }
+}
diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs
@@ -0,0 +1,734 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ assertion::{get_thumbnail_image_type, Assertion, AssertionBase},
+ assertions::{self, labels, Metadata, Relationship, Thumbnail},
+ cbor_types::BytesT,
+ claim::Claim,
+ error::{Error, Result},
+ hashed_uri::HashedUri,
+ jumbf,
+ store::Store,
+ validation_status::{self, ValidationStatus},
+};
+use std::ops::Deref;
+
+#[cfg(feature = "file_io")]
+use crate::{error::wrap_io_err, validation_status::status_for_store, xmp_inmemory_utils::XmpInfo};
+use log::{debug, error};
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "file_io")]
+use std::path::Path;
+#[derive(Debug, Deserialize, Serialize)]
+/// An ingredient is any external asset that has been used in the creation of an image
+///
+/// This structure captures information about that asset so a user can
+pub struct Ingredient {
+ /// A human readable title, generally source filename
+ title: String,
+
+ /// The format of the source file as a mime type or extension
+ format: String,
+
+ /// Document ID from `xmpMM:DocumentID` in XMP metadata
+ #[serde(skip_serializing_if = "Option::is_none")]
+ document_id: Option<String>,
+
+ /// Instance ID from `xmpMM:InstanceID` in XMP metadata
+ instance_id: String,
+
+ /// URI from `dcterms:provenance` in XMP metadata
+ #[serde(skip_serializing_if = "Option::is_none")]
+ provenance: Option<String>,
+
+ /// A thumbnail image capturing the visual state at the time of import
+ /// A tuple of thumbnail mime format (i.e. image/jpg) and binary bits of the image
+ #[serde(skip_serializing)]
+ thumbnail: Option<(String, BytesT)>,
+
+ /// An optional hash of the asset to prevent duplicates
+ #[serde(skip_serializing_if = "Option::is_none")]
+ hash: Option<String>,
+
+ /// Set to True if this is a parent asset
+ #[serde(skip_serializing_if = "Option::is_none")]
+ is_parent: Option<bool>,
+
+ /// The active manifest label if one exists
+ /// If this ingredient has a ManifestStore, this will hold the label of the active Manifest
+ #[serde(skip_serializing_if = "Option::is_none")]
+ active_manifest: Option<String>,
+
+ /// Validation results
+ #[serde(skip_serializing_if = "Option::is_none")]
+ validation_status: Option<Vec<ValidationStatus>>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ /// any additional Metadata as defined in the C2PA spec
+ metadata: Option<Metadata>,
+
+ /// A ManifestStore from the source asset extracted as a binary c2pa blob
+ #[serde(skip_serializing)]
+ manifest_data: Option<Vec<u8>>,
+}
+
+impl Ingredient {
+ pub fn new(title: &str, format: &str, instance_id: &str) -> Self {
+ Self {
+ title: title.to_owned(),
+ format: format.to_owned(),
+ document_id: None,
+ instance_id: instance_id.to_owned(),
+ provenance: None,
+ thumbnail: None,
+ hash: None,
+ is_parent: None,
+ active_manifest: None,
+ validation_status: None,
+ metadata: None,
+ manifest_data: None,
+ }
+ }
+
+ /// Returns a user displayable title for this ingredient
+ pub fn title(&self) -> &str {
+ self.title.as_str()
+ }
+
+ /// Returns a mime content_type for this asset associated with this ingredient
+ pub fn format(&self) -> &str {
+ self.format.as_str()
+ }
+
+ /// Returns a document identifier if one exists
+ pub fn document_id(&self) -> Option<&str> {
+ self.document_id.as_deref()
+ }
+
+ /// Returns the instance identifier
+ pub fn instance_id(&self) -> &str {
+ self.instance_id.as_str()
+ }
+
+ /// Returns the provenance uri if available
+ pub fn provenance(&self) -> Option<&str> {
+ self.provenance.as_deref()
+ }
+
+ /// Returns a tuple with thumbnail format and image bytes or None
+ pub fn thumbnail(&self) -> Option<(&str, &[u8])> {
+ self.thumbnail
+ .as_ref()
+ .map(|(format, image)| (format.as_str(), image.deref()))
+ }
+
+ /// Returns an optional Blake3 hash made from the bits of the original image
+ pub fn hash(&self) -> Option<&[u8]> {
+ self.manifest_data.as_deref()
+ }
+
+ /// Returns true if this is labeled as the parent ingredient
+ pub fn is_parent(&self) -> bool {
+ self.is_parent.unwrap_or(false)
+ }
+
+ /// Returns an optional label for the active manifest in this ingredient
+ /// If None, the ingredient has no Manifests
+ pub fn active_manifest(&self) -> Option<&str> {
+ self.active_manifest.as_deref()
+ }
+
+ /// Returns a reference the [ValidationStatus] Vec or None
+ pub fn validation_status(&self) -> Option<&[ValidationStatus]> {
+ self.validation_status.as_deref()
+ }
+
+ /// Returns an optional reference to [Metadata]
+ /// todo: figure out how to not clone this
+ pub fn metadata(&self) -> Option<Metadata> {
+ self.metadata.clone()
+ }
+
+ /// Returns an optional reference to c2pa manifest data
+ /// This is the binary form of a manifest store in .c2pa format
+ pub fn manifest_data(&self) -> Option<&[u8]> {
+ self.manifest_data.as_deref()
+ }
+
+ pub fn set_title(&mut self, title: String) -> &mut Self {
+ self.title = title;
+ self
+ }
+
+ /// Sets an optional document_id -- usually from XMP DocumentId.
+ pub fn set_document_id(&mut self, document_id: String) -> &mut Self {
+ self.document_id = Some(document_id);
+ self
+ }
+
+ /// Use Manifest.set_parent() for this
+ pub(crate) fn set_parent_state(&mut self, is_parent: bool) -> &mut Self {
+ self.is_parent = if is_parent { Some(true) } else { None };
+ self
+ }
+
+ /// set the thumbnail image
+ pub fn set_thumbnail(&mut self, format: String, thumbnail: Vec<u8>) -> &mut Self {
+ self.thumbnail = Some((format, BytesT(thumbnail)));
+ self
+ }
+
+ // Add any desired metadata to this ingredient
+ pub fn set_metadata(&mut self, metadata: Metadata) -> &mut Self {
+ self.metadata = Some(metadata);
+ self
+ }
+
+ // Gathers filename, extension and format from a file path
+ #[cfg(feature = "file_io")]
+ fn get_path_info(path: &std::path::Path) -> (String, String, String) {
+ let title = path
+ .file_name()
+ .map(|name| name.to_string_lossy().into_owned())
+ .unwrap_or_else(|| "".into());
+
+ let extension = path
+ .extension()
+ .map(|e| e.to_string_lossy().into_owned())
+ .unwrap_or_else(|| "".into())
+ .to_lowercase();
+
+ let format = match extension.as_ref() {
+ "jpg" | "jpeg" => "image/jpeg",
+ "png" => "image/png",
+ "gif" => "image/gif",
+ "psd" => "image/vnd.adobe.photoshop",
+ "tiff" => "image/tiff",
+ "svg" => "image/svg+xml",
+ "ico" => "image/vnd.microsoft.icon",
+ "bmp" => "image/bmp",
+ "webp" => "image/webp",
+ _ => "application/octet-stream",
+ }
+ .to_owned();
+ (title, extension, format)
+ }
+
+ /// Gets the basic info from a file path, including xmp info from the file if available
+ /// This is used for making asset ingredients that should not load ManifestStores
+ #[cfg(feature = "file_io")]
+
+ pub fn from_file_info<P: AsRef<Path>>(path: P) -> Self {
+ fn make_id(id_type: &str) -> String {
+ use uuid::Uuid;
+ let uuid = Uuid::new_v4();
+ //warn!("Generating fake id {}", uuid);
+ format!("xmp:{}id:{}", id_type, uuid)
+ }
+
+ // get required information from the file path
+ let (title, _, format) = Self::get_path_info(path.as_ref());
+
+ // if we can open the file try tto get xmp info
+ let xmp_info = match std::fs::File::open(path).map_err(wrap_io_err) {
+ Ok(mut file) => XmpInfo::from_source(&mut file, &format),
+ Err(_) => XmpInfo::default(),
+ };
+
+ // instance id is required so generate one if we don't have one
+ let instance_id = xmp_info.instance_id.unwrap_or_else(|| make_id("i"));
+
+ let mut ingredient = Self::new(&title, &format, &instance_id);
+ ingredient.document_id = xmp_info.document_id; // use document id if one exists
+ ingredient.provenance = xmp_info.provenance;
+
+ ingredient
+ }
+
+ #[cfg(feature = "file_io")]
+ /// Creates an Ingredient from a file path
+ pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
+ let options = IngredientOptions::default();
+ Self::from_file_with_options(path.as_ref(), &options)
+ }
+
+ fn thumbnail_from_assertion(assertion: &Assertion) -> (String, Vec<u8>) {
+ (
+ format!(
+ "image/{}",
+ get_thumbnail_image_type(&assertion.label_root())
+ ),
+ assertion.data().to_vec(),
+ )
+ }
+
+ /// Creates an Ingredient from a file path and options
+ // TODO: Review possible error conditions. InvalidJumbfError no longer exists.
+ #[cfg(feature = "file_io")]
+ pub fn from_file_with_options<P: AsRef<Path>>(
+ path: P,
+ options: &IngredientOptions,
+ ) -> Result<Self> {
+ Self::from_file_impl(path.as_ref(), options)
+ }
+
+ // internal implementation to avoid code bloat
+ #[cfg(feature = "file_io")]
+ fn from_file_impl(path: &Path, options: &IngredientOptions) -> Result<Self> {
+ // these are declared inside this function in order to isolate them for wasm builds
+ use crate::jumbf_io;
+ use crate::status_tracker::{DetailedStatusTracker, StatusTracker};
+
+ #[cfg(feature = "diagnostics")]
+ let _t = crate::utils::time_it::TimeIt::new("Ingredient:from_file_with_options");
+
+ // from the source file we need to get the XMP, JUMBF and generate a thumbnail
+ debug!("ingredient {:?}", path);
+
+ // get required information from the file path
+ let mut ingredient = Self::from_file_info(path);
+
+ if !path.exists() {
+ return Err(Error::FileNotFound(ingredient.title));
+ }
+
+ // if options includes a title, use it
+ if let Some(opt_title) = options.title {
+ ingredient.title = opt_title.to_string();
+ }
+ // read the file into a buffer for processing
+ let buf = std::fs::read(path).map_err(wrap_io_err)?;
+
+ // generate a hash so we know if the file has changed
+ // todo:: make hash algorithm an option fn taking stream
+ ingredient.hash = options
+ .make_hash
+ .then(|| blake3::hash(&buf).to_hex().as_str().to_owned());
+
+ let mut report = DetailedStatusTracker::new();
+
+ // generate a store from the buffer and then validate from the asset path
+ // load and verify store in single call - no need to call low level jumbf_io functions
+ match Store::load_from_memory(&ingredient.format, &buf, true, &mut report) {
+ Ok(store) => {
+ // generate ValidationStatus from ValidationItems filtering for only errors
+ let statuses = status_for_store(&store, &mut report);
+
+ if let Some(claim) = store.provenance_claim() {
+ // if the parent claim is valid and has a thumbnail, use it
+ if statuses.is_empty() {
+ //todo: need a better test here
+ if let Some(claim_assertion) =
+ claim.get_claim_assertion(labels::JPEG_CLAIM_THUMBNAIL, 0)
+ {
+ let (format, image) =
+ Self::thumbnail_from_assertion(claim_assertion.assertion());
+ ingredient.set_thumbnail(format, image);
+ }
+ }
+ ingredient.active_manifest = Some(claim.label().to_string());
+ }
+ ingredient.manifest_data =
+ jumbf_io::load_jumbf_from_memory(&ingredient.format, &buf).ok();
+ ingredient.validation_status = if statuses.is_empty() {
+ None
+ } else {
+ Some(statuses)
+ };
+ }
+ Err(Error::JumbfNotFound)
+ | Err(Error::ProvenanceMissing)
+ | Err(Error::UnsupportedType) => {} // no claims but valid file
+ Err(Error::BadParam(desc)) if desc == *"unrecognized file type" => {}
+ Err(e) => {
+ // we can ignore the error here because it should have a log entry corresponding to it
+ debug!("ingredient {:?}", e);
+ // convert any other error to a validation status
+ let statuses: Vec<ValidationStatus> = report
+ .get_log()
+ .iter()
+ .filter_map(ValidationStatus::from_validation_item)
+ .filter(|s| !validation_status::is_success(s.code()))
+ .collect();
+ ingredient.validation_status = if statuses.is_empty() {
+ None
+ } else {
+ Some(statuses)
+ };
+ }
+ }
+
+ // create a thumbnail if we don't already have a claim with a thumb we can use
+ if ingredient.thumbnail.is_none() {
+ use crate::utils::thumbnail::make_thumbnail;
+ if let Ok((format, image)) = make_thumbnail(path) {
+ ingredient.set_thumbnail(format, image);
+ }
+ }
+
+ Ok(ingredient)
+ }
+
+ /// Creates an Ingredient from a store and a uri to an ingredient assertion
+ pub fn from_ingredient_uri(store: &Store, ingredient_uri: &str) -> Result<Self> {
+ let assertion =
+ store
+ .get_assertion_from_uri(ingredient_uri)
+ .ok_or(Error::AssertionMissing {
+ url: ingredient_uri.to_owned(),
+ })?;
+ let ingredient_assertion = assertions::Ingredient::from_assertion(assertion)?;
+
+ let mut validation_status = match ingredient_assertion.validation_status.as_ref() {
+ Some(status) => status.clone(),
+ None => Vec::new(),
+ };
+
+ let is_parent = match ingredient_assertion.relationship {
+ Relationship::ParentOf => Some(true),
+ Relationship::ComponentOf => None,
+ };
+
+ let active_manifest = ingredient_assertion
+ .c2pa_manifest
+ .and_then(|hash_url| jumbf::labels::manifest_label_from_uri(&hash_url.url()));
+
+ let thumbnail = ingredient_assertion.thumbnail.and_then(|hashed_uri| {
+ // if we have a relative thumbnail pass in URI and Claim to search
+ match store.get_assertion_from_uri_and_claim(&hashed_uri.url(), ingredient_uri) {
+ Some(assertion) => Some(Self::thumbnail_from_assertion(assertion)),
+ None => {
+ error!("failed to get {} from {}", hashed_uri.url(), ingredient_uri);
+ validation_status.push(
+ ValidationStatus::new(validation_status::ASSERTION_MISSING.to_string())
+ .set_url(hashed_uri.url()),
+ );
+ None
+ }
+ }
+ });
+
+ debug!(
+ "Adding Ingredient {} {:?}",
+ ingredient_assertion.title, &active_manifest
+ );
+
+ // todo: find a better way to do this if we keep this code
+ let mut ingredient = Ingredient::new(
+ &ingredient_assertion.title,
+ &ingredient_assertion.format,
+ &ingredient_assertion.instance_id,
+ );
+ ingredient.document_id = ingredient_assertion.document_id;
+ if let Some((format, image)) = thumbnail {
+ ingredient.set_thumbnail(format, image);
+ }
+
+ ingredient.is_parent = is_parent;
+ ingredient.active_manifest = active_manifest;
+ ingredient.validation_status = ingredient_assertion.validation_status;
+ ingredient.metadata = ingredient_assertion.metadata;
+ Ok(ingredient)
+ }
+
+ pub fn add_to_claim(
+ &self,
+ claim: &mut Claim,
+ redactions: Option<Vec<String>>,
+ ) -> Result<HashedUri> {
+ let mut thumbnail = None;
+
+ // add the ingredient manifest_data to the claim
+ // this is how any existing claims are added to the new store
+ let c2pa_manifest = match self.manifest_data() {
+ Some(buffer) => {
+ let manifest_label = self
+ .active_manifest
+ .clone()
+ .ok_or(Error::IngredientNotFound)?;
+
+ //if this is the parent ingredient then apply any redactions, converting from labels to uris
+ let redactions = match self.is_parent() {
+ true => redactions.as_ref().map(|redactions| {
+ redactions
+ .iter()
+ .map(|r| jumbf::labels::to_assertion_uri(&manifest_label, r))
+ .collect()
+ }),
+ false => None,
+ };
+
+ // have Store check and load ingredients and add them to a claim
+ Store::load_ingredient_to_claim(claim, &manifest_label, buffer, redactions)?;
+
+ // get the ingredient map loaded in previous
+ match claim.claim_ingredient(&manifest_label) {
+ Some(ingredient_claims) => {
+ // get the ingredient active claim from the ingredients claim map
+ if let Some(ingredient_active_claim) = ingredient_claims
+ .iter()
+ .find(|c| c.label() == manifest_label)
+ {
+ let hash = ingredient_active_claim.hash();
+ let uri = jumbf::labels::to_manifest_uri(&manifest_label);
+
+ // if there are validations and they have all passed, then use the parent claim thumbnail if available
+ if let Some(validation_status) = self.validation_status.as_ref() {
+ if validation_status.iter().all(|r| r.passed()) {
+ thumbnail = ingredient_active_claim
+ .assertions()
+ .iter()
+ .find(|hashed_uri| {
+ hashed_uri.url().contains(labels::CLAIM_THUMBNAIL)
+ })
+ .map(|t| {
+ // convert ingredient uris to absolute when adding them
+ // since this uri references a different manifest
+ let assertion_label =
+ jumbf::labels::assertion_label_from_uri(&t.url())
+ .unwrap_or_default();
+ let url = jumbf::labels::to_assertion_uri(
+ &manifest_label,
+ &assertion_label,
+ );
+ HashedUri::new(url, t.alg(), &t.hash())
+ });
+ }
+ }
+ // generate c2pa_manifest hashed_uri
+ Some(crate::hashed_uri::HashedUri::new(
+ uri,
+ Some(ingredient_active_claim.alg().to_owned()),
+ hash.as_ref(),
+ ))
+ } else {
+ None
+ }
+ }
+ None => None,
+ }
+ }
+ None => None,
+ };
+
+ let relationship = if self.is_parent() {
+ Relationship::ParentOf
+ } else {
+ Relationship::ComponentOf
+ };
+
+ // add ingredient thumbnail assertion if one is given and we don't already have one from the parent claim
+ if thumbnail.is_none() {
+ if let Some((format, image)) = &self.thumbnail() {
+ let hash_url = claim.add_assertion(&Thumbnail::new(
+ &labels::add_thumbnail_format(labels::INGREDIENT_THUMBNAIL, format),
+ image.to_vec(),
+ ))?;
+
+ thumbnail = Some(hash_url);
+ }
+ }
+
+ let mut ingredient_assertion = assertions::Ingredient::new(
+ &self.title,
+ &self.format,
+ &self.instance_id,
+ self.document_id.as_deref(),
+ );
+
+ ingredient_assertion.c2pa_manifest = c2pa_manifest;
+ ingredient_assertion.relationship = relationship;
+ ingredient_assertion.thumbnail = thumbnail;
+ ingredient_assertion.metadata = self.metadata.clone();
+ ingredient_assertion.validation_status = self.validation_status.clone();
+ claim.add_assertion(&ingredient_assertion)
+ }
+
+ pub fn stats(&self) -> usize {
+ let thumb_size = self.thumbnail().map_or(0, |(_, image)| image.len());
+ let manifest_data_size = self.manifest_data().map_or(0, |v| v.len());
+
+ println!(
+ " {} instance_id: {}, thumb size: {}, manifest_data size: {}",
+ self.title, self.instance_id, thumb_size, manifest_data_size,
+ );
+ self.title.len() + self.instance_id.len() + thumb_size + manifest_data_size
+ }
+}
+
+impl std::fmt::Display for Ingredient {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let report = serde_json::to_string_pretty(self).unwrap_or_default();
+ f.write_str(&report)
+ }
+}
+
+#[derive(Default)]
+/// This defines optional actions when creating ingredients from files
+pub struct IngredientOptions {
+ /// This allows setting the title for the ingredient (the default is usually the file name)
+ pub title: Option<&'static str>,
+ /// If true, then generate a Blake3 hash over the source asset and store it here
+ /// This can be used to test for duplicate ingredients or if a source file has changed
+ pub make_hash: bool,
+}
+
+#[cfg(test)]
+#[cfg(feature = "file_io")]
+mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use crate::utils::test::fixture_path;
+
+ //use serde_cbor::{ser::IoWrite, Serializer};
+
+ const MANIFEST_JPEG: &str = "C.jpg";
+ const BAD_SIGNATURE_JPEG: &str = "CAICAI_BAD_SIG.jpg";
+ const BAD_JUMBF_JPEG: &str = "bigjumbf.jpg";
+ const PRERELEASE_JPEG: &str = "prerelease.jpg";
+
+ #[test]
+ fn test_psd() {
+ // std::env::set_var("RUST_LOG", "debug");
+ // env_logger::init();
+ let ap = fixture_path("Purple Square.psd");
+ let ingredient = Ingredient::from_file(&ap).expect("from_file");
+ ingredient.stats();
+
+ println!("ingredient = {}", ingredient);
+ assert_eq!(&ingredient.title, "Purple Square.psd");
+ assert_eq!(&ingredient.format, "image/vnd.adobe.photoshop");
+ assert!(ingredient.thumbnail.is_none());
+ assert!(ingredient.manifest_data.is_none());
+ }
+
+ #[test]
+ fn test_jpg() {
+ let ap = fixture_path(MANIFEST_JPEG);
+ let ingredient = Ingredient::from_file(&ap).expect("from_file");
+ ingredient.stats();
+
+ println!("ingredient = {}", ingredient);
+ assert_eq!(&ingredient.title, MANIFEST_JPEG);
+ assert_eq!(&ingredient.format, "image/jpeg");
+ assert!(ingredient.thumbnail.is_some());
+ assert!(ingredient.provenance.is_some());
+ assert!(ingredient.manifest_data.is_some());
+ assert!(ingredient.metadata.is_none());
+ }
+
+ #[test]
+ fn test_jpg_options() {
+ let options = IngredientOptions {
+ make_hash: true,
+ title: Some("MyTitle"),
+ };
+
+ let ap = fixture_path(MANIFEST_JPEG);
+ let ingredient = Ingredient::from_file_with_options(&ap, &options).expect("from_file");
+ ingredient.stats();
+
+ println!("ingredient = {}", ingredient);
+ assert_eq!(&ingredient.title, "MyTitle");
+ assert_eq!(&ingredient.format, "image/jpeg");
+ assert!(ingredient.hash.is_some());
+ assert!(ingredient.thumbnail.is_some());
+ assert!(ingredient.provenance.is_some());
+ assert!(ingredient.manifest_data.is_some());
+ assert!(ingredient.metadata.is_none());
+ }
+
+ #[test]
+ fn test_png_no_claim() {
+ let ap = fixture_path("libpng-test.png");
+ let ingredient = Ingredient::from_file(&ap).expect("from_file");
+ ingredient.stats();
+
+ println!("ingredient = {}", ingredient);
+ assert_eq!(ingredient.title(), "libpng-test.png");
+ assert!(ingredient.thumbnail().is_some());
+ assert_eq!(ingredient.thumbnail().unwrap().0, "image/png");
+ assert!(ingredient.manifest_data.is_none());
+ }
+
+ #[test]
+ fn test_jpg_bad_signature() {
+ let ap = fixture_path(BAD_SIGNATURE_JPEG);
+ let ingredient = Ingredient::from_file(&ap).expect("from_file");
+ ingredient.stats();
+
+ println!("ingredient = {}", ingredient);
+ assert_eq!(&ingredient.title, BAD_SIGNATURE_JPEG);
+ assert_eq!(&ingredient.format, "image/jpeg");
+ assert!(ingredient.thumbnail.is_some());
+ assert!(ingredient.provenance.is_some());
+ assert!(ingredient.manifest_data.is_some());
+ assert!(ingredient.validation_status.is_some());
+ assert!(ingredient
+ .validation_status
+ .unwrap()
+ .iter()
+ .any(|s| s.code() == validation_status::CLAIM_SIGNATURE_MISMATCH));
+ }
+
+ #[test]
+ fn test_jpg_prerelease() {
+ let ap = fixture_path(PRERELEASE_JPEG);
+ let ingredient = Ingredient::from_file(&ap).expect("from_file");
+ ingredient.stats();
+
+ println!("ingredient = {}", ingredient);
+ assert_eq!(&ingredient.title, PRERELEASE_JPEG);
+ assert_eq!(&ingredient.format, "image/jpeg");
+ assert!(ingredient.thumbnail.is_some());
+ assert!(ingredient.provenance.is_some());
+ assert!(ingredient.manifest_data.is_none());
+ assert!(ingredient.validation_status.is_some());
+ assert_eq!(
+ ingredient.validation_status.unwrap()[0].code(),
+ validation_status::STATUS_PRERELEASE
+ );
+ }
+
+ #[test]
+ fn test_jpg_bad_jumbf() {
+ let ap = fixture_path(BAD_JUMBF_JPEG);
+ let ingredient = Ingredient::from_file(&ap).expect("from_file");
+ ingredient.stats();
+
+ println!("ingredient = {}", ingredient);
+ assert_eq!(&ingredient.title, BAD_JUMBF_JPEG);
+ assert_eq!(&ingredient.format, "image/jpeg");
+ assert!(ingredient.thumbnail.is_some());
+ assert!(ingredient.provenance.is_some());
+ assert!(ingredient.manifest_data.is_none());
+ assert!(ingredient.validation_status.is_some());
+ assert_eq!(
+ ingredient.validation_status.unwrap()[0].code(),
+ validation_status::STATUS_PRERELEASE
+ );
+ }
+
+ #[test]
+ fn test_jpg_nested() {
+ let ap = fixture_path("CIE-sig-CA.jpg");
+ let ingredient = Ingredient::from_file(&ap).expect("new_from_file");
+ println!("ingredient = {}", ingredient);
+ assert_eq!(ingredient.validation_status, None);
+ }
+}
diff --git a/sdk/src/jumbf/boxes.rs b/sdk/src/jumbf/boxes.rs
@@ -0,0 +1,2779 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! This is a library for generating ISO BMFF/JUMBF boxes
+//!
+//! It is based on the work of Takeru Ohta <phjgt308@gmail.com>
+//! and [mse_fmp4](https://github.com/sile/mse_fmp4) and enhanced
+//! by Leonard Rosenthol <lrosenth@adobe.com>
+//
+//! # References
+//!
+//! - [ISO BMFF Byte Stream Format](https://w3c.github.io/media-source/isobmff-byte-stream-format.html)
+//! - [JPEG universal metadata box format](https://www.iso.org/standard/73604.html)
+
+use std::{
+ any::Any,
+ convert::TryInto,
+ ffi::CString,
+ fmt,
+ io::{Read, Result as IoResult, Seek, SeekFrom, Write},
+};
+
+use hex::FromHex;
+use log::debug;
+use thiserror::Error;
+
+use crate::jumbf::{boxio, labels};
+
+/// `JumbfParseError` enumerates errors detected while parsing JUMBF data structures.
+#[derive(Debug, Error)]
+pub enum JumbfParseError {
+ // TODO before merging PR: Add doc comments for these.
+ // Is there more to say than the description string?
+ #[error("unexpected end of file")]
+ UnexpectedEof,
+
+ #[error("invalid box start")]
+ InvalidBoxStart,
+
+ #[error("invalid box header")]
+ InvalidBoxHeader,
+
+ #[error("invalid box range")]
+ InvalidBoxRange,
+
+ #[error("invalid JUMBF header")]
+ InvalidJumbfHeader,
+
+ #[error("invalid JUMB box")]
+ InvalidJumbBox,
+
+ #[error("invalid UUID label")]
+ InvalidUuidValue,
+
+ #[error("invalid JSON box")]
+ InvalidJsonBox,
+
+ #[error("invalid CBOR box")]
+ InvalidCborBox,
+
+ #[error("invalid JP2C box")]
+ InvalidJp2cBox,
+
+ #[error("invalid UUID box")]
+ InvalidUuidBox,
+
+ #[error("invalid embedded file box")]
+ InvalidEmbeddedFileBox,
+
+ #[error("invalid box of unknown type")]
+ InvalidUnknownBox,
+
+ #[error("expected JUMD")]
+ ExpectedJumdError,
+
+ #[error(transparent)]
+ IoError(#[from] std::io::Error),
+
+ #[error("assertion salt must be 16 bytes or greater")]
+ InvalidSalt,
+
+ #[error("invalid JUMD box")]
+ InvalidDescriptionBox,
+}
+
+/// A specialized `JumbfParseResult` type for JUMBF parsing operations.
+pub type JumbfParseResult<T> = std::result::Result<T, JumbfParseError>;
+
+//-----------------
+// ANCHOR ISO BMFF
+//-----------------
+macro_rules! write_u8 {
+ ($w:expr, $n:expr) => {{
+ use byteorder::WriteBytesExt;
+ $w.write_u8($n)?
+ }};
+}
+// macro_rules! write_u16 {
+// ($w:expr, $n:expr) => {{
+// use byteorder::{BigEndian, WriteBytesExt};
+// $w.write_u16::<BigEndian>($n)?;
+// }};
+// }
+// macro_rules! write_i16 {
+// ($w:expr, $n:expr) => {{
+// use byteorder::{BigEndian, WriteBytesExt};
+// $w.write_i16::<BigEndian>($n)?;
+// }};
+// }
+// macro_rules! write_u24 {
+// ($w:expr, $n:expr) => {{
+// use byteorder::{BigEndian, WriteBytesExt};
+// $w.write_uint::<BigEndian>($n as u64, 3)?;
+// }};
+// }
+macro_rules! write_u32 {
+ ($w:expr, $n:expr) => {{
+ use byteorder::{BigEndian, WriteBytesExt};
+ $w.write_u32::<BigEndian>($n)?;
+ }};
+}
+// macro_rules! write_i32 {
+// ($w:expr, $n:expr) => {{
+// use byteorder::{BigEndian, WriteBytesExt};
+// $w.write_i32::<BigEndian>($n)?;
+// }};
+// }
+// macro_rules! write_u64 {
+// ($w:expr, $n:expr) => {{
+// use byteorder::{BigEndian, WriteBytesExt};
+// $w.write_u64::<BigEndian>($n)?;
+// }};
+// }
+macro_rules! write_all {
+ ($w:expr, $n:expr) => {
+ $w.write_all($n)?;
+ };
+}
+// macro_rules! write_zeroes {
+// ($w:expr, $n:expr) => {
+// $w.write_all(&[0; $n][..])?;
+// };
+// }
+// macro_rules! write_box {
+// ($w:expr, $b:expr) => {
+// $b.write_box(&mut $w)?;
+// };
+// }
+// macro_rules! write_boxes {
+// ($w:expr, $bs:expr) => {
+// for b in $bs {
+// b.write_box(&mut $w)?;
+// }
+// };
+// }
+macro_rules! box_size {
+ ($b:expr) => {
+ $b.box_size()?
+ };
+}
+// macro_rules! optional_box_size {
+// ($b:expr) => {
+// if let Some(ref b) = $b.as_ref() {
+// b.box_size()?
+// } else {
+// 0
+// }
+// };
+// }
+macro_rules! boxes_size {
+ ($b:expr) => {{
+ let mut size = 0;
+ for b in $b.iter() {
+ size += box_size!(b);
+ }
+ size
+ }};
+}
+
+/// ISO BMFF box.
+pub trait BMFFBox: Any {
+ // "Any is the closest thing to reflection there is in Rust"
+ /// Box type code.
+ fn box_type(&self) -> &'static [u8; 4];
+
+ /// Box UUID (used by JUMBF)
+ fn box_uuid(&self) -> &'static str;
+
+ /// Box size.
+ fn box_size(&self) -> IoResult<u32> {
+ // if it a real box...
+ let mut size = if self.box_type() != b" " { 8 } else { 0 };
+ size += self.box_payload_size()?;
+
+ Ok(size as u32)
+ }
+
+ /// Payload size of the box.
+ fn box_payload_size(&self) -> IoResult<u32>;
+
+ /// Writes the box to the given writer.
+ fn write_box(&self, writer: &mut dyn Write) -> IoResult<()> {
+ if self.box_type() != b" " {
+ // it's a real box...
+ write_u32!(writer, self.box_size()?);
+ write_all!(writer, self.box_type());
+ }
+
+ self.write_box_payload(writer)?;
+ Ok(())
+ }
+
+ /// Writes the payload of the box to the given writer.
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()>;
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any;
+}
+
+impl fmt::Debug for dyn BMFFBox {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("BMFFBox")
+ .field("type", self.box_type())
+ .field("size", &self.box_size())
+ .finish()
+ }
+}
+
+//---------------
+// SECTION JUMBF
+//---------------
+pub const JUMB_FOURCC: &str = "6A756D62";
+pub const JUMD_FOURCC: &str = "6A756D64";
+
+// ANCHOR JUMBF superbox
+/// JUMBF superbox (ISO 19566-5:2019, Annex A)
+#[derive(Debug)]
+pub struct JUMBFSuperBox {
+ desc_box: JUMBFDescriptionBox,
+ data_boxes: Vec<Box<dyn BMFFBox>>,
+}
+
+impl JUMBFSuperBox {
+ pub fn new(box_label: &str, a_type: Option<&str>) -> Self {
+ JUMBFSuperBox {
+ desc_box: JUMBFDescriptionBox::new(box_label, a_type),
+ data_boxes: vec![],
+ }
+ }
+
+ pub fn from(a_box: JUMBFDescriptionBox) -> Self {
+ JUMBFSuperBox {
+ desc_box: a_box,
+ data_boxes: vec![],
+ }
+ }
+
+ // add a data box *WITHOUT* taking ownership of the box
+ pub fn add_data_box(&mut self, b: Box<dyn BMFFBox>) {
+ self.data_boxes.push(b)
+ }
+
+ // getters
+ pub fn desc_box(&self) -> &JUMBFDescriptionBox {
+ &self.desc_box
+ }
+
+ pub fn data_box_count(&self) -> usize {
+ self.data_boxes.len()
+ }
+
+ pub fn data_box(&self, index: usize) -> &dyn BMFFBox {
+ self.data_boxes[index].as_ref()
+ }
+
+ pub fn data_box_as_superbox(&self, index: usize) -> Option<&JUMBFSuperBox> {
+ let da_box = &self.data_boxes[index];
+ da_box.as_ref().as_any().downcast_ref::<JUMBFSuperBox>()
+ }
+
+ pub fn data_box_as_json_box(&self, index: usize) -> Option<&JUMBFJSONContentBox> {
+ let da_box = &self.data_boxes[index];
+ da_box
+ .as_ref()
+ .as_any()
+ .downcast_ref::<JUMBFJSONContentBox>()
+ }
+
+ pub fn data_box_as_cbor_box(&self, index: usize) -> Option<&JUMBFCBORContentBox> {
+ let da_box = &self.data_boxes[index];
+ da_box
+ .as_ref()
+ .as_any()
+ .downcast_ref::<JUMBFCBORContentBox>()
+ }
+
+ pub fn data_box_as_jp2c_box(&self, index: usize) -> Option<&JUMBFCodestreamContentBox> {
+ let da_box = &self.data_boxes[index];
+ da_box
+ .as_ref()
+ .as_any()
+ .downcast_ref::<JUMBFCodestreamContentBox>()
+ }
+
+ pub fn data_box_as_uuid_box(&self, index: usize) -> Option<&JUMBFUUIDContentBox> {
+ let da_box = &self.data_boxes[index];
+ da_box
+ .as_ref()
+ .as_any()
+ .downcast_ref::<JUMBFUUIDContentBox>()
+ }
+
+ pub fn data_box_as_embedded_file_content_box(
+ &self,
+ index: usize,
+ ) -> Option<&JUMBFEmbeddedFileContentBox> {
+ let da_box = &self.data_boxes[index];
+ da_box
+ .as_ref()
+ .as_any()
+ .downcast_ref::<JUMBFEmbeddedFileContentBox>()
+ }
+
+ pub fn data_box_as_embedded_media_type_box(
+ &self,
+ index: usize,
+ ) -> Option<&JUMBFEmbeddedFileDescriptionBox> {
+ let da_box = &self.data_boxes[index];
+ da_box
+ .as_ref()
+ .as_any()
+ .downcast_ref::<JUMBFEmbeddedFileDescriptionBox>()
+ }
+}
+
+impl BMFFBox for JUMBFSuperBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"jumb"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ JUMB_FOURCC
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let mut size = 0;
+ size += box_size!(self.desc_box);
+ if !self.data_boxes.is_empty() {
+ size += boxes_size!(self.data_boxes)
+ }
+ Ok(size)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ let res = self.desc_box.write_box(writer);
+ for b in &self.data_boxes {
+ b.write_box(writer)?;
+ }
+ res
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+// ANCHOR JUMBF Description box
+/// JUMBF Description box (ISO 19566-5:2019, Annex A)
+#[derive(Debug)]
+pub struct JUMBFDescriptionBox {
+ box_uuid: [u8; 16], // a 128-bit UUID for the type
+ toggles: u8, // bit field for valid values
+ label: CString, // Null terminated UTF-8 string (OPTIONAL)
+ box_id: Option<u32>, // user assigned value (OPTIONAL)
+ signature: Option<[u8; 32]>, // SHA-256 hash of the payload (OPTIONAL)
+ private: Option<CAISaltContentBox>, // private salt content box
+}
+
+impl JUMBFDescriptionBox {
+ /// Makes a new `JUMBFDescriptionBox` instance.
+ pub fn new(box_label: &str, a_type: Option<&str>) -> Self {
+ JUMBFDescriptionBox {
+ box_uuid: match a_type {
+ Some(ref t) => <[u8; 16]>::from_hex(t).unwrap_or([0u8; 16]),
+ None => [0u8; 16], // init to all zeros
+ },
+ toggles: 3, // 0x11 (Requestable + Label Present)
+ label: CString::new(box_label).unwrap_or_default(),
+ box_id: None,
+ signature: None,
+ private: None,
+ }
+ }
+
+ pub fn set_salt(&mut self, salt: Vec<u8>) -> JumbfParseResult<()> {
+ if salt.len() < 16 {
+ return Err(JumbfParseError::InvalidSalt);
+ }
+
+ self.private = Some(CAISaltContentBox::new(salt));
+ self.toggles = 19; // 0x10011 (Requestable + Label Present + Private)
+
+ Ok(())
+ }
+
+ pub fn get_salt(&self) -> Option<Vec<u8>> {
+ self.private.as_ref().map(|saltbox| saltbox.salt.clone())
+ }
+
+ /// Makes a new `JUMBFDescriptionBox` instance from read in data
+ pub fn from(
+ uuid: &[u8; 16],
+ togs: u8,
+ box_label: Vec<u8>,
+ bxid: Option<u32>,
+ sig: Option<[u8; 32]>,
+ private: Option<CAISaltContentBox>,
+ ) -> Self {
+ let c_string: CString;
+ unsafe {
+ c_string = CString::from_vec_unchecked(box_label);
+ }
+ JUMBFDescriptionBox {
+ box_uuid: *uuid,
+ toggles: togs, // will always be 0x11 (Requestable + Label Present)
+ label: c_string,
+ box_id: bxid,
+ signature: sig,
+ private,
+ }
+ }
+
+ /// getters
+ pub fn uuid(&self) -> String {
+ hex::encode(self.box_uuid).to_uppercase()
+ }
+
+ pub fn label(&self) -> String {
+ self.label.clone().into_string().unwrap_or_default()
+ }
+}
+
+impl BMFFBox for JUMBFDescriptionBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"jumd"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ JUMD_FOURCC
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ write_all!(writer, &self.box_uuid);
+ write_u8!(writer, self.toggles);
+
+ if self.label.to_str().unwrap_or_default().chars().count() > 0 {
+ write_all!(writer, self.label.as_bytes_with_nul());
+ }
+
+ if let Some(x) = self.box_id {
+ write_u32!(writer, x);
+ }
+
+ if let Some(x) = self.signature {
+ write_all!(writer, &x);
+ }
+
+ if let Some(salt) = &self.private {
+ salt.write_box(writer)?;
+ }
+
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+// ANCHOR JUMBF UUIDs
+pub const JUMBF_CODESTREAM_UUID: &str = "6579D6FBDBA2446BB2AC1B82FEEB89D1";
+pub const JUMBF_JSON_UUID: &str = "6A736F6E00110010800000AA00389B71";
+pub const JUMBF_CBOR_UUID: &str = "63626F7200110010800000AA00389B71";
+// pub const JUMBF_XML_UUID: &str = "786D6C2000110010800000AA00389B71";
+pub const JUMBF_UUID_UUID: &str = "7575696400110010800000AA00389B71";
+pub const JUMBF_EMBEDDED_FILE_UUID: &str = "40CB0C32BB8A489DA70B2AD6F47F4369";
+// ANCHOR JUMBF Content box
+/// JUMBF Content box (ISO 19566-5:2019, Annex B)
+#[derive(Debug, Default)]
+pub struct JUMBFContentBox;
+
+impl BMFFBox for JUMBFContentBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"jumd"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ "" // base JUMBF boxes don't have any...
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ Ok(0) // it isn't a real box, just a base class
+ }
+
+ fn write_box_payload(&self, _writer: &mut dyn Write) -> IoResult<()> {
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+// ANCHOR JUMB Padding Box
+#[derive(Debug, Default)]
+pub struct JUMBFPaddingContentBox {
+ padding: Vec<u8>, // arbitrary number of zero'd bytes...
+}
+
+impl BMFFBox for JUMBFPaddingContentBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"free"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ "" // base JUMBF boxes don't have any...
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = self.padding.len();
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ if !self.padding.is_empty() {
+ write_all!(writer, &self.padding);
+ }
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl JUMBFPaddingContentBox {
+ pub fn new_with_vec(padding: Vec<u8>) -> Self {
+ JUMBFPaddingContentBox { padding }
+ }
+
+ // we do not take a vec to ensure the box contains only zeros
+ pub fn new(box_size: usize) -> Self {
+ JUMBFPaddingContentBox {
+ padding: vec![0; box_size],
+ }
+ }
+}
+
+// ANCHOR JUMBF JSON Content box
+/// JUMBF JSON Content box (ISO 19566-5:2019, Annex B.4)
+#[derive(Debug, Default)]
+pub struct JUMBFJSONContentBox {
+ json: Vec<u8>, // arbitrary bunch of bytes...
+}
+
+impl BMFFBox for JUMBFJSONContentBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"json"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ JUMBF_JSON_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = self.json.len();
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ if !self.json.is_empty() {
+ write_all!(writer, &self.json);
+ }
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl JUMBFJSONContentBox {
+ // the content box takes ownership of the data!
+ pub fn new(json_in: Vec<u8>) -> Self {
+ JUMBFJSONContentBox { json: json_in }
+ }
+
+ // getter
+ pub fn json(&self) -> &Vec<u8> {
+ &self.json
+ }
+}
+
+pub struct JUMBFCBORContentBox {
+ cbor: Vec<u8>, // arbitrary bunch of bytes...
+}
+
+impl BMFFBox for JUMBFCBORContentBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"cbor"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ JUMBF_CBOR_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = self.cbor.len();
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ if !self.cbor.is_empty() {
+ write_all!(writer, &self.cbor);
+ }
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl JUMBFCBORContentBox {
+ // the content box takes ownership of the data!
+ pub fn new(cbor_in: Vec<u8>) -> Self {
+ JUMBFCBORContentBox { cbor: cbor_in }
+ }
+
+ // getter
+ pub fn cbor(&self) -> &Vec<u8> {
+ &self.cbor
+ }
+}
+
+// ANCHOR JUMBF Codestream Content box
+/// JUMBF Codestream Content box (ISO 19566-5:2019, Annex B.2)
+#[derive(Debug, Default)]
+pub struct JUMBFCodestreamContentBox {
+ data: Vec<u8>, // arbitrary bunch of bytes...
+}
+
+impl BMFFBox for JUMBFCodestreamContentBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"jp2c"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ JUMBF_CODESTREAM_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = self.data.len();
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ if !self.data.is_empty() {
+ write_all!(writer, &self.data);
+ }
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl JUMBFCodestreamContentBox {
+ // the content box takes ownership of the data!
+ pub fn new(data_in: Vec<u8>) -> Self {
+ JUMBFCodestreamContentBox { data: data_in }
+ }
+
+ // getter
+ pub fn data(&self) -> &Vec<u8> {
+ &self.data
+ }
+}
+
+// ANCHOR JUMBF UUID Content box
+/// JUMBF UUID Content box (ISO 19566-5:2019, Annex B.5)
+#[derive(Debug, Default)]
+pub struct JUMBFUUIDContentBox {
+ uuid: [u8; 16], // a 128-bit UUID for the type
+ data: Vec<u8>, // arbitrary bunch of bytes...
+}
+
+impl BMFFBox for JUMBFUUIDContentBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"uuid"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ JUMBF_UUID_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = 16 /*UUID*/ + self.data.len();
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ if !self.data.is_empty() {
+ write_all!(writer, &self.uuid);
+ write_all!(writer, &self.data);
+ }
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl JUMBFUUIDContentBox {
+ // the content box takes ownership of the data!
+ pub fn new(uuid_in: &[u8; 16], data_in: Vec<u8>) -> Self {
+ let mut u: [u8; 16] = Default::default();
+ u.copy_from_slice(uuid_in);
+
+ JUMBFUUIDContentBox {
+ uuid: u,
+ data: data_in,
+ }
+ }
+
+ // getters
+ pub fn uuid(&self) -> &[u8; 16] {
+ &self.uuid
+ }
+
+ // getter
+ pub fn data(&self) -> &Vec<u8> {
+ &self.data
+ }
+}
+
+// !SECTION
+
+//---------------
+// SECTION CAI
+//---------------
+pub const CAI_BLOCK_UUID: &str = "6332706100110010800000AA00389B71"; // c2pa
+pub const CAI_STORE_UUID: &str = "63326D6100110010800000AA00389B71"; // c2ma
+pub const CAI_UPDATE_MANIFEST_UUID: &str = "6332756D00110010800000AA00389B71"; // c2um
+pub const CAI_ASSERTION_STORE_UUID: &str = "6332617300110010800000AA00389B71"; // c2as
+pub const CAI_INGREDIENT_STORE_UUID: &str = "6361697300110010800000AA00389B71"; //cais
+pub const CAI_JSON_ASSERTION_UUID: &str = "6A736F6E00110010800000AA00389B71"; // json
+pub const CAI_CBOR_ASSERTION_UUID: &str = "63626F7200110010800000AA00389B71"; // cbor
+pub const CAI_CODESTREAM_ASSERTION_UUID: &str = "6579D6FBDBA2446BB2AC1B82FEEB89D1";
+pub const CAI_INGREDIENT_UUID: &str = "6361696E00110010800000AA00389B71"; // cain
+pub const CAI_CLAIM_UUID: &str = "6332636C00110010800000AA00389B71"; // c2cl
+pub const CAI_SIGNATURE_UUID: &str = "6332637300110010800000AA00389B71"; // c2cs
+pub const CAI_EMBEDDED_FILE_UUID: &str = "40CB0C32BB8A489DA70B2AD6F47F4369";
+pub const CAI_EMBEDDED_FILE_DESCRIPTION_UUID: &str = "6266646200110010800000AA00389B71"; // bfdb
+pub const CAI_EMBEDED_FILE_DATA_UUID: &str = "6269646200110010800000AA00389B71"; // bidb
+pub const CAI_VERIFIABLE_CREDENTIALS_STORE_UUID: &str = "6332766300110010800000AA00389B71"; //c2vc
+pub const CAI_UUID_ASSERTION_UUID: &str = "7575696400110010800000AA00389B71"; // uuid
+
+// ANCHOR Salt Content Box
+/// Salt Content Box
+#[derive(Debug)]
+pub struct CAISaltContentBox {
+ salt: Vec<u8>, // salt data...
+}
+
+impl BMFFBox for CAISaltContentBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"c2sh"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ "" // base JUMBF boxes don't have any...
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = self.salt.len();
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ write_all!(writer, &self.salt);
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAISaltContentBox {
+ pub fn new(data_in: Vec<u8>) -> Self {
+ CAISaltContentBox { salt: data_in }
+ }
+}
+// ANCHOR Signature Content Box
+/// Signature Content Box
+#[derive(Debug)]
+pub struct CAISignatureContentBox {
+ uuid: [u8; 16], // a 128-bit UUID
+ sig_data: Vec<u8>, // signature data...
+}
+
+impl BMFFBox for CAISignatureContentBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"uuid"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ "" // base JUMBF boxes don't have any...
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ write_all!(writer, &self.uuid);
+ write_all!(writer, &self.sig_data);
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAISignatureContentBox {
+ pub fn new(data_in: Vec<u8>) -> Self {
+ CAISignatureContentBox {
+ uuid: <[u8; 16]>::from_hex(CAI_SIGNATURE_UUID).unwrap_or_default(),
+ sig_data: data_in,
+ }
+ }
+}
+
+// ANCHOR Signature Box
+/// Signature Box
+#[derive(Debug)]
+pub struct CAISignatureBox {
+ sig_box: JUMBFSuperBox,
+}
+
+impl BMFFBox for CAISignatureBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_SIGNATURE_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.sig_box.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAISignatureBox {
+ pub fn new() -> Self {
+ CAISignatureBox {
+ sig_box: JUMBFSuperBox::new(labels::SIGNATURE, Some(CAI_SIGNATURE_UUID)),
+ }
+ }
+
+ // add a signature content box *WITHOUT* taking ownership of the box
+ pub fn add_signature(&mut self, b: Box<dyn BMFFBox>) {
+ self.sig_box.add_data_box(b)
+ }
+}
+
+impl Default for CAISignatureBox {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+// ANCHOR Claim Box
+/// Claim Box
+#[derive(Debug)]
+pub struct CAIClaimBox {
+ claim_box: JUMBFSuperBox,
+}
+
+impl BMFFBox for CAIClaimBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_CLAIM_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.claim_box.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAIClaimBox {
+ pub fn new() -> Self {
+ CAIClaimBox {
+ claim_box: JUMBFSuperBox::new(labels::CLAIM, Some(CAI_CLAIM_UUID)),
+ }
+ }
+
+ // add a JUMBFCBORContentBox box, with the claim's CBOR
+ // *WITHOUT* taking ownership of the box
+ pub fn add_claim(&mut self, b: Box<dyn BMFFBox>) {
+ self.claim_box.add_data_box(b)
+ }
+}
+
+impl Default for CAIClaimBox {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+// ANCHOR UUID Assertion Box
+/// UUID Assertion Box
+#[derive(Debug)]
+pub struct CAIUUIDAssertionBox {
+ assertion_box: JUMBFSuperBox,
+}
+
+impl BMFFBox for CAIUUIDAssertionBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_UUID_ASSERTION_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.assertion_box.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAIUUIDAssertionBox {
+ pub fn new(box_label: &str) -> Self {
+ CAIUUIDAssertionBox {
+ assertion_box: JUMBFSuperBox::new(box_label, Some(CAI_UUID_ASSERTION_UUID)),
+ }
+ }
+
+ // add a JUMBFJSONContentBox box, with the assertion's JSON
+ // takes ownership of the JSON
+ pub fn add_uuid(&mut self, uuid_str: &str, data: Vec<u8>) -> JumbfParseResult<()> {
+ let uuid = hex::decode(uuid_str).map_err(|_e| JumbfParseError::InvalidUuidValue)?;
+ if uuid.len() != 16 {
+ // the uuid is defined a as 16 bytes
+ return Err(JumbfParseError::InvalidUuidValue);
+ }
+
+ let mut u: [u8; 16] = Default::default();
+ u.copy_from_slice(&uuid);
+ let assertion_content = JUMBFUUIDContentBox::new(&u, data);
+ self.assertion_box.add_data_box(Box::new(assertion_content));
+
+ Ok(())
+ }
+
+ pub fn set_salt(&mut self, salt: Vec<u8>) -> JumbfParseResult<()> {
+ self.assertion_box.desc_box.set_salt(salt)
+ }
+
+ pub fn super_box(&self) -> &dyn BMFFBox {
+ &self.assertion_box
+ }
+}
+
+// ANCHOR JSON Assertion Box
+/// JSON Assertion Box
+#[derive(Debug)]
+pub struct CAIJSONAssertionBox {
+ assertion_box: JUMBFSuperBox,
+}
+
+impl BMFFBox for CAIJSONAssertionBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_JSON_ASSERTION_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.assertion_box.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAIJSONAssertionBox {
+ pub fn new(box_label: &str) -> Self {
+ CAIJSONAssertionBox {
+ assertion_box: JUMBFSuperBox::new(box_label, Some(CAI_JSON_ASSERTION_UUID)),
+ }
+ }
+
+ // add a JUMBFJSONContentBox box, with the assertion's JSON
+ // takes ownership of the JSON
+ pub fn add_json(&mut self, json_in: Vec<u8>) {
+ let assertion_content = JUMBFJSONContentBox::new(json_in);
+ self.assertion_box.add_data_box(Box::new(assertion_content));
+ }
+
+ pub fn set_salt(&mut self, salt: Vec<u8>) -> JumbfParseResult<()> {
+ self.assertion_box.desc_box.set_salt(salt)
+ }
+
+ pub fn super_box(&self) -> &dyn BMFFBox {
+ &self.assertion_box
+ }
+}
+
+pub struct CAICBORAssertionBox {
+ assertion_box: JUMBFSuperBox,
+}
+
+impl BMFFBox for CAICBORAssertionBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_CBOR_ASSERTION_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.assertion_box.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAICBORAssertionBox {
+ pub fn new(box_label: &str) -> Self {
+ CAICBORAssertionBox {
+ assertion_box: JUMBFSuperBox::new(box_label, Some(CAI_CBOR_ASSERTION_UUID)),
+ }
+ }
+
+ // add a JUMBFCBORContentBox box, with the assertion's CBOR
+ // takes ownership of the CBOR
+ pub fn add_cbor(&mut self, cbor_in: Vec<u8>) {
+ let assertion_content = JUMBFCBORContentBox::new(cbor_in);
+ self.assertion_box.add_data_box(Box::new(assertion_content));
+ }
+
+ pub fn set_salt(&mut self, salt: Vec<u8>) -> JumbfParseResult<()> {
+ self.assertion_box.desc_box.set_salt(salt)
+ }
+
+ pub fn super_box(&self) -> &dyn BMFFBox {
+ &self.assertion_box
+ }
+}
+
+// ANCHOR Ingredient Box
+/// Ingedient Box
+#[derive(Debug)]
+pub struct CAIIngredientBox {
+ ingredient_box: JUMBFSuperBox,
+}
+
+impl BMFFBox for CAIIngredientBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_INGREDIENT_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.ingredient_box.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAIIngredientBox {
+ pub fn new(box_label: &str) -> Self {
+ CAIIngredientBox {
+ ingredient_box: JUMBFSuperBox::new(box_label, Some(CAI_INGREDIENT_UUID)),
+ }
+ }
+
+ // add a JUMBFCodestreamContentBox box, with the codestream data
+ // takes ownership of the data
+ pub fn add_data(&mut self, data_in: Vec<u8>) {
+ let ingredient_content = JUMBFCodestreamContentBox::new(data_in);
+ self.ingredient_box
+ .add_data_box(Box::new(ingredient_content));
+ }
+}
+
+// ANCHOR Assertion Store
+/// Assertion Store
+#[derive(Debug)]
+pub struct CAIAssertionStore {
+ store: JUMBFSuperBox,
+}
+
+impl BMFFBox for CAIAssertionStore {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_ASSERTION_STORE_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.store.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAIAssertionStore {
+ pub fn new() -> Self {
+ CAIAssertionStore {
+ store: JUMBFSuperBox::new(labels::ASSERTIONS, Some(CAI_ASSERTION_STORE_UUID)),
+ }
+ }
+
+ pub fn from(in_box: JUMBFSuperBox) -> Self {
+ CAIAssertionStore { store: in_box }
+ }
+
+ // add an assertion box (of various types) *WITHOUT* taking ownership of the box
+ pub fn add_assertion(&mut self, b: Box<dyn BMFFBox>) {
+ self.store.add_data_box(b)
+ }
+}
+
+impl Default for CAIAssertionStore {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+// ANCHOR Verifiable Credential Store
+/// Ingredients Store
+#[derive(Debug)]
+pub struct CAIVerifiableCredentialStore {
+ store: JUMBFSuperBox,
+}
+
+impl BMFFBox for CAIVerifiableCredentialStore {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_VERIFIABLE_CREDENTIALS_STORE_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.store.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAIVerifiableCredentialStore {
+ pub fn new() -> Self {
+ CAIVerifiableCredentialStore {
+ store: JUMBFSuperBox::new(
+ labels::CREDENTIALS,
+ Some(CAI_VERIFIABLE_CREDENTIALS_STORE_UUID),
+ ),
+ }
+ }
+
+ pub fn from(in_box: JUMBFSuperBox) -> Self {
+ CAIVerifiableCredentialStore { store: in_box }
+ }
+
+ // add an credential box *WITHOUT* taking ownership of the box
+ pub fn add_credential(&mut self, b: Box<dyn BMFFBox>) {
+ self.store.add_data_box(b)
+ }
+}
+
+impl Default for CAIVerifiableCredentialStore {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+// ANCHOR CAI Store
+/// CAI Store
+#[derive(Debug)]
+pub struct CAIStore {
+ is_update_manifest: bool,
+ store: JUMBFSuperBox,
+}
+
+impl BMFFBox for CAIStore {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ if self.is_update_manifest {
+ CAI_UPDATE_MANIFEST_UUID
+ } else {
+ CAI_STORE_UUID
+ }
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.store.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl CAIStore {
+ pub fn new(box_label: &str, update_manifest: bool) -> Self {
+ let id = if update_manifest {
+ Some(CAI_UPDATE_MANIFEST_UUID)
+ } else {
+ Some(CAI_STORE_UUID)
+ };
+ let sbox = JUMBFSuperBox::new(box_label, id);
+ CAIStore {
+ is_update_manifest: update_manifest,
+ store: sbox,
+ }
+ }
+
+ pub fn from(sbox: JUMBFSuperBox) -> Self {
+ let update_manifest = sbox.box_uuid() == CAI_UPDATE_MANIFEST_UUID;
+
+ CAIStore {
+ is_update_manifest: update_manifest,
+ store: sbox,
+ }
+ }
+
+ /// add a box (of various types) *WITHOUT* taking ownership of the box
+ pub fn add_box(&mut self, b: Box<dyn BMFFBox>) {
+ self.store.add_data_box(b)
+ }
+
+ // getters
+ pub fn super_box(&self) -> &JUMBFSuperBox {
+ &self.store
+ }
+
+ pub fn desc_box(&self) -> &JUMBFDescriptionBox {
+ &self.store.desc_box
+ }
+
+ pub fn data_box_count(&self) -> usize {
+ self.store.data_boxes.len()
+ }
+
+ pub fn data_box(&self, index: usize) -> &dyn BMFFBox {
+ self.store.data_boxes[index].as_ref()
+ }
+
+ pub fn assertion_store(&self) -> Option<&JUMBFSuperBox> {
+ // we REALLY want to return a CAIAssertionStore but can't do to referencing...
+ self.store.data_box_as_superbox(0)
+ }
+}
+
+// ANCHOR CAI Block
+/// CAI Block
+#[derive(Debug)]
+pub struct Cai {
+ sbox: JUMBFSuperBox,
+}
+
+impl BMFFBox for Cai {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_BLOCK_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.sbox.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl Cai {
+ pub fn new() -> Self {
+ Cai {
+ sbox: JUMBFSuperBox::new(labels::MANIFEST_STORE, Some(CAI_BLOCK_UUID)),
+ }
+ }
+
+ pub fn from(in_box: JUMBFSuperBox) -> Self {
+ Cai { sbox: in_box }
+ }
+
+ /// add a box (of various types) *WITHOUT* taking ownership of the box
+ pub fn add_box(&mut self, b: Box<dyn BMFFBox>) {
+ self.sbox.add_data_box(b)
+ }
+
+ // getters
+ pub fn super_box(&self) -> &JUMBFSuperBox {
+ &self.sbox
+ }
+
+ pub fn desc_box(&self) -> &JUMBFDescriptionBox {
+ &self.sbox.desc_box
+ }
+
+ pub fn data_box_count(&self) -> usize {
+ self.sbox.data_boxes.len()
+ }
+
+ pub fn data_box(&self, index: usize) -> &dyn BMFFBox {
+ self.sbox.data_boxes[index].as_ref()
+ }
+
+ pub fn data_box_as_superbox(&self, index: usize) -> Option<&JUMBFSuperBox> {
+ let da_box = &self.sbox.data_boxes[index];
+ da_box.as_ref().as_any().downcast_ref::<JUMBFSuperBox>()
+ }
+
+ pub fn store(&self) -> Option<&JUMBFSuperBox> {
+ // we REALLY want to return a UpdateManifest but can't do to referencing...
+ self.sbox.data_box_as_superbox(0)
+ }
+}
+
+impl Default for Cai {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+pub struct JumbfEmbeddedFileBox {
+ embedding_box: JUMBFSuperBox,
+}
+
+impl BMFFBox for JumbfEmbeddedFileBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b" "
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ JUMBF_EMBEDDED_FILE_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ self.embedding_box.write_box(writer)
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl JumbfEmbeddedFileBox {
+ pub fn new(box_label: &str) -> Self {
+ JumbfEmbeddedFileBox {
+ embedding_box: JUMBFSuperBox::new(box_label, Some(JUMBF_EMBEDDED_FILE_UUID)),
+ }
+ }
+
+ // add a JUMBFJSONContentBox box, with the claim's JSON
+ // *WITHOUT* taking ownership of the box
+ pub fn add_data(&mut self, data: Vec<u8>, media_type: String, file_name: Option<String>) {
+ // add media type box
+ let m = JUMBFEmbeddedFileDescriptionBox::new(media_type, file_name);
+ self.embedding_box.add_data_box(Box::new(m));
+
+ // add data box
+ let d = JUMBFEmbeddedFileContentBox::new(data);
+ self.embedding_box.add_data_box(Box::new(d));
+ }
+
+ pub fn media_type_box(&self) -> Option<&JUMBFEmbeddedFileDescriptionBox> {
+ let efd_box = &self.embedding_box.data_boxes[0];
+ efd_box
+ .as_ref()
+ .as_any()
+ .downcast_ref::<JUMBFEmbeddedFileDescriptionBox>()
+ }
+
+ pub fn data_box(&self) -> Option<&JUMBFEmbeddedFileContentBox> {
+ let efc_box = &self.embedding_box.data_boxes[1];
+ efc_box
+ .as_ref()
+ .as_any()
+ .downcast_ref::<JUMBFEmbeddedFileContentBox>()
+ }
+
+ pub fn set_salt(&mut self, salt: Vec<u8>) -> JumbfParseResult<()> {
+ self.embedding_box.desc_box.set_salt(salt)
+ }
+
+ pub fn get_salt(&self) -> Option<Vec<u8>> {
+ self.embedding_box
+ .desc_box
+ .private
+ .as_ref()
+ .map(|saltbox| saltbox.salt.clone())
+ }
+
+ pub fn super_box(&self) -> &dyn BMFFBox {
+ &self.embedding_box
+ }
+}
+
+impl Default for JumbfEmbeddedFileBox {
+ fn default() -> Self {
+ Self::new("")
+ }
+}
+#[derive(Debug, Default)]
+pub struct JUMBFEmbeddedFileContentBox {
+ data: Vec<u8>, // arbitrary bunch of bytes...
+}
+
+impl BMFFBox for JUMBFEmbeddedFileContentBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"bidb"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_EMBEDED_FILE_DATA_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = self.data.len();
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ if !self.data.is_empty() {
+ write_all!(writer, &self.data);
+ }
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl JUMBFEmbeddedFileContentBox {
+ // the content box takes ownership of the data!
+ pub fn new(data_in: Vec<u8>) -> Self {
+ JUMBFEmbeddedFileContentBox { data: data_in }
+ }
+
+ // getter
+ pub fn data(&self) -> &Vec<u8> {
+ &self.data
+ }
+}
+
+#[derive(Debug)]
+pub struct JUMBFEmbeddedFileDescriptionBox {
+ toggles: u8, // media togles
+ media_type: CString, // file media type
+ file_name: Option<CString>, // optional file name
+}
+
+impl BMFFBox for JUMBFEmbeddedFileDescriptionBox {
+ fn box_type(&self) -> &'static [u8; 4] {
+ b"bfdb"
+ }
+
+ fn box_uuid(&self) -> &'static str {
+ CAI_EMBEDDED_FILE_DESCRIPTION_UUID
+ }
+
+ fn box_payload_size(&self) -> IoResult<u32> {
+ let size = boxio::ByteCounter::calculate(|w| self.write_box_payload(w))?;
+ Ok(size as u32)
+ }
+
+ fn write_box_payload(&self, writer: &mut dyn Write) -> IoResult<()> {
+ write_u8!(writer, self.toggles);
+ if self.media_type.to_str().unwrap_or_default().chars().count() > 0 {
+ write_all!(writer, self.media_type.as_bytes_with_nul());
+ }
+ /*
+ if let Some(name) = &self.file_name {
+ if name
+ .to_str()
+ .expect("Incompatible string representation")
+ .chars()
+ .count()
+ > 0
+ {
+ write_all!(writer, name.as_bytes_with_nul())
+ }
+ }
+ */
+ Ok(())
+ }
+
+ // Necessary method to enable conversion between types...
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+impl JUMBFEmbeddedFileDescriptionBox {
+ pub fn new(media_type: String, file_name: Option<String>) -> Self {
+ let mut new_toggles = 0;
+
+ let cfile_name = match file_name {
+ Some(f) => {
+ new_toggles = 1;
+ Some(CString::new(f).unwrap_or_default())
+ }
+ None => None,
+ };
+
+ JUMBFEmbeddedFileDescriptionBox {
+ toggles: new_toggles,
+ media_type: CString::new(media_type).unwrap_or_default(),
+ file_name: cfile_name,
+ }
+ }
+
+ fn to_rust_str(&self, s: &CString) -> String {
+ let bytes = s.clone().into_bytes();
+
+ let nul_range_end = bytes
+ .iter()
+ .position(|&c| c == b'\0')
+ .unwrap_or(bytes.len());
+
+ if let Ok(r_str) = String::from_utf8(bytes[0..nul_range_end].to_vec()) {
+ r_str
+ } else {
+ String::new()
+ }
+ }
+
+ pub fn media_type(&self) -> String {
+ self.to_rust_str(&self.media_type)
+ }
+
+ pub fn file_name(&self) -> Option<String> {
+ self.file_name.as_ref().map(|f| self.to_rust_str(f))
+ }
+
+ /// Makes a new `JUMBFDescriptionBox` instance from read in data
+ pub fn from(togs: u8, mt_bytes: Vec<u8>, fn_bytes: Option<Vec<u8>>) -> Self {
+ let mt_cstring: CString = unsafe { CString::from_vec_unchecked(mt_bytes) };
+ let fn_cstring = fn_bytes.map(|b| unsafe { CString::from_vec_unchecked(b) });
+
+ JUMBFEmbeddedFileDescriptionBox {
+ toggles: togs, // media togles
+ media_type: mt_cstring, // file media type
+ file_name: fn_cstring, // optional file name
+ }
+ }
+}
+
+// !SECTION
+
+//---------------
+// SECTION Box Reader
+//---------------
+
+const HEADER_SIZE: u64 = 8;
+const TOGGLE_SIZE: u64 = 1;
+
+/// method for getting the current position
+pub fn current_pos<R: Seek>(seeker: &mut R) -> JumbfParseResult<u64> {
+ Ok(seeker.seek(SeekFrom::Current(0))?)
+}
+
+/// method for seeking back to the start of the box (header)
+pub fn box_start<R: Seek>(seeker: &mut R) -> JumbfParseResult<u64> {
+ Ok(current_pos(seeker).map_err(|_| JumbfParseError::InvalidBoxStart)? - HEADER_SIZE)
+}
+
+/// method for skipping over `size` bytes
+pub fn skip_bytes<S: Seek>(seeker: &mut S, size: u64) -> JumbfParseResult<()> {
+ seeker.seek(SeekFrom::Current(size as i64))?;
+ Ok(())
+}
+
+/// method for skipping to a specific position (`pos`)
+pub fn skip_bytes_to<S: Seek>(seeker: &mut S, pos: u64) -> JumbfParseResult<()> {
+ seeker.seek(SeekFrom::Start(pos))?;
+ Ok(())
+}
+
+// method to skip over an entire box
+pub fn skip_box<S: Seek>(seeker: &mut S, size: u64) -> JumbfParseResult<()> {
+ let start = box_start(seeker)?;
+ skip_bytes_to(seeker, start + size)?;
+ Ok(())
+}
+
+/// method for skipping backwards `size` bytes
+pub fn unread_bytes<S: Seek>(seeker: &mut S, size: u64) -> JumbfParseResult<()> {
+ let new_loc = -(size as i64);
+ seeker.seek(SeekFrom::Current(new_loc))?;
+ Ok(())
+}
+
+/// macro for dealing with the type of a BMFF/JUMBF box
+macro_rules! boxtype {
+ ($( $name:ident => $value:expr ),*) => {
+ #[derive(Debug, Clone, Copy, PartialEq)]
+ pub enum BoxType {
+ $( $name, )*
+ UnknownBox(u32),
+ }
+
+ impl From<u32> for BoxType {
+ fn from(t: u32) -> BoxType {
+ match t {
+ $( $value => BoxType::$name, )*
+ _ => BoxType::UnknownBox(t),
+ }
+ }
+ }
+
+ }
+}
+
+boxtype! {
+ Empty => 0x0000_0000,
+ Jumb => 0x6A75_6D62,
+ Jumd => 0x6A75_6D64,
+ Padding => 0x6672_6565,
+ SaltHash => 0x6332_7368,
+ Json => 0x6A73_6F6E,
+ Uuid => 0x7575_6964,
+ Jp2c => 0x6A70_3263,
+ Cbor => 0x6362_6F72,
+ EmbedMediaDesc => 0x6266_6462,
+ EmbedContent => 0x6269_6462
+}
+
+// ANCHOR BlockHeader
+/// class for storing the header of a block
+pub struct BoxHeader {
+ pub name: BoxType,
+ pub size: u64,
+}
+impl BoxHeader {
+ pub fn new(name: BoxType, size: u64) -> Self {
+ Self { name, size }
+ }
+}
+
+// ANCHOR BoxReader
+/// class for reading BMFF/JUMBF boxes
+pub struct BoxReader {}
+
+impl BoxReader {
+ pub fn read_header<R: Read>(reader: &mut R) -> JumbfParseResult<BoxHeader> {
+ // Create and read to buf.
+ let mut buf = [0u8; 8]; // 8 bytes for box header.
+ let bytes_read = reader.read(&mut buf)?;
+
+ if bytes_read == 0 {
+ // end of file!
+ return Ok(BoxHeader::new(BoxType::Empty, 0));
+ }
+
+ // Get size.
+ let s = buf[0..4]
+ .try_into()
+ .map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ let size = u32::from_be_bytes(s);
+
+ // Get box type string.
+ let t = buf[4..8]
+ .try_into()
+ .map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ let typ = u32::from_be_bytes(t);
+
+ // Get large size if size is 1
+ if size == 1 {
+ reader.read_exact(&mut buf)?;
+ let s = buf; //.try_into().unwrap();
+ let large_size = u64::from_be_bytes(s);
+
+ Ok(BoxHeader {
+ name: BoxType::from(typ),
+ size: large_size,
+ })
+ } else {
+ Ok(BoxHeader {
+ name: BoxType::from(typ),
+ size: size as u64,
+ })
+ }
+ }
+
+ pub fn read_desc_box<R: Read + Seek>(
+ reader: &mut R,
+ size: u64,
+ ) -> JumbfParseResult<JUMBFDescriptionBox> {
+ let mut bytes_left = size;
+ let mut uuid = [0u8; 16]; // 16 bytes for the UUID
+ let bytes_read = reader.read(&mut uuid)?;
+ if bytes_read == 0 {
+ // end of file!
+ return Ok(JUMBFDescriptionBox::new("", None));
+ }
+ bytes_left -= bytes_read as u64;
+
+ let mut togs = [0u8]; // 1 byte of toggles
+ reader.read_exact(&mut togs)?;
+ bytes_left -= 1;
+
+ if togs[0] & 0x03 == 0x03 {
+ // must be requestable and labeled
+ // read label
+ let mut sbuf = Vec::with_capacity(64);
+ loop {
+ let mut buf = [0; 1];
+ reader.read_exact(&mut buf)?;
+ bytes_left -= 1;
+ if buf[0] == 0x00 {
+ break;
+ } else {
+ sbuf.push(buf[0]);
+ }
+ }
+
+ // if there is a signature, we need to read it...
+ let sig = if togs[0] & 0x08 == 0x08 {
+ let mut sigbuf: [u8; 32] = [0; 32];
+ reader.read_exact(&mut sigbuf)?;
+ bytes_left -= 32;
+ Some(sigbuf)
+ } else {
+ None
+ };
+
+ // read private box if necessary
+ let private = if togs[0] & 0x10 == 0x10 {
+ let header = BoxReader::read_header(reader)
+ .map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ if header.size == 0 {
+ // bad read,
+ return Err(JumbfParseError::InvalidBoxHeader);
+ } else if header.size != bytes_left - HEADER_SIZE {
+ // this means that we started w/o the header...
+ unread_bytes(reader, HEADER_SIZE)?;
+ }
+
+ if header.name == BoxType::SaltHash {
+ let data_len = header.size - HEADER_SIZE;
+ let mut buf = vec![0u8; data_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ bytes_left -= header.size;
+
+ Some(CAISaltContentBox::new(buf))
+ } else {
+ return Err(JumbfParseError::InvalidBoxHeader);
+ }
+ } else {
+ None
+ };
+
+ if bytes_left != HEADER_SIZE {
+ // make sure we have consumed the entire box
+ return Err(JumbfParseError::InvalidBoxHeader);
+ }
+
+ return Ok(JUMBFDescriptionBox::from(
+ &uuid, togs[0], sbuf, None, sig, private,
+ ));
+ }
+ Err(JumbfParseError::InvalidDescriptionBox)
+ }
+
+ pub fn read_json_box<R: Read + Seek>(
+ reader: &mut R,
+ size: u64,
+ ) -> JumbfParseResult<JUMBFJSONContentBox> {
+ let header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ if header.size == 0 {
+ // bad read, return empty box...
+ return Ok(JUMBFJSONContentBox::new(Vec::new()));
+ } else if header.size != size {
+ // this means that we started w/o the header...
+ unread_bytes(reader, HEADER_SIZE)?;
+ }
+
+ let json_len = size - HEADER_SIZE;
+ let mut buf = vec![0u8; json_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ Ok(JUMBFJSONContentBox::new(buf))
+ }
+
+ pub fn read_cbor_box<R: Read + Seek>(
+ reader: &mut R,
+ size: u64,
+ ) -> JumbfParseResult<JUMBFCBORContentBox> {
+ let header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ if header.size == 0 {
+ // bad read, return empty box...
+ return Ok(JUMBFCBORContentBox::new(Vec::new()));
+ } else if header.size != size {
+ // this means that we started w/o the header...
+ unread_bytes(reader, HEADER_SIZE)?;
+ }
+
+ let cbor_len = size - HEADER_SIZE;
+ let mut buf = vec![0u8; cbor_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ Ok(JUMBFCBORContentBox::new(buf))
+ }
+
+ pub fn read_padding_box<R: Read + Seek>(
+ reader: &mut R,
+ size: u64,
+ ) -> JumbfParseResult<JUMBFPaddingContentBox> {
+ let header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ if header.size == 0 {
+ // bad read, return empty box...
+ return Ok(JUMBFPaddingContentBox::new(0));
+ } else if header.size != size {
+ // this means that we started w/o the header...
+ unread_bytes(reader, HEADER_SIZE)?;
+ }
+
+ let padding_len = size - HEADER_SIZE;
+ let mut buf = vec![0u8; padding_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ Ok(JUMBFPaddingContentBox::new_with_vec(buf))
+ }
+
+ pub fn read_jp2c_box<R: Read + Seek>(
+ reader: &mut R,
+ size: u64,
+ ) -> JumbfParseResult<JUMBFCodestreamContentBox> {
+ let header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ if header.size == 0 {
+ // bad read, return empty box...
+ return Ok(JUMBFCodestreamContentBox::new(Vec::new()));
+ } else if header.size != size {
+ // this means that we started w/o the header...
+ unread_bytes(reader, HEADER_SIZE)?;
+ }
+
+ // read the data itself...
+ let data_len = size - HEADER_SIZE;
+ let mut buf = vec![0u8; data_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ Ok(JUMBFCodestreamContentBox::new(buf))
+ }
+
+ pub fn read_uuid_box<R: Read + Seek>(
+ reader: &mut R,
+ size: u64,
+ ) -> JumbfParseResult<JUMBFUUIDContentBox> {
+ let header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ if header.size == 0 {
+ // bad read, return empty box...
+ return Ok(JUMBFUUIDContentBox::new(&[0u8; 16], Vec::new()));
+ } else if header.size != size {
+ // this means that we started w/o the header...
+ unread_bytes(reader, HEADER_SIZE)?;
+ }
+
+ // now read the UUID
+ let mut uuid = [0u8; 16]; // 16 bytes of UUID
+ reader.read_exact(&mut uuid)?;
+
+ // and finally the data itself...
+ let data_len = size - HEADER_SIZE - 16 /*UUID*/;
+ let mut buf = vec![0u8; data_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ Ok(JUMBFUUIDContentBox::new(&uuid, buf))
+ }
+
+ pub fn read_embedded_media_desc_box<R: Read + Seek>(
+ reader: &mut R,
+ size: u64,
+ ) -> JumbfParseResult<JUMBFEmbeddedFileDescriptionBox> {
+ let header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ if header.size == 0 {
+ // bad read, return empty box...
+ return Ok(JUMBFEmbeddedFileDescriptionBox::new("".to_string(), None));
+ } else if header.size != size {
+ // this means that we started w/o the header...
+ unread_bytes(reader, HEADER_SIZE)?;
+ }
+
+ //toggles: u8, // media togles
+ //media_type: CString, // file media type
+ //file_name: Option<CString>, // optional file name
+
+ // now read the media_type
+ let mut togs = [0u8]; // 1 byte of toggles
+ reader.read_exact(&mut togs)?;
+
+ // read the data itself...
+ let data_len = size - HEADER_SIZE - TOGGLE_SIZE;
+ let mut buf = vec![0u8; data_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ let (media_type, file_name) = match togs[0] {
+ 1 => {
+ // there may be two c strings in this vec
+ match buf.iter().position(|&x| x == 0) {
+ Some(pos) => {
+ if pos != buf.len() - 1 {
+ (buf, None)
+ } else {
+ let (first, second) = buf.split_at(pos);
+ (first.to_vec(), Some(second.to_vec()))
+ }
+ }
+ None => (buf, None),
+ }
+ }
+ _ => (buf, None),
+ };
+
+ Ok(JUMBFEmbeddedFileDescriptionBox::from(
+ togs[0], media_type, file_name,
+ ))
+ }
+
+ pub fn read_embedded_content_box<R: Read + Seek>(
+ reader: &mut R,
+ size: u64,
+ ) -> JumbfParseResult<JUMBFEmbeddedFileContentBox> {
+ let header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ if header.size == 0 {
+ // bad read, return empty box...
+ return Ok(JUMBFEmbeddedFileContentBox::new(Vec::new()));
+ } else if header.size != size {
+ // this means that we started w/o the header...
+ unread_bytes(reader, HEADER_SIZE)?;
+ }
+
+ // read data itself...
+ let data_len = size - HEADER_SIZE;
+ let mut buf = vec![0u8; data_len as usize];
+ reader.read_exact(&mut buf)?;
+
+ Ok(JUMBFEmbeddedFileContentBox::new(buf))
+ }
+
+ pub fn read_super_box<R: Read + Seek>(reader: &mut R) -> JumbfParseResult<JUMBFSuperBox> {
+ // find out where we're starting...
+ let start_pos = current_pos(reader).map_err(|_| JumbfParseError::InvalidBoxRange)?;
+
+ // start with the initial jumb
+ let jumb_header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::InvalidJumbfHeader)?;
+ if jumb_header.name == BoxType::Empty {
+ return Err(JumbfParseError::UnexpectedEof);
+ } else if jumb_header.name != BoxType::Jumb {
+ return Err(JumbfParseError::InvalidJumbfHeader);
+ }
+
+ // figure out where this particular box ends...
+ let dest_pos = start_pos + jumb_header.size;
+
+ // now let's load the jumd
+ let jumd_header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::ExpectedJumdError)?;
+ if jumb_header.name == BoxType::Empty {
+ return Err(JumbfParseError::UnexpectedEof);
+ } else if jumd_header.name != BoxType::Jumd {
+ return Err(JumbfParseError::ExpectedJumdError);
+ }
+
+ // load the description box & create a new superbox from it
+ let jdesc = BoxReader::read_desc_box(reader, jumd_header.size)
+ .map_err(|_| JumbfParseError::UnexpectedEof)?;
+ if jdesc.label().is_empty() {
+ return Err(JumbfParseError::UnexpectedEof);
+ }
+ let box_label = jdesc.label();
+ debug!(
+ "{}",
+ format!("START#Label: {:?}", box_label /*jdesc.label()*/)
+ );
+ let mut sbox = JUMBFSuperBox::from(jdesc);
+
+ // read each following box and add it to the sbox
+ let mut found = true;
+ while found {
+ let box_header =
+ BoxReader::read_header(reader).map_err(|_| JumbfParseError::InvalidJumbfHeader)?;
+ if box_header.name == BoxType::Empty {
+ found = false;
+ } else {
+ unread_bytes(reader, HEADER_SIZE)?; // seek back to the beginning of the box
+ let next_box: Box<dyn BMFFBox> = match box_header.name {
+ BoxType::Jumb => Box::new(
+ BoxReader::read_super_box(reader)
+ .map_err(|_| JumbfParseError::InvalidJumbBox)?,
+ ),
+ BoxType::Json => Box::new(
+ BoxReader::read_json_box(reader, box_header.size)
+ .map_err(|_| JumbfParseError::InvalidJsonBox)?,
+ ),
+ BoxType::Cbor => Box::new(
+ BoxReader::read_cbor_box(reader, box_header.size)
+ .map_err(|_| JumbfParseError::InvalidCborBox)?,
+ ),
+ BoxType::Padding => Box::new(
+ BoxReader::read_padding_box(reader, box_header.size)
+ .map_err(|_| JumbfParseError::InvalidCborBox)?,
+ ),
+ BoxType::Jp2c => Box::new(
+ BoxReader::read_jp2c_box(reader, box_header.size)
+ .map_err(|_| JumbfParseError::InvalidJp2cBox)?,
+ ),
+
+ BoxType::Uuid => Box::new(
+ BoxReader::read_uuid_box(reader, box_header.size)
+ .map_err(|_| JumbfParseError::InvalidUuidBox)?,
+ ),
+ BoxType::EmbedMediaDesc => Box::new(
+ BoxReader::read_embedded_media_desc_box(reader, box_header.size)
+ .map_err(|_| JumbfParseError::InvalidEmbeddedFileBox)?,
+ ),
+ BoxType::EmbedContent => Box::new(
+ BoxReader::read_embedded_content_box(reader, box_header.size)
+ .map_err(|_| JumbfParseError::InvalidEmbeddedFileBox)?,
+ ),
+ _ => {
+ debug!("{}", format!("Unknown Boxtype: {:?}", box_header.name));
+ // per the jumbf spec ignore unknown boxes so skip by if possible
+ let header = BoxReader::read_header(reader)
+ .map_err(|_| JumbfParseError::InvalidBoxHeader)?;
+ if header.size == 0 {
+ // bad read, return empty box...
+ return Err(JumbfParseError::InvalidUnknownBox);
+ } else if header.size != box_header.size {
+ // this means that we started w/o the header...
+ unread_bytes(reader, HEADER_SIZE)?;
+ }
+
+ // read data itself...
+ let data_len = box_header.size - HEADER_SIZE;
+ let mut buf = vec![0u8; data_len as usize];
+ reader.read_exact(&mut buf)?;
+ continue;
+ }
+ };
+ sbox.add_data_box(next_box);
+ }
+
+ // if our current position is past the size, bail out...
+ if let Ok(p) = current_pos(reader) {
+ if p >= dest_pos {
+ found = false;
+ }
+ }
+ }
+
+ debug!(
+ "{}",
+ format!("END#Label: {:?}", box_label /*jdesc.label()*/)
+ );
+
+ // return the filled out sbox
+ Ok(sbox)
+ }
+}
+
+// !SECTION
+
+//---------------
+// SECTION Tests
+//---------------
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use extfmt::*;
+
+ use std::io::Cursor;
+
+ // base_len = size (u32) + type (u32)
+ // desc_len = base + 16 (UUID type) + 1 (TOGGLE)
+ // cont_len = base
+ // sig_len = base + 16 (UUID type)
+ const BOX_BASE_LEN: usize = 4 + 4;
+ const DESC_BOX_BASE: usize = BOX_BASE_LEN + 16 + 1;
+ const CONT_BOX_BASE: usize = BOX_BASE_LEN;
+ const SIG_BOX_BASE: usize = BOX_BASE_LEN + 16;
+ const EMBED_MEDIA_BASE: usize = BOX_BASE_LEN + 1;
+ const EMBED_DATA_BASE: usize = BOX_BASE_LEN;
+
+ fn compute_desc_box_size(box_label: &str) -> usize {
+ DESC_BOX_BASE + box_label.len() + 1
+ }
+
+ // len is base + desc (base + len(label) + 1 (null term))
+ fn compute_super_box_size(box_label: &str) -> usize {
+ let mut len = BOX_BASE_LEN;
+ len += compute_desc_box_size(box_label);
+ len
+ }
+
+ fn compute_content_box_size(box_label: &str, data_size: usize) -> usize {
+ let content_box_expected_len = CONT_BOX_BASE + data_size;
+ let desc_box_expected_len = compute_desc_box_size(box_label);
+ BOX_BASE_LEN + desc_box_expected_len + content_box_expected_len
+ }
+
+ fn compute_signature_box_size(sig_size: usize) -> usize {
+ let content_box_expected_len = SIG_BOX_BASE + sig_size;
+ let desc_box_expected_len = compute_desc_box_size(labels::SIGNATURE);
+ BOX_BASE_LEN + desc_box_expected_len + content_box_expected_len
+ }
+
+ fn compute_media_type_box_size(media_type: &str, file_name: Option<&str>) -> usize {
+ let mut len = EMBED_MEDIA_BASE + media_type.len() + 1;
+ if let Some(f) = file_name {
+ len += f.len() + 1;
+ }
+ len
+ }
+
+ fn compute_embedded_box_size(data_size: usize) -> usize {
+ EMBED_DATA_BASE + data_size
+ }
+
+ fn compute_thumbnail_box_size(
+ box_label: &str,
+ data_size: usize,
+ media_type: &str,
+ file_name: Option<&str>,
+ ) -> usize {
+ let mut len = compute_super_box_size(box_label);
+ len += compute_media_type_box_size(media_type, file_name);
+ len += compute_embedded_box_size(data_size);
+ len
+ }
+
+ // ANCHOR: DescBox
+ #[test]
+ fn description_box() {
+ let box_label = "test.descbox";
+ let jdb = JUMBFDescriptionBox::new(box_label, None);
+ let mut mem_box: Vec<u8> = Vec::new();
+
+ jdb.write_box(&mut mem_box)
+ .expect("Unable to write description box");
+
+ println!("DescriptionBox:\t{}", Hexlify(&mem_box));
+ assert_eq!(
+ format!("{}", Hexlify(&mem_box)),
+ "000000266a756d640000000000000000000000000000000003746573742e64657363626f7800"
+ );
+
+ let expected_len = compute_desc_box_size(box_label);
+ assert_eq!(mem_box.len(), expected_len); // make sure the length is correct
+ }
+
+ // ANCHOR: SuperBox
+ #[test]
+ fn super_box() {
+ let box_label = "test.superbox";
+ let jsb = JUMBFSuperBox::new(box_label, None);
+ let mut mem_box: Vec<u8> = Vec::new();
+
+ jsb.write_box(&mut mem_box)
+ .expect("Unable to write superbox");
+
+ let expected_len = compute_super_box_size(box_label);
+ assert_eq!(mem_box.len(), expected_len); // make sure the length is correct
+
+ println!("SuperBox:\t{}", Hexlify(&mem_box));
+ assert_eq!(format!("{}", Hexlify(&mem_box)), "0000002f6a756d62000000276a756d640000000000000000000000000000000003746573742e7375706572626f7800");
+ }
+
+ // ANCHOR: SuperBox + Data Box
+ #[test]
+ fn super_box_with_one_data_box() {
+ let box_label = "test.superbox_databox";
+ let mut jsb = JUMBFSuperBox::new(box_label, None);
+
+ let data_box_label = "test.databox";
+ let jdb = Box::new(JUMBFSuperBox::new(data_box_label, None));
+ jsb.add_data_box(jdb);
+
+ // now write it and see what we get!!
+ let mut mem_box: Vec<u8> = Vec::new();
+ jsb.write_box(&mut mem_box)
+ .expect("Unable to write superbox");
+
+ let data_box_expected_len = compute_super_box_size(data_box_label);
+ let expected_len = data_box_expected_len + compute_super_box_size(box_label);
+ assert_eq!(mem_box.len(), expected_len); // make sure the length is correct
+
+ println!("SuperBox + DataBox:\t{}", Hexlify(&mem_box));
+ assert_eq!(format!("{}", Hexlify(&mem_box)), "000000656a756d620000002f6a756d640000000000000000000000000000000003746573742e7375706572626f785f64617461626f78000000002e6a756d62000000266a756d640000000000000000000000000000000003746573742e64617461626f7800");
+ }
+
+ // ANCHOR: Signature Box
+ #[test]
+ fn cai_signature_box() {
+ let mut sigb = CAISignatureBox::new();
+
+ let some_data = String::from("this would normally be binary signature data...");
+ let sig_len = some_data.len();
+ let sigc = CAISignatureContentBox::new(some_data.into_bytes());
+ sigb.add_signature(Box::new(sigc));
+
+ let mut mem_box: Vec<u8> = Vec::new();
+ sigb.write_box(&mut mem_box)
+ .expect("Unable to write CAI Signature");
+
+ // expected_len is base + desc_box + content+box
+ let expected_len = compute_signature_box_size(sig_len);
+ assert_eq!(mem_box.len(), expected_len); // make sure the length is correct
+
+ println!("CAISignatureBox:\t{}", Hexlify(&mem_box));
+ assert_eq!(format!("{}", Hexlify(&mem_box)), "000000776a756d62000000286a756d646332637300110010800000aa00389b7103633270612e7369676e61747572650000000047757569646332637300110010800000aa00389b717468697320776f756c64206e6f726d616c6c792062652062696e617279207369676e617475726520646174612e2e2e");
+ }
+
+ // ANCHOR: Claim Box
+ #[test]
+ fn cai_claim_box() {
+ let mut cb = CAIClaimBox::new();
+
+ let claim_json = String::from(
+ "{
+ \"recorder\" : \"Photoshop\",
+ \"parent_claim\" : \"self#jumbf=c_tpic_1/c2pa.claim?hl=6E6DD0923B57DCE\",
+ \"signature\" : \"self#jumbf=s_adbe_1\",
+ \"assertions\" : [
+ \"self#jumbf=as_adbe_1/c2pa.identity?hl=45919681DCCAF6ABAD\",
+ \"self#jumbf=as_adbe_1/c2pa.thumbnail.jpeg?hl=76142BD62363F\"
+ ],
+ \"redacted_assertions\" : [
+ \"self#jumbf=as_tp_1/c2pa.location.precise\"
+ ],
+ \"asset_hashes\": []
+ }",
+ );
+
+ let clen = claim_json.len();
+ let cjson = JUMBFJSONContentBox::new(claim_json.into_bytes());
+ cb.add_claim(Box::new(cjson));
+
+ let mut mem_box: Vec<u8> = Vec::new();
+ cb.write_box(&mut mem_box)
+ .expect("Unable to write CAI Claim");
+
+ let expected_len = compute_content_box_size(labels::CLAIM, clen);
+ assert_eq!(mem_box.len(), expected_len); // make sure the length is correct
+
+ println!("CAIClaimBox:\t{}", Hexlify(&mem_box));
+ assert_eq!(format!("{}", Hexlify(&mem_box)), "0000023b6a756d62000000246a756d646332636c00110010800000aa00389b7103633270612e636c61696d000000020f6a736f6e7b0a202020202020202020202020227265636f7264657222203a202250686f746f73686f70222c0a20202020202020202020202022706172656e745f636c61696d22203a202273656c66236a756d62663d635f747069635f312f633270612e636c61696d3f686c3d364536444430393233423537444345222c0a202020202020202020202020227369676e617475726522203a202273656c66236a756d62663d735f616462655f31222c0a20202020202020202020202022617373657274696f6e7322203a205b0a202020202020202020202020202020202273656c66236a756d62663d61735f616462655f312f633270612e6964656e746974793f686c3d343539313936383144434341463641424144222c0a202020202020202020202020202020202273656c66236a756d62663d61735f616462655f312f633270612e7468756d626e61696c2e6a7065673f686c3d37363134324244363233363346220a2020202020202020202020205d2c0a2020202020202020202020202272656461637465645f617373657274696f6e7322203a205b0a202020202020202020202020202020202273656c66236a756d62663d61735f74705f312f633270612e6c6f636174696f6e2e70726563697365220a2020202020202020202020205d2c0a2020202020202020202020202261737365745f686173686573223a205b5d0a20202020202020207d");
+ }
+
+ // ANCHOR: Location assertion
+ #[test]
+ fn cai_location_assertion_box() {
+ let box_label = "c2pa.location.broad";
+ let location = String::from("{ \"location\": \"San Francisco\"}");
+ let loc_len = location.len();
+
+ let mut cb = CAIJSONAssertionBox::new(box_label);
+ cb.add_json(location.into_bytes());
+
+ let mut mem_box: Vec<u8> = Vec::new();
+ cb.write_box(&mut mem_box)
+ .expect("Unable to write location.broad assertion");
+
+ let expected_len = compute_content_box_size(box_label, loc_len);
+ assert_eq!(mem_box.len(), expected_len); // make sure the length is correct
+
+ println!("CAI Broad Location:\t{}", Hexlify(&mem_box));
+ assert_eq!(format!("{}", Hexlify(&mem_box)), "0000005b6a756d620000002d6a756d646a736f6e00110010800000aa00389b7103633270612e6c6f636174696f6e2e62726f616400000000266a736f6e7b20226c6f636174696f6e223a202253616e204672616e636973636f227d");
+ }
+
+ // ANCHOR: Assertion Store
+ #[test]
+ fn assertion_store() {
+ // create the assertion store
+ let mut a_store = CAIAssertionStore::new();
+
+ // create some assertions & add to the store
+ let th_box_label = "c2pa.claim.thumbnail";
+ let img = String::from("<image data goes here>");
+ let img_len = img.len();
+ let mut tb = JumbfEmbeddedFileBox::new(th_box_label);
+ tb.add_data(img.into_bytes(), "image/jpeg".to_string(), None);
+ a_store.add_assertion(Box::new(tb));
+ let tb_len = compute_thumbnail_box_size(th_box_label, img_len, "image/jpeg", None);
+
+ let id_box_label = "c2pa.identity";
+ let identity = String::from("{ \"uri\": \"did:adobe:lrosenth@adobe.com\"}");
+ let id_len = identity.len();
+ let mut ib = CAIJSONAssertionBox::new(id_box_label);
+ ib.add_json(identity.into_bytes());
+ a_store.add_assertion(Box::new(ib));
+ let ib_len = compute_content_box_size(id_box_label, id_len);
+
+ // write it to memory
+ let mut mem_box: Vec<u8> = Vec::new();
+ a_store
+ .write_box(&mut mem_box)
+ .expect("Unable to write assertion store");
+
+ // and test the results
+ let store_sup_len = compute_super_box_size("c2pa.assertions");
+ let expected_len = store_sup_len + tb_len + ib_len;
+ assert_eq!(mem_box.len(), expected_len); // make sure the length is correct
+
+ println!("CAI Assertion Store:\t{}", Hexlify(&mem_box));
+ assert_eq!(format!("{}", Hexlify(&mem_box)), "000000f86a756d62000000296a756d646332617300110010800000aa00389b7103633270612e617373657274696f6e7300000000686a756d620000002e6a756d6440cb0c32bb8a489da70b2ad6f47f436903633270612e636c61696d2e7468756d626e61696c00000000146266646200696d6167652f6a706567000000001e626964623c696d616765206461746120676f657320686572653e0000005f6a756d62000000276a756d646a736f6e00110010800000aa00389b7103633270612e6964656e7469747900000000306a736f6e7b2022757269223a20226469643a61646f62653a6c726f73656e74684061646f62652e636f6d227d");
+ }
+
+ // ANCHOR: CAI Store
+ #[test]
+ fn cai_store() {
+ // create the CAI store
+ let store_label = "cb.adobe_1";
+ let mut cai_store = CAIStore::new(store_label, false);
+
+ // create the assertion store
+ let mut a_store = CAIAssertionStore::new();
+
+ // create an assertions & add to the store
+ let th_box_label = "c2pa.claim.thumbnail";
+ let img = String::from("<image data goes here>");
+ let img_len = img.len();
+ let mut tb = JumbfEmbeddedFileBox::new(th_box_label);
+ tb.add_data(img.into_bytes(), "image/jpeg".to_string(), None);
+ a_store.add_assertion(Box::new(tb));
+
+ // add the assertion store to the cai store
+ cai_store.add_box(Box::new(a_store));
+
+ // create a claim & add it to the cai store
+ let mut cb = CAIClaimBox::new();
+ let claim_json = String::from(
+ "{
+ \"recorder\" : \"Photoshop\",
+ \"signature\" : \"self#jumbf=s_adobe_1\",
+ \"assertions\" : [
+ \"self#jumbf=as_adobe_1/c2pa.thumbnail.jpeg?hl=76142BD62363F\"
+ ]
+ }",
+ );
+
+ let clen = claim_json.len();
+ let cjson = JUMBFJSONContentBox::new(claim_json.into_bytes());
+ cb.add_claim(Box::new(cjson));
+ cai_store.add_box(Box::new(cb));
+
+ // create a signature & add to the cai store
+ let mut sigb = CAISignatureBox::new();
+ let some_data = String::from("this would normally be binary signature data...");
+ let sig_len = some_data.len();
+ let sigc = CAISignatureContentBox::new(some_data.into_bytes());
+ sigb.add_signature(Box::new(sigc));
+ cai_store.add_box(Box::new(sigb));
+
+ // write it to memory
+ let mut mem_box: Vec<u8> = Vec::new();
+ cai_store
+ .write_box(&mut mem_box)
+ .expect("Unable to write CAI store");
+
+ // and test the results
+ let cai_store_sup_len = compute_super_box_size(store_label);
+ let a_store_sup_len = compute_super_box_size("c2pa.assertions");
+ let tb_len = compute_thumbnail_box_size(th_box_label, img_len, "image/jpeg", None);
+ let claim_len = compute_content_box_size(labels::CLAIM, clen);
+ let sig_box_len = compute_signature_box_size(sig_len);
+ let expected_len = cai_store_sup_len + a_store_sup_len + tb_len + claim_len + sig_box_len;
+ assert_eq!(mem_box.len(), expected_len); // make sure the length is correct
+
+ println!("C2PA Store:\t{}", Hexlify(&mem_box));
+ assert_eq!(format!("{}", Hexlify(&mem_box)), "0000024b6a756d62000000246a756d6463326d6100110010800000aa00389b710363622e61646f62655f3100000000996a756d62000000296a756d646332617300110010800000aa00389b7103633270612e617373657274696f6e7300000000686a756d620000002e6a756d6440cb0c32bb8a489da70b2ad6f47f436903633270612e636c61696d2e7468756d626e61696c00000000146266646200696d6167652f6a706567000000001e626964623c696d616765206461746120676f657320686572653e0000010f6a756d62000000246a756d646332636c00110010800000aa00389b7103633270612e636c61696d00000000e36a736f6e7b0a202020202020202020202020227265636f7264657222203a202250686f746f73686f70222c0a202020202020202020202020227369676e617475726522203a202273656c66236a756d62663d735f61646f62655f31222c0a20202020202020202020202022617373657274696f6e7322203a205b0a202020202020202020202020202020202273656c66236a756d62663d61735f61646f62655f312f633270612e7468756d626e61696c2e6a7065673f686c3d37363134324244363233363346220a2020202020202020202020205d0a20202020202020207d000000776a756d62000000286a756d646332637300110010800000aa00389b7103633270612e7369676e61747572650000000047757569646332637300110010800000aa00389b717468697320776f756c64206e6f726d616c6c792062652062696e617279207369676e617475726520646174612e2e2e");
+ }
+
+ // ANCHOR: CAI block
+ #[test]
+ fn cai_block() {
+ // create the CAI block
+ let mut cai_block = Cai::new();
+
+ // create the CAI store
+ let store_label = "cb.adobe_1";
+ let mut cai_store = CAIStore::new(store_label, false);
+
+ // create the assertion store
+ let mut a_store = CAIAssertionStore::new();
+
+ // create an assertions & add to the store
+ let loc_box_label = "c2pa.location.broad";
+ let location = String::from("{ \"location\": \"Margate City, NJ\"}");
+ let loc_len = location.len();
+ let mut loc_box = CAIJSONAssertionBox::new(loc_box_label);
+ loc_box.add_json(location.into_bytes());
+ a_store.add_assertion(Box::new(loc_box));
+
+ // add the assertion store to the cai store
+ cai_store.add_box(Box::new(a_store));
+
+ // create a claim & add it to the cai store
+ let mut cb = CAIClaimBox::new();
+ let claim_json = String::from(
+ "{
+ \"recorder\" : \"Photoshop\",
+ \"signature\" : \"self#jumbf=s_adobe_1\",
+ \"assertions\" : [
+ \"self#jumbf=as_adobe_1/c2pa.location.broad?hl=76142BD62363F\"
+ ]
+ }",
+ );
+
+ let clen = claim_json.len();
+ let cjson = JUMBFJSONContentBox::new(claim_json.into_bytes());
+ cb.add_claim(Box::new(cjson));
+ cai_store.add_box(Box::new(cb));
+
+ // create a signature & add to the cai store
+ let mut sigb = CAISignatureBox::new();
+ let some_data = String::from("this would normally be binary signature data...");
+ let sig_len = some_data.len();
+ let sigc = CAISignatureContentBox::new(some_data.into_bytes());
+ sigb.add_signature(Box::new(sigc));
+ cai_store.add_box(Box::new(sigb));
+
+ // finally add the completed cai store into the cai block
+ cai_block.add_box(Box::new(cai_store));
+
+ // write it to memory
+ let mut mem_box: Vec<u8> = Vec::new();
+ cai_block
+ .write_box(&mut mem_box)
+ .expect("Unable to write CAI block");
+
+ // and test the results
+ let cai_block_sup_len = compute_super_box_size(labels::MANIFEST_STORE);
+ let cai_store_sup_len = compute_super_box_size(store_label);
+ let a_store_sup_len = compute_super_box_size("c2pa.assertions");
+ let lb_len = compute_content_box_size(loc_box_label, loc_len);
+ let claim_len = compute_content_box_size(labels::CLAIM, clen);
+ let sig_box_len = compute_signature_box_size(sig_len);
+
+ let expected_len = cai_block_sup_len
+ + cai_store_sup_len
+ + a_store_sup_len
+ + lb_len
+ + claim_len
+ + sig_box_len;
+
+ assert_eq!(mem_box.len(), expected_len); // make sure the length is correct
+
+ println!("CAI Block:\t{}", Hexlify(&mem_box));
+ assert_eq!(format!("{}", Hexlify(&mem_box)), "000002676a756d620000001e6a756d646332706100110010800000aa00389b71036332706100000002416a756d62000000246a756d6463326d6100110010800000aa00389b710363622e61646f62655f31000000008f6a756d62000000296a756d646332617300110010800000aa00389b7103633270612e617373657274696f6e73000000005e6a756d620000002d6a756d646a736f6e00110010800000aa00389b7103633270612e6c6f636174696f6e2e62726f616400000000296a736f6e7b20226c6f636174696f6e223a20224d61726761746520436974792c204e4a227d0000010f6a756d62000000246a756d646332636c00110010800000aa00389b7103633270612e636c61696d00000000e36a736f6e7b0a202020202020202020202020227265636f7264657222203a202250686f746f73686f70222c0a202020202020202020202020227369676e617475726522203a202273656c66236a756d62663d735f61646f62655f31222c0a20202020202020202020202022617373657274696f6e7322203a205b0a202020202020202020202020202020202273656c66236a756d62663d61735f61646f62655f312f633270612e6c6f636174696f6e2e62726f61643f686c3d37363134324244363233363346220a2020202020202020202020205d0a20202020202020207d000000776a756d62000000286a756d646332637300110010800000aa00389b7103633270612e7369676e61747572650000000047757569646332637300110010800000aa00389b717468697320776f756c64206e6f726d616c6c792062652062696e617279207369676e617475726520646174612e2e2e");
+ }
+
+ // ANCHOR: JUMB BlockReader
+ #[test]
+ fn jumb_box_reader() {
+ const JUMB_TEST: &str = "000000026A756D62";
+ let buffer = hex::decode(JUMB_TEST).expect("decode failed");
+ let mut buf_reader = Cursor::new(buffer);
+ let jumb_header = BoxReader::read_header(&mut buf_reader).unwrap();
+ assert_eq!(jumb_header.size, 2);
+ assert_eq!(jumb_header.name, BoxType::Jumb);
+ }
+
+ // ANCHOR: DescriptionBox Reader
+ /*
+ #[test]
+ fn desc_box_reader() {
+ const JUMD_DESC: &str =
+ "000000256A756D62000000216A756D646332706100110010800000AA00389B7103633270612E763100";
+ let buffer = hex::decode(JUMD_DESC).expect("decode failed");
+ let mut buf_reader = Cursor::new(buffer);
+
+ let jumb_header = BoxReader::read_header(&mut buf_reader).unwrap();
+ assert_eq!(jumb_header.size, 0x25);
+ assert_eq!(jumb_header.name, BoxType::JumbBox);
+
+ let jumd_header = BoxReader::read_header(&mut buf_reader).unwrap();
+ assert_eq!(jumd_header.size, 0x21);
+ assert_eq!(jumd_header.name, BoxType::JumdBox);
+
+ let desc_box = BoxReader::read_desc_box(&mut buf_reader, jumd_header.size).unwrap();
+ assert_eq!(desc_box.label(), labels::MANIFEST_STORE);
+ assert_eq!(desc_box.uuid(), "6332706100110010800000AA00389B71");
+ }
+ */
+ // ANCHOR: JSON Content Box Reader
+ #[test]
+ fn json_box_reader() {
+ const JSON_BOX: &str ="0000005a6a756d620000002d6a756d646a736f6e00110010800000aa00389b7103633270612e6c6f636174696f6e2e62726f616400000000266a736f6e7b20226c6f636174696f6e223a202253616e204672616e636973636f227d";
+
+ let buffer = hex::decode(JSON_BOX).expect("decode failed");
+ let mut buf_reader = Cursor::new(buffer);
+ let super_box = BoxReader::read_super_box(&mut buf_reader).unwrap();
+
+ let desc_box = super_box.desc_box();
+ assert_eq!(desc_box.label(), "c2pa.location.broad");
+ assert_eq!(desc_box.uuid(), CAI_JSON_ASSERTION_UUID);
+ assert_eq!(super_box.data_box_count(), 1);
+
+ let json_box = super_box.data_box_as_json_box(0).unwrap();
+ assert_eq!(json_box.box_uuid(), JUMBF_JSON_UUID);
+ assert_eq!(json_box.json().len(), 30);
+ }
+
+ #[allow(dead_code)]
+ fn check_one_box(
+ parent_box: &JUMBFSuperBox,
+ index: usize,
+ count: usize,
+ label: &str,
+ uuid: &str,
+ ) {
+ let superbox = parent_box.data_box_as_superbox(index).unwrap();
+ assert_eq!(superbox.box_uuid(), JUMB_FOURCC);
+ assert_eq!(superbox.data_box_count(), count);
+
+ let desc_box = superbox.desc_box();
+ assert_eq!(desc_box.label(), label);
+ assert_eq!(desc_box.uuid(), uuid);
+ }
+
+ // ANCHOR: Full CAI Block Reader
+ /*
+ #[test]
+ fn cai_box_reader() {
+ const CAI_BOX: &str ="0000026a6a756d62000000216a756d646332706100110010800000AA00389B71036332706100000002446a756d62000000246a756d6463326D6100110010800000AA00389B710363622e61646f62655f31000000008f6a756d62000000296a756d646332617300110010800000AA00389B7103633270612e617373657274696f6e73000000005e6a756d620000002d6a756d646a736f6e00110010800000aa00389b7103633270612e6c6f636174696f6e2e62726f616400000000296a736f6e7b20226c6f636174696f6e223a20224d61726761746520436974792c204e4a227d000001126a756d62000000276a756d646332636C00110010800000AA00389B7103633270612e636c61696d2e763100000000e36a736f6e7b0a202020202020202020202020227265636f7264657222203a202250686f746f73686f70222c0a202020202020202020202020227369676e617475726522203a202273656c66236a756d62663d735f61646f62655f31222c0a20202020202020202020202022617373657274696f6e7322203a205b0a202020202020202020202020202020202273656c66236a756d62663d61735f61646f62655f312f633270612e6c6f636174696f6e2e62726f61643f686c3d37363134324244363233363346220a2020202020202020202020205d0a20202020202020207d000000776a756d62000000286a756d646332637300110010800000AA00389B7103633270612e7369676e61747572650000000047757569646332637300110010800000AA00389B717468697320776f756c64206e6f726d616c6c792062652062696e617279207369676e617475726520646174612e2e2e";
+
+ let buffer = hex::decode(CAI_BOX).expect("decode failed");
+ let mut buf_reader = Cursor::new(buffer);
+
+ // this loads up all the boxes...
+ let super_box = BoxReader::read_super_box(&mut buf_reader).unwrap();
+ let cai_block = Cai::from(super_box);
+
+ // check the CAI Block
+ let desc_box = cai_block.desc_box();
+ assert_eq!(desc_box.label(), labels::MANIFEST_STORE);
+ assert_eq!(desc_box.uuid(), CAI_BLOCK_UUID);
+
+ // it's children are the CAI stores
+ // for this test, we only have one...
+ assert_eq!(cai_block.data_box_count(), 1);
+
+ // retrieve the CAI store & validate it
+ // a standard one has 3 children (assertion store, claim & sig)
+ check_one_box(&cai_block.super_box(), 0, 3, "cb.adobe_1", CAI_STORE_UUID);
+ let cai_store_box = cai_block.store();
+
+ // retrieve the assertion store & validate
+ check_one_box(
+ &cai_store_box,
+ 0,
+ 1,
+ "c2pa.assertions",
+ CAI_ASSERTION_STORE_UUID,
+ );
+
+ let assertion_store_box = cai_store_box.data_box_as_superbox(0);
+
+ // there is only one in our test, but doing a loop on general principle
+ let num_assertions = assertion_store_box.data_box_count();
+ assert_eq!(num_assertions, 1);
+
+ for idx in 0..num_assertions {
+ check_one_box(
+ &assertion_store_box,
+ idx,
+ 1,
+ "c2pa.location.broad",
+ CAI_JSON_ASSERTION_UUID,
+ );
+
+ let assertion_box = assertion_store_box.data_box_as_superbox(idx);
+ let assertion_desc_box = assertion_box.desc_box();
+
+ if assertion_desc_box.uuid() == CAI_JSON_ASSERTION_UUID {
+ let json_box = assertion_box.data_box_as_json_box(0);
+ assert_eq!(json_box.box_uuid(), JUMBF_JSON_UUID);
+ assert_eq!(json_box.json().len(), 33);
+ } else if assertion_desc_box.uuid() == CAI_CODESTREAM_ASSERTION_UUID {
+ // this is where we'd validate for a thumbnail if we had one...
+ }
+ }
+
+ // retrieve the claim & validate
+ check_one_box(&cai_store_box, 1, 1, "c2pa.claim.v1", CAI_CLAIM_UUID);
+ let claim_superbox = cai_store_box.data_box_as_superbox(1);
+ let claim_desc_box = claim_superbox.desc_box();
+
+ if claim_desc_box.uuid() == CAI_JSON_ASSERTION_UUID {
+ // better be, but just in case...
+ let json_box = claim_superbox.data_box_as_json_box(0);
+ assert_eq!(json_box.box_uuid(), JUMBF_JSON_UUID);
+ assert_eq!(json_box.json().len(), 164);
+ }
+
+ // retrieve the signature & validate
+ check_one_box(&cai_store_box, 2, 1, "c2pa.signature", CAI_SIGNATURE_UUID);
+ let sig_superbox = cai_store_box.data_box_as_superbox(2);
+ let sig_desc_box = sig_superbox.desc_box();
+ if sig_desc_box.uuid() == CAI_SIGNATURE_UUID {
+ // better be, but just in case...
+ let sig_box = sig_superbox.data_box_as_uuid_box(0);
+ assert_eq!(sig_box.box_uuid(), JUMBF_UUID_UUID);
+ assert_eq!(sig_box.data().len(), 47);
+ }
+ }
+ */
+}
+
+// !SECTION
diff --git a/sdk/src/jumbf/boxio.rs b/sdk/src/jumbf/boxio.rs
@@ -0,0 +1,62 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! This is a library for I/O related constituent elements
+//!
+//! It is based on the work of Takeru Ohta <phjgt308@gmail.com>
+//! and [mse_fmp4](https://github.com/sile/mse_fmp4)
+
+use std::io::{sink, Result as IoResult, Sink, Write};
+
+#[derive(Debug)]
+pub struct ByteCounter<T> {
+ inner: T,
+ count: usize,
+}
+
+impl<T> ByteCounter<T> {
+ pub fn new(inner: T) -> Self {
+ ByteCounter { inner, count: 0 }
+ }
+
+ pub fn count(&self) -> usize {
+ self.count
+ }
+}
+
+impl ByteCounter<Sink> {
+ pub fn with_sink() -> Self {
+ Self::new(sink())
+ }
+
+ pub fn calculate<F>(f: F) -> IoResult<u64>
+ where
+ F: FnOnce(&mut Self) -> IoResult<()>,
+ {
+ let mut writer = ByteCounter::with_sink();
+ f(&mut writer)?;
+ Ok(writer.count() as u64)
+ }
+}
+
+impl<T: Write> Write for ByteCounter<T> {
+ fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
+ let size = self.inner.write(buf)?;
+ self.count += size;
+ Ok(size)
+ }
+
+ fn flush(&mut self) -> IoResult<()> {
+ self.inner.flush()
+ }
+}
diff --git a/sdk/src/jumbf/labels.rs b/sdk/src/jumbf/labels.rs
@@ -0,0 +1,241 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#![deny(missing_docs)]
+
+//! Labels for JUMBF boxes as defined in C2PA 1.0 Specification.
+//!
+//! See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_c2pa_box_details>.
+
+/// Label for the C2PA manifest store.
+///
+/// This value should be used when possible, since it may contain a version suffix
+/// when needed to support a future version of the spec.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_c2pa_box_details>.
+pub const MANIFEST_STORE: &str = "c2pa";
+
+/// Label for the C2PA assertion store box.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_c2pa_box_details>.
+pub const ASSERTIONS: &str = "c2pa.assertions";
+
+/// Label for the C2PA claim box.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_c2pa_box_details>.
+pub const CLAIM: &str = "c2pa.claim";
+
+/// Label for the C2PA claim signature box.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_c2pa_box_details>.
+pub const SIGNATURE: &str = "c2pa.signature";
+
+/// Label for the credentials store box.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_credential_storage>.
+pub const CREDENTIALS: &str = "c2pa.credentials";
+
+const JUMBF_PREFIX: &str = "self#jumbf";
+
+// Converts a manifest label to a JUMBF URI.
+pub(crate) fn to_manifest_uri(manifest_label: &str) -> String {
+ format!("{}=/{}/{}", JUMBF_PREFIX, MANIFEST_STORE, manifest_label)
+}
+
+// Converts a manifest label and an assertion label into a JUMBF URI.
+pub(crate) fn to_assertion_uri(manifest_label: &str, assertion_label: &str) -> String {
+ format!(
+ "{}/{}/{}",
+ to_manifest_uri(manifest_label),
+ ASSERTIONS,
+ assertion_label
+ )
+}
+
+// Converts a manifest label to a JUMBF URI for its signature.
+pub(crate) fn to_signature_uri(manifest_label: &str) -> String {
+ format!("{}/{}", to_manifest_uri(manifest_label), SIGNATURE)
+}
+
+// Converts a manifest label and an assertion label to a JUMBF
+// verifiable credential URL.
+pub(crate) fn to_verifiable_credential_uri(manifest_label: &str, vc_id: &str) -> String {
+ // TO CONSIDER: Does this now belong in jumbf::labels?
+ format!(
+ "{}/{}/{}",
+ to_manifest_uri(manifest_label),
+ CREDENTIALS,
+ vc_id
+ )
+}
+
+// Split off JUMBF prefix.
+pub(crate) fn to_normalized_uri(uri: &str) -> String {
+ let uri_parts: Vec<&str> = uri.split('=').collect();
+
+ let output = if uri_parts.len() == 1 {
+ uri_parts[0].to_string()
+ } else {
+ uri_parts[1].to_string()
+ };
+
+ // Add leading "/" if needed.
+ let mut manifest_store_part = MANIFEST_STORE.to_string();
+ manifest_store_part.push('/');
+
+ if !output.is_empty() && output.starts_with(&manifest_store_part) {
+ format!("{}{}", "/", output)
+ } else {
+ output
+ }
+}
+
+// Converts an absolute JUMBF URI to a URI relative to the manifest store.
+pub(crate) fn to_relative_uri(uri: &str) -> String {
+ let raw_uri = to_normalized_uri(uri);
+ let parts: Vec<&str> = raw_uri.split('/').collect();
+
+ if parts.len() > 4 && parts[1] == MANIFEST_STORE {
+ return format!("{}={}", JUMBF_PREFIX, parts[3..].join("/"));
+ } else {
+ // Doesn't look like an absolute URI, so we'll return it as-is.
+ uri.to_string()
+ }
+}
+
+// Given a JUMBF URI, return the manifest label contained within it.
+pub(crate) fn manifest_label_from_uri(uri: &str) -> Option<String> {
+ let raw_uri = to_normalized_uri(uri);
+ let parts: Vec<&str> = raw_uri.split('/').collect();
+ if parts.len() > 2 && parts[1] == MANIFEST_STORE {
+ Some(parts[2].to_string())
+ } else {
+ None
+ }
+}
+
+// Extract an assertion label from a JUMBF URI.
+pub(crate) fn assertion_label_from_uri(uri: &str) -> Option<String> {
+ let raw_uri = to_normalized_uri(uri);
+ let parts: Vec<&str> = raw_uri.split('/').collect();
+ if parts.len() > 4 && parts[1] == MANIFEST_STORE && parts[3] == ASSERTIONS {
+ Some(parts[4].to_string())
+ } else if parts[0] == ASSERTIONS {
+ Some(parts[1].to_string())
+ } else {
+ None
+ }
+}
+
+// Extract the box the label points to.
+pub(crate) fn box_name_from_uri(uri: &str) -> Option<String> {
+ let raw_uri = to_normalized_uri(uri);
+ let parts: Vec<&str> = raw_uri.split('/').collect();
+
+ parts.last().map(|b| b.to_string())
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ #[test]
+ fn test_manifest_uri() {
+ assert_eq!(
+ to_manifest_uri("acme::urn:uuid::123:456:789"),
+ "self#jumbf=/c2pa/acme::urn:uuid::123:456:789"
+ );
+ }
+
+ #[test]
+ fn test_assertion_uri() {
+ assert_eq!(
+ to_assertion_uri("acme::urn:uuid::123:456:789", "c2pa.thumbnail.claim.jpeg"),
+ "self#jumbf=/c2pa/acme::urn:uuid::123:456:789/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ );
+ }
+
+ #[test]
+ fn test_signature_uri() {
+ assert_eq!(
+ to_signature_uri("acme::urn:uuid::123:456:789"),
+ "self#jumbf=/c2pa/acme::urn:uuid::123:456:789/c2pa.signature"
+ );
+ }
+
+ #[test]
+ fn test_verifiable_credential_uri() {
+ assert_eq!(
+ to_verifiable_credential_uri("acme::urn:uuid::123:456:789", "12315142234@acme.com"),
+ "self#jumbf=/c2pa/acme::urn:uuid::123:456:789/c2pa.credentials/12315142234@acme.com"
+ );
+ }
+
+ #[test]
+ fn test_relative_uri() {
+ assert_eq!(
+ to_relative_uri(
+ "self#jumbf=/c2pa/acme::urn:uuid::123:456:789/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ ),
+ "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ );
+ }
+
+ #[test]
+ fn test_paths() {
+ let manifest = "acme::urn:uuid::123:456:789";
+ let assertion = "c2pa.thumbnail.claim.jpeg";
+ let empty_uri = "";
+ let absolute_uri = to_manifest_uri(manifest);
+
+ let raw_uri = to_normalized_uri(&absolute_uri);
+
+ let raw_uri_no_slash =
+ to_normalized_uri(&format!("{}={}/{}", JUMBF_PREFIX, MANIFEST_STORE, manifest));
+
+ let raw_empty_uri = to_normalized_uri(empty_uri);
+
+ assert_eq!(raw_uri, raw_uri_no_slash);
+ assert_eq!(raw_empty_uri, "");
+
+ let manifest_label_from_absolute = manifest_label_from_uri(&absolute_uri);
+ let manifest_label_from_nomalized = manifest_label_from_uri(&raw_uri);
+
+ assert_eq!(manifest_label_from_absolute, manifest_label_from_nomalized);
+
+ let assertion_uri = to_assertion_uri(manifest, assertion);
+
+ assert_eq!(
+ Some(manifest.to_string()),
+ manifest_label_from_uri(&assertion_uri)
+ );
+ assert_eq!(
+ Some(assertion.to_string()),
+ assertion_label_from_uri(&assertion_uri)
+ );
+ assert_eq!(None, assertion_label_from_uri(&absolute_uri));
+
+ let assertion_relative = to_relative_uri(&assertion_uri);
+
+ assert_eq!(
+ assertion_relative,
+ format!("{}={}/{}", JUMBF_PREFIX, ASSERTIONS, assertion)
+ );
+ assert_eq!(
+ Some(assertion.to_string()),
+ assertion_label_from_uri(&assertion_relative)
+ );
+ }
+}
diff --git a/sdk/src/jumbf/mod.rs b/sdk/src/jumbf/mod.rs
@@ -0,0 +1,16 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+pub mod boxes;
+pub mod boxio;
+pub mod labels;
diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs
@@ -0,0 +1,213 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::fs;
+use std::io::Cursor;
+use std::path::{Path, PathBuf};
+
+use crate::asset_handlers::{c2pa_io::C2paIO, jpeg_io::JpegIO, png_io::PngIO};
+use crate::asset_io::{AssetIO, CAILoader, HashObjectPositions};
+use crate::error::{Error, Result};
+use crate::status_tracker::StatusTracker;
+use crate::store::Store;
+
+static SUPPORTED_TYPES: &[&str; 6] = &[
+ "c2pa", // stand-alone manifest file
+ "jpg",
+ "jpeg",
+ "png",
+ "image/jpeg",
+ "image/png",
+];
+
+/// Return jumbf block from in memory asset
+pub fn load_jumbf_from_memory(asset_type: &str, data: &[u8]) -> Result<Vec<u8>> {
+ let mut buf_reader = Cursor::new(data);
+
+ let cai_block = match get_cailoader_handler(asset_type) {
+ Some(asset_handler) => asset_handler.read_cai(&mut buf_reader)?,
+ None => return Err(Error::UnsupportedType),
+ };
+ if cai_block.is_empty() {
+ return Err(Error::JumbfNotFound);
+ }
+ Ok(cai_block)
+}
+
+/// Return Store from in memory asset
+pub fn load_cai_from_memory(
+ asset_type: &str,
+ data: &[u8],
+ validation_log: &mut impl StatusTracker,
+) -> Result<Store> {
+ load_jumbf_from_memory(asset_type, data).and_then(|cai_block| {
+ // load and validate with CAI toolkit and dump if desired
+ Store::from_jumbf(&cai_block, validation_log)
+ })
+}
+
+// TODO [scouten]: Find a cleaner way to opt in or out of PDF IO.
+#[cfg(not(target_arch = "wasm32"))]
+pub fn get_assetio_handler(ext: &str) -> Option<Box<dyn AssetIO>> {
+ match ext {
+ "c2pa" => Some(Box::new(C2paIO {})),
+ "jpg" | "jpeg" => Some(Box::new(JpegIO {})),
+ "png" => Some(Box::new(PngIO {})),
+ _ => None,
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn get_assetio_handler(ext: &str) -> Option<Box<dyn AssetIO>> {
+ match ext {
+ "c2pa" => Some(Box::new(C2paIO {})),
+ "jpg" | "jpeg" => Some(Box::new(JpegIO {})),
+ "png" => Some(Box::new(PngIO {})),
+ _ => None,
+ }
+}
+
+// TODO [scouten]: Find a cleaner way to opt in or out of PDF IO.
+#[cfg(not(target_arch = "wasm32"))]
+pub fn get_cailoader_handler(asset_type: &str) -> Option<Box<dyn CAILoader>> {
+ match asset_type {
+ "c2pa" | "application/c2pa" => Some(Box::new(C2paIO {})),
+ "jpg" | "jpeg" | "image/jpeg" => Some(Box::new(JpegIO {})),
+ "png" | "image/png" => Some(Box::new(PngIO {})),
+ _ => None,
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn get_cailoader_handler(asset_type: &str) -> Option<Box<dyn CAILoader>> {
+ match asset_type {
+ "c2pa" | "application/c2pa" => Some(Box::new(C2paIO {})),
+ "jpg" | "jpeg" | "image/jpeg" => Some(Box::new(JpegIO {})),
+ "png" | "image/png" => Some(Box::new(PngIO {})),
+ _ => None,
+ }
+}
+
+pub fn get_file_extension(path: &Path) -> Option<String> {
+ let ext_osstr = path.extension()?;
+
+ let ext = ext_osstr.to_str()?;
+
+ Some(ext.to_lowercase())
+}
+
+pub fn get_supported_file_extension(path: &Path) -> Option<String> {
+ let ext = get_file_extension(path)?;
+
+ if SUPPORTED_TYPES.contains(&ext.as_ref()) {
+ Some(ext)
+ } else {
+ None
+ }
+}
+
+/// save_jumbf to a file
+/// in_path - path is source file
+/// out_path - path to the output file
+/// If no output file is given an new file will be created with "-c2pa" appending to file name e.g. "test.jpg" => "test-c2pa.jpg"
+/// If input == output then the input file will be overwritten.
+pub fn save_jumbf_to_file(data: &[u8], in_path: &Path, out_path: Option<&Path>) -> Result<()> {
+ let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
+
+ // if no output path make a new file based off of source file name
+ let img_out_path: PathBuf = match out_path {
+ Some(p) => p.to_owned(),
+ None => {
+ let filename_osstr = in_path.file_stem().ok_or(Error::UnsupportedType)?;
+ let filename = filename_osstr.to_str().ok_or(Error::UnsupportedType)?;
+
+ let out_name = format!("{}-c2pa.{}", filename, ext);
+ in_path.to_owned().with_file_name(out_name)
+ }
+ };
+
+ // clone output to be overwritten
+ if in_path != img_out_path {
+ fs::copy(&in_path, &img_out_path).map_err(Error::IoError)?;
+ }
+
+ match get_assetio_handler(&ext) {
+ Some(asset_handler) => asset_handler.save_cai_store(&img_out_path, data),
+ _ => Err(Error::UnsupportedType),
+ }
+}
+
+/// Updates jumbf content in a file, this will directly patch the contents no other processing is done.
+/// The search for content to replace only occurs over the jumbf content.
+/// Note: it is recommended that the replace contents be <= length of the search content so that the length of the
+/// file does not change. If it does that could make the new file unreadable. This function is primarily useful for
+/// generating test data since depending on how the file is rewritten the hashing mechanism should detect any tampering of the data.
+///
+/// out_path - path to file to be updated
+/// search_bytes - bytes to be replaced
+/// replace_bytes - replacement bytes
+/// returns the location where splice occurred
+#[cfg(test)] // this only used in unit tests
+pub fn update_file_jumbf(
+ out_path: &Path,
+ search_bytes: &[u8],
+ replace_bytes: &[u8],
+) -> Result<usize> {
+ use crate::utils::patch::patch_bytes;
+
+ let mut jumbf = load_jumbf_from_file(out_path)?;
+
+ let splice_point = patch_bytes(&mut jumbf, search_bytes, replace_bytes)?;
+
+ save_jumbf_to_file(&jumbf, out_path, Some(out_path))?;
+
+ Ok(splice_point)
+}
+
+/// load the JUMBF block from an asset if available
+pub fn load_jumbf_from_file(in_path: &Path) -> Result<Vec<u8>> {
+ let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
+
+ match get_assetio_handler(&ext) {
+ Some(asset_handler) => asset_handler.read_cai_store(in_path),
+ _ => Err(Error::UnsupportedType),
+ }
+}
+
+/// load a CAI store from a file
+///
+/// in_path - path to source file
+/// validation_log - optional vec to contain addition info about the asset
+pub fn load_cai_from_file(
+ in_path: &Path,
+ validation_log: &mut impl StatusTracker,
+) -> Result<Store> {
+ // get jumbf block
+ load_jumbf_from_file(in_path).and_then(|buffer| {
+ if buffer.is_empty() {
+ return Err(Error::JumbfNotFound);
+ }
+
+ // load and validate with CAI toolkit and dump if desired
+ Store::from_jumbf(&buffer, validation_log)
+ })
+}
+
+pub fn object_locations(in_path: &Path) -> Result<Vec<HashObjectPositions>> {
+ let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
+
+ match get_assetio_handler(&ext) {
+ Some(asset_handler) => asset_handler.get_object_locations(in_path),
+ _ => Err(Error::UnsupportedType),
+ }
+}
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
@@ -0,0 +1,118 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#![deny(warnings)]
+#![deny(clippy::expect_used)]
+#![deny(clippy::panic)]
+#![deny(clippy::unwrap_used)]
+
+//! This library supports reading, creating and embedding C2PA data
+//! with JPEG and PNG images.
+//!
+//! # Example: Reading and displaying a manifest as JSON
+//!
+//! ```
+//! # use c2pa::Result;
+//! use c2pa::ManifestStore;
+//! # fn main() -> Result<()> {
+//! let manifest_store = ManifestStore::from_file("tests/fixtures/C.jpg")?;
+//! println!("{}", manifest_store);
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! # Example: Adding a manifest to a file
+//!
+//! ```
+//! # use c2pa::Result;
+//! use c2pa::{
+//! Manifest,
+//! openssl::temp_signer::get_signer,
+//! assertions::User
+//! };
+//! use std::path::PathBuf;
+//! use tempfile::tempdir;
+//! # fn main() -> Result<()> {
+//! let mut manifest = Manifest::new("my_app".to_owned());
+//! manifest.add_assertion(&User::new("org.contentauth.mylabel",r#"{"my_tag":"Anything I want"}"#))?;
+//! let source = PathBuf::from("tests/fixtures/C.jpg");
+//! let dir = tempdir()?;
+//! let dest = dir.path().join("test_file.jpg");
+//! let (signer, _) = get_signer(&dir.path());
+//! manifest.embed(&source, &dest, &signer)?;
+//! # Ok(())
+//! # }
+//! ```
+
+pub use assertion::{
+ Assertion, AssertionBase, AssertionCbor, AssertionDecodeResult, AssertionJson,
+};
+pub mod assertions;
+mod cose_validator;
+mod error;
+pub use error::{Error, Result};
+mod ingredient;
+pub use ingredient::{Ingredient, IngredientOptions};
+pub mod jumbf_io; // used by make_tests
+mod manifest;
+pub use manifest::{Manifest, ManifestAssertion};
+mod manifest_store;
+pub use manifest_store::ManifestStore;
+mod manifest_store_report;
+pub use manifest_store_report::ManifestStoreReport;
+
+#[cfg(feature = "file_io")]
+pub(crate) mod ocsp_utils;
+#[cfg(feature = "file_io")]
+pub mod openssl;
+#[cfg(feature = "file_io")]
+pub mod signer;
+#[cfg(feature = "async_signer")]
+pub use signer::{AsyncPlaceholder, AsyncSigner};
+/// crate private declarations
+#[allow(dead_code, clippy::enum_variant_names)]
+pub(crate) mod asn1;
+pub(crate) mod assertion;
+pub(crate) mod asset_handlers;
+pub(crate) mod asset_io;
+pub(crate) mod claim;
+pub mod validation_status;
+// TODO: Make this a private module again once we no longer need
+// access to this from claims signer.
+#[cfg(feature = "file_io")]
+pub(crate) mod cose_sign;
+
+#[cfg(feature = "file_io")]
+pub(crate) mod embedded_xmp;
+
+pub(crate) mod hashed_uri;
+#[allow(dead_code)]
+pub(crate) mod jumbf;
+pub(crate) mod salt;
+#[cfg(feature = "file_io")]
+pub(crate) use signer::Signer;
+pub(crate) mod status_tracker;
+pub(crate) mod store;
+pub(crate) mod time_stamp;
+pub(crate) mod utils;
+pub(crate) use utils::cbor_types;
+pub(crate) use utils::hash_utils;
+pub(crate) use utils::xmp_inmemory_utils;
+pub(crate) mod validator;
+#[cfg(target_arch = "wasm32")]
+pub mod wasm;
+
+/// The internal name of the C2PA SDK
+pub const NAME: &str = "c2pa-rs";
+/// The version of this C2PA SDK
+pub const VERSION: &str = env!("CARGO_PKG_VERSION");
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -0,0 +1,795 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#[cfg(feature = "file_io")]
+use crate::utils::thumbnail::make_thumbnail;
+use crate::{
+ assertion::{AssertionBase, AssertionData, AssertionDecodeError},
+ assertions::{labels, Actions, CreativeWork, SchemaDotOrg, Thumbnail, UserCbor},
+ claim::Claim,
+ error::{Error, Result},
+ jumbf,
+ store::Store,
+ Ingredient,
+};
+
+#[cfg(feature = "file_io")]
+use crate::Signer;
+use log::{debug, error, warn};
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use serde_json::Value;
+use std::collections::HashMap;
+#[cfg(feature = "file_io")]
+use std::path::Path;
+
+const GH_UA: &str = "Sec-CH-UA";
+
+/// A Manifest represents all the information in a c2pa manifest
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Manifest {
+ /// Optional prefix added to the generated Manifest Label
+ /// This is typically Internet domain name for the vendor (i.e. `adobe`)
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub vendor: Option<String>,
+
+ /// A User Agent formatted string identifying the software/hardware/system produced this claim
+ /// Spaces are not allowed in names, versions can be specified with product/1.0 syntax
+ pub claim_generator: String,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ claim_generator_hints: Option<HashMap<String, Value>>,
+
+ /// Information about the asset associated with this manifest
+ #[serde(skip_serializing_if = "Option::is_none")]
+ asset: Option<Ingredient>,
+
+ /// A List of ingredients
+ ingredients: Vec<Ingredient>,
+
+ /// A List of verified credentials
+ #[serde(skip_serializing_if = "Option::is_none")]
+ credentials: Option<Vec<Value>>,
+
+ /// A list of assertions
+ assertions: Vec<ManifestAssertion>,
+
+ /// A list of redactions - URIs to a redacted assertions
+ #[serde(skip_serializing_if = "Option::is_none")]
+ redactions: Option<Vec<String>>,
+
+ /// Signature data (only used for reporting)
+ #[serde(skip_serializing_if = "Option::is_none")]
+ signature_info: Option<SignatureInfo>,
+}
+
+impl Manifest {
+ /// Create a new Manifest
+ /// requires a claim_generator string (User Agent))
+ pub fn new(claim_generator: String) -> Self {
+ Self {
+ vendor: None,
+ claim_generator,
+ claim_generator_hints: None,
+ asset: None,
+ ingredients: Vec::new(),
+ assertions: Vec::new(),
+ redactions: None,
+ credentials: None,
+ signature_info: None,
+ }
+ }
+
+ pub fn claim_generator(&self) -> &str {
+ self.claim_generator.as_str()
+ }
+
+ /// Returns an [Ingredient] reference to the asset associated with this manifest
+ pub fn asset(&self) -> Option<&Ingredient> {
+ self.asset.as_ref()
+ }
+
+ /// Returns the [Ingredient]s used by this Manifest
+ /// This can include a parent as well as any placed assets
+ pub fn ingredients(&self) -> &[Ingredient] {
+ &self.ingredients
+ }
+
+ /// Returns Assertions for this Manifest
+ pub fn assertions(&self) -> &[ManifestAssertion] {
+ &self.assertions
+ }
+
+ /// Returns Verifiable Credentials
+ pub fn credentials(&self) -> Option<&[Value]> {
+ self.credentials.as_deref()
+ }
+
+ /// Sets the vendor prefix to be used when generating manifest labels
+ /// Optional prefix added to the generated Manifest Label
+ /// This is typically a lower case Internet domain name for the vendor (i.e. `adobe`)
+ pub fn set_vendor(&mut self, vendor: String) -> &mut Self {
+ self.vendor = Some(vendor);
+ self
+ }
+
+ /// Sets a human readable name for the product that created this manifest
+ pub fn set_claim_generator(&mut self, generator: String) -> &mut Self {
+ self.claim_generator = generator;
+ self
+ }
+
+ /// Sets an ingredient as the container asset
+ pub fn set_asset(&mut self, ingredient: Ingredient) -> &mut Self {
+ self.asset = Some(ingredient);
+ self
+ }
+
+ pub fn signature_info(&self) -> Option<&SignatureInfo> {
+ self.signature_info.as_ref()
+ }
+
+ /// Sets the parent ingredient, assuring it is first and setting the is_parent flag
+ pub fn set_parent(&mut self, mut ingredient: Ingredient) -> Result<&mut Self> {
+ // there should only be one parent so return an error if we already have one
+ if self.ingredients.iter().any(|i| i.is_parent()) {
+ error!("parent already added");
+ return Err(Error::BadParam("Parent parent already added".to_owned()));
+ }
+ // if the hash of our new ingredient does not match any of the ingredients
+ // then add it
+ if !self
+ .ingredients
+ .iter()
+ .any(|i| ingredient.hash().is_some() && i.hash() == ingredient.hash())
+ {
+ debug!("ws:set_parent {:?}", ingredient.title());
+ ingredient.set_parent_state(true);
+ self.ingredients.insert(0, ingredient);
+ } else {
+ // dup so just keep the ingredient instead of adding the parent
+ warn!("duplicate parent {}", ingredient.title());
+ }
+
+ Ok(self)
+ }
+
+ /// Add an ingredient removing duplicates (consumes the asset)
+ pub fn add_ingredient(&mut self, ingredient: Ingredient) -> &mut Self {
+ // if the hash of the new asset does not match any of the ingredients
+ // then add it
+ if !self
+ .ingredients
+ .iter()
+ .any(|i| ingredient.hash().is_some() && i.hash() == ingredient.hash())
+ {
+ debug!("Manifest:add_ingredient {:?}", ingredient.title());
+ self.ingredients.push(ingredient);
+ } else {
+ warn!("duplicate ingredient {}", ingredient.title());
+ }
+ self
+ }
+
+ /// Adds assertion using given label - the data for predefined assertions must be in correct format
+ pub fn add_labeled_assertion<T: Serialize>(
+ &mut self,
+ label: &str,
+ data: &T,
+ ) -> Result<&mut Self> {
+ self.assertions
+ .push(ManifestAssertion::from_labeled_assertion(label, data)?);
+ Ok(self)
+ }
+
+ /// Adds assertions, data for predefined assertions must be in correct format
+ pub fn add_assertion<T: Serialize + AssertionBase>(&mut self, data: &T) -> Result<&mut Self> {
+ self.assertions
+ .push(ManifestAssertion::from_assertion(data)?);
+ Ok(self)
+ }
+
+ /// Retrieves an assertion by label if it exists or Error::NotFound
+ pub fn find_assertion<T: DeserializeOwned>(&mut self, label: &str) -> Result<T> {
+ if let Some(manifest_assertion) = self.assertions.iter().find(|a| a.label == label) {
+ manifest_assertion.to_assertion()
+ } else {
+ Err(Error::NotFound)
+ }
+ }
+
+ // keep this private until we support it externally
+ #[allow(dead_code)]
+ pub(crate) fn add_redaction(&mut self, label: &str) -> Result<&mut Self> {
+ // todo: any way to verify if this assertion exists in the parent claim here?
+ match self.redactions.as_mut() {
+ Some(redactions) => redactions.push(label.to_string()),
+ None => self.redactions = Some([label.to_string()].to_vec()),
+ }
+ Ok(self)
+ }
+
+ /// Add verifiable credentials
+ pub fn add_verifiable_credential<T: Serialize>(&mut self, data: &T) -> Result<&mut Self> {
+ let value = serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?;
+ match self.credentials.as_mut() {
+ Some(credentials) => credentials.push(value),
+ None => self.credentials = Some([value].to_vec()),
+ }
+ Ok(self)
+ }
+
+ /// Sets the signature information for the report
+ pub fn set_signature(&mut self, issuer: Option<&String>, time: Option<&String>) -> &mut Self {
+ self.signature_info = Some(SignatureInfo {
+ issuer: issuer.cloned(),
+ time: time.cloned(),
+ });
+ self
+ }
+
+ /// Returns the name of the signature issuer
+ pub fn issuer(&self) -> Option<String> {
+ self.signature_info.to_owned().and_then(|sig| sig.issuer)
+ }
+
+ /// Returns the time that the manifest was signed
+ pub fn time(&self) -> Option<String> {
+ self.signature_info.to_owned().and_then(|sig| sig.time)
+ }
+
+ // Generates a Manifest given a store and a manifest label
+ pub(crate) fn from_store(store: &Store, manifest_label: &str) -> Result<Self> {
+ let claim = store
+ .get_claim(manifest_label)
+ .ok_or_else(|| Error::ClaimMissing {
+ label: manifest_label.to_owned(),
+ })?;
+
+ // extract vendor from claim label
+ let claim_generator = claim.claim_generator().to_owned();
+ let mut manifest = Manifest::new(claim_generator);
+
+ manifest.claim_generator_hints = claim.get_claim_generator_hint_map().cloned();
+
+ // get credentials converting from AssertionData to Value
+ manifest.credentials = Some(
+ claim
+ .get_verifiable_credentials()
+ .iter()
+ .filter_map(|d| match d {
+ AssertionData::Json(s) => serde_json::from_str(s).ok(),
+ _ => None,
+ })
+ .collect(),
+ );
+
+ manifest.redactions = claim.redactions().map(|rs| {
+ rs.iter()
+ .filter_map(|r| jumbf::labels::assertion_label_from_uri(r))
+ .collect()
+ });
+
+ let title = claim.title().map_or("".to_owned(), |s| s.to_owned());
+ let format = claim.format();
+ let instance_id = claim.instance_id();
+
+ let mut asset = Ingredient::new(&title, format, instance_id);
+
+ for claim_assertion in claim.claim_assertion_store().iter() {
+ let assertion = claim_assertion.assertion();
+ let label = assertion.label();
+ debug!("assertion = {}", label);
+ match label.as_ref() {
+ labels::INGREDIENT => {
+ let assertion_uri = jumbf::labels::to_assertion_uri(claim.label(), &label);
+ let ingredient = Ingredient::from_ingredient_uri(store, &assertion_uri)?;
+ manifest.add_ingredient(ingredient);
+ }
+ Actions::LABEL => {
+ let actions = Actions::from_assertion(assertion)?;
+ manifest.add_assertion(&actions)?; // assertion.as_json_object()?)?;
+ }
+ label if label.starts_with(labels::CLAIM_THUMBNAIL) => {
+ let thumbnail = Thumbnail::from_assertion(assertion)?;
+ asset.set_thumbnail(thumbnail.content_type, thumbnail.data);
+ }
+ _ => {
+ // inject assertions for all json data
+ match assertion.decode_data() {
+ AssertionData::Json(_x) => {
+ let value = assertion.as_json_object()?;
+ manifest.add_labeled_assertion(&label, &value)?;
+ }
+ AssertionData::Cbor(_x) => {
+ let value = assertion.as_json_object()?; //todo: should this be cbor?
+ manifest.add_labeled_assertion(&label, &value)?;
+ }
+ AssertionData::Binary(_x) => {
+ //let _value = Value::String("<omitted>".to_owned());
+ // claim_report.add_assertion(&label, &value)?;
+ }
+ AssertionData::Uuid(_, _) => {}
+ }
+ }
+ }
+ }
+
+ manifest.set_asset(asset);
+
+ let issuer = claim.signing_issuer();
+ let signing_time = claim
+ .signing_time()
+ .map(|signing_time| signing_time.to_rfc3339());
+
+ if issuer.is_some() || signing_time.is_some() {
+ debug!(
+ "added signature issuer={:?} time={:?}",
+ issuer, signing_time
+ );
+ manifest.set_signature(issuer.as_ref(), signing_time.as_ref());
+ }
+
+ Ok(manifest)
+ }
+
+ /// Sets the asset field from data in a file
+ /// the information in the claim should reflect the state of the asset it is embedded in
+ /// this method can be used to ensure that data is correct
+ /// it will extract filename,format and xmp info and generate a thumbnail
+ #[cfg(feature = "file_io")]
+ pub fn set_asset_from_path<P: AsRef<Path>>(&mut self, path: P) {
+ // Gather the information we need from the target path
+ let mut ingredient = Ingredient::from_file_info(path.as_ref());
+
+ if let Ok((format, image)) = make_thumbnail(path.as_ref()) {
+ ingredient.set_thumbnail(format, image);
+ }
+
+ // if there is already an asset title preserve it
+ if let Some(title) = self.asset.as_ref().map(|i| i.title()) {
+ ingredient.set_title(title.to_string());
+ };
+
+ // set asset to newly created ingredient
+ self.asset = Some(ingredient);
+ }
+
+ // Convert a Manifest into a Store
+ pub(crate) fn to_store(&self) -> Result<Store> {
+ // add library identifier to claim_generator
+ let generator = format!(
+ "{} {}/{}",
+ &self.claim_generator,
+ crate::NAME,
+ crate::VERSION
+ );
+ let mut claim = Claim::new(&generator, self.vendor.as_deref());
+
+ // add any verified credentials - needs to happen early so we can reference them
+ let mut vc_table = HashMap::new();
+ if let Some(verified_credentials) = self.credentials.as_ref() {
+ for vc in verified_credentials {
+ let vc_str = &vc.to_string();
+ let id = Claim::vc_id(vc_str)?;
+ vc_table.insert(id, claim.add_verifiable_credential(vc_str)?);
+ }
+ }
+
+ // if the Manifest has an asset field use it to set these claim fields
+ if let Some(asset) = self.asset.as_ref() {
+ claim.set_title(Some(asset.title().to_owned()));
+ claim.format = asset.format().to_owned();
+ claim.instance_id = asset.instance_id().to_owned();
+ if let Some((format, image)) = asset.thumbnail() {
+ claim.add_assertion(&Thumbnail::new(
+ &labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, format),
+ image.to_vec(),
+ ))?;
+ }
+ }
+
+ // add all ingredients to the claim
+ for ingredient in &self.ingredients {
+ ingredient.add_to_claim(&mut claim, self.redactions.clone())?;
+ }
+
+ // add a claim_generator_hint for the version of the library used to create the claim
+ let lib_hint = format!("\"{}\";v=\"{}\"", crate::NAME, crate::VERSION);
+ claim.add_claim_generator_hint(GH_UA, Value::from(lib_hint));
+
+ // add any additional assertions
+ for assertion in &self.assertions {
+ match assertion.label.as_str() {
+ Actions::LABEL => {
+ // todo: fixup parameters field from instance_id to ingredient uri for
+ // c2pa.transcoded, c2pa.repackaged, and c2pa.placed action
+ claim.add_assertion(&Actions::from_json_value(&assertion.data)?)
+ }
+ CreativeWork::LABEL => {
+ let mut cw = CreativeWork::from_json_str(&assertion.data.to_string())?;
+
+ // insert a credentials field if we have a vc that matches the identifier
+ // todo: this should apply to any person, not just author
+ if let Some(cw_authors) = cw.author() {
+ let mut authors = Vec::new();
+ for a in cw_authors {
+ authors.push(
+ a.identifier()
+ .and_then(|i| {
+ vc_table
+ .get(&i)
+ .map(|uri| a.clone().add_credential(uri.clone()))
+ })
+ .unwrap_or_else(|| Ok(a.clone()))?,
+ );
+ }
+ cw = cw.set_author(&authors)?;
+ }
+ claim.add_assertion(&cw)
+ }
+ labels::CLAIM_REVIEW => {
+ claim.add_assertion(&SchemaDotOrg::from_json_str(&assertion.data.to_string())?)
+ }
+ _ => {
+ // default to creating UserCbor assertions
+ claim.add_assertion(&UserCbor::new(
+ &assertion.label,
+ serde_cbor::to_vec(&assertion.data)?,
+ ))
+ // todo: add option to use json
+ //claim.add_assertion(&User::new(&assertion.label, &assertion.data.to_string()), &NoSalt{})?;
+ }
+ }?;
+ }
+
+ // commit the claim
+ let mut store = Store::new();
+ let _provenance = store.commit_claim(claim)?;
+
+ Ok(store)
+ }
+
+ /// Embed a signed manifest into the target file using a supplied signer
+ #[cfg(feature = "file_io")]
+ pub fn embed(
+ &mut self,
+ source_path: &Path,
+ dest_path: &Path,
+ signer: &dyn Signer,
+ ) -> Result<Store> {
+ if !source_path.exists() {
+ let path = source_path.to_string_lossy().into_owned();
+ return Err(Error::FileNotFound(path));
+ }
+ // we need to copy the source to target before setting the asset info
+ if !dest_path.exists() {
+ std::fs::copy(&source_path, &dest_path)?;
+ }
+ // first add the information about the target file
+ self.set_asset_from_path(dest_path);
+ // convert the manifest to a store
+ let mut store = self.to_store()?;
+ // sign and write our store to to the output image file
+ store.save_to_asset(source_path, signer, dest_path.as_ref())?;
+
+ // todo: update xmp
+ Ok(store)
+ }
+
+ /// Embed a signed manifest into the target file using a supplied async signer
+ #[cfg(feature = "file_io")]
+ #[cfg(feature = "async_signer")]
+ pub async fn embed_async<P: AsRef<Path>>(
+ &mut self,
+ target_path: &P,
+ signer: &dyn crate::signer::AsyncSigner,
+ ) -> Result<Store> {
+ // first add the information about the target file
+ self.set_asset_from_path(target_path);
+ // convert the manifest to a store
+ let mut store = self.to_store()?;
+ // sign and write our store to to the output image file
+ store
+ .save_to_asset_async(target_path.as_ref(), signer, target_path.as_ref())
+ .await?;
+
+ // todo: update xmp
+ Ok(store)
+ }
+}
+
+impl std::fmt::Display for Manifest {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let json = serde_json::to_string_pretty(self).unwrap_or_default();
+ f.write_str(&json)
+ }
+}
+#[derive(Debug, Deserialize, Serialize, Clone)]
+/// A labeled container for an Assertion value in a Manifest
+pub struct ManifestAssertion {
+ /// An assertion label in reverse domain format
+ pub label: String,
+ /// The data of the assertion as Value
+ pub data: Value,
+}
+
+impl ManifestAssertion {
+ pub fn from_labeled_assertion<T: Serialize>(label: &str, data: &T) -> Result<Self> {
+ Ok(Self {
+ label: label.to_owned(),
+ data: serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?,
+ })
+ }
+
+ pub fn from_assertion<T: Serialize + AssertionBase>(data: &T) -> Result<Self> {
+ Ok(Self {
+ label: data.label().to_owned(),
+ data: serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?,
+ })
+ }
+
+ pub fn to_assertion<T: DeserializeOwned>(&self) -> Result<T> {
+ serde_json::from_value(self.data.clone()).map_err(|e| {
+ Error::AssertionDecoding(AssertionDecodeError::from_json_err(
+ self.label.to_owned(),
+ None,
+ "application/json".to_owned(),
+ e,
+ ))
+ })
+ }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+/// Holds information about a signature
+pub struct SignatureInfo {
+ /// human readable issuing authority for this signature
+ #[serde(skip_serializing_if = "Option::is_none")]
+ issuer: Option<String>,
+ /// the time the signature was created
+ #[serde(skip_serializing_if = "Option::is_none")]
+ time: Option<String>,
+}
+#[cfg(test)]
+#[cfg(feature = "file_io")]
+pub(crate) mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::{Ingredient, Manifest, Store};
+
+ use crate::{
+ assertions::{c2pa_action, Action, Actions},
+ openssl::temp_signer::get_signer,
+ status_tracker::{DetailedStatusTracker, StatusTracker},
+ utils::test::{fixture_path, temp_dir_path, temp_fixture_path, TEST_SMALL_JPEG, TEST_VC},
+ };
+
+ use tempfile::tempdir;
+
+ // example of random data structure as an assertion
+ #[derive(serde::Serialize)]
+ struct MyStruct {
+ l1: String,
+ l2: u32,
+ }
+
+ fn test_manifest() -> Manifest {
+ Manifest::new("test".to_owned())
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn from_file() {
+ let mut manifest = test_manifest();
+ let source_path = fixture_path(TEST_SMALL_JPEG);
+ manifest
+ .set_vendor("vendor".to_owned())
+ .set_parent(Ingredient::from_file(&source_path).expect("from_file"))
+ .expect("set_parent");
+
+ let vc: serde_json::Value = serde_json::from_str(TEST_VC).unwrap();
+ manifest
+ .add_verifiable_credential(&vc)
+ .expect("verifiable_credential");
+
+ manifest
+ .add_labeled_assertion(
+ "my.assertion",
+ &MyStruct {
+ l1: "some data".to_owned(),
+ l2: 5,
+ },
+ )
+ .expect("add_assertion");
+
+ let mut actions = Actions::new();
+
+ actions.add_action(
+ Action::new(c2pa_action::EDITED)
+ .set_parameter("name".to_owned(), "gaussian_blur")
+ .unwrap(),
+ );
+
+ manifest.add_assertion(&actions).expect("add_assertion");
+
+ manifest.add_ingredient(Ingredient::from_file(&source_path).expect("from_file"));
+
+ // generate json and omit binary thumbnails for printout
+ let mut json = serde_json::to_string_pretty(&manifest).expect("error to json");
+ while let Some(index) = json.find("\"thumbnail\": [") {
+ if let Some(idx2) = json[index..].find(']') {
+ json = format!(
+ "{}\"thumbnail\": \"<omitted>\"{}",
+ &json[..index],
+ &json[index + idx2 + 1..]
+ );
+ }
+ }
+
+ // copy an image to use as our target
+ let dir = tempdir().expect("temp dir");
+ let test_output = dir.path().join("wc_embed_test.jpg");
+
+ //embed a claim generated from this manifest
+ let (signer, _) = get_signer(&dir.path());
+
+ let _store = manifest
+ .embed(&source_path, &test_output, &signer)
+ .expect("embed");
+
+ let ingredient = Ingredient::from_file(&test_output).expect("load_from_asset");
+ assert!(ingredient.active_manifest().is_some());
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ /// test assertion validation on actions, should generate an error
+ fn ws_bad_assertion() {
+ // copy an image to use as our target for embedding
+ let ap = fixture_path(TEST_SMALL_JPEG);
+ let temp_dir = tempdir().expect("temp dir");
+ let test_output = temp_dir_path(&temp_dir, "ws_bad_assertion.jpg");
+ std::fs::copy(&ap, &test_output).expect("copy");
+
+ let mut manifest = test_manifest();
+
+ manifest
+ .add_labeled_assertion(
+ "c2pa.actions",
+ &MyStruct {
+ // add something that isn't an actions struct
+ l1: "some data".to_owned(),
+ l2: 5,
+ },
+ )
+ .expect("add_assertion");
+
+ // convert to store
+ let result = manifest.to_store();
+
+ println!("{:?}", result);
+ assert!(result.is_err())
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_verifiable_credential() {
+ let mut manifest = test_manifest();
+ let vc: serde_json::Value = serde_json::from_str(TEST_VC).unwrap();
+ manifest
+ .add_verifiable_credential(&vc)
+ .expect("verifiable_credential");
+ let store = manifest.to_store().expect("to_store");
+ let claim = store.provenance_claim().unwrap();
+ assert!(!claim.get_verifiable_credentials().is_empty());
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_assertion_user_cbor() {
+ use crate::assertions::UserCbor;
+ use crate::Manifest;
+ const LABEL: &str = "org.cai.test";
+ const DATA: &str = r#"{ "l1":"some data", "l2":"some other data" }"#;
+ let json: serde_json::Value = serde_json::from_str(DATA).unwrap();
+ let data = serde_cbor::to_vec(&json).unwrap();
+ let cbor = UserCbor::new(LABEL, data);
+ let mut manifest = test_manifest();
+ manifest.add_assertion(&cbor).expect("add_assertion");
+ manifest.add_assertion(&cbor).expect("add_assertion");
+ let store = manifest.to_store().expect("to_store");
+
+ let _manifest2 =
+ Manifest::from_store(&store, &store.provenance_label().unwrap()).expect("from_store");
+ println!("{}", store);
+ println!("{:?}", _manifest2);
+ let cbor2: UserCbor = manifest.find_assertion(LABEL).expect("get_assertion");
+ assert_eq!(cbor, cbor2);
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_redaction() {
+ const ASSERTION_LABEL: &str = "stds.schema-org.CreativeWork";
+
+ let temp_dir = tempdir().expect("temp dir");
+ let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
+ let output2 = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG);
+
+ let mut manifest = test_manifest();
+
+ manifest
+ .add_labeled_assertion(
+ ASSERTION_LABEL,
+ &serde_json::json! (
+ {
+ "@context": "https://schema.org",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "Joe Bloggs"
+ },
+
+ ]
+ }),
+ )
+ .expect("add_assertion");
+
+ let (signer, _) = get_signer(&temp_dir.path());
+
+ let store1 = manifest.embed(&output, &output, &signer).expect("embed");
+ let claim1_label = store1.provenance_label().unwrap();
+ let claim = store1.provenance_claim().unwrap();
+ assert!(claim.get_claim_assertion(ASSERTION_LABEL, 0).is_some()); // verify the assertion is there
+
+ // create a new claim and make the previous file a parent
+ let mut manifest2 = test_manifest();
+ manifest2
+ .set_parent(Ingredient::from_file(&output).expect("from_file"))
+ .expect("set_parent");
+
+ // todo: add a test to validate that actions assertions cannot be redacted
+ // let mut actions = Actions::new();
+ // actions.add_action(Action::new(C2PA_ACTION_EDITED).parameters("gaussian_blur"));
+ // ws.add_assertion("c2pa.actions", &actions).expect("add_assertion"); // must use .get() with Actions
+
+ // redact the assertion
+ manifest2
+ .add_redaction(ASSERTION_LABEL)
+ .expect("add_redaction");
+ let temp_dir = tempdir().expect("temp dir");
+
+ //embed a claim in output2
+ let (signer, _) = get_signer(&temp_dir.path());
+
+ let _store2 = manifest2.embed(&output2, &output2, &signer).expect("embed");
+
+ let mut report = DetailedStatusTracker::new();
+ let store3 = Store::load_from_asset(&output2, true, &mut report).unwrap();
+ let claim2 = store3.provenance_claim().unwrap();
+
+ // assert!(!claim2.get_verifiable_credentials().is_empty());
+
+ // test that the redaction is in the new claim and the assertion is removed from the first one
+
+ assert!(claim2.redactions().is_some());
+ assert!(!claim2.redactions().unwrap().is_empty());
+ assert!(!report.get_log().is_empty());
+ let redacted_uri = &claim2.redactions().unwrap()[0];
+
+ let claim1 = store3.get_claim(&claim1_label).unwrap();
+ assert!(claim1.get_claim_assertion(redacted_uri, 0).is_none());
+ }
+}
diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs
@@ -0,0 +1,267 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ status_tracker::{DetailedStatusTracker, StatusTracker},
+ store::Store,
+ validation_status::{status_for_store, ValidationStatus},
+ Manifest, Result,
+};
+use serde::Serialize;
+use std::collections::HashMap;
+
+#[cfg(feature = "file_io")]
+use std::path::Path;
+
+#[derive(Serialize)]
+/// A Container for a set of Manifests and a ValidationStatus list
+///
+pub struct ManifestStore {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ /// A label for the active (most recent) manifest in the store
+ active_manifest: Option<String>,
+ /// A HashMap of Manifests
+ manifests: HashMap<String, Manifest>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ /// ValidationStatus generated when loading the ManifestStore from an asset
+ validation_status: Option<Vec<ValidationStatus>>,
+}
+
+impl ManifestStore {
+ /// allocates a new empty ManifestStore
+ pub(crate) fn new() -> Self {
+ ManifestStore {
+ active_manifest: None,
+ manifests: HashMap::<String, Manifest>::new(),
+ validation_status: None,
+ }
+ }
+
+ /// Returns a reference to the active manifest label or None
+ pub fn active_label(&self) -> Option<&str> {
+ self.active_manifest.as_deref()
+ }
+
+ /// Returns a reference to the active manifest or None
+ pub fn get_active(&self) -> Option<&Manifest> {
+ if let Some(label) = self.active_manifest.as_ref() {
+ self.get(label)
+ } else {
+ None
+ }
+ }
+
+ /// Returns a reference to manifest HashMap
+ pub fn manifests(&self) -> &HashMap<String, Manifest> {
+ &self.manifests
+ }
+
+ /// Returns a reference to the requested manifest or None
+ pub fn get(&self, label: &str) -> Option<&Manifest> {
+ self.manifests.get(label)
+ }
+
+ /// Returns a reference the [ValidationStatus] Vec or None
+ pub fn validation_status(&self) -> Option<&[ValidationStatus]> {
+ self.validation_status.as_deref()
+ }
+
+ /// creates a ManifestStore from a Store
+ pub(crate) fn from_store(
+ store: &Store,
+ validation_log: &mut impl StatusTracker,
+ ) -> ManifestStore {
+ let mut statuses = status_for_store(store, validation_log);
+
+ let mut manifest_store = ManifestStore::new();
+ manifest_store.active_manifest = store.provenance_label();
+
+ for claim in store.claims() {
+ let manifest_label = claim.label();
+ match Manifest::from_store(store, manifest_label) {
+ Ok(manifest) => {
+ manifest_store
+ .manifests
+ .insert(manifest_label.to_owned(), manifest);
+ }
+ Err(e) => {
+ statuses.push(ValidationStatus::from_error(&e));
+ }
+ };
+ }
+
+ if !statuses.is_empty() {
+ manifest_store.validation_status = Some(statuses);
+ }
+
+ manifest_store
+ }
+
+ /// Creates a new Manifest Store from a Manifest
+ pub fn from_manifest(manifest: &Manifest) -> Result<Self> {
+ use crate::status_tracker::OneShotStatusTracker;
+ let store = manifest.to_store()?;
+ Ok(Self::from_store(&store, &mut OneShotStatusTracker::new()))
+ }
+
+ /// generate a Store from a format string and bytes
+ pub fn from_bytes(format: &str, image_bytes: Vec<u8>, verify: bool) -> Option<ManifestStore> {
+ let mut validation_log = DetailedStatusTracker::new();
+
+ match Store::load_from_memory(format, &image_bytes, verify, &mut validation_log) {
+ Ok(store) => Some(Self::from_store(&store, &mut validation_log)),
+ Err(_err) => None,
+ }
+ }
+
+ #[cfg(feature = "file_io")]
+ /// Loads a ManifestStore from a file
+ /// Example:
+ ///
+ /// ```
+ /// # use c2pa::Result;
+ /// use c2pa::ManifestStore;
+ /// # fn main() -> Result<()> {
+ /// let manifest_store = ManifestStore::from_file("tests/fixtures/C.jpg")?;
+ /// println!("{}", manifest_store);
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn from_file<P: AsRef<Path>>(path: P) -> Result<ManifestStore> {
+ let mut validation_log = DetailedStatusTracker::new();
+
+ let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?;
+ Ok(Self::from_store(&store, &mut validation_log))
+ }
+
+ /// Loads a ManifestStore from a file
+ pub async fn from_bytes_async(
+ format: &str,
+ image_bytes: Vec<u8>,
+ verify: bool,
+ ) -> Option<ManifestStore> {
+ let mut validation_log = DetailedStatusTracker::new();
+
+ match Store::load_from_memory_async(format, &image_bytes, verify, &mut validation_log).await
+ {
+ Ok(store) => Some(Self::from_store(&store, &mut validation_log)),
+ Err(_err) => None,
+ }
+ }
+}
+
+impl Default for ManifestStore {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl std::fmt::Display for ManifestStore {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let mut json = serde_json::to_string_pretty(self).unwrap_or_default();
+
+ fn omit_tag(mut json: String, tag: &str) -> String {
+ while let Some(index) = json.find(&format!("\"{}\": [", tag)) {
+ if let Some(idx2) = json[index..].find(']') {
+ json = format!(
+ "{}\"{}\": \"<omitted>\"{}",
+ &json[..index],
+ tag,
+ &json[index + idx2 + 1..]
+ );
+ }
+ }
+ json
+ }
+
+ // Make a base64 hash from Vec<u8> values.
+ fn b64_tag(mut json: String, tag: &str) -> String {
+ while let Some(index) = json.find(&format!("\"{}\": [", tag)) {
+ if let Some(idx2) = json[index..].find(']') {
+ let idx3 = json[index..].find('[').unwrap_or_default();
+
+ let bytes: Vec<u8> =
+ serde_json::from_slice(json[index + idx3..index + idx2 + 1].as_bytes())
+ .unwrap_or_default();
+
+ json = format!(
+ "{}\"{}\": \"{}\"{}",
+ &json[..index],
+ tag,
+ base64::encode(&bytes),
+ &json[index + idx2 + 1..]
+ );
+ }
+ }
+
+ json
+ }
+
+ json = b64_tag(json, "hash");
+ json = omit_tag(json, "pad");
+
+ f.write_str(&json)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ use crate::{status_tracker::OneShotStatusTracker, utils::test::create_test_store};
+
+ #[cfg(target_arch = "wasm32")]
+ use wasm_bindgen_test::*;
+
+ #[cfg(target_arch = "wasm32")]
+ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+ // #[cfg_attr(not(target_arch = "wasm32"), test)]
+ // #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ #[test]
+ fn manifest_report() {
+ let store = create_test_store().expect("creating test store");
+
+ let manifest_store = ManifestStore::from_store(&store, &mut OneShotStatusTracker::new());
+ assert!(manifest_store.active_manifest.is_some());
+ assert!(!manifest_store.manifests.is_empty());
+ let manifest = manifest_store.get_active().unwrap();
+ assert!(!manifest.ingredients().is_empty());
+
+ let full_report =
+ ManifestStore::from_store(&store, &mut OneShotStatusTracker::new()).to_string();
+ assert!(!full_report.is_empty());
+ println!("{}", full_report);
+ }
+
+ #[test]
+ fn manifest_report_image() {
+ let image_bytes = include_bytes!("../tests/fixtures/CAICAI.jpg");
+
+ let manifest_store =
+ ManifestStore::from_bytes("image/jpeg", image_bytes.to_vec(), true).unwrap();
+
+ assert!(!manifest_store.manifests.is_empty());
+ assert!(manifest_store.active_label().is_some());
+ assert!(manifest_store.get_active().is_some());
+ assert!(!manifest_store.manifests().is_empty());
+ assert!(manifest_store.validation_status().is_none());
+ let manifest = manifest_store.get_active().unwrap();
+ assert!(!manifest.ingredients().is_empty());
+ assert_eq!(manifest.issuer().unwrap(), "Some Company");
+ assert!(manifest.time().is_some());
+ }
+}
diff --git a/sdk/src/manifest_store_report.rs b/sdk/src/manifest_store_report.rs
@@ -0,0 +1,244 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ assertion::AssertionData,
+ claim::Claim,
+ status_tracker::{DetailedStatusTracker, StatusTracker},
+ store::Store,
+ validation_status::ValidationStatus,
+ Result,
+};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::collections::HashMap;
+#[cfg(feature = "file_io")]
+use std::path::Path;
+
+/// Low level JSON based representation of Manifest Store - used for debugging
+#[non_exhaustive]
+#[derive(Serialize, Deserialize, Debug, Default)]
+pub struct ManifestStoreReport {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ active_manifest: Option<String>,
+ manifests: HashMap<String, ManifestReport>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ validation_status: Option<Vec<ValidationStatus>>,
+}
+
+impl ManifestStoreReport {
+ /// Creates a ManifestStoreReport from an existing Store
+ pub(crate) fn from_store(store: &Store) -> Result<Self> {
+ let mut manifests = HashMap::<String, ManifestReport>::new();
+ for claim in store.claims() {
+ manifests.insert(claim.label().to_owned(), ManifestReport::from_claim(claim)?);
+ }
+
+ Ok(ManifestStoreReport {
+ active_manifest: store.provenance_label(),
+ manifests,
+ validation_status: None,
+ })
+ }
+
+ /// Creates a ManifestStoreReport from an existing Store and a validation log
+ pub fn from_store_with_log(
+ store: &Store,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<Self> {
+ let mut report = Self::from_store(store)?;
+
+ // convert log items to ValidationStatus
+ let mut statuses = Vec::new();
+ for item in validation_log.get_log() {
+ if let Some(status) = item.validation_status.as_ref() {
+ statuses.push(
+ ValidationStatus::new(status.to_string())
+ .set_url(item.label.to_string())
+ .set_explanation(item.description.to_string()),
+ );
+ }
+ }
+ if !statuses.is_empty() {
+ report.validation_status = Some(statuses);
+ }
+ Ok(report)
+ }
+
+ /// Creates a ManifestStoreReport from image bytes and a format
+ pub fn from_bytes(format: &str, image_bytes: &[u8]) -> Result<Self> {
+ let mut validation_log = DetailedStatusTracker::new();
+ let store = Store::load_from_memory(format, image_bytes, true, &mut validation_log)?;
+ Self::from_store_with_log(&store, &mut validation_log)
+ }
+
+ /// Creates a ManifestStoreReport from a file
+ #[cfg(feature = "file_io")]
+ pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
+ let mut validation_log = DetailedStatusTracker::new();
+ let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?;
+ Self::from_store_with_log(&store, &mut validation_log)
+ }
+
+ /// create a json string representation of this structure, omitting binaries
+ fn to_json(&self) -> String {
+ let mut json = serde_json::to_string_pretty(self).unwrap_or_else(|e| e.to_string());
+
+ json = b64_tag(json, "hash");
+ json = omit_tag(json, "pad");
+
+ json
+ }
+}
+
+impl std::fmt::Display for ManifestStoreReport {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(&self.to_json())
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, Default)]
+struct ManifestReport {
+ claim: Value,
+ assertion_store: HashMap<String, Value>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ credential_store: Option<Vec<Value>>,
+ signature: SignatureReport,
+}
+
+impl ManifestReport {
+ fn from_claim(claim: &Claim) -> Result<Self> {
+ let mut assertion_store = HashMap::<String, Value>::new();
+ let claim_assertions = claim.claim_assertion_store();
+ for claim_assertion in claim_assertions.iter() {
+ let hashlink = claim_assertion.label();
+ let (label, instance) = Claim::assertion_label_from_link(&hashlink);
+ let label = Claim::label_with_instance(&label, instance);
+ let value = match claim_assertion.assertion().decode_data() {
+ AssertionData::Json(_) | AssertionData::Cbor(_) => {
+ claim_assertion.assertion().as_json_object()? // todo: this may cause data loss
+ }
+ AssertionData::Binary(x) => {
+ serde_json::to_value(format!("<omitted> len = {}", x.len()))?
+ }
+ AssertionData::Uuid(s, x) => {
+ serde_json::to_value(format!("uuid: {}, data: {}", s, base64::encode(x)))?
+ }
+ };
+ assertion_store.insert(label, value);
+ }
+
+ // convert credential store to json values
+ let credential_store: Vec<Value> = claim
+ .get_verifiable_credentials()
+ .iter()
+ .filter_map(|d| match d {
+ AssertionData::Json(s) => serde_json::from_str(s).ok(),
+ _ => None,
+ })
+ .collect();
+
+ let signature = match claim.signature_info() {
+ Some(info) => SignatureReport {
+ alg: info.alg,
+ issuer: info.issuer_org,
+ time: info.date.map(|d| d.to_rfc3339()),
+ },
+ None => SignatureReport::default(),
+ };
+ Ok(Self {
+ claim: serde_json::to_value(claim)?, // todo: this will lose tagging info
+ assertion_store,
+ credential_store: (!credential_store.is_empty()).then(|| credential_store),
+ signature,
+ })
+ }
+ /// create a json string representation of this structure, omitting binaries
+ fn to_json(&self) -> String {
+ let mut json = serde_json::to_string_pretty(self).unwrap_or_else(|e| e.to_string());
+
+ json = b64_tag(json, "hash");
+ json = omit_tag(json, "pad");
+
+ json
+ }
+}
+
+impl std::fmt::Display for ManifestReport {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(&self.to_json())
+ }
+}
+
+// used to report information from signature data
+#[derive(Default, Debug, Deserialize, Serialize)]
+struct SignatureReport {
+ alg: String,
+ // human readable issuing authority for this signature
+ #[serde(skip_serializing_if = "Option::is_none")]
+ issuer: Option<String>,
+ // the time the signature was created
+ #[serde(skip_serializing_if = "Option::is_none")]
+ time: Option<String>,
+}
+
+// replace the value of any field in the json string with a given key with the string <omitted>
+fn omit_tag(mut json: String, tag: &str) -> String {
+ while let Some(index) = json.find(&format!("\"{}\": [", tag)) {
+ if let Some(idx2) = json[index..].find(']') {
+ json = format!(
+ "{}\"{}\": \"<omitted>\"{}",
+ &json[..index],
+ tag,
+ &json[index + idx2 + 1..]
+ );
+ }
+ }
+ json
+}
+
+// make a base64 hash from the value of any field in the json string with key base64 hash
+fn b64_tag(mut json: String, tag: &str) -> String {
+ while let Some(index) = json.find(&format!("\"{}\": [", tag)) {
+ if let Some(idx2) = json[index..].find(']') {
+ let idx3 = json[index..].find('[').unwrap_or_default(); // ok since we just found it
+ let bytes: Vec<u8> =
+ serde_json::from_slice(json[index + idx3..index + idx2 + 1].as_bytes())
+ .unwrap_or_default();
+ json = format!(
+ "{}\"{}\": \"{}\"{}",
+ &json[..index],
+ tag,
+ base64::encode(&bytes),
+ &json[index + idx2 + 1..]
+ );
+ }
+ }
+ json
+}
+
+#[cfg(feature = "file_io")]
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::expect_used)]
+
+ use super::ManifestStoreReport;
+ use crate::utils::test::fixture_path;
+
+ #[test]
+ fn manifest_store_report() {
+ let path = fixture_path("CAICAI_BAD_SIG.jpg");
+ let report = ManifestStoreReport::from_file(&path).expect("load_from_asset");
+ println!("{}", report);
+ }
+}
diff --git a/sdk/src/ocsp_utils.rs b/sdk/src/ocsp_utils.rs
@@ -0,0 +1,290 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::error::{Error, Result};
+use crate::openssl::check_chain_order_der;
+use crate::status_tracker::{log_item, StatusTracker};
+use crate::validation_status;
+use chrono::{DateTime, NaiveDateTime, Utc};
+use conv::ConvUtil;
+use openssl::ocsp::{self, OcspBasicResponse, OcspCertStatus, OcspRevokedStatus};
+use std::io::Read;
+
+const DATE_FMT: &str = "%b %d %H:%M:%S %Y %Z";
+
+/// OcspData - struct to contain the OCSPResponse DER and the time
+/// for the next OCSP check
+pub struct OcspData {
+ pub ocsp_der: Vec<u8>,
+ pub next_update: DateTime<Utc>,
+}
+
+impl OcspData {
+ pub fn new() -> Self {
+ OcspData {
+ ocsp_der: Vec::new(),
+ next_update: Utc::now(),
+ }
+ }
+}
+
+impl Default for OcspData {
+ fn default() -> Self {
+ Self {
+ ocsp_der: Vec::new(),
+ next_update: Utc::now(),
+ }
+ }
+}
+
+fn get_ocsp_responders(cert_der: &[u8]) -> Option<Vec<String>> {
+ let cert = openssl::x509::X509::from_der(cert_der).ok()?;
+
+ if let Ok(stack) = cert.ocsp_responders() {
+ let mut output: Vec<String> = Vec::new();
+ for responder in stack {
+ output.push(responder.to_string());
+ }
+ Some(output)
+ } else {
+ None
+ }
+}
+
+/// Check the supplied cert chain for an OCSP responder in the end-entity cert. If found it will attempt to
+/// retrieve the OCSPResponse.
+/// If successful returns OcspData containing the DER encoded OCSPResponse and the DateTime for when this cached response should
+/// be refreshed. None otherwise.
+pub fn get_ocsp_response(certs: &[Vec<u8>]) -> Option<OcspData> {
+ //} Option<DateTime<Utc>>) {
+ // must be in hierarchical order for this to work
+ if certs.len() < 2 || !check_chain_order_der(certs) {
+ return None;
+ }
+
+ if let Some(responders) = get_ocsp_responders(&certs[0]) {
+ for r in responders {
+ let url = url::Url::parse(&r).ok()?;
+ let subject = openssl::x509::X509::from_der(&certs[0]).ok()?;
+ let issuer = openssl::x509::X509::from_der(&certs[1]).ok()?;
+
+ let cert_id = openssl::ocsp::OcspCertId::from_cert(
+ openssl::hash::MessageDigest::sha1(),
+ &subject,
+ &issuer,
+ )
+ .ok()?;
+
+ let mut ocsp_req = ocsp::OcspRequest::new().ok()?;
+ ocsp_req.add_id(cert_id).ok()?;
+ let request_str = base64::encode(ocsp_req.to_der().ok()?);
+
+ let req_url = url.join(&request_str).ok()?;
+
+ let request = ureq::get(req_url.as_str());
+ let response = if let Some(host) = url.host() {
+ request.set("Host", &host.to_string()).call().ok()? // for responders that don't support http 1.0
+ } else {
+ request.call().ok()?
+ };
+
+ if response.status() == 200 {
+ let len = response
+ .header("Content-Length")
+ .and_then(|s| s.parse::<usize>().ok())
+ .unwrap_or(2000);
+
+ let mut ocsp_rsp: Vec<u8> = Vec::with_capacity(len);
+
+ response
+ .into_reader()
+ .take(1000000)
+ .read_to_end(&mut ocsp_rsp)
+ .ok()?;
+
+ // sanity check response
+ let ocsp_response = ocsp::OcspResponse::from_der(&ocsp_rsp).ok()?;
+ if ocsp_response.status() == ocsp::OcspResponseStatus::SUCCESSFUL {
+ if let Ok(basic_response) = ocsp_response.basic() {
+ if let Some(cert_status) =
+ get_end_entity_cert_status(certs, &basic_response)
+ {
+ if cert_status.status == OcspCertStatus::GOOD
+ || cert_status.status == OcspCertStatus::REVOKED
+ && cert_status.reason == OcspRevokedStatus::REMOVE_FROM_CRL
+ {
+ let next_update = NaiveDateTime::parse_from_str(
+ &cert_status.next_update.to_string(),
+ DATE_FMT,
+ )
+ .ok()?;
+
+ let output = OcspData {
+ ocsp_der: ocsp_rsp,
+ next_update: DateTime::from_utc(next_update, chrono::Utc),
+ };
+
+ return Some(output);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ None
+}
+
+// find the certificate to check
+fn get_end_entity_cert_status<'a>(
+ certs: &[Vec<u8>],
+ basic_response: &'a OcspBasicResponse,
+) -> Option<ocsp::OcspStatus<'a>> {
+ if certs.len() < 2 || !check_chain_order_der(certs) {
+ return None;
+ }
+
+ let subject = openssl::x509::X509::from_der(&certs[0]).ok()?;
+ let issuer = openssl::x509::X509::from_der(&certs[1]).ok()?;
+
+ let cert_id = openssl::ocsp::OcspCertId::from_cert(
+ openssl::hash::MessageDigest::sha1(),
+ &subject,
+ &issuer,
+ )
+ .ok()?;
+
+ basic_response.find_status(&cert_id)
+}
+
+// check to OCSP response against the supplied certs and signing time (if available)
+// Returns - empty result on success
+pub(crate) fn _check_ocsp_response(
+ ocsp_response_der: &[u8],
+ certs: &[Vec<u8>],
+ signing_time: Option<chrono::DateTime<chrono::Utc>>,
+ validation_log: &mut impl StatusTracker,
+) -> Result<()> {
+ if certs.len() < 2 || !check_chain_order_der(certs) {
+ return Err(Error::BadParam("certs vector not valid".to_string()));
+ }
+
+ if let Ok(ocsp_response) = ocsp::OcspResponse::from_der(ocsp_response_der) {
+ if ocsp_response.status() == ocsp::OcspResponseStatus::SUCCESSFUL {
+ if let Ok(basic_response) = ocsp_response.basic() {
+ if let Some(cert_status) = get_end_entity_cert_status(certs, &basic_response) {
+ if cert_status.status == OcspCertStatus::GOOD
+ || cert_status.status == OcspCertStatus::REVOKED
+ && cert_status.reason == OcspRevokedStatus::REMOVE_FROM_CRL
+ {
+ // check cert range against signing time
+ let this_update = NaiveDateTime::parse_from_str(
+ &cert_status.this_update.to_string(),
+ DATE_FMT,
+ )
+ .map_err(|_e| Error::CoseInvalidCert)?
+ .timestamp();
+ let next_update = NaiveDateTime::parse_from_str(
+ &cert_status.next_update.to_string(),
+ DATE_FMT,
+ )
+ .map_err(|_e| Error::CoseInvalidCert)?
+ .timestamp();
+
+ // check to see if we are within range or current time within range
+ let in_range = if let Some(st) = signing_time {
+ println!("{}, {}, {}", this_update, next_update, st.timestamp());
+ st.timestamp() >= this_update && st.timestamp() <= next_update
+ } else {
+ // no timestamp so check against current time
+ // use instant to avoid wasm issues
+ let now_f64 = instant::now() / 1000.0;
+ let now: i64 = now_f64
+ .approx_as::<i64>()
+ .map_err(|_e| Error::BadParam("system time invalid".to_string()))?;
+
+ now >= this_update && now <= next_update
+ };
+
+ if !in_range {
+ let log_item = log_item!(
+ "OCSP_RESPONSE",
+ "certificate revoked",
+ "check_ocsp_response"
+ )
+ .error(Error::CoseCertRevoked)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_REVOKED);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseCertRevoked);
+ }
+ } else if cert_status.status == OcspCertStatus::REVOKED
+ && cert_status.reason != OcspRevokedStatus::REMOVE_FROM_CRL
+ {
+ // if it was revoked check if was revoked after signing time
+ if let Some(revocation_time) = cert_status.revocation_time {
+ // check cert range against signing time
+ let revoked_at = NaiveDateTime::parse_from_str(
+ &revocation_time.to_string(),
+ DATE_FMT,
+ )
+ .map_err(|_e| Error::CoseInvalidCert)?
+ .timestamp();
+
+ // check to see if we are within range or current time within range
+ let in_range = if let Some(st) = signing_time {
+ revoked_at > st.timestamp()
+ } else {
+ // no timestamp so check against current time
+ // use instant to avoid wasm issues
+ let now_f64 = instant::now() / 1000.0;
+ let now: i64 = now_f64.approx_as::<i64>().map_err(|_e| {
+ Error::BadParam("system time invalid".to_string())
+ })?;
+
+ revoked_at > now
+ };
+
+ if !in_range {
+ let log_item = log_item!(
+ "OCSP_RESPONSE",
+ "certificate revoked",
+ "check_ocsp_response"
+ )
+ .error(Error::CoseCertRevoked)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_REVOKED);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseCertRevoked);
+ }
+ } else {
+ let log_item = log_item!(
+ "OCSP_RESPONSE",
+ "certificate revoked",
+ "check_ocsp_response"
+ )
+ .error(Error::CoseCertRevoked)
+ .validation_status(validation_status::SIGNING_CREDENTIAL_REVOKED);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::CoseCertRevoked);
+ }
+ }
+ }
+ };
+ }
+ }
+
+ // Per the spec if we cannot interpret the OCSP data treat it as if it did not exist
+ Ok(())
+}
diff --git a/sdk/src/openssl/ec_signer.rs b/sdk/src/openssl/ec_signer.rs
@@ -0,0 +1,259 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::{fs, path::Path};
+
+use crate::{
+ error::{wrap_io_err, wrap_openssl_err, Error, Result},
+ signer::ConfigurableSigner,
+ Signer,
+};
+use openssl::hash::MessageDigest;
+use openssl::pkey::PKey;
+use openssl::{ec::EcKey, pkey::Private, x509::X509};
+use x509_parser::der_parser::{
+ self,
+ der::{parse_der_integer, parse_der_sequence_defined_g},
+};
+
+use super::check_chain_order;
+
+/// Implements `Signer` trait using OpenSSL's implementation of
+/// ECDSA encryption.
+pub struct EcSigner {
+ signcerts: Vec<X509>,
+ pkey: EcKey<Private>,
+
+ certs_size: usize,
+ timestamp_size: usize,
+
+ alg: String,
+ tsa_url: Option<String>,
+}
+
+impl ConfigurableSigner for EcSigner {
+ fn from_files<P: AsRef<Path>>(
+ signcert_path: P,
+ pkey_path: P,
+ alg: String,
+ tsa_url: Option<String>,
+ ) -> Result<Self> {
+ let signcert = fs::read(signcert_path).map_err(wrap_io_err)?;
+ let pkey = fs::read(pkey_path).map_err(wrap_io_err)?;
+
+ Self::from_signcert_and_pkey(&signcert, &pkey, alg, tsa_url)
+ }
+
+ fn from_signcert_and_pkey(
+ signcert: &[u8],
+ pkey: &[u8],
+ alg: String,
+ tsa_url: Option<String>,
+ ) -> Result<Self> {
+ let certs_size = signcert.len();
+ let pkey = EcKey::private_key_from_pem(pkey).map_err(wrap_openssl_err)?;
+ let signcerts = X509::stack_from_pem(signcert).map_err(wrap_openssl_err)?;
+
+ // make sure cert chains are in order
+ if !check_chain_order(&signcerts) {
+ return Err(Error::BadParam(
+ "certificate chain is not in correct order".to_string(),
+ ));
+ }
+
+ Ok(EcSigner {
+ signcerts,
+ pkey,
+ certs_size,
+ timestamp_size: 4096, // todo: call out to TSA to get actual timestamp and use that size
+ alg,
+ tsa_url,
+ })
+ }
+}
+
+impl Signer for EcSigner {
+ fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
+ let key = PKey::from_ec_key(self.pkey.clone()).map_err(wrap_openssl_err)?;
+
+ let mut signer = match self.alg.as_ref() {
+ "es256" => openssl::sign::Signer::new(MessageDigest::sha256(), &key)?,
+ "es384" => openssl::sign::Signer::new(MessageDigest::sha384(), &key)?,
+ "es512" => openssl::sign::Signer::new(MessageDigest::sha512(), &key)?,
+ _ => return Err(Error::UnsupportedType),
+ };
+
+ signer.update(data).map_err(wrap_openssl_err)?;
+ let der_sig = signer.sign_to_vec().map_err(wrap_openssl_err)?;
+
+ der_to_p1363(&der_sig, &self.alg)
+ }
+
+ fn alg(&self) -> Option<String> {
+ Some(self.alg.to_owned())
+ }
+
+ fn certs(&self) -> Result<Vec<Vec<u8>>> {
+ let mut certs: Vec<Vec<u8>> = Vec::new();
+
+ for c in &self.signcerts {
+ let cert = c.to_der().map_err(wrap_openssl_err)?;
+ certs.push(cert);
+ }
+
+ Ok(certs)
+ }
+
+ fn time_authority_url(&self) -> Option<String> {
+ self.tsa_url.clone()
+ }
+
+ fn reserve_size(&self) -> usize {
+ 1024 + self.certs_size + self.timestamp_size // the Cose_Sign1 contains complete certs and timestamps so account for size
+ }
+}
+
+// C2PA use P1363 format for EC signatures so we must
+// convert from ASN.1 DER to IEEE P1363 format to verify.
+struct ECSigComps<'a> {
+ r: &'a [u8],
+ s: &'a [u8],
+}
+
+fn parse_ec_sig(data: &[u8]) -> der_parser::error::BerResult<ECSigComps> {
+ parse_der_sequence_defined_g(|content: &[u8], _| {
+ let (rem1, r) = parse_der_integer(content)?;
+ let (_rem2, s) = parse_der_integer(rem1)?;
+
+ Ok((
+ data,
+ ECSigComps {
+ r: r.as_slice()?,
+ s: s.as_slice()?,
+ },
+ ))
+ })(data)
+}
+
+fn der_to_p1363(data: &[u8], alg: &str) -> Result<Vec<u8>> {
+ // P1363 format: r | s
+
+ let (_, p) = parse_ec_sig(data).map_err(|_err| Error::InvalidEcdsaSignature)?;
+
+ let mut r = extfmt::Hexlify(p.r).to_string();
+ let mut s = extfmt::Hexlify(p.s).to_string();
+
+ let sig_len: usize = match alg {
+ "es256" => 64,
+ "es384" => 96,
+ "es512" => 132,
+ _ => return Err(Error::UnsupportedType),
+ };
+
+ // pad or truncate as needed
+ let rp = if r.len() > sig_len {
+ // truncate
+ let offset = r.len() - sig_len;
+ &r[offset..r.len()]
+ } else {
+ // pad
+ while r.len() != sig_len {
+ r.insert(0, '0');
+ }
+ r.as_ref()
+ };
+
+ let sp = if s.len() > sig_len {
+ // truncate
+ let offset = s.len() - sig_len;
+ &s[offset..s.len()]
+ } else {
+ // pad
+ while s.len() != sig_len {
+ s.insert(0, '0');
+ }
+ s.as_ref()
+ };
+
+ if rp.len() != sig_len || rp.len() != sp.len() {
+ return Err(Error::InvalidEcdsaSignature);
+ }
+
+ // merge r and s strings
+ let mut new_sig = rp.to_string();
+ new_sig.push_str(sp);
+
+ // convert back from hex string to byte array
+ (0..new_sig.len())
+ .step_by(2)
+ .map(|i| {
+ u8::from_str_radix(&new_sig[i..i + 2], 16).map_err(|_err| Error::InvalidEcdsaSignature)
+ })
+ .collect()
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use tempfile::tempdir;
+
+ use crate::openssl::temp_signer;
+
+ #[test]
+ fn es256_signer() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, _) = temp_signer::get_ec_signer(&temp_dir.path(), "es256", None);
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature.len = {}", signature.len());
+ assert!(signature.len() >= 64);
+ assert!(signature.len() <= signer.reserve_size());
+ }
+
+ #[test]
+ fn es384_signer() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, _) = temp_signer::get_ec_signer(&temp_dir.path(), "es384", None);
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature.len = {}", signature.len());
+ assert!(signature.len() >= 64);
+ assert!(signature.len() <= signer.reserve_size());
+ }
+
+ #[test]
+ fn es512_signer() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, _) = temp_signer::get_ec_signer(&temp_dir.path(), "es512", None);
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature.len = {}", signature.len());
+ assert!(signature.len() >= 64);
+ assert!(signature.len() <= signer.reserve_size());
+ }
+}
diff --git a/sdk/src/openssl/ec_validator.rs b/sdk/src/openssl/ec_validator.rs
@@ -0,0 +1,225 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{validator::CoseValidator, Error, Result};
+use openssl::ec::EcKey;
+use openssl::hash::MessageDigest;
+use openssl::pkey::PKey;
+
+pub struct EcValidator {
+ alg: String,
+}
+
+impl EcValidator {
+ pub fn new(alg: &str) -> Self {
+ EcValidator {
+ alg: alg.to_owned(),
+ }
+ }
+}
+
+impl CoseValidator for EcValidator {
+ fn validate(&self, sig: &[u8], data: &[u8], pkey: &[u8]) -> Result<bool> {
+ let public_key = EcKey::public_key_from_der(pkey).map_err(|_err| Error::CoseSignature)?;
+ let key = PKey::from_ec_key(public_key).map_err(wrap_openssl_err)?;
+
+ let mut verifier = match self.alg.as_ref() {
+ "es256" => openssl::sign::Verifier::new(MessageDigest::sha256(), &key)?,
+ "es384" => openssl::sign::Verifier::new(MessageDigest::sha384(), &key)?,
+ "es512" => openssl::sign::Verifier::new(MessageDigest::sha512(), &key)?,
+ _ => return Err(Error::UnsupportedType),
+ };
+
+ // is this an expected P1363 sig size
+ if sig.len()
+ != match self.alg.as_ref() {
+ "es256" => 64,
+ "es384" => 96,
+ "es512" => 132,
+ _ => return Err(Error::UnsupportedType),
+ }
+ {
+ return Err(Error::CoseSignature);
+ }
+
+ // convert P1363 sig to DER sig
+ let sig_len = sig.len() / 2;
+ let r = openssl::bn::BigNum::from_slice(&sig[0..sig_len])
+ .map_err(|_err| Error::CoseSignature)?;
+ let s = openssl::bn::BigNum::from_slice(&sig[sig_len..])
+ .map_err(|_err| Error::CoseSignature)?;
+
+ let ecdsa_sig = openssl::ecdsa::EcdsaSig::from_private_components(r, s)
+ .map_err(|_err| Error::CoseSignature)?;
+ let sig_der = ecdsa_sig.to_der().map_err(|_err| Error::CoseSignature)?;
+
+ verifier.update(data).map_err(wrap_openssl_err)?;
+ verifier
+ .verify(&sig_der)
+ .map_err(|_err| Error::CoseSignature)
+ }
+}
+
+fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error {
+ Error::OpenSslError(err)
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+ use super::*;
+
+ use tempfile::tempdir;
+
+ use crate::{
+ openssl::{ec_signer::EcSigner, temp_signer},
+ signer::ConfigurableSigner,
+ utils::test::fixture_path,
+ Signer,
+ };
+
+ #[test]
+ fn sign_and_validate_es256() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, cert_path) = temp_signer::get_ec_signer(&temp_dir.path(), "es256", None);
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature.len = {}", signature.len());
+ assert!(signature.len() >= 64);
+ assert!(signature.len() <= signer.reserve_size());
+
+ let cert_bytes = std::fs::read(&cert_path).unwrap();
+
+ let signcert = openssl::x509::X509::from_pem(&cert_bytes).unwrap();
+ let pub_key = signcert.public_key().unwrap().public_key_to_der().unwrap();
+
+ let validator = EcValidator::new("es256");
+ assert!(validator.validate(&signature, data, &pub_key).unwrap());
+ }
+
+ #[test]
+ fn sign_and_validate_es384() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, cert_path) = temp_signer::get_ec_signer(&temp_dir.path(), "es384", None);
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature.len = {}", signature.len());
+ assert!(signature.len() >= 64);
+ assert!(signature.len() <= signer.reserve_size());
+
+ let cert_bytes = std::fs::read(&cert_path).unwrap();
+
+ let signcert = openssl::x509::X509::from_pem(&cert_bytes).unwrap();
+ let pub_key = signcert.public_key().unwrap().public_key_to_der().unwrap();
+
+ let validator = EcValidator::new("es384");
+ assert!(validator.validate(&signature, data, &pub_key).unwrap());
+ }
+
+ #[test]
+ fn sign_and_validate_es512() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, cert_path) = temp_signer::get_ec_signer(&temp_dir.path(), "es512", None);
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature.len = {}", signature.len());
+ assert!(signature.len() >= 64);
+ assert!(signature.len() <= signer.reserve_size());
+
+ let cert_bytes = std::fs::read(&cert_path).unwrap();
+
+ let signcert = openssl::x509::X509::from_pem(&cert_bytes).unwrap();
+ let pub_key = signcert.public_key().unwrap().public_key_to_der().unwrap();
+
+ let validator = EcValidator::new("es512");
+ assert!(validator.validate(&signature, data, &pub_key).unwrap());
+ }
+
+ #[test]
+ fn bad_sig_es256() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, cert_path) = temp_signer::get_ec_signer(&temp_dir.path(), "es256", None);
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+ let mut signature = signer.sign(data).unwrap();
+
+ signature.push(10);
+
+ let cert_bytes = std::fs::read(&cert_path).unwrap();
+ let signcert = openssl::x509::X509::from_pem(&cert_bytes).unwrap();
+ let pub_key = signcert.public_key().unwrap().public_key_to_der().unwrap();
+
+ let validator = EcValidator::new("es256");
+ let validated = validator.validate(&signature, data, &pub_key);
+ assert!(validated.is_err());
+ }
+
+ #[test]
+ fn bad_data_es256() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, cert_path) = temp_signer::get_ec_signer(&temp_dir.path(), "es256", None);
+
+ let mut data = b"some sample content to sign".to_vec();
+ println!("data len = {}", data.len());
+ let signature = signer.sign(&data).unwrap();
+
+ data[5] = 10;
+ data[6] = 11;
+
+ let cert_bytes = std::fs::read(&cert_path).unwrap();
+ let signcert = openssl::x509::X509::from_pem(&cert_bytes).unwrap();
+ let pub_key = signcert.public_key().unwrap().public_key_to_der().unwrap();
+
+ let validator = EcValidator::new("es256");
+ assert!(!validator.validate(&signature, &data, &pub_key).unwrap());
+ }
+
+ #[test]
+ fn sign_and_validate_with_chain() {
+ let pkey_path = fixture_path("bob.key");
+ let cert_path = fixture_path("bob.pem");
+
+ let signer =
+ EcSigner::from_files(&cert_path, &pkey_path, "es256".to_string(), None).unwrap();
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature.len = {}", signature.len());
+ assert!(signature.len() >= 64);
+ assert!(signature.len() <= signer.reserve_size());
+
+ let cert_bytes = &signer.certs().unwrap()[0];
+ let signcert = openssl::x509::X509::from_der(cert_bytes).unwrap();
+
+ let pub_key = signcert.public_key().unwrap().public_key_to_der().unwrap();
+ let validator = EcValidator::new("es256");
+ assert!(validator.validate(&signature, data, &pub_key).unwrap());
+ }
+}
diff --git a/sdk/src/openssl/ed_signer.rs b/sdk/src/openssl/ed_signer.rs
@@ -0,0 +1,148 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::{fs, path::Path};
+
+use crate::{signer::ConfigurableSigner, Error, Result, Signer};
+
+use openssl::{
+ pkey::{PKey, Private},
+ x509::X509,
+};
+
+use super::check_chain_order;
+
+/// Implements `Signer` trait using OpenSSL's implementation of
+/// Edwards Curve encryption.
+pub struct EdSigner {
+ signcerts: Vec<X509>,
+ pkey: PKey<Private>,
+
+ certs_size: usize,
+ timestamp_size: usize,
+
+ alg: String,
+ tsa_url: Option<String>,
+}
+
+impl ConfigurableSigner for EdSigner {
+ fn from_files<P: AsRef<Path>>(
+ signcert_path: P,
+ pkey_path: P,
+ alg: String,
+ tsa_url: Option<String>,
+ ) -> Result<Self> {
+ let signcert = fs::read(signcert_path).map_err(wrap_io_err)?;
+ let pkey = fs::read(pkey_path).map_err(wrap_io_err)?;
+
+ Self::from_signcert_and_pkey(&signcert, &pkey, alg, tsa_url)
+ }
+
+ fn from_signcert_and_pkey(
+ signcert: &[u8],
+ pkey: &[u8],
+ alg: String,
+ tsa_url: Option<String>,
+ ) -> Result<Self> {
+ let certs_size = signcert.len();
+ let signcerts = X509::stack_from_pem(signcert).map_err(wrap_openssl_err)?;
+ let pkey = PKey::private_key_from_pem(pkey).map_err(wrap_openssl_err)?;
+
+ if alg.to_lowercase() != "ed25519" {
+ return Err(Error::UnsupportedType); // only ed25519 is supported by C2PA
+ }
+
+ // make sure cert chains are in order
+ if !check_chain_order(&signcerts) {
+ return Err(Error::BadParam(
+ "certificate chain is not in correct order".to_string(),
+ ));
+ }
+
+ Ok(EdSigner {
+ signcerts,
+ pkey,
+ certs_size,
+ timestamp_size: 4096, // todo: call out to TSA to get actual timestamp and use that size
+ alg: "ed25519".to_string(),
+ tsa_url,
+ })
+ }
+}
+
+impl Signer for EdSigner {
+ fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
+ let mut signer =
+ openssl::sign::Signer::new_without_digest(&self.pkey).map_err(wrap_openssl_err)?;
+
+ let signed_data = signer.sign_oneshot_to_vec(data)?;
+
+ Ok(signed_data)
+ }
+
+ fn alg(&self) -> Option<String> {
+ Some(self.alg.to_owned())
+ }
+
+ fn certs(&self) -> Result<Vec<Vec<u8>>> {
+ let mut certs: Vec<Vec<u8>> = Vec::new();
+
+ for c in &self.signcerts {
+ let cert = c.to_der().map_err(wrap_openssl_err)?;
+ certs.push(cert);
+ }
+
+ Ok(certs)
+ }
+
+ fn time_authority_url(&self) -> Option<String> {
+ self.tsa_url.clone()
+ }
+
+ fn reserve_size(&self) -> usize {
+ 1024 + self.certs_size + self.timestamp_size // the Cose_Sign1 contains complete certs and timestamps so account for size
+ }
+}
+
+fn wrap_io_err(err: std::io::Error) -> Error {
+ Error::IoError(err)
+}
+
+fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error {
+ Error::OpenSslError(err)
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+ use super::*;
+
+ use tempfile::tempdir;
+
+ use crate::openssl::temp_signer;
+
+ #[test]
+ fn ed25519_signer() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, _) = temp_signer::get_ed_signer(&temp_dir.path(), "ed25519", None);
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature.len = {}", signature.len());
+ assert!(signature.len() >= 64);
+ assert!(signature.len() <= signer.reserve_size());
+ }
+}
diff --git a/sdk/src/openssl/ed_validator.rs b/sdk/src/openssl/ed_validator.rs
@@ -0,0 +1,97 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use openssl::pkey::PKey;
+
+use crate::{validator::CoseValidator, Error, Result};
+
+pub struct EdValidator {
+ _alg: String,
+}
+
+impl EdValidator {
+ pub fn new(alg: &str) -> Self {
+ EdValidator {
+ _alg: alg.to_owned(),
+ }
+ }
+}
+
+impl CoseValidator for EdValidator {
+ fn validate(&self, sig: &[u8], data: &[u8], pkey: &[u8]) -> Result<bool> {
+ let public_key = PKey::public_key_from_der(pkey).map_err(|_err| Error::CoseSignature)?;
+
+ let mut verifier = openssl::sign::Verifier::new_without_digest(&public_key)
+ .map_err(|_err| Error::CoseSignature)?;
+
+ verifier
+ .verify_oneshot(sig, data)
+ .map_err(|_err| Error::CoseSignature)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use tempfile::tempdir;
+
+ use crate::{openssl::temp_signer, Signer};
+
+ #[test]
+ fn sign_and_validate() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, cert_path) = temp_signer::get_ed_signer(&temp_dir.path(), "ed25519", None);
+
+ let data = b"some sample content to sign";
+ println!("data len = {}", data.len());
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature.len = {}", signature.len());
+ assert!(signature.len() >= 64);
+ assert!(signature.len() <= signer.reserve_size());
+
+ let cert_bytes = std::fs::read(&cert_path).unwrap();
+
+ let signcert = openssl::x509::X509::from_pem(&cert_bytes).unwrap();
+ let pub_key = signcert.public_key().unwrap().public_key_to_der().unwrap();
+ let validator = EdValidator::new("ed25519");
+ assert!(validator.validate(&signature, data, &pub_key).unwrap());
+ }
+
+ #[test]
+ fn bad_data() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, cert_path) = temp_signer::get_ed_signer(&temp_dir.path(), "ed25519", None);
+
+ let mut data = b"some sample content to sign".to_vec();
+ println!("data len = {}", data.len());
+ let signature = signer.sign(&data).unwrap();
+
+ data[5] = 10;
+ data[6] = 11;
+
+ let cert_bytes = std::fs::read(&cert_path).unwrap();
+ let signcert = openssl::x509::X509::from_pem(&cert_bytes).unwrap();
+ let pub_key = signcert.public_key().unwrap().public_key_to_der().unwrap();
+
+ let validator = EdValidator::new("es256");
+ // ^^ REVIEW with @mfisher: Is this correct? Shouldn't it be ed25519?
+
+ assert!(!validator.validate(&signature, &data, &pub_key).unwrap());
+ }
+}
diff --git a/sdk/src/openssl/mod.rs b/sdk/src/openssl/mod.rs
@@ -0,0 +1,85 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+mod rsa_signer;
+pub use rsa_signer::RsaSigner;
+
+mod rsa_validator;
+pub use rsa_validator::RsaValidator;
+
+mod ec_signer;
+pub use ec_signer::EcSigner;
+
+mod ec_validator;
+pub use ec_validator::EcValidator;
+
+mod ed_signer;
+pub use ed_signer::EdSigner;
+
+mod ed_validator;
+pub use ed_validator::EdValidator;
+
+pub mod temp_signer;
+
+use openssl::x509::X509;
+
+pub(crate) fn check_chain_order(certs: &[X509]) -> bool {
+ if certs.len() > 1 {
+ for (i, c) in certs.iter().enumerate() {
+ if let Some(next_c) = certs.get(i + 1) {
+ if let Ok(pkey) = next_c.public_key() {
+ if let Ok(verified) = c.verify(&pkey) {
+ if !verified {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+ }
+ true
+}
+
+pub(crate) fn check_chain_order_der(cert_ders: &[Vec<u8>]) -> bool {
+ if cert_ders.len() > 1 {
+ let mut certs: Vec<X509> = Vec::new();
+ for cert_der in cert_ders {
+ if let Ok(cert) = X509::from_der(cert_der) {
+ certs.push(cert);
+ } else {
+ return false;
+ }
+ }
+
+ for (i, c) in certs.iter().enumerate() {
+ if let Some(next_c) = certs.get(i + 1) {
+ if let Ok(pkey) = next_c.public_key() {
+ if let Ok(verified) = c.verify(&pkey) {
+ if !verified {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+ }
+ true
+}
diff --git a/sdk/src/openssl/rsa_signer.rs b/sdk/src/openssl/rsa_signer.rs
@@ -0,0 +1,262 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ ocsp_utils::{get_ocsp_response, OcspData},
+ signer::ConfigurableSigner,
+ Error, Result, Signer,
+};
+use std::{cell::Cell, fs, path::Path};
+
+//use extfmt::Hexlify;
+use openssl::{
+ hash::MessageDigest,
+ pkey::{PKey, Private},
+ rsa::Rsa,
+ x509::X509,
+};
+
+use super::check_chain_order;
+
+/// Implements `Signer` trait using OpenSSL's implementation of
+/// SHA256 + RSA encryption.
+pub struct RsaSigner {
+ signcerts: Vec<X509>,
+ pkey: PKey<Private>,
+
+ certs_size: usize,
+ timestamp_size: usize,
+ ocsp_size: Cell<usize>,
+
+ alg: String,
+ tsa_url: Option<String>,
+ ocsp_rsp: Cell<OcspData>,
+}
+
+impl RsaSigner {
+ pub fn update_ocsp(&self) {
+ // do we need an update
+ let now = chrono::offset::Utc::now();
+
+ // is it time for an OCSP update
+ let ocsp_data = self.ocsp_rsp.take();
+ let next_update = ocsp_data.next_update;
+ self.ocsp_rsp.set(ocsp_data);
+ if now < next_update {
+ return;
+ }
+
+ if let Ok(certs) = self.certs() {
+ if let Some(ocsp_rsp) = get_ocsp_response(&certs) {
+ self.ocsp_size.set(ocsp_rsp.ocsp_der.len());
+ self.ocsp_rsp.set(ocsp_rsp);
+ }
+ }
+ }
+}
+
+impl ConfigurableSigner for RsaSigner {
+ fn from_files<P: AsRef<Path>>(
+ signcert_path: P,
+ pkey_path: P,
+ alg: String,
+ tsa_url: Option<String>,
+ ) -> Result<Self> {
+ let signcert = fs::read(signcert_path).map_err(wrap_io_err)?;
+ let pkey = fs::read(pkey_path).map_err(wrap_io_err)?;
+
+ Self::from_signcert_and_pkey(&signcert, &pkey, alg, tsa_url)
+ }
+
+ fn from_signcert_and_pkey(
+ signcert: &[u8],
+ pkey: &[u8],
+ alg: String,
+ tsa_url: Option<String>,
+ ) -> Result<Self> {
+ let signcerts = X509::stack_from_pem(signcert).map_err(wrap_openssl_err)?;
+ let rsa = Rsa::private_key_from_pem(pkey).map_err(wrap_openssl_err)?;
+ let pkey = PKey::from_rsa(rsa).map_err(wrap_openssl_err)?;
+
+ // make sure cert chains are in order
+ if !check_chain_order(&signcerts) {
+ return Err(Error::BadParam(
+ "certificate chain is not in correct order".to_string(),
+ ));
+ }
+
+ let signer = RsaSigner {
+ signcerts,
+ pkey,
+ certs_size: signcert.len(),
+ timestamp_size: 4096, // todo: call out to TSA to get actual timestamp and use that size
+ ocsp_size: Cell::new(0),
+ alg,
+ tsa_url,
+ ocsp_rsp: Cell::new(OcspData::new()),
+ };
+
+ // get OCSP if possible
+ signer.update_ocsp();
+
+ Ok(signer)
+ }
+}
+
+impl Signer for RsaSigner {
+ fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
+ let mut signer = match self.alg.as_str() {
+ "ps256" => {
+ let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &self.pkey)
+ .map_err(wrap_openssl_err)?;
+
+ signer.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS)?; // use C2PA recommended padding
+ signer.set_rsa_mgf1_md(MessageDigest::sha256())?;
+ signer.set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::DIGEST_LENGTH)?;
+ signer
+ }
+ "ps384" => {
+ let mut signer = openssl::sign::Signer::new(MessageDigest::sha384(), &self.pkey)
+ .map_err(wrap_openssl_err)?;
+
+ signer.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS)?; // use C2PA recommended padding
+ signer.set_rsa_mgf1_md(MessageDigest::sha384())?;
+ signer.set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::DIGEST_LENGTH)?;
+ signer
+ }
+ "ps512" => {
+ let mut signer = openssl::sign::Signer::new(MessageDigest::sha512(), &self.pkey)
+ .map_err(wrap_openssl_err)?;
+
+ signer.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS)?; // use C2PA recommended padding
+ signer.set_rsa_mgf1_md(MessageDigest::sha512())?;
+ signer.set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::DIGEST_LENGTH)?;
+ signer
+ }
+ "rs256" => openssl::sign::Signer::new(MessageDigest::sha256(), &self.pkey)
+ .map_err(wrap_openssl_err)?,
+ "rs384" => openssl::sign::Signer::new(MessageDigest::sha384(), &self.pkey)
+ .map_err(wrap_openssl_err)?,
+ "rs512" => openssl::sign::Signer::new(MessageDigest::sha512(), &self.pkey)
+ .map_err(wrap_openssl_err)?,
+ _ => return Err(Error::UnsupportedType),
+ };
+
+ let signed_data = signer.sign_oneshot_to_vec(data)?;
+
+ // println!("sig: {}", Hexlify(&signed_data));
+
+ Ok(signed_data)
+ }
+
+ fn reserve_size(&self) -> usize {
+ 1024 + self.certs_size + self.timestamp_size + self.ocsp_size.get() // the Cose_Sign1 contains complete certs, timestamps and ocsp so account for size
+ }
+
+ fn certs(&self) -> Result<Vec<Vec<u8>>> {
+ let mut certs: Vec<Vec<u8>> = Vec::new();
+
+ for c in &self.signcerts {
+ let cert = c.to_der().map_err(wrap_openssl_err)?;
+ certs.push(cert);
+ }
+
+ Ok(certs)
+ }
+
+ fn alg(&self) -> Option<String> {
+ Some(self.alg.to_owned())
+ }
+
+ fn time_authority_url(&self) -> Option<String> {
+ self.tsa_url.clone()
+ }
+
+ fn ocsp_val(&self) -> Option<Vec<u8>> {
+ // update OCSP if needed
+ self.update_ocsp();
+
+ let ocsp_data = self.ocsp_rsp.take();
+ let ocsp_rsp = ocsp_data.ocsp_der.clone();
+ self.ocsp_rsp.set(ocsp_data);
+ if !ocsp_rsp.is_empty() {
+ Some(ocsp_rsp)
+ } else {
+ None
+ }
+ }
+}
+
+fn wrap_io_err(err: std::io::Error) -> Error {
+ Error::IoError(err)
+}
+
+fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error {
+ Error::OpenSslError(err)
+}
+
+#[allow(unused_imports)]
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use tempfile::tempdir;
+
+ use crate::{openssl::temp_signer::get_signer, Signer};
+
+ #[test]
+ fn signer_from_files() {
+ let temp_dir = tempdir().unwrap();
+
+ let (signer, _) = get_signer(&temp_dir.path());
+ let data = b"some sample content to sign";
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature len = {}", signature.len());
+ assert!(signature.len() <= signer.reserve_size());
+ }
+
+ #[test]
+ fn sign_ps256() {
+ let cert_bytes = include_bytes!("../../tests/fixtures/temp_cert.data");
+ let key_bytes = include_bytes!("../../tests/fixtures/temp_priv_key.data");
+
+ let signer =
+ RsaSigner::from_signcert_and_pkey(cert_bytes, key_bytes, "ps256".to_string(), None)
+ .unwrap();
+
+ let data = b"some sample content to sign";
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature len = {}", signature.len());
+ assert!(signature.len() <= signer.reserve_size());
+ }
+
+ #[test]
+ fn sign_rs256() {
+ let cert_bytes = include_bytes!("../../tests/fixtures/temp_cert.data");
+ let key_bytes = include_bytes!("../../tests/fixtures/temp_priv_key.data");
+
+ let signer =
+ RsaSigner::from_signcert_and_pkey(cert_bytes, key_bytes, "rs256".to_string(), None)
+ .unwrap();
+
+ let data = b"some sample content to sign";
+
+ let signature = signer.sign(data).unwrap();
+ println!("signature len = {}", signature.len());
+ assert!(signature.len() <= signer.reserve_size());
+ }
+}
diff --git a/sdk/src/openssl/rsa_validator.rs b/sdk/src/openssl/rsa_validator.rs
@@ -0,0 +1,167 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{validator::CoseValidator, Error, Result};
+use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
+
+pub struct RsaValidator {
+ alg: String,
+}
+
+impl RsaValidator {
+ pub fn new(alg: &str) -> Self {
+ RsaValidator {
+ alg: alg.to_owned(),
+ }
+ }
+}
+
+impl CoseValidator for RsaValidator {
+ fn validate(&self, sig: &[u8], data: &[u8], pkey: &[u8]) -> Result<bool> {
+ let rsa = Rsa::public_key_from_der(pkey)?;
+ let pkey = PKey::from_rsa(rsa)?;
+
+ let mut verifier = match self.alg.as_str() {
+ "ps256" => {
+ let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &pkey)?;
+ verifier.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS)?; // use C2PA recommended padding
+ verifier.set_rsa_mgf1_md(MessageDigest::sha256())?;
+ verifier
+ }
+ "ps384" => {
+ let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha384(), &pkey)?;
+ verifier.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS)?; // use C2PA recommended padding
+ verifier.set_rsa_mgf1_md(MessageDigest::sha384())?;
+ verifier
+ }
+ "ps512" => {
+ let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha512(), &pkey)?;
+ verifier.set_rsa_padding(openssl::rsa::Padding::PKCS1_PSS)?; // use C2PA recommended padding
+ verifier.set_rsa_mgf1_md(MessageDigest::sha512())?;
+ verifier
+ }
+ "rs256" => openssl::sign::Verifier::new(MessageDigest::sha256(), &pkey)?,
+ "rs384" => openssl::sign::Verifier::new(MessageDigest::sha384(), &pkey)?,
+ "rs512" => openssl::sign::Verifier::new(MessageDigest::sha512(), &pkey)?,
+ _ => return Err(Error::UnsupportedType),
+ };
+
+ verifier
+ .verify_oneshot(sig, data)
+ .map_err(|_err| Error::CoseSignature)
+ }
+}
+
+#[allow(unused_imports)]
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ use crate::{signer::ConfigurableSigner, Signer};
+
+ #[test]
+ fn verify_rsa_signatures() {
+ let cert_bytes = include_bytes!("../../tests/fixtures/temp_cert.data");
+ let key_bytes = include_bytes!("../../tests/fixtures/temp_priv_key.data");
+
+ let signcert = openssl::x509::X509::from_pem(cert_bytes).unwrap();
+ let pkey = signcert.public_key().unwrap().public_key_to_der().unwrap();
+
+ let data = b"some sample content to sign";
+
+ println!("Test RS256");
+ let mut signer = crate::openssl::RsaSigner::from_signcert_and_pkey(
+ cert_bytes,
+ key_bytes,
+ "rs256".to_string(),
+ None,
+ )
+ .unwrap();
+
+ let mut signature = signer.sign(data).unwrap();
+ println!("signature len = {}", signature.len());
+ let mut validator = RsaValidator::new("rs256");
+ assert!(validator.validate(&signature, data, &pkey).unwrap());
+
+ println!("Test RS384");
+ signer = crate::openssl::RsaSigner::from_signcert_and_pkey(
+ cert_bytes,
+ key_bytes,
+ "rs384".to_string(),
+ None,
+ )
+ .unwrap();
+
+ signature = signer.sign(data).unwrap();
+ println!("signature len = {}", signature.len());
+ validator = RsaValidator::new("rs384");
+ assert!(validator.validate(&signature, data, &pkey).unwrap());
+
+ println!("Test RS512");
+ signer = crate::openssl::RsaSigner::from_signcert_and_pkey(
+ cert_bytes,
+ key_bytes,
+ "rs512".to_string(),
+ None,
+ )
+ .unwrap();
+
+ signature = signer.sign(data).unwrap();
+ println!("signature len = {}", signature.len());
+ validator = RsaValidator::new("rs512");
+ assert!(validator.validate(&signature, data, &pkey).unwrap());
+
+ println!("Test PS256");
+ signer = crate::openssl::RsaSigner::from_signcert_and_pkey(
+ cert_bytes,
+ key_bytes,
+ "ps256".to_string(),
+ None,
+ )
+ .unwrap();
+
+ signature = signer.sign(data).unwrap();
+ println!("signature len = {}", signature.len());
+ validator = RsaValidator::new("ps256");
+ assert!(validator.validate(&signature, data, &pkey).unwrap());
+
+ println!("Test PS384");
+ signer = crate::openssl::RsaSigner::from_signcert_and_pkey(
+ cert_bytes,
+ key_bytes,
+ "ps384".to_string(),
+ None,
+ )
+ .unwrap();
+
+ signature = signer.sign(data).unwrap();
+ println!("signature len = {}", signature.len());
+ validator = RsaValidator::new("ps384");
+ assert!(validator.validate(&signature, data, &pkey).unwrap());
+
+ println!("Test PS512");
+ signer = crate::openssl::RsaSigner::from_signcert_and_pkey(
+ cert_bytes,
+ key_bytes,
+ "ps512".to_string(),
+ None,
+ )
+ .unwrap();
+
+ signature = signer.sign(data).unwrap();
+ println!("signature len = {}", signature.len());
+ validator = RsaValidator::new("ps512");
+ assert!(validator.validate(&signature, data, &pkey).unwrap());
+ }
+}
diff --git a/sdk/src/openssl/temp_signer.rs b/sdk/src/openssl/temp_signer.rs
@@ -0,0 +1,415 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#![deny(missing_docs)]
+
+//! Temporary signing instances for testing purposes.
+//!
+//! This module contains functions to create self-signed certificates
+//! and provision [`Signer`] instances for each of the supported signature
+//! formats.
+//!
+//! Private-key and signing certificate pairs are created in a directory
+//! provided by the caller. It is recommended to use a temporary directory
+//! that is deleted upon completion of the test. (We recommend using
+//! the [tempfile](https://crates.io/crates/tempfile) crate.)
+//!
+//! This module should be used only for testing purposes.
+
+// Since this module is intended for testing purposes, all of
+// its functions are allowed to panic.
+#![allow(clippy::panic)]
+#![allow(clippy::unwrap_used)]
+
+use std::{
+ io::Write,
+ path::{Path, PathBuf},
+ process::{Child, Command, Stdio},
+};
+
+use crate::{
+ openssl::{EcSigner, EdSigner, RsaSigner},
+ signer::ConfigurableSigner,
+ Signer,
+};
+
+/// Create a [`Signer`] instance that can be used for testing purposes.
+///
+/// This is a suitable default for use when you need a [`Signer`], but
+/// don't care what the format is.
+///
+/// # Arguments
+///
+/// * `path` - A directory (which must already exist) to receive the temporary
+/// private key / certificate pair.
+///
+/// # Returns
+///
+/// Returns a tuple of `(signer, sign_cert_path)` where `signer` is
+/// the [`Signer`] instance and `sign_cert_path` is the path to the
+/// signing certificate.
+///
+/// # Panics
+///
+/// Can panic if unable to invoke OpenSSL executable properly.
+pub fn get_signer<P: AsRef<Path>>(path: P) -> (RsaSigner, PathBuf) {
+ let (sign_cert_path, pem_key_path) = make_key_path_pair(path, "temp_key");
+
+ create_x509_key_pair(
+ &sign_cert_path,
+ &pem_key_path,
+ false,
+ Some("rsa_padding_mode:pss"),
+ Some("-sha256"),
+ );
+
+ (
+ RsaSigner::from_files(&sign_cert_path, &pem_key_path, "ps256".to_string(), None).unwrap(),
+ sign_cert_path,
+ )
+}
+
+/// Create an OpenSSL ES256 signer that can be used for testing purposes.
+///
+/// # Arguments
+///
+/// * `path` - A directory (which must already exist) to receive the temporary
+/// private key / certificate pair.
+/// * `alg` - A format for signing. Must be one of (`es256`, `es384`, or `es512`).
+/// * `tsa_url` - Optional URL for a timestamp authority.
+///
+/// # Returns
+///
+/// Returns a tuple of `(signer, sign_cert_path)` where `signer` is
+/// the [`Signer`] instance and `sign_cert_path` is the path to the
+/// signing certificate.
+///
+/// # Panics
+///
+/// Can panic if unable to invoke OpenSSL executable properly.
+pub fn get_ec_signer<P: AsRef<Path>>(
+ path: P,
+ alg: &str,
+ tsa_url: Option<String>,
+) -> (EcSigner, PathBuf) {
+ let (key_name, ec_key_name) = match alg {
+ "es256" => ("ec256_key", "prime256v1"),
+ "es384" => ("ec384_key", "secp384r1"),
+ "es512" => ("ec512_key", "secp521r1"),
+ _ => {
+ panic!("Unknown EC signer alg {:#?}", alg);
+ }
+ };
+
+ let (sign_cert_path, pem_key_path) = make_key_path_pair(path, key_name);
+
+ let mut openssl = Command::new("openssl");
+ openssl
+ .arg("ecparam")
+ .arg("-genkey")
+ .arg("-name")
+ .arg(ec_key_name)
+ .arg("-noout")
+ .arg("-out")
+ .arg(&pem_key_path)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped());
+
+ process_openssl_output(spawn_openssl(&mut openssl));
+
+ create_x509_key_pair(&sign_cert_path, &pem_key_path, true, None, Some("-sha256"));
+
+ (
+ EcSigner::from_files(&sign_cert_path, &pem_key_path, alg.to_string(), tsa_url).unwrap(),
+ sign_cert_path,
+ )
+}
+
+/// Create an OpenSSL ES256 signer that can be used for testing purposes.
+///
+/// # Arguments
+///
+/// * `path` - A directory (which must already exist) to receive the temporary
+/// private key / certificate pair.
+/// * `alg` - A format for signing. Must be `ed25519`.
+/// * `tsa_url` - Optional URL for a timestamp authority.
+///
+/// # Returns
+///
+/// Returns a tuple of `(signer, sign_cert_path)` where `signer` is
+/// the [`Signer`] instance and `sign_cert_path` is the path to the
+/// signing certificate.
+///
+/// # Panics
+///
+/// Can panic if unable to invoke OpenSSL executable properly.
+pub fn get_ed_signer<P: AsRef<Path>>(
+ path: P,
+ alg: &str,
+ tsa_url: Option<String>,
+) -> (EdSigner, PathBuf) {
+ let (key_name, openssl_alg_name) = match alg {
+ "ed25519" => ("ed25519_key", "ED25519"),
+ _ => {
+ panic!("Unknown ED signer alg {:#?}", alg);
+ }
+ };
+
+ let (sign_cert_path, pem_key_path) = make_key_path_pair(path, key_name);
+
+ let mut openssl = Command::new("openssl");
+ openssl
+ .arg("genpkey")
+ .arg("-algorithm")
+ .arg(&openssl_alg_name)
+ .arg("-out")
+ .arg(&pem_key_path)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped());
+
+ process_openssl_output(spawn_openssl(&mut openssl));
+
+ create_x509_key_pair(&sign_cert_path, &pem_key_path, true, None, None);
+
+ (
+ EdSigner::from_files(&sign_cert_path, &pem_key_path, alg.to_string(), tsa_url).unwrap(),
+ sign_cert_path,
+ )
+}
+
+/// Create an OpenSSL SHA+RSA signer that can be used for testing purposes.
+///
+/// # Arguments
+///
+/// * `path` - A directory (which must already exist) to receive the temporary
+/// private key / certificate pair.
+/// * `alg` - A format for signing. Must be one of (`rs256`, `rs384`, `rs512`,
+/// `ps256`, `ps384`, or `ps512`).
+/// * `tsa_url` - Optional URL for a timestamp authority.
+///
+/// # Returns
+///
+/// Returns a tuple of `(signer, sign_cert_path)` where `signer` is
+/// the [`Signer`] instance and `sign_cert_path` is the path to the
+/// signing certificate.
+///
+/// # Panics
+///
+/// Can panic if unable to invoke OpenSSL executable properly.
+pub fn get_rsa_signer<P: AsRef<Path>>(
+ path: P,
+ alg: &str,
+ tsa_url: Option<String>,
+) -> (RsaSigner, PathBuf) {
+ let (key_name, sha_mode, rsa_padding_mode) = match alg {
+ "rs256" => ("rsa256_key", "-sha256", None),
+ "rs384" => ("rsa384_key", "-sha384", None),
+ "rs512" => ("rsa512_key", "-sha512", None),
+ "ps256" => ("rsa-pss256_key", "-sha256", Some("rsa_padding_mode:pss")),
+ "ps384" => ("rsa-pss384_key", "-sha384", Some("rsa_padding_mode:pss")),
+ "ps512" => ("rsa-pss512_key", "-sha512", Some("rsa_padding_mode:pss")),
+ _ => {
+ panic!("Unknown RSA signer alg {:#?}", alg);
+ }
+ };
+
+ let (sign_cert_path, pem_key_path) = make_key_path_pair(path, key_name);
+
+ create_x509_key_pair(
+ &sign_cert_path,
+ &pem_key_path,
+ false,
+ rsa_padding_mode,
+ Some(sha_mode),
+ );
+
+ (
+ RsaSigner::from_files(&sign_cert_path, &pem_key_path, alg.to_string(), tsa_url).unwrap(),
+ sign_cert_path,
+ )
+}
+
+/// Create a signer that can be used for testing purposes.
+///
+/// Can generate a [`Signer`] instance for all supported formats.
+///
+/// # Arguments
+///
+/// * `path` - A directory (which must already exist) to receive the temporary
+/// private key / certificate pair.
+/// * `alg` - A format for signing. Must be one of (`rs256`, `rs384`, `rs512`,
+/// `ps256`, `ps384`, `ps512`, `es256`, `es384`, `es512`, or `ed25519`).
+/// * `tsa_url` - Optional URL for a timestamp authority.
+///
+/// # Returns
+///
+/// Returns a tuple of `(signer, sign_cert_path)` where `signer` is
+/// the [`Signer`] instance and `sign_cert_path` is the path to the
+/// signing certificate.
+///
+/// # Panics
+///
+/// Can panic if unable to invoke OpenSSL executable properly.
+pub fn get_signer_by_alg<P: AsRef<Path>>(
+ path: P,
+ alg: &str,
+ tsa_url: Option<String>,
+) -> (Box<dyn Signer>, PathBuf) {
+ match alg.to_lowercase().as_str() {
+ "rs256" | "rs384" | "rs512" | "ps256" | "ps384" | "ps512" => {
+ let (signer, sign_cert_path) = get_rsa_signer(path, alg, tsa_url);
+ (Box::new(signer), sign_cert_path)
+ }
+ "es256" | "es384" | "es512" => {
+ let (signer, sign_cert_path) = get_ec_signer(path, alg, tsa_url);
+ (Box::new(signer), sign_cert_path)
+ }
+
+ "ed25519" => {
+ let (signer, sign_cert_path) = get_ed_signer(path, alg, tsa_url);
+ (Box::new(signer), sign_cert_path)
+ }
+ _ => {
+ let (signer, sign_cert_path) = get_rsa_signer(path, "ps256", tsa_url);
+ (Box::new(signer), sign_cert_path)
+ }
+ }
+}
+
+fn make_key_path_pair<P: AsRef<Path>>(path: P, key_name: &str) -> (PathBuf, PathBuf) {
+ let mut sign_cert_path = path.as_ref().to_path_buf();
+ sign_cert_path.push(key_name);
+ sign_cert_path.set_extension("pub");
+ //println!("sign_cert_path = {:#?}", sign_cert_path);
+
+ let mut pem_key_path = sign_cert_path.clone();
+ pem_key_path.set_extension("pem");
+ //println!("pem_key_path = {:#?}", pem_key_path);
+
+ (sign_cert_path, pem_key_path)
+}
+
+// The .x509 directory at the root of this repo is flagged
+// as outside of source control via .gitignore.
+//
+// This function panics if unable to invoke openssl as expected.
+fn create_x509_key_pair(
+ sign_cert_path: &Path,
+ pem_key_path: &Path,
+ has_priv_key: bool,
+ rsa_padding_mode: Option<&str>,
+ sha_mode: Option<&str>,
+) {
+ let mut openssl = Command::new("openssl");
+ openssl.arg("req").arg("-new");
+
+ if !has_priv_key {
+ openssl.arg("-newkey").arg("rsa:4096").arg("-nodes");
+ }
+
+ if let Some(rsa_padding_mode) = rsa_padding_mode {
+ openssl.arg("-sigopt").arg(rsa_padding_mode);
+ }
+
+ openssl
+ .arg("-days")
+ .arg("180")
+ .arg("-extensions")
+ .arg("v3_ca")
+ .arg("-addext")
+ .arg("keyUsage = digitalSignature")
+ .arg("-addext")
+ .arg("extendedKeyUsage = emailProtection")
+ .arg("-x509")
+ .arg(if has_priv_key { "-key" } else { "-keyout" })
+ .arg(pem_key_path)
+ .arg("-out")
+ .arg(sign_cert_path);
+
+ if let Some(sha_mode) = sha_mode {
+ openssl.arg(sha_mode);
+ }
+
+ openssl
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped());
+
+ let mut openssl = spawn_openssl(&mut openssl);
+
+ let mut openssl_stdin = openssl.stdin.take().unwrap();
+ writeln!(&mut openssl_stdin, "us").unwrap();
+ writeln!(&mut openssl_stdin, "ca").unwrap();
+ writeln!(&mut openssl_stdin, "Somewhere").unwrap();
+ writeln!(&mut openssl_stdin, "Some Company").unwrap();
+ writeln!(&mut openssl_stdin, "FOR TESTING ONLY").unwrap();
+ writeln!(&mut openssl_stdin, "example.com").unwrap();
+ writeln!(&mut openssl_stdin).unwrap(); // Don't provide an email address.
+ drop(openssl_stdin);
+
+ process_openssl_output(openssl);
+}
+
+fn spawn_openssl(openssl: &mut Command) -> Child {
+ //println!("openssl command = {:#?}", openssl);
+
+ match openssl.spawn() {
+ Ok(openssl) => openssl,
+ Err(e) => {
+ eprintln!("Please ensure that openssl is installed on this device.");
+ print_mac_openssl_warning();
+ panic!("Unable to invoke openssl\n\n{:#?}", e);
+ }
+ }
+}
+
+fn process_openssl_output(openssl: Child) {
+ let output = openssl.wait_with_output().unwrap();
+
+ if !output.status.success() {
+ eprintln!("openssl exited with status {:?}\n\n", output.status);
+
+ if let Ok(stdout) = String::from_utf8(output.stdout) {
+ eprintln!("stdout\n\n{:?}\n\n", stdout);
+ }
+ if let Ok(stderr) = String::from_utf8(output.stderr) {
+ eprintln!("stderr\n\n{:?}\n\n", stderr);
+ }
+
+ print_mac_openssl_warning();
+
+ panic!("Unable to construct public/private key pair; exiting");
+ }
+}
+
+fn print_mac_openssl_warning() {
+ #[cfg(target_os = "macos")]
+ {
+ eprintln!();
+ eprintln!("If you have problems generating certs on MacOS, you may need to replace the built-in version");
+ eprintln!("of openssl with a more current version. Try this:");
+ eprintln!();
+ eprintln!(" $ openssl version");
+ eprintln!();
+ eprintln!("If your version is 1.x.x, try this:");
+ eprintln!();
+ eprintln!(" $ brew install openssl");
+ eprintln!();
+ eprintln!("and then update your path (via .zshrc or similar) as follows:");
+ eprintln!();
+ eprintln!(" $ export PATH=\"/usr/local/opt/openssl@3/bin:$PATH\"");
+ eprintln!();
+ }
+}
diff --git a/sdk/src/salt.rs b/sdk/src/salt.rs
@@ -0,0 +1,69 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+/// The Saltgenerator trait always the caller to supply
+/// a funtion to generate a salt value used when hashing
+/// data. Providing a unique salt ensures a unique hash for
+/// a given data set.
+pub trait SaltGenerator {
+ /// generate a salt vector
+ fn generate_salt(&self) -> Option<Vec<u8>>;
+}
+
+/// NoSalt return a no salt option to a function
+pub struct NoSalt {}
+
+impl SaltGenerator for NoSalt {
+ fn generate_salt(&self) -> Option<Vec<u8>> {
+ None
+ }
+}
+
+/// const NoSalt instance that can be used when no salting is required
+pub const NO_SALT: &NoSalt = &NoSalt {};
+
+/// Default salt generator
+/// This generator uses OpenSSL to generate a
+/// salt of the specified length (default 16 bytes)
+pub struct DefaultSalt {
+ salt_len: usize,
+}
+
+impl DefaultSalt {
+ /// Set the length of the generated salt vector
+ #[allow(dead_code)]
+ pub fn set_salt_length(&mut self, len: usize) {
+ self.salt_len = len;
+ }
+}
+
+impl Default for DefaultSalt {
+ fn default() -> Self {
+ DefaultSalt { salt_len: 16 }
+ }
+}
+
+impl SaltGenerator for DefaultSalt {
+ fn generate_salt(&self) -> Option<Vec<u8>> {
+ #[cfg(feature = "file_io")] // auto generation not supported on wasm
+ {
+ let mut salt = vec![0; self.salt_len];
+ openssl::rand::rand_bytes(&mut salt).ok()?;
+ Some(salt)
+ }
+ #[cfg(not(feature = "file_io"))]
+ {
+ None
+ }
+ }
+}
diff --git a/sdk/src/signer.rs b/sdk/src/signer.rs
@@ -0,0 +1,131 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::Result;
+
+/// The `Signer` trait generates a cryptographic signature over a byte array.
+///
+/// This trait exists to allow the signature mechanism to be extended.
+pub trait Signer {
+ /// Returns a new byte array which is a signature over the original.
+ fn sign(&self, data: &[u8]) -> Result<Vec<u8>>;
+
+ /// Returns the algorithm of the Signer.
+ fn alg(&self) -> Option<String>;
+
+ /// Returns the certificates as a Vec containing a Vec of DER bytes for each certificate.
+ fn certs(&self) -> Result<Vec<Vec<u8>>>;
+
+ /// Returns the size in bytes of the largest possible expected signature.
+ /// Signing will fail if the result of the `sign` function is larger
+ /// than this value.
+ fn reserve_size(&self) -> usize;
+
+ /// URL for time authority to time stamp the signature
+ fn time_authority_url(&self) -> Option<String> {
+ None
+ }
+
+ /// OCSP response for the signing cert if available
+ /// This is the only C2PA supported cert revocation method.
+ /// By pre-querying the value for a your signing cert the value can
+ /// be cached taking pressure off of the CA (recommended by C2PA spec)
+ fn ocsp_val(&self) -> Option<Vec<u8>> {
+ None
+ }
+}
+
+/// Trait to allow loading of signing credential from external sources
+pub trait ConfigurableSigner: Signer + Sized {
+ /// Create signer form credential files
+ fn from_files<P: AsRef<std::path::Path>>(
+ signcert_path: P,
+ pkey_path: P,
+ alg: String,
+ tsa_url: Option<String>,
+ ) -> Result<Self>;
+
+ /// Create signer from credentials data
+ fn from_signcert_and_pkey(
+ signcert: &[u8],
+ pkey: &[u8],
+ alg: String,
+ tsa_url: Option<String>,
+ ) -> Result<Self>;
+}
+
+/// The `Placeholder` implementation provides a placeholder "signer" for use
+/// in testing and development contexts where a valid signature is not required.
+/// To state the obvious, claims signed using this implementation will not verify.
+pub struct Placeholder {}
+
+impl Signer for Placeholder {
+ // sign the provided bytes
+ fn sign(&self, _data: &[u8]) -> Result<Vec<u8>> {
+ Ok(b"invalid signature".to_vec())
+ }
+
+ // algoritim iddentifer string for this Signer
+ fn alg(&self) -> Option<String> {
+ None
+ }
+
+ // list of certificates in der format, with last being cert that signed the claim
+ fn certs(&self) -> Result<Vec<Vec<u8>>> {
+ Ok(Vec::new())
+ }
+
+ // bytes to reserve for a fully signed claim
+ fn reserve_size(&self) -> usize {
+ 128
+ }
+}
+
+#[cfg(feature = "async_signer")]
+use async_trait::async_trait;
+
+/// The `AsyncSigner` trait generates a cryptographic signature over a byte array.
+///
+/// This trait exists to allow the signature mechanism to be extended.
+///
+/// Use this when the implementation is asynchronous.
+#[cfg(feature = "async_signer")]
+#[async_trait]
+pub trait AsyncSigner: Sync {
+ /// Returns a new byte array which is a signature over the original.
+ async fn sign(&self, data: &[u8]) -> Result<Vec<u8>>;
+
+ /// Returns the size in bytes of the largest possible expected signature.
+ /// Signing will fail if the result of the `sign` function is larger
+ /// than this value.
+ fn reserve_size(&self) -> usize;
+}
+
+/// The `AsyncPlaceholder` implementation provides a placeholder "async signer"
+/// for use in testing and development contexts where a valid signature is not
+/// required. To state the obvious, claims signed using this implementation
+/// will not verify.
+#[cfg(feature = "async_signer")]
+pub struct AsyncPlaceholder {}
+
+#[cfg(feature = "async_signer")]
+#[async_trait]
+impl AsyncSigner for AsyncPlaceholder {
+ async fn sign(&self, _data: &[u8]) -> Result<Vec<u8>> {
+ Ok(b"invalid signature".to_vec())
+ }
+
+ fn reserve_size(&self) -> usize {
+ 128
+ }
+}
diff --git a/sdk/src/status_tracker.rs b/sdk/src/status_tracker.rs
@@ -0,0 +1,301 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::error::{Error, Result};
+
+use std::fmt;
+
+#[derive(Debug)]
+pub struct LogItem {
+ pub label: String, // JUBMF label of the item if available, or other descriptive label
+ pub file: String, // File where failure occurred
+ pub function: String, // Function where failure occurred
+ pub line: String, // Line number for error
+ pub description: String, // Description of the failure
+ pub err_val: Option<Error>, // Actual error code
+ pub validation_status: Option<String>, // C2PA code if available
+}
+
+impl LogItem {
+ pub fn new(label: &str, description: &str, function: &str, file: &str, line: u32) -> Self {
+ LogItem {
+ label: label.to_string(),
+ file: file.to_string(),
+ function: function.to_string(),
+ line: line.to_string(),
+ description: description.to_string(),
+ err_val: None,
+ validation_status: None,
+ }
+ }
+
+ // add an error value
+ pub fn error(self, err: Error) -> Self {
+ LogItem {
+ err_val: Some(err),
+ ..self
+ }
+ }
+
+ // add an error value
+ pub fn validation_status(self, status: &str) -> Self {
+ LogItem {
+ validation_status: Some(status.to_string()),
+ ..self
+ }
+ }
+}
+
+pub trait StatusTracker {
+ // should we stop on the first error
+ fn stop_on_error(&self) -> bool;
+
+ // return refernce to current set of validation items
+ fn get_log(&self) -> &Vec<LogItem>;
+
+ // return mutable refernce to current set of validation items
+ fn get_log_mut(&mut self) -> &mut Vec<LogItem>;
+
+ // Log an item. Returns err if available
+ // and stop_on_error is true. Otherwise success OK(())
+ // log_item - LogItem to be recorded
+ // err - optional Error value to be returned if stop_on_error is true and item contain an error,
+ // otherwise if None, Error:LogStop will be returned if stop_on_err is true.
+ // The actual error is always available in the log_item. This allows the caller
+ // to return an error even when the error does not implement Clone.
+ fn log(&mut self, log_item: LogItem, err: Option<Error>) -> Result<()>;
+
+ // Log an item. No special consideration are given to the contents of the log item.
+ fn log_silent(&mut self, log_item: LogItem);
+}
+
+impl fmt::Display for dyn StatusTracker {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self.get_log())
+ }
+}
+
+impl fmt::Debug for dyn StatusTracker {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self.get_log())
+ }
+}
+
+// Logger that returns success regardless of if LogItem was for an error condition
+#[derive(Default, Debug)]
+pub struct DetailedStatusTracker {
+ logged_items: Vec<LogItem>,
+ stop_on_error: bool,
+}
+
+impl DetailedStatusTracker {
+ pub fn new() -> Self {
+ DetailedStatusTracker {
+ logged_items: Vec::new(),
+ stop_on_error: false,
+ }
+ }
+}
+
+impl StatusTracker for DetailedStatusTracker {
+ fn stop_on_error(&self) -> bool {
+ self.stop_on_error
+ }
+
+ fn get_log(&self) -> &Vec<LogItem> {
+ &self.logged_items
+ }
+
+ fn get_log_mut(&mut self) -> &mut Vec<LogItem> {
+ &mut self.logged_items
+ }
+
+ fn log(&mut self, log_item: LogItem, err: Option<Error>) -> Result<()> {
+ let item_has_err = log_item.err_val.is_some();
+ self.logged_items.push(log_item);
+ if self.stop_on_error && item_has_err {
+ Err(err.unwrap_or(Error::LogStop))
+ } else {
+ Ok(())
+ }
+ }
+
+ fn log_silent(&mut self, log_item: LogItem) {
+ self.logged_items.push(log_item);
+ }
+}
+
+// Logger that will returns error values on LogItems with error
+#[derive(Default, Debug)]
+pub struct OneShotStatusTracker {
+ logged_items: Vec<LogItem>,
+ stop_on_error: bool,
+}
+
+impl OneShotStatusTracker {
+ pub fn new() -> Self {
+ OneShotStatusTracker {
+ logged_items: Vec::new(),
+ stop_on_error: true,
+ }
+ }
+}
+
+impl StatusTracker for OneShotStatusTracker {
+ fn stop_on_error(&self) -> bool {
+ self.stop_on_error
+ }
+
+ fn get_log(&self) -> &Vec<LogItem> {
+ &self.logged_items
+ }
+
+ fn get_log_mut(&mut self) -> &mut Vec<LogItem> {
+ &mut self.logged_items
+ }
+
+ fn log(&mut self, log_item: LogItem, err: Option<Error>) -> Result<()> {
+ let item_has_err = log_item.err_val.is_some();
+ self.logged_items.push(log_item);
+ if self.stop_on_error && item_has_err {
+ Err(err.unwrap_or(Error::LogStop))
+ } else {
+ Ok(())
+ }
+ }
+
+ fn log_silent(&mut self, log_item: LogItem) {
+ self.logged_items.push(log_item);
+ }
+}
+/// Check to see if report contains a specific C2PA status code
+#[allow(dead_code)] // in case we make use of these or export this
+pub fn report_has_status(report: &[LogItem], val: &str) -> bool {
+ report.iter().any(|vi| {
+ if let Some(vs) = &vi.validation_status {
+ vs == val
+ } else {
+ false
+ }
+ })
+}
+
+/// Check to see if report contains a specific error
+/// Note: Only the out error object is matched for nested errors like
+/// "Error::InvalidClaim(InvalidClaimError::ClaimSignatureDescriptionBoxInvalid)".
+/// In this case any "InvalidClaim" would match
+#[allow(dead_code)] // in case we make use of these or export this
+pub fn report_has_err(report: &[LogItem], err: Error) -> bool {
+ report.iter().any(|vi| {
+ if let Some(e) = &vi.err_val {
+ std::mem::discriminant(e) == std::mem::discriminant(&err)
+ } else {
+ false
+ }
+ })
+}
+
+/// Split Errors off from rest of report
+#[allow(dead_code)] // in case we make use of these or export this
+pub fn report_split_errors(report: &mut Vec<LogItem>) -> Vec<LogItem> {
+ let mut output: Vec<LogItem> = Vec::new();
+
+ let mut i = 0;
+ while i < report.len() {
+ if report[i].err_val.is_some() {
+ output.push(report.remove(i));
+ } else {
+ i += 1;
+ }
+ }
+ output
+}
+/// log_item create a log item suitable for StatusTracker
+/// label - name of object this LogItem references
+/// description - reason for this LogItem
+/// function - name of the function generating this LogItem
+macro_rules! log_item {
+ ($label:expr, $description:expr, $function:expr) => {{
+ use crate::status_tracker::LogItem;
+ LogItem::new(
+ &$label.to_string(),
+ &$description.to_string(),
+ &$function.to_string(),
+ file!(),
+ line!(),
+ )
+ }};
+}
+
+pub(crate) use log_item;
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ use crate::validation_status;
+ //validation_item::log_item!;
+
+ #[test]
+ fn test_standard_tracker_stopping_for_error() {
+ let mut tracker = OneShotStatusTracker::new();
+
+ // item without error
+ let item1 = LogItem::new("test1", "test item 1", "test func", file!(), line!());
+ assert!(tracker.log(item1, None).is_ok());
+
+ // item with an error
+ let item2 = LogItem::new("test2", "test item 1", "test func", file!(), line!())
+ .error(Error::NotFound); // add arbitrary error
+ assert!(tracker.log(item2, None).is_err());
+
+ // item with error with caller specified error response, testing macro for generation
+ let item3 = log_item!("test3", "test item 3 from macro", "test func")
+ .error(Error::UnsupportedType)
+ .validation_status(validation_status::ALGORITHM_UNSUPPORTED);
+ assert!(matches!(
+ tracker.log(item3, Some(Error::NotFound)),
+ Err(Error::NotFound)
+ ));
+ }
+
+ #[test]
+ fn test_standard_tracker_no_stopping_for_error() {
+ let mut tracker = DetailedStatusTracker::new();
+
+ // item without error
+ let item1 = LogItem::new("test1", "test item 1", "test func", file!(), line!());
+ assert!(tracker.log(item1, None).is_ok());
+
+ // item with an error
+ let item2 = LogItem::new("test2", "test item 1", "test func", file!(), line!())
+ .error(Error::NotFound); // add arbitrary error
+ assert!(tracker.log(item2, None).is_ok());
+
+ // item with error with caller specified error response, testing macro for generation
+ let item3 =
+ log_item!("test3", "test item 3 from macro", "test func").error(Error::UnsupportedType);
+ assert!(tracker.log(item3, Some(Error::NotFound)).is_ok());
+
+ // item with error with caller specified error response, testing macro for generation, test validation_status
+ let item4 = log_item!("test3", "test item 3 from macro", "test func")
+ .error(Error::UnsupportedType)
+ .validation_status(validation_status::ALGORITHM_UNSUPPORTED);
+ assert!(tracker.log(item4, None).is_ok());
+
+ // there should be two items with error
+ let errors = report_split_errors(tracker.get_log_mut());
+ assert_eq!(errors.len(), 3);
+ }
+}
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -0,0 +1,2418 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ assertion::{Assertion, AssertionBase, AssertionDecodeError, AssertionDecodeErrorCause},
+ assertions::{labels, Ingredient, Relationship},
+ claim::{Claim, ClaimAssertion},
+ error::{Error, Result},
+ hash_utils::{hash_by_alg, vec_compare, verify_by_alg},
+ jumbf::{self, boxes::*},
+ jumbf_io::{get_cailoader_handler, load_cai_from_memory},
+ status_tracker::{log_item, OneShotStatusTracker, StatusTracker},
+ validation_status,
+ xmp_inmemory_utils::extract_provenance,
+};
+
+#[cfg(feature = "file_io")]
+use crate::{
+ assertion::AssertionData,
+ assertions::DataHash,
+ asset_io::{HashBlockObjectType, HashObjectPositions},
+ cose_sign::cose_sign,
+ cose_validator::verify_cose,
+ embedded_xmp,
+ jumbf_io::{
+ get_supported_file_extension, load_cai_from_file, object_locations, save_jumbf_to_file,
+ },
+ utils::{
+ hash_utils::{hash256, Exclusion},
+ patch::patch_bytes,
+ },
+ Signer,
+};
+
+#[cfg(feature = "async_signer")]
+use crate::AsyncSigner;
+use crate::ManifestStoreReport;
+#[cfg(feature = "file_io")]
+use log::error;
+use std::{collections::HashMap, io::Cursor};
+#[cfg(feature = "file_io")]
+use std::{fs, path::Path};
+
+/// A `Store` maintains a list of `Claim` structs.
+///
+/// Typically, this list of `Claim`s represents all of the claims in an asset.
+#[derive(Debug, PartialEq)]
+pub struct Store {
+ claims_map: HashMap<String, usize>,
+ claims: Vec<Claim>,
+ label: String,
+ provenance_path: Option<String>,
+}
+
+struct ManifestInfo<'a> {
+ pub desc_box: &'a JUMBFDescriptionBox,
+ pub sbox: &'a JUMBFSuperBox,
+}
+
+trait PushGetIndex {
+ type Item;
+ fn push_get_index(&mut self, item: Self::Item) -> usize;
+}
+
+impl<T> PushGetIndex for Vec<T> {
+ type Item = T;
+ fn push_get_index(&mut self, item: T) -> usize {
+ let index = self.len();
+ self.push(item);
+ index
+ }
+}
+
+impl Default for Store {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Store {
+ /// Create a new, empty claims store.
+ pub fn new() -> Self {
+ Self::new_with_label(jumbf::labels::MANIFEST_STORE)
+ }
+
+ /// Create a new, empty claims store with a custom label.
+ ///
+ /// In most cases, calling [`Store::new()`] is preferred.
+ pub fn new_with_label(label: &str) -> Self {
+ Store {
+ claims_map: HashMap::new(),
+ claims: Vec::new(),
+ label: label.to_string(),
+ provenance_path: None,
+ }
+ }
+
+ /// Return label for the store
+ pub fn label(&self) -> &str {
+ &self.label
+ }
+
+ /// Get the provenance if available.
+ /// If loaded from an existing asset it will be provenance from that XMP
+ /// If a new claim is committed that will be the provenance claim
+ pub fn provenance_path(&self) -> Option<String> {
+ if self.provenance_path.is_none() {
+ // if we have claims and no provenance, return last claim
+ if let Some(claim) = self.claims.last() {
+ return Some(Claim::to_claim_uri(claim.label()));
+ }
+ }
+ self.provenance_path.as_ref().cloned()
+ }
+
+ // set the path of the current provenance claim
+ fn set_provenance_path(&mut self, claim_label: &str) {
+ let path = Claim::to_claim_uri(claim_label);
+ self.provenance_path = Some(path);
+ }
+
+ /// get the list of claims for this store
+ pub fn claims(&self) -> &Vec<Claim> {
+ &self.claims
+ }
+
+ /// Add a new Claim to this Store. The claim label
+ /// may be updated to reflect is position in the Claim Store
+ /// if there are conflicting label names. The function
+ /// will return the label of the claim used
+ pub fn commit_claim(&mut self, mut claim: Claim) -> Result<String> {
+ // verify the claim is valid
+ claim.build()?;
+
+ // load the claim ingredients
+ // preparse first to make sure we can load them
+ let mut ingredient_claims: Vec<Claim> = Vec::new();
+ for (pc, claims) in claim.claim_ingredient_store() {
+ let mut valid_pc = false;
+
+ // expand for flat list insertion
+ for ingredient_claim in claims {
+ // recreate claim from original bytes
+ let claim_clone = ingredient_claim.clone();
+ if pc == claim_clone.label() {
+ valid_pc = true;
+ }
+ ingredient_claims.push(claim_clone);
+ }
+ if !valid_pc {
+ return Err(Error::IngredientNotFound);
+ }
+ }
+
+ // update the provenance path
+ self.set_provenance_path(claim.label());
+
+ let claim_label = claim.label().to_string();
+
+ // insert ingredients if needed
+ for ingredient_claim in ingredient_claims {
+ let label = ingredient_claim.label().to_owned();
+
+ if let std::collections::hash_map::Entry::Vacant(e) = self.claims_map.entry(label) {
+ let index = self.claims.push_get_index(ingredient_claim);
+ e.insert(index);
+ }
+ }
+
+ // add claim to store after ingredients
+ let index = self.claims.push_get_index(claim);
+ self.claims_map.insert(claim_label.clone(), index);
+
+ Ok(claim_label)
+ }
+
+ /// Add a new update manifest to this Store. The manifest label
+ /// may be updated to reflect is position in the manifest Store
+ /// if there are conflicting label names. The function
+ /// will return the label of the claim used
+ pub fn commit_update_manifest(&mut self, mut claim: Claim) -> Result<String> {
+ claim.set_update_manifest(true);
+
+ // check for disallowed assertions
+ if claim.has_assertion_type(labels::DATA_HASH)
+ || claim.has_assertion_type(labels::ACTIONS)
+ || claim.has_assertion_type(labels::BMFF_HASH)
+ {
+ return Err(Error::ClaimInvalidContent);
+ }
+
+ // must have exactly one ingredient
+ let ingredient = match claim.get_assertion(Ingredient::LABEL, 0) {
+ Some(i) => {
+ if claim.count_instances(Ingredient::LABEL) > 1 {
+ return Err(Error::ClaimInvalidContent);
+ } else {
+ i
+ }
+ }
+ None => return Err(Error::ClaimInvalidContent),
+ };
+
+ let ingredient_helper = Ingredient::from_assertion(ingredient)?;
+
+ // must have a parent relationship
+ if ingredient_helper.relationship != Relationship::ParentOf {
+ return Err(Error::IngredientNotFound);
+ }
+
+ // make sure ingredient c2pa.manifest points to provenance claim
+ if let Some(c2pa_manifest) = ingredient_helper.c2pa_manifest {
+ // the manifest should refer to provenance claim
+ if let Some(pc) = self.provenance_claim() {
+ if !c2pa_manifest.url().contains(pc.label()) {
+ return Err(Error::IngredientNotFound);
+ }
+ } else {
+ return Err(Error::IngredientNotFound);
+ }
+ } else {
+ return Err(Error::IngredientNotFound);
+ }
+
+ self.commit_claim(claim)
+ }
+
+ /// Get Claim by label
+ // Returns Option<&Claim>
+ pub fn get_claim(&self, label: &str) -> Option<&Claim> {
+ #![allow(clippy::unwrap_used)] // since it's only in a debug_assert
+ let index = self.claims_map.get(label)?;
+ debug_assert!(self.claims.get(*index).unwrap().label() == label);
+ self.claims.get(*index)
+ }
+
+ /// Get Claim by label
+ // Returns Option<&Claim>
+ pub fn get_claim_mut(&mut self, label: &str) -> Option<&mut Claim> {
+ #![allow(clippy::unwrap_used)] // since it's only in a debug_assert
+ let index = self.claims_map.get(label)?;
+ debug_assert!(self.claims.get(*index).unwrap().label() == label);
+ self.claims.get_mut(*index)
+ }
+
+ /// returns a Claim given a jumbf uri
+ pub fn get_claim_from_uri(&self, uri: &str) -> Result<&Claim> {
+ let claim_label = Store::manifest_label_from_path(uri);
+ self.get_claim(&claim_label)
+ .ok_or_else(|| Error::ClaimMissing {
+ label: claim_label.to_owned(),
+ })
+ }
+
+ /// returns a ClaimAssertion given a jumbf uri, resolving to the right claim in the store
+ pub fn get_claim_assertion_from_uri(&self, uri: &str) -> Result<&ClaimAssertion> {
+ // first find the right claim and then look for the assertion there
+ let claim = self.get_claim_from_uri(uri)?;
+ let (label, instance) = Claim::assertion_label_from_link(uri);
+ claim
+ .get_claim_assertion(&label, instance)
+ .ok_or_else(|| Error::ClaimMissing {
+ label: label.to_owned(),
+ })
+ }
+
+ /// Returns an Assertion referenced by JUMBF URI. The URI should be absolute and include
+ /// the desired Claim in the path. If you need to specify the Claim for this URI use
+ /// get_assertion_from_uri_and_claim.
+ /// uri - The JUMBF URI for desired Assertion.
+ pub fn get_assertion_from_uri(&self, uri: &str) -> Option<&Assertion> {
+ let claim_label = Store::manifest_label_from_path(uri);
+ let (assertion_label, instance) = Claim::assertion_label_from_link(uri);
+
+ if let Some(claim) = self.get_claim(&claim_label) {
+ claim.get_assertion(&assertion_label, instance)
+ } else {
+ None
+ }
+ }
+
+ /// Returns an Assertion referenced by JUMBF URI. Only the Claim specified by target_claim_label
+ /// will be searched. The target_claim_label can be a Claim label or JUMBF URI.
+ /// uri - The JUMBF URI for desired Assertion.
+ /// target_claim_label - Label or URI of the Claim to search for the case when the URI is a relative path.
+ pub fn get_assertion_from_uri_and_claim(
+ &self,
+ uri: &str,
+ target_claim_label: &str,
+ ) -> Option<&Assertion> {
+ let (assertion_label, instance) = Claim::assertion_label_from_link(uri);
+
+ let label = Store::manifest_label_from_path(target_claim_label);
+
+ if let Some(claim) = self.get_claim(&label) {
+ claim.get_assertion(&assertion_label, instance)
+ } else {
+ None
+ }
+ }
+
+ // Returns placeholder that will be searched for and replaced
+ // with actual signature data.
+ #[cfg(feature = "file_io")]
+ fn sign_claim_placeholder(&self, claim: &Claim, min_reserve_size: usize) -> Vec<u8> {
+ let placeholder_str = format!("signature placeholder:{}", claim.label());
+ let mut placeholder = hash256(placeholder_str.as_bytes()).as_bytes().to_vec();
+
+ use std::cmp::max;
+ placeholder.resize(max(placeholder.len(), min_reserve_size), 0);
+
+ placeholder
+ }
+
+ /// Sign the claim and return signature.
+ #[cfg(feature = "file_io")]
+ pub fn sign_claim(
+ &self,
+ claim: &Claim,
+ signer: &dyn Signer,
+ box_size: usize,
+ ) -> Result<Vec<u8>> {
+ let claim_bytes = claim.data()?;
+
+ cose_sign(signer, &claim_bytes, box_size).and_then(|sig| {
+ // Sanity check: Ensure that this signature is valid.
+
+ let mut cose_log = OneShotStatusTracker::new();
+ match verify_cose(&sig, &claim_bytes, b"", false, &mut cose_log) {
+ Ok(_) => Ok(sig),
+ Err(err) => {
+ error!(
+ "Signature that was just generated does not validate: {:#?}",
+ err
+ );
+ Err(err)
+ }
+ }
+ })
+ }
+
+ /// Sign the claim asynchronously and return signature.
+ #[cfg(feature = "async_signer")]
+ pub async fn sign_claim_async(
+ &self,
+ claim: &Claim,
+ signer: &dyn AsyncSigner,
+ ) -> Result<Vec<u8>> {
+ let claim_bytes = claim.data()?;
+ signer.sign(&claim_bytes).await
+ }
+
+ /// return the current provenance claim label if available
+ pub fn provenance_label(&self) -> Option<String> {
+ self.provenance_path()
+ .map(|provenance| Store::manifest_label_from_path(&provenance))
+ }
+
+ /// return the current provenance claim if available
+ pub fn provenance_claim(&self) -> Option<&Claim> {
+ match self.provenance_path() {
+ Some(provenance) => {
+ let claim_label = Store::manifest_label_from_path(&provenance);
+ self.get_claim(&claim_label)
+ }
+ None => None,
+ }
+ }
+
+ /// return the current provenance claim as mutable if available
+ pub fn provenance_claim_mut(&mut self) -> Option<&mut Claim> {
+ match self.provenance_path() {
+ Some(provenance) => {
+ let claim_label = Store::manifest_label_from_path(&provenance);
+ self.get_claim_mut(&claim_label)
+ }
+ None => None,
+ }
+ }
+
+ // add a restored claim
+ fn insert_restored_claim(&mut self, label: String, claim: Claim) {
+ let index = self.claims.push_get_index(claim);
+ self.claims_map.insert(label, index);
+ }
+
+ #[cfg(feature = "file_io")]
+ fn add_assertion_to_jumbf_store(
+ store: &mut CAIAssertionStore,
+ claim_assertion: &ClaimAssertion,
+ ) -> Result<()> {
+ // Grab assertion data object.
+ let d = claim_assertion.assertion().decode_data();
+
+ match d {
+ AssertionData::Json(_) => {
+ let mut json_data = CAIJSONAssertionBox::new(&claim_assertion.label());
+ json_data.add_json(claim_assertion.assertion().data().to_vec());
+ if let Some(salt) = claim_assertion.salt() {
+ json_data.set_salt(salt.clone())?;
+ }
+ store.add_assertion(Box::new(json_data));
+ }
+ AssertionData::Binary(_) => {
+ // TODO: Handle other binary box types if needed.
+ let mut data = JumbfEmbeddedFileBox::new(&claim_assertion.label());
+ data.add_data(
+ claim_assertion.assertion().data().to_vec(),
+ claim_assertion.assertion().mime_type(),
+ None,
+ );
+ if let Some(salt) = claim_assertion.salt() {
+ data.set_salt(salt.clone())?;
+ }
+ store.add_assertion(Box::new(data));
+ }
+ AssertionData::Cbor(_) => {
+ let mut cbor_data = CAICBORAssertionBox::new(&claim_assertion.label());
+ cbor_data.add_cbor(claim_assertion.assertion().data().to_vec());
+ if let Some(salt) = claim_assertion.salt() {
+ cbor_data.set_salt(salt.clone())?;
+ }
+ store.add_assertion(Box::new(cbor_data));
+ }
+ AssertionData::Uuid(s, _) => {
+ let mut uuid_data = CAIUUIDAssertionBox::new(&claim_assertion.label());
+ uuid_data.add_uuid(s, claim_assertion.assertion().data().to_vec())?;
+ if let Some(salt) = claim_assertion.salt() {
+ uuid_data.set_salt(salt.clone())?;
+ }
+ store.add_assertion(Box::new(uuid_data));
+ }
+ }
+ Ok(())
+ }
+
+ // look for old style hashing to determine if this is a pre 1.0 claim
+ fn is_old_assertion(alg: &str, data: &[u8], original_hash: &[u8]) -> bool {
+ let old_hash = hash_by_alg(alg, data, None);
+ vec_compare(&old_hash, original_hash)
+ }
+
+ fn get_assertion_from_jumbf_store(
+ claim: &Claim,
+ assertion_box: &JUMBFSuperBox,
+ label: &str,
+ check_for_legacy_assertion: bool,
+ ) -> Result<ClaimAssertion> {
+ let assertion_desc_box = assertion_box.desc_box();
+
+ let (raw_label, instance) = Claim::assertion_label_from_link(label);
+ let instance_label = Claim::label_with_instance(&raw_label, instance);
+ let assertion_hashed_uri = claim
+ .assertion_hashed_uri_from_label(&instance_label)
+ .ok_or_else(|| {
+ Error::AssertionDecoding(AssertionDecodeError {
+ label: instance_label.to_string(),
+ version: None, // TODO: Plumb this through
+ content_type: "TO DO: Get content type".to_string(),
+ source: AssertionDecodeErrorCause::AssertionDataIncorrect,
+ })
+ })?;
+
+ let alg = match assertion_hashed_uri.alg() {
+ Some(ref a) => a.clone(),
+ None => claim.alg().to_string(),
+ };
+
+ // get salt value if set
+ let salt = assertion_desc_box.get_salt();
+
+ let result = match assertion_desc_box.uuid().as_ref() {
+ CAI_JSON_ASSERTION_UUID => {
+ let json_box = assertion_box
+ .data_box_as_json_box(0)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let assertion = Assertion::from_data_json(&raw_label, json_box.json())?;
+ let hash = Claim::calc_box_hash(label, &assertion, salt.clone(), &alg)?;
+ Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt))
+ }
+ CAI_EMBEDDED_FILE_UUID => {
+ let ef_box = assertion_box
+ .data_box_as_embedded_media_type_box(0)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let data_box = assertion_box
+ .data_box_as_embedded_file_content_box(1)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let media_type = ef_box.media_type();
+ let assertion =
+ Assertion::from_data_binary(&raw_label, &media_type, data_box.data());
+ let hash = Claim::calc_box_hash(label, &assertion, salt.clone(), &alg)?;
+ Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt))
+ }
+ CAI_CBOR_ASSERTION_UUID => {
+ let cbor_box = assertion_box
+ .data_box_as_cbor_box(0)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let assertion = Assertion::from_data_cbor(&raw_label, cbor_box.cbor());
+ let hash = Claim::calc_box_hash(label, &assertion, salt.clone(), &alg)?;
+ Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt))
+ }
+ CAI_UUID_ASSERTION_UUID => {
+ let uuid_box = assertion_box
+ .data_box_as_uuid_box(0)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let uuid_str = hex::encode(uuid_box.uuid());
+ let assertion = Assertion::from_data_uuid(&raw_label, &uuid_str, uuid_box.data());
+
+ let hash = Claim::calc_box_hash(label, &assertion, salt.clone(), &alg)?;
+ Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt))
+ }
+ _ => Err(Error::JumbfCreationError),
+ };
+
+ if check_for_legacy_assertion {
+ // make sure this is not pre 1.0 data
+ match result {
+ Ok(r) => {
+ // look for old style hashing
+ if Store::is_old_assertion(
+ &alg,
+ r.assertion().data(),
+ &assertion_hashed_uri.hash(),
+ ) {
+ Err(Error::PrereleaseError)
+ } else {
+ Ok(r)
+ }
+ }
+ Err(e) => Err(e),
+ }
+ } else {
+ result
+ }
+ }
+
+ /// Convert this claims store to a JUMBF box.
+ #[cfg(feature = "file_io")]
+ pub fn to_jumbf(&self, signer: &dyn Signer) -> Result<Vec<u8>> {
+ self.to_jumbf_internal(signer.reserve_size())
+ }
+
+ /// Convert this claims store to a JUMBF box.
+ #[cfg(feature = "async_signer")]
+ pub fn to_jumbf_async(&self, signer: &dyn AsyncSigner) -> Result<Vec<u8>> {
+ self.to_jumbf_internal(signer.reserve_size())
+ }
+
+ #[cfg(feature = "file_io")]
+ fn to_jumbf_internal(&self, min_reserve_size: usize) -> Result<Vec<u8>> {
+ // Create the CAI block.
+ let mut cai_block = Cai::new();
+
+ // Add claims and assertions in this store to the JUMBF store.
+ for claim in &self.claims {
+ let label = claim.label();
+
+ let mut cai_store = CAIStore::new(label, claim.update_manifest());
+
+ // Add claim box. Note the order of the boxes are set by the spec
+ let mut cb = CAIClaimBox::new();
+
+ // Create the CAI assertion store.
+ let mut a_store = CAIAssertionStore::new();
+
+ // Add assertions to CAI assertion store.
+ let cas = claim.claim_assertion_store();
+ for assertion in cas {
+ Store::add_assertion_to_jumbf_store(&mut a_store, assertion)?;
+ }
+
+ // Add the CAI assertion store to the CAI store.
+ cai_store.add_box(Box::new(a_store));
+
+ // Add the Claim json
+ let claim_cbor_bytes = claim.data()?;
+ let c_cbor = JUMBFCBORContentBox::new(claim_cbor_bytes);
+ cb.add_claim(Box::new(c_cbor));
+ cai_store.add_box(Box::new(cb));
+
+ // Create a signature and add placeholder data to the CAI store.
+ let mut sigb = CAISignatureBox::new();
+ let signed_data = match claim.signature_val().is_empty() {
+ false => claim.signature_val().clone(), // existing claims have sig values
+ true => self.sign_claim_placeholder(claim, min_reserve_size), // empty is the new sig to be replaced
+ };
+
+ let sigc = JUMBFCBORContentBox::new(signed_data);
+ sigb.add_signature(Box::new(sigc));
+ cai_store.add_box(Box::new(sigb));
+
+ // add vc_store if needed
+ if !claim.get_verifiable_credentials().is_empty() {
+ // Create VC store.
+ let mut vc_store = CAIVerifiableCredentialStore::new();
+
+ // Add assertions to CAI assertion store.
+ let vcs = claim.get_verifiable_credentials();
+ for assertion_data in vcs {
+ if let AssertionData::Json(j) = assertion_data {
+ let id = Claim::vc_id(j)?;
+ let mut json_data = CAIJSONAssertionBox::new(&id);
+ json_data.add_json(j.as_bytes().to_vec());
+ vc_store.add_credential(Box::new(json_data));
+ } else {
+ return Err(Error::BadParam("VC data must be JSON".to_string()));
+ }
+ }
+
+ // Add the CAI assertion store to the CAI store.
+ cai_store.add_box(Box::new(vc_store));
+ }
+
+ // Finally add the completed CAI store into the CAI block.
+ cai_block.add_box(Box::new(cai_store));
+ }
+
+ // Write it to memory.
+ let mut mem_box: Vec<u8> = Vec::new();
+ cai_block.write_box(&mut mem_box)?;
+
+ if mem_box.is_empty() {
+ Err(Error::JumbfCreationError)
+ } else {
+ Ok(mem_box)
+ }
+ }
+
+ fn manifest_map<'a>(sb: &'a JUMBFSuperBox) -> Result<HashMap<String, ManifestInfo<'a>>> {
+ let mut box_info: HashMap<String, ManifestInfo<'a>> = HashMap::new();
+ for i in 0..sb.data_box_count() {
+ let sbox = sb.data_box_as_superbox(i).ok_or(Error::JumbfBoxNotFound)?;
+ let desc_box = sbox.desc_box();
+
+ let label = desc_box.uuid();
+
+ let mi = ManifestInfo { desc_box, sbox };
+
+ box_info.insert(label, mi);
+ }
+
+ Ok(box_info)
+ }
+
+ // Compare two version labels
+ // base_version_label - is the source label
+ // desired_version_label - is the label to compare to the base
+ // returns true if desired version is <= base version
+ fn check_label_version(base_version_label: &str, desired_version_label: &str) -> bool {
+ if let Some(desired_version) = labels::version(desired_version_label) {
+ if let Some(base_version) = labels::version(base_version_label) {
+ if desired_version > base_version {
+ return false;
+ }
+ }
+ }
+ true
+ }
+
+ pub fn from_jumbf(buffer: &[u8], validation_log: &mut impl StatusTracker) -> Result<Store> {
+ let mut store = Store::new();
+
+ // setup a cursor for reading the buffer...
+ let mut buf_reader = Cursor::new(buffer);
+
+ // this loads up all the boxes...
+ let super_box = BoxReader::read_super_box(&mut buf_reader)?;
+
+ // this loads up all the boxes...
+ let cai_block = Cai::from(super_box);
+
+ // check the CAI Block
+ let desc_box = cai_block.desc_box();
+ if desc_box.uuid() != CAI_BLOCK_UUID {
+ let log_item = log_item!("JUMBF", "c2pa box not found", "from_jumbf")
+ .error(Error::InvalidClaim(InvalidClaimError::C2paBlockNotFound));
+ validation_log.log(
+ log_item,
+ Some(Error::InvalidClaim(InvalidClaimError::C2paBlockNotFound)),
+ )?;
+
+ return Err(Error::InvalidClaim(InvalidClaimError::C2paBlockNotFound));
+ }
+
+ let num_stores = cai_block.data_box_count();
+ for idx in 0..num_stores {
+ let cai_store_box = cai_block
+ .data_box_as_superbox(idx)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let cai_store_desc_box = cai_store_box.desc_box();
+
+ // ignore unknown boxes per the spec
+ if cai_store_desc_box.uuid() != CAI_UPDATE_MANIFEST_UUID
+ && cai_store_desc_box.uuid() != CAI_STORE_UUID
+ {
+ continue;
+ }
+
+ // make sure there are not multiple claim boxes
+ let mut claim_box_cnt = 0;
+ for i in 0..cai_store_box.data_box_count() {
+ let sbox = cai_store_box
+ .data_box_as_superbox(i)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let desc_box = sbox.desc_box();
+
+ if desc_box.uuid() == CAI_CLAIM_UUID {
+ claim_box_cnt += 1;
+ }
+
+ if claim_box_cnt > 1 {
+ let log_item =
+ log_item!("JUMBF", "c2pa multiple claim boxes found", "from_jumbf")
+ .error(Error::InvalidClaim(
+ InvalidClaimError::C2paMultipleClaimBoxes,
+ ))
+ .validation_status(validation_status::CLAIM_MULTIPLE);
+ validation_log.log(
+ log_item,
+ Some(Error::InvalidClaim(
+ InvalidClaimError::C2paMultipleClaimBoxes,
+ )),
+ )?;
+
+ return Err(Error::InvalidClaim(
+ InvalidClaimError::C2paMultipleClaimBoxes,
+ ));
+ }
+ }
+
+ let is_update_manifest = cai_store_desc_box.uuid() == CAI_UPDATE_MANIFEST_UUID;
+
+ // get map of boxes in this manifest
+ let manifest_boxes = Store::manifest_map(cai_store_box)?;
+
+ // retrieve the claim & validate
+ let claim_superbox = manifest_boxes
+ .get(CAI_CLAIM_UUID)
+ .ok_or(Error::InvalidClaim(
+ InvalidClaimError::ClaimSuperboxNotFound,
+ ))?
+ .sbox;
+ let claim_desc_box = manifest_boxes
+ .get(CAI_CLAIM_UUID)
+ .ok_or(Error::InvalidClaim(
+ InvalidClaimError::ClaimDescriptionBoxNotFound,
+ ))?
+ .desc_box;
+
+ // check if version is supported
+ let claim_box_ver = claim_desc_box.label();
+ if !Self::check_label_version(Claim::build_version(), &claim_box_ver) {
+ return Err(Error::InvalidClaim(InvalidClaimError::ClaimVersionTooNew));
+ }
+
+ // check box contents
+ if claim_desc_box.uuid() == CAI_CLAIM_UUID {
+ // must be have only one claim
+ if claim_superbox.data_box_count() > 1 {
+ return Err(Error::InvalidClaim(InvalidClaimError::DuplicateClaimBox {
+ label: claim_desc_box.label(),
+ }));
+ }
+ // better be, but just in case...
+
+ let cbor_box = match claim_superbox.data_box_as_cbor_box(0) {
+ Some(c) => c,
+ None => {
+ // check for old claims for reporting
+ match claim_superbox.data_box_as_json_box(0) {
+ Some(_c) => {
+ let log_item =
+ log_item!("JUMBF", "error loading claim data", "from_jumbf")
+ .error(Error::PrereleaseError);
+ validation_log.log_silent(log_item);
+
+ return Err(Error::PrereleaseError);
+ }
+ None => {
+ let log_item =
+ log_item!("JUMBF", "error loading claim data", "from_jumbf")
+ .error(Error::InvalidClaim(
+ InvalidClaimError::ClaimBoxData,
+ ));
+ validation_log.log_silent(log_item);
+ return Err(Error::InvalidClaim(InvalidClaimError::ClaimBoxData));
+ }
+ }
+ }
+ };
+
+ if cbor_box.box_uuid() != JUMBF_CBOR_UUID {
+ return Err(Error::InvalidClaim(
+ InvalidClaimError::ClaimDescriptionBoxInvalid,
+ ));
+ }
+ }
+
+ // retrieve the signature
+ let sig_superbox = manifest_boxes
+ .get(CAI_SIGNATURE_UUID)
+ .ok_or(Error::InvalidClaim(
+ InvalidClaimError::ClaimSignatureBoxNotFound,
+ ))?
+ .sbox;
+ let sig_desc_box = manifest_boxes
+ .get(CAI_SIGNATURE_UUID)
+ .ok_or(Error::InvalidClaim(
+ InvalidClaimError::ClaimSignatureDescriptionBoxNotFound,
+ ))?
+ .desc_box;
+
+ // check box contents
+ if sig_desc_box.uuid() == CAI_SIGNATURE_UUID {
+ // better be, but just in case...
+ let sig_box = sig_superbox
+ .data_box_as_cbor_box(0)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ if sig_box.box_uuid() != JUMBF_CBOR_UUID {
+ return Err(Error::InvalidClaim(
+ InvalidClaimError::ClaimSignatureDescriptionBoxInvalid,
+ ));
+ }
+ }
+ // save signature to be validated on load
+ let sig_data = sig_superbox
+ .data_box_as_cbor_box(0)
+ .ok_or(Error::JumbfBoxNotFound)?;
+
+ // Create a new Claim object from jumbf data after validations
+ let cbor_box = claim_superbox
+ .data_box_as_cbor_box(0)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let mut claim = Claim::from_data(&cai_store_desc_box.label(), cbor_box.cbor())?;
+
+ // set the type of manifest
+ claim.set_update_manifest(is_update_manifest);
+
+ // retrieve & set signature for each claim
+ claim.set_signature_val(sig_data.cbor().clone()); // load the stored signature
+
+ // retrieve the assertion store
+ let assertion_store_box = manifest_boxes
+ .get(CAI_ASSERTION_STORE_UUID)
+ .ok_or(Error::InvalidClaim(
+ InvalidClaimError::AssertionStoreSuperboxNotFound,
+ ))?
+ .sbox;
+
+ let num_assertions = assertion_store_box.data_box_count();
+
+ // loop over all assertions...
+ let mut check_for_legacy_assertion = true;
+ for idx in 0..num_assertions {
+ let assertion_box = assertion_store_box
+ .data_box_as_superbox(idx)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let assertion_desc_box = assertion_box.desc_box();
+
+ // Add assertions to claim after validation
+ let label = assertion_desc_box.label();
+ match Store::get_assertion_from_jumbf_store(
+ &claim,
+ assertion_box,
+ &label,
+ check_for_legacy_assertion,
+ ) {
+ Ok(assertion) => {
+ claim.put_assertion_store(assertion); // restore assertion data to claim
+ check_for_legacy_assertion = false; // only need to check once
+ }
+ Err(e) => {
+ // if this is an old manifest always return
+ if std::mem::discriminant(&e)
+ == std::mem::discriminant(&Error::PrereleaseError)
+ {
+ let log_item =
+ log_item!("JUMBF", "error loading assertion", "from_jumbf")
+ .error(e);
+ validation_log.log_silent(log_item);
+ return Err(Error::PrereleaseError);
+ } else {
+ let log_item =
+ log_item!("JUMBF", "error loading assertion", "from_jumbf")
+ .error(e);
+ validation_log.log(log_item, None)?;
+ }
+ }
+ }
+ }
+
+ // load vc_store if available
+ if let Some(mi) = manifest_boxes.get(CAI_VERIFIABLE_CREDENTIALS_STORE_UUID) {
+ let vc_store = mi.sbox;
+ let num_vcs = vc_store.data_box_count();
+
+ for idx in 0..num_vcs {
+ let vc_box = vc_store
+ .data_box_as_superbox(idx)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let vc_json = vc_box
+ .data_box_as_json_box(0)
+ .ok_or(Error::JumbfBoxNotFound)?;
+ let vc_desc_box = vc_box.desc_box();
+ let _id = vc_desc_box.label();
+
+ let json_str = String::from_utf8(vc_json.json().to_vec())
+ .map_err(|_| InvalidClaimError::VerifiableCredentialStoreInvalid)?;
+
+ claim.add_verifiable_credential(&json_str)?;
+ }
+ }
+
+ // add claim to store
+ store.insert_restored_claim(cai_store_desc_box.label(), claim);
+ }
+
+ Ok(store)
+ }
+
+ // Get the store label from jumbf path
+ pub fn manifest_label_from_path(claim_path: &str) -> String {
+ if let Some(s) = jumbf::labels::manifest_label_from_uri(claim_path) {
+ s
+ } else {
+ claim_path.to_owned()
+ }
+ }
+
+ // verify the provenance of the claim
+ fn provenance_checks<'a>(
+ store: &'a Store,
+ xmp_opt: Option<String>,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<&'a Claim> {
+ #[cfg(feature = "diagnostics")]
+ let _t = crate::utils::time_it::TimeIt::new("verify_store");
+
+ // look for the active manifest in xmp if available
+ let provenance_claim = match xmp_opt {
+ Some(xmp_str) => match extract_provenance(&xmp_str) {
+ Some(c) => c,
+ None => store.provenance_path().unwrap_or_else(|| "".to_string()), // if not explicitly set use active manifest
+ },
+ None => store.provenance_path().unwrap_or_else(|| "".to_string()), // if not explicitly set use active manifest
+ };
+
+ // get claim that matches the provenance label
+ let claim_label = Store::manifest_label_from_path(&provenance_claim);
+ let claim = match store.get_claim(&claim_label) {
+ Some(c) => c,
+ None => {
+ let log_item = log_item!(
+ &claim_label,
+ "could not find active manifest",
+ "verify_store"
+ )
+ .error(Error::ProvenanceMissing)
+ .validation_status(validation_status::CLAIM_MISSING);
+ validation_log.log(log_item, Some(Error::ProvenanceMissing))?;
+
+ return Err(Error::ProvenanceMissing);
+ }
+ };
+
+ Ok(claim)
+ }
+
+ // wake the ingredients and validate
+ fn ingredient_checks(
+ store: &Store,
+ claim: &Claim,
+ asset_bytes: &[u8],
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<()> {
+ let mut num_parent_ofs = 0;
+
+ // walk the ingredients
+ for i in claim.ingredient_assertions() {
+ let ingredient_assertion = Ingredient::from_assertion(&i)?;
+
+ // is this an ingredient
+ if let Some(ref c2pa_manifest) = &ingredient_assertion.c2pa_manifest {
+ let label = Store::manifest_label_from_path(&c2pa_manifest.url());
+
+ // check for parentOf relationships
+ if ingredient_assertion.relationship == Relationship::ParentOf {
+ num_parent_ofs += 1;
+ }
+
+ if let Some(ingredient) = store.get_claim(&label) {
+ let alg = match c2pa_manifest.alg() {
+ Some(a) => a,
+ None => ingredient.alg().to_owned(),
+ };
+ if !verify_by_alg(&alg, &c2pa_manifest.hash(), &ingredient.data()?, None) {
+ let log_item = log_item!(
+ &c2pa_manifest.url(),
+ "ingredient hash incorrect",
+ "ingredient_checks"
+ )
+ .error(Error::HashMismatch(
+ "ingredient hash does not match found ingredient".to_string(),
+ ))
+ .validation_status(validation_status::INGREDIENT_HASHEDURI_MISMATCH);
+ validation_log.log(
+ log_item,
+ Some(Error::HashMismatch(
+ "ingredient hash does not match found ingredient".to_string(),
+ )),
+ )?;
+ }
+
+ // make sure
+ // verify the ingredient claim
+ Claim::verify_claim(ingredient, asset_bytes, false, validation_log)?;
+ } else {
+ let log_item = log_item!(
+ &c2pa_manifest.url(),
+ "ingredient not found",
+ "ingredient_checks"
+ )
+ .error(Error::ClaimVerification(format!(
+ "ingredient: {} is missing",
+ label
+ )))
+ .validation_status(validation_status::CLAIM_MISSING);
+ validation_log.log(
+ log_item,
+ Some(Error::ClaimVerification(format!(
+ "ingredient: {} is missing",
+ label
+ ))),
+ )?;
+ }
+ }
+ }
+
+ // check ingredient rules
+ if claim.update_manifest() {
+ if !(num_parent_ofs == 1 && claim.ingredient_assertions().len() == 1) {
+ let log_item = log_item!(
+ &claim.uri(),
+ "update manifest must have one parent",
+ "ingredient_checks"
+ )
+ .error(Error::ClaimVerification(
+ "update manifest must have one parent".to_string(),
+ ))
+ .validation_status(validation_status::MANIFEST_UPDATE_WRONG_PARENTS);
+ validation_log.log(
+ log_item,
+ Some(Error::ClaimVerification(
+ "update manifest must have one parent".to_string(),
+ )),
+ )?;
+ }
+ } else if num_parent_ofs > 1 {
+ let log_item = log_item!(
+ &claim.uri(),
+ "too many ingredient parents",
+ "ingredient_checks"
+ )
+ .error(Error::ClaimVerification(
+ "ingredient has more than one parent".to_string(),
+ ))
+ .validation_status(validation_status::MANIFEST_MULTIPLE_PARENTS);
+ validation_log.log(
+ log_item,
+ Some(Error::ClaimVerification(
+ "ingredient has more than one parent".to_string(),
+ )),
+ )?;
+ }
+
+ Ok(())
+ }
+
+ // wake the ingredients and validate
+ async fn ingredient_checks_async(
+ store: &Store,
+ claim: &Claim,
+ asset_bytes: &[u8],
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<()> {
+ // walk the ingredients
+ for i in claim.ingredient_assertions() {
+ let ingredient_assertion = Ingredient::from_assertion(&i)?;
+
+ // is this an ingredient
+ if let Some(ref c2pa_manifest) = &ingredient_assertion.c2pa_manifest {
+ let label = Store::manifest_label_from_path(&c2pa_manifest.url());
+
+ if let Some(ingredient) = store.get_claim(&label) {
+ if !verify_by_alg(
+ ingredient.alg(),
+ &c2pa_manifest.hash(),
+ &ingredient.data()?,
+ None,
+ ) {
+ let log_item = log_item!(
+ &c2pa_manifest.url(),
+ "ingredient hash incorrect",
+ "ingredient_checks_async"
+ )
+ .error(Error::HashMismatch(
+ "ingredient hash does not match found ingredient".to_string(),
+ ))
+ .validation_status(validation_status::INGREDIENT_HASHEDURI_MISMATCH);
+ validation_log.log(
+ log_item,
+ Some(Error::HashMismatch(
+ "ingredient hash does not match found ingredient".to_string(),
+ )),
+ )?;
+ }
+ // verify the ingredient claim
+ Claim::verify_claim_async(ingredient, asset_bytes, false, validation_log)
+ .await?;
+ } else {
+ let log_item = log_item!(
+ &c2pa_manifest.url(),
+ "ingredient not found",
+ "ingredient_checks_async"
+ )
+ .error(Error::ClaimVerification(format!(
+ "ingredient: {} is missing",
+ label
+ )))
+ .validation_status(validation_status::CLAIM_MISSING);
+ validation_log.log(
+ log_item,
+ Some(Error::ClaimVerification(format!(
+ "ingredient: {} is missing",
+ label
+ ))),
+ )?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Verify Store
+ /// store: Store to validate
+ /// xmp_str: String containing entire XMP block of the asset
+ /// asset_bytes: bytes of the asset to be verified
+ /// validation_log: If present all found errors are logged and returned, other wise first error causes exit and is returned
+ pub async fn verify_store_async(
+ store: &Store,
+ xmp_opt: Option<String>,
+ asset_bytes: &[u8],
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<()> {
+ let claim = Store::provenance_checks(store, xmp_opt, validation_log)?;
+
+ // verify the provenance claim
+ Claim::verify_claim_async(claim, asset_bytes, true, validation_log).await?;
+
+ Store::ingredient_checks_async(store, claim, asset_bytes, validation_log).await?;
+
+ Ok(())
+ }
+
+ /// Verify Store
+ /// store: Store to validate
+ /// xmp_str: String containing entire XMP block of the asset
+ /// asset_bytes: bytes of the asset to be verified
+ /// validation_log: If present all found errors are logged and returned, other wise first error causes exit and is returned
+ pub fn verify_store(
+ store: &Store,
+ xmp_opt: Option<String>,
+ asset_bytes: &[u8],
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<()> {
+ let claim = Store::provenance_checks(store, xmp_opt, validation_log)?;
+
+ // verify the provenance claim
+ Claim::verify_claim(claim, asset_bytes, true, validation_log)?;
+
+ Store::ingredient_checks(store, claim, asset_bytes, validation_log)?;
+
+ Ok(())
+ }
+
+ // generate a list of AssetHashes based on the location of objects in the file
+ #[cfg(feature = "file_io")]
+ fn generate_data_hashes(
+ asset_path: &Path,
+ alg: &str,
+ block_locations: &mut Vec<HashObjectPositions>,
+ calc_hashes: bool,
+ ) -> Result<Vec<DataHash>> {
+ if block_locations.is_empty() {
+ return Err(Error::BadParam(
+ "No asset hash locations specified".to_owned(),
+ ));
+ }
+
+ let metadata = asset_path.metadata().map_err(crate::error::wrap_io_err)?;
+ let file_len: u64 = metadata.len();
+ let mut hashes: Vec<DataHash> = Vec::new();
+
+ // sort blocks by offset
+ block_locations.sort_by(|a, b| a.offset.cmp(&b.offset));
+
+ // generate default data hash that excludes jumbf block
+ // find the first jumbf block (ours are always in order)
+ // find the first block after the jumbf blocks
+ let mut block_start: usize = 0;
+ let mut block_end: usize = 0;
+ let mut found_jumbf = false;
+ for item in block_locations {
+ // find start of jumbf
+ if !found_jumbf && item.htype == HashBlockObjectType::Cai {
+ block_start = item.offset;
+ found_jumbf = true;
+ }
+
+ // find start of block after jumbf blocks
+ if found_jumbf && item.htype == HashBlockObjectType::Cai {
+ block_end = item.offset + item.length;
+ }
+ }
+
+ if block_end as u64 > file_len {
+ return Err(Error::BadParam(
+ "data hash exclusions out of range".to_string(),
+ ));
+ }
+
+ if found_jumbf {
+ // add exclusion hash for bytes before and after jumbf
+ let mut dh = DataHash::new("jumbf manifest", alg, None);
+ dh.add_exclusion(Exclusion::new(block_start, block_end - block_start));
+ if calc_hashes {
+ dh.gen_hash(asset_path)?;
+ } else {
+ match alg {
+ "sha256" => dh.set_hash([0u8; 32].to_vec()),
+ "sha384" => dh.set_hash([0u8; 48].to_vec()),
+ "sha512" => dh.set_hash([0u8; 64].to_vec()),
+ _ => return Err(Error::UnsupportedType),
+ }
+ }
+ hashes.push(dh);
+ }
+
+ Ok(hashes)
+ }
+
+ /// Embed the claims store as jumbf into an asset. Updates XMP with provenance record.
+ #[cfg(feature = "file_io")]
+ pub fn save_to_asset(
+ &mut self,
+ asset_path: &Path,
+ signer: &dyn Signer,
+ output_path: &Path,
+ ) -> Result<()> {
+ let jumbf_bytes = self.start_save(asset_path, output_path, signer.reserve_size())?;
+
+ let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
+ let sig = self.sign_claim(pc, signer, signer.reserve_size())?;
+ let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size());
+
+ match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) {
+ Ok(v) => {
+ // save sig so store is up to date
+ let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
+ pc_mut.set_signature_val(v);
+ Ok(())
+ }
+ Err(e) => Err(e),
+ }
+ }
+
+ /// Embed the claims store as jumbf into an asset using an async signer. Updates XMP with provenance record.
+ #[cfg(feature = "async_signer")]
+ pub async fn save_to_asset_async(
+ &mut self,
+ asset_path: &Path,
+ signer: &dyn AsyncSigner,
+ output_path: &Path,
+ ) -> Result<()> {
+ let jumbf_bytes = self.start_save(asset_path, output_path, signer.reserve_size())?;
+
+ let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
+ let sig = self.sign_claim_async(pc, signer).await?;
+ let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size());
+
+ match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) {
+ Ok(v) => {
+ // save sig so store is up to date
+ let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
+ pc_mut.set_signature_val(v);
+ Ok(())
+ }
+ Err(e) => Err(e),
+ }
+ }
+
+ #[cfg(feature = "file_io")]
+ fn start_save(
+ &mut self,
+ asset_path: &Path,
+ output_path: &Path,
+ reserve_size: usize,
+ ) -> Result<Vec<u8>> {
+ // clone the source to working copy if requested
+ get_supported_file_extension(asset_path).ok_or(Error::UnsupportedType)?; // verify extensions
+ let _ext = get_supported_file_extension(output_path).ok_or(Error::UnsupportedType)?;
+ if asset_path != output_path {
+ fs::copy(&asset_path, &output_path).map_err(Error::IoError)?;
+ }
+
+ // get the provenance claim
+ let pp = self.provenance_path();
+ let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
+
+ // update file following the steps outlined in CAI spec
+
+ // 1) Add DC provenance XMP
+ // update XMP info & add xmp hash to provenance claim
+ if let Some(provenance) = pp {
+ embedded_xmp::add_manifest_uri_to_file(output_path, &provenance)
+ .map_err(|_err| Error::XmpWriteError)?;
+ } else {
+ return Err(Error::XmpWriteError);
+ }
+
+ // 2) Get hash ranges if needed, do not generate for update manifests
+ let mut hash_ranges = object_locations(output_path)?;
+ let hashes: Vec<DataHash> = if pc.update_manifest() {
+ Vec::new()
+ } else {
+ Store::generate_data_hashes(output_path, pc.alg(), &mut hash_ranges, false)?
+ };
+
+ // add the placeholder data hashes to provenance claim so that the required space is reserved
+ for mut hash in hashes {
+ // add padding to account for possible cbor expansion of final DataHash
+ let padding: Vec<u8> = vec![0x0; 10];
+ hash.add_padding(padding);
+
+ pc.add_assertion(&hash)?;
+ }
+
+ // 3) Generate in memory CAI jumbf block
+ // and write preliminary jumbf store to file
+ // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned
+ let mut data = self.to_jumbf_internal(reserve_size)?;
+ let jumbf_size = data.len();
+ save_jumbf_to_file(&data, output_path, Some(output_path))?;
+
+ // 4) determine final object locations and patch the asset hashes with correct offset
+ // replace the source with correct asset hashes so that the claim hash will be correct
+ let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
+
+ // get the final hash ranges, but not for update manifests
+ let mut new_hash_ranges = object_locations(output_path)?;
+ let updated_hashes = if pc.update_manifest() {
+ Vec::new()
+ } else {
+ Store::generate_data_hashes(output_path, pc.alg(), &mut new_hash_ranges, true)?
+ };
+
+ // patch existing claim hash with updated data
+ for mut hash in updated_hashes {
+ hash.gen_hash(output_path)?; // generate
+ pc.update_data_hash(hash)?;
+ }
+
+ // regenerate the jumbf because the cbor changed
+ data = self.to_jumbf_internal(reserve_size)?;
+ if jumbf_size != data.len() {
+ return Err(Error::JumbfCreationError);
+ }
+
+ Ok(data) // return JUMBF data
+ }
+
+ #[cfg(feature = "file_io")]
+ fn finish_save(
+ &self,
+ mut jumbf_bytes: Vec<u8>,
+ output_path: &Path,
+ sig: Vec<u8>,
+ sig_placeholder: &[u8],
+ ) -> Result<Vec<u8>> {
+ if sig_placeholder.len() != sig.len() {
+ return Err(Error::CoseSigboxTooSmall);
+ }
+
+ patch_bytes(&mut jumbf_bytes, sig_placeholder, &sig)
+ .map_err(|_| Error::JumbfCreationError)?;
+
+ // re-save to file
+ save_jumbf_to_file(&jumbf_bytes, output_path, Some(output_path))?;
+
+ Ok(sig)
+ }
+
+ /// Verify Store from an existing asset
+ /// asset_path: path to input asset
+ /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned
+ #[cfg(feature = "file_io")]
+ pub fn verify_from_path(
+ &mut self,
+ asset_path: &Path,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<()> {
+ let ext = get_supported_file_extension(asset_path).ok_or(Error::UnsupportedType)?;
+
+ // load the bytes
+ let buf = fs::read(asset_path).map_err(crate::error::wrap_io_err)?;
+
+ self.verify_from_buffer(&buf, &ext, validation_log)
+ }
+
+ // verify from a buffer without file i/o
+ pub fn verify_from_buffer(
+ &mut self,
+ buf: &[u8],
+ asset_type: &str,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<()> {
+ let mut buf_reader = Cursor::new(buf);
+
+ let cai_loader = get_cailoader_handler(asset_type).ok_or(Error::UnsupportedType)?;
+
+ // read xmp if available
+ let xmp_opt = cai_loader.read_xmp(&mut buf_reader);
+
+ let xmp_copy = xmp_opt.clone();
+
+ Store::verify_store(self, xmp_opt, buf_reader.get_ref(), validation_log)?;
+
+ // set the provenance if there is xmp otherwise it will default to active manifest
+ if let Some(xmp) = xmp_copy {
+ if let Some(xmp_provenance) = extract_provenance(&xmp) {
+ let claim_label = Store::manifest_label_from_path(&xmp_provenance);
+ self.set_provenance_path(&claim_label);
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Load Store from claims in an existing asset
+ /// asset_path: path to input asset
+ /// verify: determines whether to verify the contents of the provenance claim. Must be set true to use validation_log
+ /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned
+ #[cfg(feature = "file_io")]
+ pub fn load_from_asset(
+ asset_path: &Path,
+ verify: bool,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<Store> {
+ // load jumbf if available
+ load_cai_from_file(asset_path, validation_log)
+ .and_then(|mut store| {
+ // verify the store
+ if verify {
+ store.verify_from_path(asset_path, validation_log)?;
+ }
+
+ Ok(store)
+ })
+ .map_err(|e| {
+ let err = match e {
+ Error::PrereleaseError => Error::PrereleaseError,
+ Error::JumbfNotFound => Error::JumbfNotFound,
+ _ => Error::LogStop,
+ };
+ let log_item = log_item!("asset", "error loading file", "load_from_asset").error(e);
+ validation_log.log_silent(log_item);
+ err
+ })
+ }
+
+ fn get_store_from_memory(
+ asset_type: &str,
+ data: &[u8],
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<(Store, Option<String>)> {
+ let cai_loader = get_cailoader_handler(asset_type).ok_or(Error::UnsupportedType)?;
+
+ let mut buf_reader = Cursor::new(data);
+
+ // check for xmp, error if not present
+ let xmp = cai_loader.read_xmp(&mut buf_reader);
+
+ // load jumbf if available
+ load_cai_from_memory(asset_type, data, validation_log)
+ .map(|store| (store, xmp))
+ .map_err(|e| {
+ let err = match e {
+ Error::PrereleaseError => Error::PrereleaseError,
+ Error::JumbfNotFound => Error::JumbfNotFound,
+ _ => Error::LogStop,
+ };
+ let log_item =
+ log_item!("asset", "error loading asset", "get_store_from_memory").error(e);
+ validation_log.log_silent(log_item);
+ err
+ })
+ }
+
+ /// Load Store from a in-memory asset
+ /// asset_type: asset extension or mime type
+ /// data: reference to bytes of the the file
+ /// verify: if true will run verification checks when loading
+ /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned
+ pub fn load_from_memory(
+ asset_type: &str,
+ data: &[u8],
+ verify: bool,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<Store> {
+ Store::get_store_from_memory(asset_type, data, validation_log).and_then(
+ |(mut store, xmp_opt)| {
+ let buf_reader = Cursor::new(data);
+
+ // verify the store
+ if verify {
+ let xmp_copy = xmp_opt.clone();
+
+ // verify store and claims
+ Store::verify_store(&store, xmp_opt, buf_reader.get_ref(), validation_log)?;
+
+ // set the provenance if checks pass & has xmp, otherwise default to active manifest
+ if let Some(xmp) = xmp_copy {
+ if let Some(xmp_provenance) = extract_provenance(&xmp) {
+ let claim_label = Store::manifest_label_from_path(&xmp_provenance);
+ store.set_provenance_path(&claim_label);
+ }
+ }
+ }
+
+ Ok(store)
+ },
+ )
+ }
+
+ /// Load Store from a in-memory asset asychronously validating
+ /// asset_type: asset extension or mime type
+ /// data: reference to bytes of the the file
+ /// verify: if true will run verification checks when loading
+ /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned
+ pub async fn load_from_memory_async(
+ asset_type: &str,
+ data: &[u8],
+ verify: bool,
+ validation_log: &mut impl StatusTracker,
+ ) -> Result<Store> {
+ let (mut store, xmp_opt) = Store::get_store_from_memory(asset_type, data, validation_log)?;
+
+ let buf_reader = Cursor::new(data);
+
+ // verify the store
+ if verify {
+ let xmp_copy = xmp_opt.clone();
+
+ // verify store and claims
+ Store::verify_store_async(&store, xmp_opt, buf_reader.get_ref(), validation_log)
+ .await?;
+
+ // set the provenance if checks pass & has xmp, otherwise default to active manifest
+ if let Some(xmp) = xmp_copy {
+ if let Some(xmp_provenance) = extract_provenance(&xmp) {
+ let claim_label = Store::manifest_label_from_path(&xmp_provenance);
+ store.set_provenance_path(&claim_label);
+ }
+ }
+ }
+
+ Ok(store)
+ }
+
+ /// Load Store from memory and add its content as a claim ingredient
+ /// claim: claim to add an ingredient
+ /// provenance_label: label of the provenance claim used as key into ingredient map
+ /// data: jumbf data block
+ pub fn load_ingredient_to_claim(
+ claim: &mut Claim,
+ provenance_label: &str,
+ data: &[u8],
+ redactions: Option<Vec<String>>,
+ ) -> Result<()> {
+ let mut report = OneShotStatusTracker::new();
+ let store = Store::from_jumbf(data, &mut report)?;
+ claim.add_ingredient_data(provenance_label, store.claims, redactions)
+ }
+}
+
+impl std::fmt::Display for Store {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let report = &ManifestStoreReport::from_store(self).unwrap_or_default();
+ f.write_str(&format!("{}", &report))
+ }
+}
+
+/// `InvalidClaimError` provides additional detail on error cases for [`Store::from_jumbf`].
+#[derive(Debug, thiserror::Error)]
+pub enum InvalidClaimError {
+ /// The "c2pa" block was not found in the asset.
+ #[error("\"c2pa\" block not found")]
+ C2paBlockNotFound,
+
+ #[error("\"c2pa\" multiple claim boxes found in manifest")]
+ C2paMultipleClaimBoxes,
+
+ /// The claim superbox was not found.
+ #[error("claim superbox not found")]
+ ClaimSuperboxNotFound,
+
+ /// The claim description box was not found.
+ #[error("claim description box not found")]
+ ClaimDescriptionBoxNotFound,
+
+ /// More than one claim description box was found.
+ #[error("more than one claim description box was found for {label}")]
+ DuplicateClaimBox { label: String },
+
+ /// The expected data not found in claim box.
+ #[error("claim cbor box not valid")]
+ ClaimBoxData,
+
+ /// The claim has a version that is newer than supported by this crate.
+ #[error("claim version is too new, not supported")]
+ ClaimVersionTooNew,
+
+ /// The claim description box could not be parsed.
+ #[error("claim description box was invalid")]
+ ClaimDescriptionBoxInvalid,
+
+ /// The claim signature box was not found.
+ #[error("claim signature box was not found")]
+ ClaimSignatureBoxNotFound,
+
+ /// The claim signature description box was not found.
+ #[error("claim signature description box was not found")]
+ ClaimSignatureDescriptionBoxNotFound,
+
+ /// The claim signature description box was invalid.
+ #[error("claim signature description box was invalid")]
+ ClaimSignatureDescriptionBoxInvalid,
+
+ /// The assertion store superbox was not found.
+ #[error("assertion store superbox not found")]
+ AssertionStoreSuperboxNotFound,
+
+ /// The verifiable credentials store could not be read.
+ #[error("the verifiable credentials store could not be read")]
+ VerifiableCredentialStoreInvalid,
+
+ /// The assertion store does not contain the expected number of assertions.
+ #[error(
+ "unexpected number of assertions in assertion store (expected {expected}, found {found})"
+ )]
+ AssertionCountMismatch { expected: usize, found: usize },
+}
+
+#[cfg(test)]
+#[cfg(feature = "file_io")]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::panic)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ use tempfile::tempdir;
+ use thiserror::private::PathAsDisplay;
+ use twoway::find_bytes;
+
+ use crate::{
+ assertions::{Action, Actions, Ingredient, Uuid},
+ claim::Claim,
+ jumbf_io::{load_jumbf_from_file, save_jumbf_to_file},
+ status_tracker::*,
+ utils::test::{create_test_claim, fixture_path, temp_dir_path, temp_fixture_path},
+ };
+
+ use crate::{
+ claim::AssertionStoreJsonFormat, jumbf_io::update_file_jumbf,
+ openssl::temp_signer::get_signer, utils::patch::patch_file,
+ };
+
+ fn create_editing_claim(claim: &mut Claim) -> Result<&mut Claim> {
+ let uuid_str = "deadbeefdeadbeefdeadbeefdeadbeef";
+
+ // add a binary thumbnail assertion ('deadbeefadbeadbe')
+ let some_binary_data: Vec<u8> = vec![
+ 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
+ 0x0b, 0x0e,
+ ];
+
+ let uuid_assertion = Uuid::new("test uuid", uuid_str.to_string(), some_binary_data);
+
+ claim.add_assertion(&uuid_assertion)?;
+
+ Ok(claim)
+ }
+
+ fn create_capture_claim(claim: &mut Claim) -> Result<&mut Claim> {
+ let mut actions = Actions::new();
+ actions.add_action(Action::new("c2pa.created"));
+
+ claim.add_assertion(&actions)?;
+
+ Ok(claim)
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_jumbf_generation() {
+ // test adding to actual image
+ let ap = fixture_path("earth_apollo17.jpg");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "test-image.jpg");
+
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let claim1 = create_test_claim().unwrap();
+
+ // Create a new claim.
+ let mut claim2 = Claim::new("Photoshop", Some("Adobe"));
+ create_editing_claim(&mut claim2).unwrap();
+
+ // Create a 3rd party claim
+ let mut claim_capture = Claim::new("capture", Some("claim_capture"));
+ create_capture_claim(&mut claim_capture).unwrap();
+
+ // Do we generate JUMBF?
+ let temp_dir = tempdir().unwrap();
+ let (signer, _) = get_signer(&temp_dir.path());
+
+ // Test generate JUMBF
+ // Get labels for label test
+ let claim1_label = claim1.label().to_string();
+ let capture = claim_capture.label().to_string();
+ let claim2_label = claim2.label().to_string();
+
+ // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits
+ store.commit_claim(claim1).unwrap();
+ store.save_to_asset(&ap, &signer, &op).unwrap();
+ store.commit_claim(claim_capture).unwrap();
+ store.save_to_asset(&op, &signer, &op).unwrap();
+ store.commit_claim(claim2).unwrap();
+ store.save_to_asset(&op, &signer, &op).unwrap();
+
+ // test finding claims by label
+ let c1 = store.get_claim(&claim1_label);
+ let c2 = store.get_claim(&capture);
+ let c3 = store.get_claim(&claim2_label);
+ assert_eq!(&claim1_label, c1.unwrap().label());
+ assert_eq!(&capture, c2.unwrap().label());
+ assert_eq!(claim2_label, c3.unwrap().label());
+
+ // write to new file
+ println!("Provenance: {}\n", store.provenance_path().unwrap());
+
+ // read from new file
+ let new_store =
+ Store::load_from_asset(&op, true, &mut OneShotStatusTracker::new()).unwrap();
+
+ // can we get by the ingredient data back
+ let _some_binary_data: Vec<u8> = vec![
+ 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
+ 0x0b, 0x0e,
+ ];
+
+ // dump store and compare to original
+ for claim in new_store.claims() {
+ let _restored_json = claim
+ .to_json(AssertionStoreJsonFormat::OrderedList, false)
+ .unwrap();
+ let _orig_json = store
+ .get_claim(claim.label())
+ .unwrap()
+ .to_json(AssertionStoreJsonFormat::OrderedList, false)
+ .unwrap();
+
+ // these better match
+ //assert_eq!(orig_json, restored_json);
+ //assert_eq!(claim.hash(), store.claims()[idx].hash());
+
+ println!(
+ "Claim: {} \n{}",
+ claim.label(),
+ claim
+ .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
+ .expect("could not restore from json")
+ );
+
+ for hashed_uri in claim.assertions() {
+ let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
+ claim.get_claim_assertion(&label, instance).unwrap();
+ }
+ }
+
+ // test patch file - bytes should be same so error should not be detected
+ let mut splice_point =
+ patch_file(&op, "thumbnail".as_bytes(), "testme".as_bytes()).unwrap();
+
+ let mut restore_point =
+ patch_file(&op, "testme".as_bytes(), "thumbnail".as_bytes()).unwrap();
+
+ assert_eq!(splice_point, restore_point);
+
+ Store::load_from_asset(&op, true, &mut OneShotStatusTracker::new())
+ .expect("Should still verify");
+
+ // test patching jumbf - error should be detected
+
+ splice_point = update_file_jumbf(&op, "thumbnail".as_bytes(), "testme".as_bytes()).unwrap();
+ restore_point =
+ update_file_jumbf(&op, "testme".as_bytes(), "thumbnail.v1".as_bytes()).unwrap();
+
+ assert_eq!(splice_point, restore_point);
+
+ Store::load_from_asset(&op, true, &mut OneShotStatusTracker::new())
+ .expect_err("Should not verify");
+ }
+
+ struct BadSigner {}
+
+ impl crate::Signer for BadSigner {
+ fn sign(&self, _data: &[u8]) -> Result<Vec<u8>> {
+ Ok(b"not a valid signature".to_vec())
+ }
+
+ fn alg(&self) -> Option<String> {
+ None
+ }
+
+ fn certs(&self) -> Result<Vec<Vec<u8>>> {
+ Ok(Vec::new())
+ }
+
+ fn reserve_size(&self) -> usize {
+ 42
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_detects_unverifiable_signature() {
+ // test adding to actual image
+ let ap = fixture_path("earth_apollo17.jpg");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "test-image-unverified.jpg");
+
+ let mut store = Store::new();
+
+ let claim = create_test_claim().unwrap();
+
+ let signer = BadSigner {};
+
+ // JUMBF generation should fail because this signature won't validate.
+ store.commit_claim(claim).unwrap();
+
+ // TO DO: This generates a log spew when running this test.
+ // I don't have time to fix this right now.
+ // [(date) ERROR c2pa::store] Signature that was just generated does not validate: CoseCbor
+
+ store.save_to_asset(&ap, &signer, &op).unwrap_err();
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_sign_with_expired_cert() {
+ use crate::{openssl::RsaSigner, signer::ConfigurableSigner};
+
+ // test adding to actual image
+ let ap = fixture_path("earth_apollo17.jpg");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "test-image-expired-cert.jpg");
+
+ let mut store = Store::new();
+
+ let claim = create_test_claim().unwrap();
+
+ let signcert_path = fixture_path("rsa-pss256_key-expired.pub");
+ let pkey_path = fixture_path("rsa-pss256-expired.pem");
+ let signer =
+ RsaSigner::from_files(signcert_path, pkey_path, "ps256".to_string(), None).unwrap();
+
+ store.commit_claim(claim).unwrap();
+
+ // JUMBF generation should fail because the certificate won't validate.
+ let r = store.save_to_asset(&ap, &signer, &op);
+ assert!(r.is_err());
+ assert_eq!(r.err().unwrap().to_string(), "COSE certificate has expired");
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_jumbf_replacement_generation() {
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let claim1 = create_test_claim().unwrap();
+ store.commit_claim(claim1).unwrap();
+
+ // do we generate JUMBF
+ let jumbf_bytes = store.to_jumbf_internal(512).unwrap();
+ assert!(!jumbf_bytes.is_empty());
+
+ // test adding to actual image
+ let ap = fixture_path("bigjumbf.jpg");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "bigjumbf_test.jpg");
+
+ // grab jumbf from original
+ let original_jumbf = load_jumbf_from_file(&ap).unwrap();
+
+ // replace with new jumbf
+ save_jumbf_to_file(&jumbf_bytes, &ap, Some(&op)).unwrap();
+
+ let saved_jumbf = load_jumbf_from_file(&op).unwrap();
+
+ // saved data should be the new data
+ assert_eq!(&jumbf_bytes, &saved_jumbf);
+
+ // original data should not be in file anymore check for first 1k
+ let buf = fs::read(&op).unwrap();
+ assert!(find_bytes(&buf, &original_jumbf[0..1024]).is_none());
+ }
+
+ /* async signing not supported at the moment
+ NOTE: Add this to Cargo.toml if this test is restored.
+
+ [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
+ actix = "0.11.0"
+
+ #[cfg(feature = "async_signer")]
+ #[actix::test]
+ async fn test_jumbf_generation_async() {
+ let signer = crate::AsyncPlaceholder {};
+
+ // test adding to actual image
+ let ap = fixture_path("earth_apollo17.jpg");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "test-async.jpg");
+
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let claim1 = create_test_claim().unwrap();
+
+ // Create a new claim.
+ let mut claim2 = Claim::new("Photoshop", Some("Adobe"));
+ create_editing_claim(&mut claim2).unwrap();
+
+ // Create a 3rd party claim
+ let mut claim_capture = Claim::new("capture", Some("claim_capture"));
+ create_capture_claim(&mut claim_capture).unwrap();
+
+ // Test generate JUMBF
+ // Get labels for label test
+ let claim1_label = claim1.label().to_string();
+ let capture = claim_capture.label().to_string();
+ let claim2_label = claim2.label().to_string();
+
+ /*
+ Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
+ */
+ store.commit_claim(claim1).unwrap();
+ store.save_to_asset_async(&ap, &signer, &op).await.unwrap();
+ store.commit_claim(claim_capture).unwrap();
+ store.save_to_asset_async(&ap, &signer, &op).await.unwrap();
+ store.commit_claim(claim2).unwrap();
+ store.save_to_asset_async(&ap, &signer, &op).await.unwrap();
+
+ // test finding claims by label
+ let c1 = store.get_claim(&claim1_label);
+ let c2 = store.get_claim(&capture);
+ let c3 = store.get_claim(&claim2_label);
+ assert_eq!(&claim1_label, c1.unwrap().label());
+ assert_eq!(&capture, c2.unwrap().label());
+ assert_eq!(claim2_label, c3.unwrap().label());
+
+ // Do we generate JUMBF
+ let jumbf_bytes = store.to_jumbf_async(&signer).unwrap();
+ assert!(!jumbf_bytes.is_empty());
+
+ // write to new file
+ println!("Provenance: {}\n", store.provenance_path().unwrap());
+
+ // read from new file
+ let mut report: Vec<ValidationItem> = Vec::new();
+ let new_store = Store::load_from_asset(&op, true, Some(&mut report)).unwrap();
+ // Async placeholder signature won't verify. We need the load to complete,
+ // but we ignore the validation log which we know will have errors.
+
+ let claim = new_store.provenance_claim().unwrap();
+ let sig = claim.signature_val();
+
+ assert_eq!(&sig[0..19], b"invalid signature\0\0");
+ }
+ */
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_png_jumbf_generation() {
+ // test adding to actual image
+ let ap = fixture_path("libpng-test.png");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png");
+
+ // Create claims store.
+ let mut store = Store::new();
+
+ // Create a new claim.
+ let claim1 = create_test_claim().unwrap();
+
+ // Create a new claim.
+ let mut claim2 = Claim::new("Photoshop", Some("Adobe"));
+ create_editing_claim(&mut claim2).unwrap();
+
+ // Create a 3rd party claim
+ let mut claim_capture = Claim::new("capture", Some("claim_capture"));
+ create_capture_claim(&mut claim_capture).unwrap();
+
+ // Do we generate JUMBF?
+ let temp_dir = tempdir().unwrap();
+ let (signer, _) = get_signer(&temp_dir.path());
+
+ // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits
+ store.commit_claim(claim1).unwrap();
+ store.save_to_asset(&ap, &signer, &op).unwrap();
+ store.commit_claim(claim_capture).unwrap();
+ store.save_to_asset(&op, &signer, &op).unwrap();
+ store.commit_claim(claim2).unwrap();
+ store.save_to_asset(&op, &signer, &op).unwrap();
+
+ // write to new file
+ println!("Provenance: {}\n", store.provenance_path().unwrap());
+
+ let mut report = DetailedStatusTracker::new();
+
+ // read from new file
+ let new_store = Store::load_from_asset(&op, true, &mut report).unwrap();
+
+ // can we get by the ingredient data back
+ let _some_binary_data: Vec<u8> = vec![
+ 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
+ 0x0b, 0x0e,
+ ];
+
+ // dump store and compare to original
+ for claim in new_store.claims() {
+ let _restored_json = claim
+ .to_json(AssertionStoreJsonFormat::OrderedList, false)
+ .unwrap();
+ let _orig_json = store
+ .get_claim(claim.label())
+ .unwrap()
+ .to_json(AssertionStoreJsonFormat::OrderedList, false)
+ .unwrap();
+
+ println!(
+ "Claim: {} \n{}",
+ claim.label(),
+ claim
+ .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
+ .expect("could not restore from json")
+ );
+
+ for hashed_uri in claim.assertions() {
+ let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
+ claim
+ .get_claim_assertion(&label, instance)
+ .expect("Should find assertion");
+ }
+ }
+ }
+
+ /* todo: disable until we can generate a valid file with no xmp
+ #[test]
+ fn test_manifest_no_xmp() {
+ let ap = fixture_path("CAICAI_NO_XMP.jpg");
+ assert!(Store::load_from_asset(&ap, true, None).is_ok());
+ }
+ */
+
+ #[test]
+ fn test_manifest_bad_sig() {
+ let ap = fixture_path("CAICAI_BAD_SIG.jpg");
+ assert!(Store::load_from_asset(&ap, true, &mut OneShotStatusTracker::new()).is_err());
+ }
+
+ #[test]
+ fn test_unsupported_type() {
+ // test bad xmp
+ let ap = fixture_path("Purple Square.psd");
+ let mut report = DetailedStatusTracker::new();
+ let _r = Store::load_from_asset(&ap, true, &mut report);
+
+ println!("Error report for {}: {:?}", ap.as_display(), report);
+ assert!(!report.get_log().is_empty());
+
+ assert!(report_has_err(report.get_log(), Error::UnsupportedType));
+ }
+
+ #[test]
+ fn test_bad_jumbf() {
+ // test bad jumbf
+ let ap = fixture_path("bigjumbf.jpg");
+ let mut report = DetailedStatusTracker::new();
+ let _r = Store::load_from_asset(&ap, true, &mut report);
+
+ // error report
+ println!("Error report for {}: {:?}", ap.as_display(), report);
+ assert!(!report.get_log().is_empty());
+
+ assert!(report_has_err(report.get_log(), Error::PrereleaseError));
+ }
+
+ #[test]
+ fn test_detect_byte_change() {
+ // test bad jumbf
+ let ap = fixture_path("bad_verify.jpeg");
+ let mut report = DetailedStatusTracker::new();
+ Store::load_from_asset(&ap, true, &mut report).unwrap();
+
+ // error report
+ println!("Error report for {}: {:?}", ap.as_display(), report);
+ assert!(!report.get_log().is_empty());
+
+ let errs = report_split_errors(report.get_log_mut());
+ assert!(report_has_status(
+ &errs,
+ validation_status::ASSERTION_DATAHASH_MISMATCH
+ ));
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_file_not_found() {
+ let ap = fixture_path("this_does_not_exist.jpg");
+ let mut report = DetailedStatusTracker::new();
+ let _result = Store::load_from_asset(&ap, true, &mut report);
+
+ println!(
+ "Error report for {}: {:?}",
+ ap.as_display(),
+ report.get_log()
+ );
+ assert!(!report.get_log().is_empty());
+ let errors = report_split_errors(report.get_log_mut());
+ assert!(matches!(
+ errors[0].err_val.as_ref(),
+ Some(Error::IoError(_err))
+ ));
+ }
+
+ #[test]
+ fn test_old_manifest() {
+ let ap = fixture_path("08manifest.jpg");
+ let mut report = DetailedStatusTracker::new();
+ let _r = Store::load_from_asset(&ap, true, &mut report);
+
+ println!(
+ "Error report for {}: {:?}",
+ ap.as_display(),
+ report.get_log()
+ );
+ assert!(!report.get_log().is_empty());
+ let errors = report_split_errors(report.get_log_mut());
+ assert!(matches!(
+ errors[0].err_val.as_ref(),
+ Some(Error::PrereleaseError)
+ ));
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_verifiable_credentials() {
+ use crate::utils::test::create_test_store;
+
+ let temp_dir = tempdir().unwrap();
+ let (signer, _) = get_signer(&temp_dir.path());
+
+ // test adding to actual image
+ let ap = fixture_path("earth_apollo17.jpg");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "update_manifest.jpg");
+
+ // get default store with default claim
+ let mut store = create_test_store().unwrap();
+
+ // save to output
+ store
+ .save_to_asset(ap.as_path(), &signer, op.as_path())
+ .unwrap();
+
+ // read back in
+ let restored_store =
+ Store::load_from_asset(op.as_path(), true, &mut OneShotStatusTracker::new()).unwrap();
+
+ let pc = restored_store.provenance_claim().unwrap();
+
+ let vc = pc.get_verifiable_credentials();
+
+ assert!(!vc.is_empty());
+ match &vc[0] {
+ AssertionData::Json(s) => {
+ assert!(s.contains("did:nppa:eb1bb9934d9896a374c384521410c7f14"))
+ }
+ _ => panic!("expected JSON assertion data"),
+ }
+ }
+
+ /// copies a fixture, replaces some bytes and returns a validation report
+ fn patch_and_report(
+ fixture_name: &str,
+ search_bytes: &[u8],
+ replace_bytes: &[u8],
+ ) -> impl StatusTracker {
+ let temp_dir = tempdir().expect("temp dir");
+ let path = temp_fixture_path(&temp_dir, fixture_name);
+ patch_file(&path, search_bytes, replace_bytes).expect("patch_file");
+ let mut report = DetailedStatusTracker::default();
+ let _r = Store::load_from_asset(&path, true, &mut report); // errs are in report
+ println!("report: {:?}", report);
+ report
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_update_manifest() {
+ use crate::{hashed_uri::HashedUri, utils::test::create_test_store};
+
+ let temp_dir = tempdir().unwrap();
+ let (signer, _) = get_signer(&temp_dir.path());
+
+ // test adding to actual image
+ let ap = fixture_path("earth_apollo17.jpg");
+ let temp_dir = tempdir().expect("temp dir");
+ let op = temp_dir_path(&temp_dir, "update_manifest.jpg");
+
+ // get default store with default claim
+ let mut store = create_test_store().unwrap();
+
+ // save to output
+ store
+ .save_to_asset(ap.as_path(), &signer, op.as_path())
+ .unwrap();
+
+ let mut report = OneShotStatusTracker::default();
+ // read back in
+ let mut restored_store = Store::load_from_asset(op.as_path(), true, &mut report).unwrap();
+
+ let pc = restored_store.provenance_claim().unwrap();
+
+ // should be a regular manifest
+ assert!(!pc.update_manifest());
+
+ // create a new update manifest
+ let mut claim = Claim::new("adobe unit test", Some("update_manfifest"));
+
+ // must contain an ingredient
+ let parent_hashed_uri = HashedUri::new(
+ restored_store.provenance_path().unwrap(),
+ Some(pc.alg().to_string()),
+ &pc.hash(),
+ );
+
+ let ingredient = Ingredient::new(
+ "update_manifest.jpg",
+ "image/jpeg",
+ "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
+ Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
+ )
+ .set_parent()
+ .set_c2pa_manifest_from_hashed_uri(Some(parent_hashed_uri));
+
+ claim.add_assertion(&ingredient).unwrap();
+
+ restored_store.commit_update_manifest(claim).unwrap();
+ restored_store
+ .save_to_asset(op.as_path(), &signer, op.as_path())
+ .unwrap();
+
+ // read back in store with update manifest
+ let um_store = Store::load_from_asset(op.as_path(), true, &mut report).unwrap();
+
+ let um = um_store.provenance_claim().unwrap();
+
+ // should be an update manifest
+ assert!(um.update_manifest());
+ }
+
+ #[test]
+ fn test_claim_decoding() {
+ // modify a required field label in the claim - causes failure to read claim from cbor
+ let report = patch_and_report("CAICAI.jpg", b"claim_generator", b"claim_generatur");
+ assert!(!report.get_log().is_empty());
+ assert!(matches!(
+ report.get_log()[0].err_val,
+ Some(Error::ClaimDecoding)
+ ));
+ //assert_eq!(report[0].validation_status.as_deref(), Some(???)); // what validation status should we have for this?
+ }
+
+ #[test]
+ fn test_modify_xmp() {
+ // modify the XMP (change xmp magic id value) - this should cause a data hash mismatch (OTGP)
+ let mut report = patch_and_report(
+ "CAICAI.jpg",
+ b"W5M0MpCehiHzreSzNTczkc9d",
+ b"W5M0MpCehiHzreSzNTczkXXX",
+ );
+ assert!(!report.get_log().is_empty());
+ let errors = report_split_errors(report.get_log_mut());
+
+ assert!(matches!(errors[0].err_val, Some(Error::HashMismatch(_))));
+ assert_eq!(
+ errors[0].validation_status.as_deref(),
+ Some(validation_status::ASSERTION_DATAHASH_MISMATCH)
+ ); // what validation status should we have for this?
+ }
+
+ #[test]
+ fn test_claim_modified() {
+ // replace the title that is inside the claim data - should cause signature to not match
+ let mut report = patch_and_report("CAICAI.jpg", b"CAICAI.jpg", b"XXXCAI.jpg");
+ assert!(!report.get_log().is_empty());
+ let errors = report_split_errors(report.get_log_mut());
+
+ assert!(report_has_err(&errors, Error::CoseSignature));
+ assert!(report_has_err(&errors, Error::CoseTimeStampMismatch));
+
+ assert!(report_has_status(
+ &errors,
+ validation_status::CLAIM_SIGNATURE_MISMATCH
+ ));
+ assert!(report_has_status(
+ &errors,
+ validation_status::TIMESTAMP_MISMATCH
+ ));
+ }
+
+ #[test]
+ fn test_assertion_hash_mismatch() {
+ // modifies content of an action assertion - causes an assertion hashuri mismatch
+ let mut report =
+ patch_and_report("CAICAI.jpg", b"brightnesscontrast", b"brightnesscontraxx");
+ let errors = report_split_errors(report.get_log_mut());
+
+ assert_eq!(
+ errors[0].validation_status.as_deref(),
+ Some(validation_status::ASSERTION_HASHEDURI_MISMATCH)
+ );
+ }
+
+ #[test]
+ fn test_claim_missing() {
+ // patch jumbf url from c2pa_manifest field in an ingredient to cause claim_missing
+ // note this includes hex for Jumbf blocks, so may need some manual tweaking
+ const SEARCH_BYTES: &[u8] =
+ b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth:urn:uuid:";
+ const REPLACE_BYTES: &[u8] =
+ b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth:urn:uuix:";
+ let mut report = patch_and_report("CAICAI.jpg", SEARCH_BYTES, REPLACE_BYTES);
+ let errors = report_split_errors(report.get_log_mut());
+ assert_eq!(
+ errors[0].validation_status.as_deref(),
+ Some(validation_status::ASSERTION_HASHEDURI_MISMATCH)
+ );
+ assert_eq!(
+ errors[1].validation_status.as_deref(),
+ Some(validation_status::CLAIM_MISSING)
+ );
+ }
+
+ /* enable when we enable OCSP validation
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_ocsp() {
+ let ap = fixture_path("ocsp_test.png");
+ let mut report = DetailedStatusTracker::new();
+ let _r = Store::load_from_asset(&ap, true, &mut report);
+
+ println!(
+ "Error report for {}: {:?}",
+ ap.as_display(),
+ report.get_log()
+ );
+ assert!(report.get_log().is_empty());
+ }
+ */
+
+ #[test]
+ fn test_display() {
+ let ap = fixture_path("CAICAI_BAD_SIG.jpg");
+ let mut report = DetailedStatusTracker::new();
+ let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset");
+ println!("store = {}", store);
+ }
+}
diff --git a/sdk/src/time_stamp.rs b/sdk/src/time_stamp.rs
@@ -0,0 +1,414 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::asn1::rfc5652::CertificateChoices::Certificate;
+use crate::asn1::rfc5652::{SignedData, OID_ID_SIGNED_DATA};
+use serde::{Deserialize, Serialize};
+use std::convert::TryFrom;
+
+/// Generate TimeStamp signature according to https://datatracker.ietf.org/doc/html/rfc3161
+/// using the specified Time Authority
+use crate::error::{Error, Result};
+use crate::hash_utils::vec_compare;
+
+use crate::asn1::rfc3161::{TimeStampResp, TstInfo, OID_CONTENT_TYPE_TST_INFO};
+
+use bcder::decode::Constructed;
+use x509_certificate::DigestAlgorithm::{self};
+
+use coset::{iana, sig_structure_data, HeaderBuilder, ProtectedHeader};
+
+#[allow(dead_code)]
+pub(crate) fn cose_countersign_data(data: &[u8], alg: &str) -> Vec<u8> {
+ let alg_id = match alg {
+ "ps256" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::PS256)
+ .build(),
+ "ps384" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::PS384)
+ .build(),
+ "ps512" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::PS512)
+ .build(),
+ "es256" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .build(),
+ "es384" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES384)
+ .build(),
+ "es512" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES512)
+ .build(),
+ "ed25519" => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::EdDSA)
+ .build(),
+ _ => HeaderBuilder::new()
+ .algorithm(iana::Algorithm::PS256)
+ .build(),
+ };
+
+ let p_header = ProtectedHeader {
+ original_data: None,
+ header: alg_id,
+ };
+ let aad: Vec<u8> = Vec::new();
+
+ // create sig_structure_data to be signed
+ sig_structure_data(
+ coset::SignatureContext::CounterSignature,
+ p_header,
+ None,
+ &aad,
+ data,
+ )
+}
+
+#[allow(dead_code)]
+pub(crate) fn cose_timestamp_countersign(data: &[u8], alg: &str, tsa_url: &str) -> Result<Vec<u8>> {
+ // create countersignature with TimeStampReq parameters
+ // payload: data
+ // context "CounterSigner"
+ // certReq true
+ // algorithm sha256
+
+ // create sig data structure to be time stamped
+ let sd = cose_countersign_data(data, alg);
+
+ timestamp_data(tsa_url, &sd)
+}
+
+#[allow(dead_code)]
+pub(crate) fn cose_sigtst_to_tstinfos(
+ sigtst_cbor: &[u8],
+ data: &[u8],
+ alg: &str,
+) -> Result<Vec<TstInfo>> {
+ let tst_container: TstContainer =
+ serde_cbor::from_slice(sigtst_cbor).map_err(|_err| Error::CoseTimeStampGeneration)?;
+
+ let mut tstinfos: Vec<TstInfo> = Vec::new();
+
+ for token in &tst_container.tst_tokens {
+ let tbs = cose_countersign_data(data, alg);
+ let tst_info = verify_timestamp(&token.val, &tbs)?;
+ tstinfos.push(tst_info);
+ }
+
+ if tstinfos.is_empty() {
+ Err(Error::NotFound)
+ } else {
+ Ok(tstinfos)
+ }
+}
+
+/// Get URL to Time Authority to use
+#[allow(dead_code)] // in case we make use of this later
+pub fn get_ta_url() -> Option<String> {
+ //const TA_URL: &str = "http://timestamp.digicert.com";
+
+ match std::env::var("CAI_TA_URL") {
+ Ok(url) => Some(url),
+ Err(_) => None,
+ }
+}
+
+/// internal only function to work around bug in serialization of TimeStampResponse
+/// so we just return the data directly
+#[cfg(feature = "file_io")]
+fn time_stamp_request_http(
+ url: &str,
+ request: &crate::asn1::rfc3161::TimeStampReq,
+) -> Result<Vec<u8>> {
+ use bcder::encode::Values;
+ use std::io::Read;
+
+ const HTTP_CONTENT_TYPE_REQUEST: &str = "application/timestamp-query";
+ const HTTP_CONTENT_TYPE_RESPONSE: &str = "application/timestamp-reply";
+
+ let mut body = Vec::<u8>::new();
+ request
+ .encode_ref()
+ .write_encoded(bcder::Mode::Der, &mut body)?;
+
+ let body_reader = std::io::Cursor::new(body);
+
+ let response = ureq::post(url)
+ .set("Content-Type", HTTP_CONTENT_TYPE_REQUEST)
+ .send(body_reader)
+ .map_err(|_err| Error::CoseTimeStampGeneration)?;
+
+ if response.status() == 200 && response.content_type() == HTTP_CONTENT_TYPE_RESPONSE {
+ let len = response
+ .header("Content-Length")
+ .and_then(|s| s.parse::<usize>().ok())
+ .unwrap_or(20000);
+
+ let mut response_bytes: Vec<u8> = Vec::with_capacity(len);
+
+ response
+ .into_reader()
+ .take(1000000)
+ .read_to_end(&mut response_bytes)
+ .map_err(|_err| Error::CoseTimeStampGeneration)?;
+
+ let res = TimeStampResponse(
+ Constructed::decode(response_bytes.as_ref(), bcder::Mode::Der, |cons| {
+ TimeStampResp::take_from(cons)
+ })
+ .map_err(|_err| Error::CoseTimeStampGeneration)?,
+ );
+
+ // Verify nonce was reflected, if present.
+ if res.is_success() {
+ if let Some(tst_info) = res
+ .tst_info()
+ .map_err(|_err| Error::CoseTimeStampGeneration)?
+ {
+ if tst_info.nonce != request.nonce {
+ return Err(Error::CoseTimeStampGeneration);
+ }
+ }
+ }
+
+ Ok(response_bytes)
+ } else {
+ Err(Error::CoseTimeStampGeneration)
+ }
+}
+
+/// Send a Time-Stamp request for a given message to an HTTP URL.
+///
+/// This is a wrapper around [time_stamp_request_http] that constructs the low-level
+/// ASN.1 request object with reasonable defaults.
+#[cfg(feature = "file_io")]
+fn time_stamp_message_http(
+ url: &str,
+ message: &[u8],
+ digest_algorithm: DigestAlgorithm,
+) -> Result<Vec<u8>> {
+ use ring::rand::SecureRandom;
+
+ let mut h = digest_algorithm.digester();
+ h.update(message);
+ let digest = h.finish();
+
+ let mut random = [0u8; 8];
+ ring::rand::SystemRandom::new()
+ .fill(&mut random)
+ .map_err(|_| Error::CoseTimeStampGeneration)?;
+
+ let request = crate::asn1::rfc3161::TimeStampReq {
+ version: bcder::Integer::from(1_u8),
+ message_imprint: crate::asn1::rfc3161::MessageImprint {
+ hash_algorithm: digest_algorithm.into(),
+ hashed_message: bcder::OctetString::new(bytes::Bytes::copy_from_slice(digest.as_ref())),
+ },
+ req_policy: None,
+ nonce: Some(bcder::Integer::from(u64::from_le_bytes(random))),
+ cert_req: Some(true),
+ extensions: None,
+ };
+
+ time_stamp_request_http(url, &request)
+}
+
+pub struct TimeStampResponse(TimeStampResp);
+
+impl std::ops::Deref for TimeStampResponse {
+ type Target = TimeStampResp;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl TimeStampResponse {
+ /// Whether the time stamp request was successful.
+ #[cfg(feature = "file_io")]
+ pub fn is_success(&self) -> bool {
+ matches!(
+ self.0.status.status,
+ crate::asn1::rfc3161::PkiStatus::Granted
+ | crate::asn1::rfc3161::PkiStatus::GrantedWithMods
+ )
+ }
+
+ fn signed_data(&self) -> Result<Option<SignedData>> {
+ if let Some(token) = &self.0.time_stamp_token {
+ if token.content_type == OID_ID_SIGNED_DATA {
+ Ok(Some(
+ token
+ .content
+ .clone()
+ .decode(|cons| SignedData::take_from(cons))
+ .map_err(|_err| Error::CoseTimeStampGeneration)?,
+ ))
+ } else {
+ Err(Error::CoseTimeStampGeneration)
+ }
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn tst_info(&self) -> Result<Option<TstInfo>> {
+ if let Some(signed_data) = self.signed_data()? {
+ if signed_data.content_info.content_type == OID_CONTENT_TYPE_TST_INFO {
+ if let Some(content) = signed_data.content_info.content {
+ Ok(Some(
+ Constructed::decode(content.to_bytes(), bcder::Mode::Der, |cons| {
+ TstInfo::take_from(cons)
+ })
+ .map_err(|_err| Error::CoseTimeStampGeneration)?,
+ ))
+ } else {
+ Ok(None)
+ }
+ } else {
+ Ok(None)
+ }
+ } else {
+ Ok(None)
+ }
+ }
+}
+/// Generate TimeStamp based on rfc3161 using "data" as MessageImprint and return raw TimeStampRsp bytes
+#[allow(unused_variables)]
+pub fn timestamp_data(url: &str, data: &[u8]) -> Result<Vec<u8>> {
+ #[cfg(feature = "file_io")]
+ {
+ let ts = time_stamp_message_http(url, data, x509_certificate::DigestAlgorithm::Sha256)?;
+
+ // sanity check
+ verify_timestamp(&ts, data)?;
+
+ Ok(ts)
+ }
+ #[cfg(not(feature = "file_io"))]
+ {
+ Err(Error::WasmNoCrypto)
+ }
+}
+
+pub fn gt_to_datetime(
+ gt: x509_certificate::asn1time::GeneralizedTime,
+) -> chrono::DateTime<chrono::Utc> {
+ gt.into()
+}
+fn time_to_datetime(t: x509_certificate::asn1time::Time) -> chrono::DateTime<chrono::Utc> {
+ match t {
+ x509_certificate::asn1time::Time::UtcTime(u) => *u,
+ x509_certificate::asn1time::Time::GeneralTime(gt) => gt_to_datetime(gt),
+ }
+}
+/// Returns TimeStamp token info if ts verifies against supplied data
+pub fn verify_timestamp(ts: &[u8], data: &[u8]) -> Result<TstInfo> {
+ let ts_resp = get_timestamp_response(ts)?;
+
+ // make sure this signature matches the expected data
+ let tst_opt = ts_resp.tst_info()?;
+ let tst = tst_opt.ok_or(Error::CoseInvalidTimeStamp)?;
+ let mi = &tst.message_imprint;
+
+ let digest_algorithm = DigestAlgorithm::try_from(&mi.hash_algorithm.algorithm)
+ .map_err(|_e| Error::UnsupportedType)?;
+
+ let mut h = digest_algorithm.digester();
+ h.update(data);
+ let digest = h.finish();
+
+ if !vec_compare(digest.as_ref(), &mi.hashed_message.to_bytes().to_vec()) {
+ return Err(Error::CoseTimeStampMismatch);
+ }
+
+ // check for timestamp expiration during stamping
+ if let Ok(Some(sd)) = ts_resp.signed_data() {
+ if let Some(cs) = sd.certificates {
+ if !cs.is_empty() {
+ let cert = match &cs[0] {
+ Certificate(c) => c,
+ _ => return Err(Error::CoseTimeStampValidity),
+ };
+
+ let signing_time = gt_to_datetime(tst.gen_time.clone()).timestamp();
+ let not_before =
+ time_to_datetime(cert.tbs_certificate.validity.not_before.clone()).timestamp();
+
+ let not_after =
+ time_to_datetime(cert.tbs_certificate.validity.not_after.clone()).timestamp();
+
+ if !(signing_time >= not_before && signing_time <= not_after) {
+ return Err(Error::CoseTimeStampValidity);
+ }
+ }
+ }
+ }
+
+ Ok(tst)
+}
+
+/// Get TimeStampResponse from DER TimeStampResp bytes
+pub fn get_timestamp_response(tsresp: &[u8]) -> Result<TimeStampResponse> {
+ let ts = TimeStampResponse(
+ Constructed::decode(tsresp, bcder::Mode::Der, |cons| {
+ TimeStampResp::take_from(cons)
+ })
+ .map_err(|_e| Error::CoseInvalidTimeStamp)?,
+ );
+
+ Ok(ts)
+}
+
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+pub struct TstToken {
+ #[serde(with = "serde_bytes")]
+ pub val: Vec<u8>,
+}
+
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+pub struct TstContainer {
+ #[serde(rename = "tstTokens")]
+ pub tst_tokens: Vec<TstToken>,
+}
+
+impl TstContainer {
+ pub fn new() -> Self {
+ TstContainer {
+ tst_tokens: Vec::new(),
+ }
+ }
+
+ #[cfg(feature = "file_io")]
+ pub fn add_token(&mut self, token: TstToken) {
+ self.tst_tokens.push(token);
+ }
+}
+
+impl Default for TstContainer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// Wrap rfc3161 TimeStampRsp in COSE sigTst object
+#[cfg(feature = "file_io")]
+pub fn make_cose_timestamp(ts_data: &[u8]) -> TstContainer {
+ let token = TstToken {
+ val: ts_data.to_vec(),
+ };
+
+ let mut container = TstContainer::new();
+ container.add_token(token);
+
+ container
+}
diff --git a/sdk/src/utils/cbor_types.rs b/sdk/src/utils/cbor_types.rs
@@ -0,0 +1,149 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use serde::de::{Deserialize, Deserializer};
+use serde::ser::{Serialize, Serializer};
+use serde_bytes::ByteBuf;
+use serde_cbor::tags::Tagged;
+use std::fmt;
+
+// New types for C2PA that will serialize to the correct
+// CBOR type specified in the C2PA spec.
+//
+// Based on samples from cbor rust git repository.
+//
+// https://tools.ietf.org/html/rfc7049#section-2.4.1
+#[derive(Clone, Debug, PartialEq)]
+pub struct DateT(pub String);
+
+impl Serialize for DateT {
+ fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
+ Tagged::new(Some(0), &self.0).serialize(s)
+ }
+}
+
+impl<'de> Deserialize<'de> for DateT {
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ let tagged = Tagged::<String>::deserialize(deserializer)?;
+ match tagged.tag {
+ Some(0) | None => Ok(DateT(tagged.value)),
+ Some(_) => Err(serde::de::Error::custom("unexpected tag")),
+ }
+ }
+}
+
+impl<'a> AsRef<str> for DateT {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl fmt::Display for DateT {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+// https://tools.ietf.org/html/rfc7049#section-2.4.4.3
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct UriT(pub String);
+
+impl Serialize for UriT {
+ fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
+ Tagged::new(Some(32), &self.0).serialize(s)
+ }
+}
+impl<'de> Deserialize<'de> for UriT {
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ let tagged = Tagged::<String>::deserialize(deserializer)?;
+ match tagged.tag {
+ // allow deserialization even if there is no tag. Allows roundtrip via other formats such as json
+ Some(32) | None => Ok(UriT(tagged.value)),
+ Some(_) => Err(serde::de::Error::custom("unexpected tag")),
+ }
+ }
+}
+
+impl<'a> AsRef<str> for UriT {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl fmt::Display for UriT {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct BytesT(pub Vec<u8>);
+
+impl Serialize for BytesT {
+ fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
+ Tagged::new(Some(64), &ByteBuf::from(self.0.clone())).serialize(s)
+ }
+}
+
+impl<'de> Deserialize<'de> for BytesT {
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ let tagged = Tagged::<ByteBuf>::deserialize(deserializer)?;
+ match tagged.tag {
+ Some(64) | None => Ok(BytesT(tagged.value.to_vec())),
+ Some(_) => Err(serde::de::Error::custom("unexpected tag")),
+ }
+ }
+}
+
+impl<'a> AsRef<Vec<u8>> for BytesT {
+ fn as_ref(&self) -> &Vec<u8> {
+ &self.0
+ }
+}
+
+impl std::ops::Deref for BytesT {
+ type Target = [u8];
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl fmt::Display for BytesT {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ format!("{:02x?}", &self.0.to_vec()).replace(',', "")
+ )
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ #[test]
+ fn test_round_trip() {
+ let uri = UriT("Some data value".into());
+
+ let uri_cbor = serde_cbor::ser::to_vec(&uri).expect("should serialize");
+
+ let uri_restored: UriT = serde_cbor::from_slice(&uri_cbor).expect("should deserialize");
+
+ assert_eq!(uri.as_ref(), uri_restored.as_ref());
+ }
+}
diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs
@@ -0,0 +1,259 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::ops::RangeInclusive;
+
+use log::{debug, warn};
+use serde::{Deserialize, Serialize};
+
+// multihash versions
+use multibase::{decode, encode};
+use multihash::{wrap, Code, Multihash, Sha2_256, Sha2_512, Sha3_256, Sha3_384, Sha3_512};
+
+use range_set::RangeSet;
+
+// direct sha functions
+use sha2::{Digest, Sha256, Sha384, Sha512};
+
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
+pub struct Exclusion {
+ start: usize,
+ length: usize,
+}
+
+impl Exclusion {
+ pub fn new(start: usize, length: usize) -> Self {
+ Exclusion { start, length }
+ }
+
+ /// update the start value
+ pub fn set_start(&mut self, start: usize) {
+ self.start = start;
+ }
+
+ /// return start as usize
+ pub fn start(&self) -> usize {
+ self.start
+ }
+
+ /// return length as usize
+ pub fn length(&self) -> usize {
+ self.length
+ }
+}
+
+/// Compare two byte vectors return true if match, false otherwise
+pub fn vec_compare(va: &[u8], vb: &[u8]) -> bool {
+ (va.len() == vb.len()) && // zip stops at the shortest
+ va.iter()
+ .zip(vb)
+ .all(|(a,b)| a == b)
+}
+
+/// Generate hash of type hash_type for supplied data array. The
+/// hash_type are those specified in the multihash specification. Currently
+/// we only support Sha2-256/512 or Sha2-256/512.
+/// Returns hash or None if incomptible type
+pub fn hash_by_type(hash_type: u8, data: &[u8]) -> Option<Multihash> {
+ match hash_type {
+ 0x12 => Some(Sha2_256::digest(data)),
+ 0x13 => Some(Sha2_512::digest(data)),
+ 0x14 => Some(Sha3_512::digest(data)),
+ 0x15 => Some(Sha3_384::digest(data)),
+ 0x16 => Some(Sha3_256::digest(data)),
+ _ => None,
+ }
+}
+
+enum Hasher {
+ SHA256(Sha256),
+ SHA384(Sha384),
+ SHA512(Sha512),
+}
+
+impl Hasher {
+ // update hash value with new data
+ fn update(&mut self, data: &[u8]) {
+ use Hasher::*;
+ // update the hash
+ match self {
+ SHA256(ref mut d) => d.update(data),
+ SHA384(ref mut d) => d.update(data),
+ SHA512(ref mut d) => d.update(data),
+ }
+ }
+
+ // comsume hasher and return the final digest
+ fn finalize(hasher_enum: Hasher) -> Vec<u8> {
+ use Hasher::*;
+ // return the hash
+ match hasher_enum {
+ SHA256(d) => d.finalize().to_vec(),
+ SHA384(d) => d.finalize().to_vec(),
+ SHA512(d) => d.finalize().to_vec(),
+ }
+ }
+}
+
+// return hash bytes for desired hashing algoritm
+pub fn hash_by_alg(alg: &str, data: &[u8], exclusions: Option<Vec<Exclusion>>) -> Vec<u8> {
+ use Hasher::*;
+ let mut hasher_enum = match alg {
+ "sha256" => SHA256(Sha256::new()),
+ "sha384" => SHA384(Sha384::new()),
+ "sha512" => SHA512(Sha512::new()),
+ _ => {
+ warn!(
+ "Unsupported hashing algorithm: {}, substituting sha256",
+ alg
+ );
+ SHA256(Sha256::new())
+ }
+ };
+
+ match exclusions {
+ Some(mut e) => {
+ // hash all content
+ if e.is_empty() {
+ // add the data
+ hasher_enum.update(data);
+
+ // return the hash
+ return Hasher::finalize(hasher_enum);
+ }
+
+ // hash data skipping excluded regions
+ // sort the exclusions
+ e.sort_by_key(|a| a.start());
+
+ // verify structure of blocks
+ let num_blocks = e.len();
+ let exclusion_end = e[num_blocks - 1].start() + e[num_blocks - 1].length();
+ let data_len = data.len();
+ let data_end = data_len - 1;
+
+ // if not enough range we will just cacl to the end
+ if data_len < exclusion_end {
+ debug!("the exclusion range exceed the data length");
+ return Vec::new();
+ }
+
+ //build final ranges
+ let mut ranges = RangeSet::<[RangeInclusive<usize>; 1]>::from(0..=data_end);
+ for exclusion in e {
+ let end = exclusion.start() + exclusion.length() - 1;
+ ranges.remove_range(exclusion.start()..=end);
+ }
+
+ // hash the data for ranges
+ for r in ranges.into_smallvec() {
+ hasher_enum.update(&data[r]);
+ }
+
+ // return the hash
+ Hasher::finalize(hasher_enum)
+ }
+ None => {
+ // add the data
+ hasher_enum.update(data);
+
+ // return the hash
+ Hasher::finalize(hasher_enum)
+ }
+ }
+}
+
+// verify the hash using the specifiied alogrithm
+pub fn verify_by_alg(
+ alg: &str,
+ hash: &[u8],
+ data: &[u8],
+ exclusions: Option<Vec<Exclusion>>,
+) -> bool {
+ // hash with the same algorithm as target
+ let data_hash = hash_by_alg(alg, data, exclusions);
+ vec_compare(hash, &data_hash)
+}
+
+/// Return a multihash (Sha256) of array of bytes
+#[allow(dead_code)]
+pub fn hash256(data: &[u8]) -> String {
+ let mh = Sha2_256::digest(data);
+ let digest = mh.digest();
+ let wrapped: Multihash = wrap(Code::Sha2_256, digest);
+
+ // Return Base-64 encoded hash.
+ encode(multibase::Base::Base64, wrapped.as_bytes())
+}
+
+/// Verify muiltihash against input data. True if match,
+/// false if no match or unsupported. The hash value should be
+/// be multibase encoded string.
+#[allow(dead_code)]
+pub fn verify_hash(hash: &str, data: &[u8]) -> bool {
+ match decode(hash) {
+ Ok((_code, mh)) => {
+ if mh.len() < 2 {
+ return false;
+ }
+
+ // multihash lead bytes
+ let hash_type = mh[0]; // hash type
+ let _hash_len = mh[1]; // hash data length
+
+ // hash with the same algorithm as target
+ if let Some(data_hash) = hash_by_type(hash_type, data) {
+ vec_compare(data_hash.digest(), &mh.as_slice()[2..])
+ } else {
+ false
+ }
+ }
+ Err(_) => false,
+ }
+}
+
+/// Return the hash of data in the same hash format in_hash
+#[allow(dead_code)]
+pub fn hash_as_source(in_hash: &str, data: &[u8]) -> Option<String> {
+ match decode(in_hash) {
+ Ok((code, mh)) => {
+ if mh.len() < 2 {
+ return None;
+ }
+
+ // multihash lead bytes
+ let hash_type = mh[0]; // hash type
+
+ // hash with the same algorithm as target
+ match hash_by_type(hash_type, data) {
+ Some(hash) => {
+ let digest = hash.digest();
+
+ let wrapped = match hash_type {
+ 0x12 => wrap(Code::Sha2_256, digest),
+ 0x13 => wrap(Code::Sha2_512, digest),
+ 0x14 => wrap(Code::Sha3_512, digest),
+ 0x15 => wrap(Code::Sha3_384, digest),
+ 0x16 => wrap(Code::Sha3_256, digest),
+ _ => return None,
+ };
+
+ // Return encoded hash.
+ Some(encode(code, wrapped.as_bytes()))
+ }
+ None => None,
+ }
+ }
+ Err(_) => None,
+ }
+}
diff --git a/sdk/src/utils/mod.rs b/sdk/src/utils/mod.rs
@@ -0,0 +1,26 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+pub(crate) mod cbor_types;
+pub(crate) mod hash_utils;
+#[allow(dead_code)] // for wasm build
+pub(crate) mod patch;
+#[cfg(feature = "file_io")]
+pub(crate) mod thumbnail;
+pub(crate) mod time_it;
+#[allow(dead_code)] // for wasm builds
+pub(crate) mod xmp_inmemory_utils;
+// shared unit testing utilities
+#[cfg(test)]
+#[allow(dead_code)] // for wasm build
+pub mod test;
diff --git a/sdk/src/utils/patch.rs b/sdk/src/utils/patch.rs
@@ -0,0 +1,99 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::error::{Error, Result};
+use twoway::find_bytes;
+
+#[cfg(all(test, feature = "file_io"))]
+use crate::error::wrap_io_err;
+/**
+Patch a sequence bytes with a new set of bytes - the search_bytes are erased and replaced with replace_bytes
+This function only patches the first occurance
+returns the location where splice occurred
+*/
+pub fn patch_bytes(data: &mut Vec<u8>, search_bytes: &[u8], replace_bytes: &[u8]) -> Result<usize> {
+ // patch data bytes in memory
+
+ if let Some(splice_start) = find_bytes(data, search_bytes) {
+ data.splice(
+ splice_start..splice_start + search_bytes.len(),
+ replace_bytes.iter().cloned(),
+ );
+ Ok(splice_start)
+ } else {
+ Err(Error::NotFound)
+ }
+}
+
+/**
+Patch new content into a file
+path - path to file to be patched
+search_bytes - bytes to be replaced
+replace_bytes - replacement bytes
+returns the location where splice occurred
+*/
+#[cfg(all(test, feature = "file_io"))]
+pub fn patch_file(
+ path: &std::path::Path,
+ search_bytes: &[u8],
+ replace_bytes: &[u8],
+) -> Result<usize> {
+ let mut buf = std::fs::read(path).map_err(wrap_io_err)?;
+
+ let splice_point = patch_bytes(&mut buf, search_bytes, replace_bytes)?;
+
+ std::fs::write(path, &buf).map_err(wrap_io_err)?;
+
+ Ok(splice_point)
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ #[test]
+ fn test_patch() {
+ let source = "Hello everyone this is a test".as_bytes();
+ let mut source_vec = source.to_vec();
+ let search_bytes = "everyone".as_bytes();
+ let replace_bytes = "world".as_bytes();
+ let replace_bytes2 = "universe".as_bytes();
+ let test_bytes = "test".as_bytes();
+ let unit_test_bytes = "unit test".as_bytes();
+
+ println!("Original string: {}", String::from_utf8_lossy(&source_vec));
+
+ patch_bytes(&mut source_vec, search_bytes, replace_bytes).unwrap();
+
+ println!(
+ "Replaced string: {}\n",
+ String::from_utf8_lossy(&source_vec)
+ );
+
+ patch_bytes(&mut source_vec, replace_bytes, replace_bytes2).unwrap();
+
+ println!(
+ "Re-Replaced string: {}\n",
+ String::from_utf8_lossy(&source_vec)
+ );
+
+ patch_bytes(&mut source_vec, test_bytes, unit_test_bytes).unwrap();
+
+ println!(
+ "Pad end of data string: {}\n",
+ String::from_utf8_lossy(&source_vec)
+ );
+ }
+}
diff --git a/sdk/src/utils/test.rs b/sdk/src/utils/test.rs
@@ -0,0 +1,174 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#![allow(clippy::unwrap_used)]
+
+use crate::{
+ assertions::{labels, Action, Actions, Ingredient, ReviewRating, SchemaDotOrg, Thumbnail},
+ claim::Claim,
+ salt::DefaultSalt,
+ store::Store,
+ Result,
+};
+use std::path::PathBuf;
+use tempfile::TempDir;
+
+pub const TEST_SMALL_JPEG: &str = "earth_apollo17.jpg";
+
+pub const TEST_VC: &str = r#"{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "http://schema.org"
+ ],
+ "type": [
+ "VerifiableCredential",
+ "NPPACredential"
+ ],
+ "issuer": "https://nppa.org/",
+ "credentialSubject": {
+ "id": "did:nppa:eb1bb9934d9896a374c384521410c7f14",
+ "name": "Bob Ross",
+ "memberOf": "https://nppa.org/"
+ },
+ "proof": {
+ "type": "RsaSignature2018",
+ "created": "2021-06-18T21:19:10Z",
+ "proofPurpose": "assertionMethod",
+ "verificationMethod":
+ "did:nppa:eb1bb9934d9896a374c384521410c7f14#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A",
+ "jws": "eyJhbGciOiJQUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19DJBMvvFAIC00nSGB6Tn0XKbbF9XrsaJZREWvR2aONYTQQxnyXirtXnlewJMBBn2h9hfcGZrvnC1b6PgWmukzFJ1IiH1dWgnDIS81BH-IxXnPkbuYDeySorc4QU9MJxdVkY5EL4HYbcIfwKj6X4LBQ2_ZHZIu1jdqLcRZqHcsDF5KKylKc1THn5VRWy5WhYg_gBnyWny8E6Qkrze53MR7OuAmmNJ1m1nN8SxDrG6a08L78J0-Fbas5OjAQz3c17GY8mVuDPOBIOVjMEghBlgl3nOi1ysxbRGhHLEK4s0KKbeRogZdgt1DkQxDFxxn41QWDw_mmMCjs9qxg0zcZzqEJw"
+ }
+}"#;
+
+/// creates a claim for testing
+pub fn create_test_claim() -> Result<Claim> {
+ let mut claim = Claim::new("adobe unit test", Some("adobe"));
+
+ // add VC entry
+ let _hu = claim.add_verifiable_credential(TEST_VC)?;
+
+ // Add assertions.
+ let mut actions = Actions::new();
+ actions
+ .add_action(
+ Action::new("c2pa.cropped")
+ .set_parameter(
+ "name".to_owned(),
+ r#"{
+ "left": 0,
+ "right": 2000,
+ "top": 1000,
+ "bottom": 4000
+ }"#,
+ )
+ .unwrap(),
+ )
+ .add_action(
+ Action::new("c2pa.filtered")
+ .set_parameter("name".to_owned(), "gaussian blur")?
+ .set_when("2015-06-26T16:43:23+0200"),
+ );
+ // add a binary thumbnail assertion ('deadbeefadbeadbe')
+ let some_binary_data: Vec<u8> = vec![
+ 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
+ 0x0e,
+ ];
+
+ // create a schema.org claim
+ let cr = r#"{
+ "@context": "https://schema.org",
+ "@type": "ClaimReview",
+ "claimReviewed": "The world is flat",
+ "reviewRating": {
+ "@type": "Rating",
+ "ratingValue": "1",
+ "bestRating": "5",
+ "worstRating": "1",
+ "alternateName": "False"
+ }
+ }"#;
+ let claim_review = SchemaDotOrg::from_json_str(cr)?;
+
+ let thumbnail_claim = Thumbnail::new(labels::JPEG_CLAIM_THUMBNAIL, some_binary_data.clone());
+
+ let thumbnail_ingred = Thumbnail::new(labels::JPEG_INGREDIENT_THUMBNAIL, some_binary_data);
+
+ claim.add_assertion(&actions)?;
+ claim.add_assertion(&claim_review)?;
+ claim.add_assertion(&thumbnail_claim)?;
+
+ let thumb_uri = claim.add_assertion_with_salt(&thumbnail_ingred, &DefaultSalt::default())?;
+
+ let review = ReviewRating::new(
+ "a 3rd party plugin was used",
+ Some("actions.unknownActionsPerformed".to_string()),
+ 1,
+ );
+
+ //let data_path = claim.add_ingredient_data("some data".as_bytes());
+ let ingredient = Ingredient::new(
+ "image 1.jpg",
+ "image/jpeg",
+ "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d",
+ Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"),
+ )
+ .set_thumbnail(Some(&thumb_uri))
+ //.set_manifest_data(&data_path)
+ .add_review(review);
+
+ claim.add_assertion_with_salt(&ingredient, &DefaultSalt::default())?;
+
+ Ok(claim)
+}
+
+/// Creates a store with an unsigned claim for testing
+pub fn create_test_store() -> Result<Store> {
+ // Create claims store.
+ let mut store = Store::new();
+
+ let claim = create_test_claim()?;
+ store.commit_claim(claim).unwrap();
+ Ok(store)
+}
+
+/// returns a path to a file in the fixtures folder
+pub fn fixture_path(file_name: &str) -> PathBuf {
+ let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ path.push("tests/fixtures");
+ path.push(file_name);
+ path
+}
+
+/// returns a path to a file in the temp_dir folder
+// note, you must pass TempDir from the caller's context
+pub fn temp_dir_path(temp_dir: &TempDir, file_name: &str) -> PathBuf {
+ let mut path = PathBuf::from(temp_dir.path());
+ path.push(file_name);
+ path
+}
+
+// copies a fixture to a temp file and returns path to copy
+pub fn temp_fixture_path(temp_dir: &TempDir, file_name: &str) -> PathBuf {
+ let fixture_src = fixture_path(file_name);
+ let fixture_copy = temp_dir_path(temp_dir, file_name);
+ std::fs::copy(&fixture_src, &fixture_copy).unwrap();
+ fixture_copy
+}
+
+#[test]
+fn test_create_test_store() {
+ #[allow(clippy::expect_used)]
+ let store = create_test_store().expect("create test store");
+
+ assert_eq!(store.claims().len(), 1);
+}
diff --git a/sdk/src/utils/thumbnail.rs b/sdk/src/utils/thumbnail.rs
@@ -0,0 +1,48 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::Result;
+use image::{GenericImageView, ImageFormat};
+
+/// utility to generate a thumbnail from a file at path
+/// returns Result (format, image_bits) if successful, otherwise Error
+pub fn make_thumbnail(path: &std::path::Path) -> Result<(String, Vec<u8>)> {
+ let format = ImageFormat::from_path(path)?;
+
+ // max edge size allowed in pixels for thumbnail creation
+ const THUMBNAIL_LONGEST_EDGE: u32 = 1024;
+ const THUMBNAIL_JPEG_QUALITY: u8 = 80; // JPEG quality 1-100
+
+ let mut img = image::open(path)?;
+ let longest_edge = THUMBNAIL_LONGEST_EDGE;
+
+ // generate a thumbnail image scaled down and in jpeg format
+ if img.width() > longest_edge || img.height() > longest_edge {
+ img = img.thumbnail(longest_edge, longest_edge);
+ }
+
+ // for png files, use png thumbnails for transparency
+ // for other supported types try a jpeg thumbnail
+ let (output_format, content_type) = match format {
+ ImageFormat::Png => (image::ImageOutputFormat::Png, "image/png"),
+ _ => (
+ image::ImageOutputFormat::Jpeg(THUMBNAIL_JPEG_QUALITY),
+ "image/jpeg",
+ ),
+ };
+ let mut thumbnail_bits = Vec::new();
+ img.write_to(&mut thumbnail_bits, output_format)?;
+
+ let format = content_type.to_owned();
+ Ok((format, thumbnail_bits))
+}
diff --git a/sdk/src/utils/time_it.rs b/sdk/src/utils/time_it.rs
@@ -0,0 +1,38 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use log::info;
+use std::time::Instant;
+
+// (Internal debugging tool.)
+// Measure and log the time from the creation of this struct until it is dropped.
+pub(crate) struct TimeIt {
+ label: &'static str,
+ start: Instant,
+}
+
+// Justification for dead_code: This is a debugging tool that is not always needed.
+#[allow(dead_code)]
+impl TimeIt {
+ pub fn new(label: &'static str) -> Self {
+ Self {
+ label,
+ start: Instant::now(),
+ }
+ }
+}
+impl Drop for TimeIt {
+ fn drop(&mut self) {
+ info!("timing for {}: {:.2?}", self.label, self.start.elapsed());
+ }
+}
diff --git a/sdk/src/utils/xmp_inmemory_utils.rs b/sdk/src/utils/xmp_inmemory_utils.rs
@@ -0,0 +1,232 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{
+ asset_io::CAIRead, jumbf_io::get_cailoader_handler, utils::hash_utils::vec_compare, Error,
+ Result,
+};
+use log::error;
+use quick_xml::{
+ events::{BytesEnd, BytesStart, Event},
+ Reader, Writer,
+};
+use std::io::Cursor;
+
+const RDF_DESCRIPTION: &[u8] = b"rdf:Description";
+
+#[derive(Default)]
+pub struct XmpInfo {
+ pub document_id: Option<String>,
+ pub instance_id: Option<String>,
+ pub provenance: Option<String>,
+}
+
+impl XmpInfo {
+ /// search xmp data for provenance, documentID and instanceID
+ pub fn from_source(source: &mut dyn CAIRead, format: &str) -> Self {
+ let xmp = get_cailoader_handler(format).and_then(|cai_loader| {
+ // read xmp if available
+ cai_loader.read_xmp(source)
+ });
+
+ // todo: do this in one pass through XMP
+ let provenance = xmp.as_deref().and_then(extract_provenance);
+ let document_id = xmp.as_deref().and_then(extract_document_id);
+ let instance_id = xmp.as_deref().and_then(extract_instance_id);
+ Self {
+ document_id,
+ instance_id,
+ provenance,
+ }
+ }
+}
+
+/// Extract an a value from XMP using a key
+fn extract_xmp_key(xmp: &str, key: &str) -> Option<String> {
+ let mut reader = Reader::from_str(xmp);
+ reader.trim_text(true);
+ let mut buf = Vec::new();
+
+ loop {
+ match reader.read_event(&mut buf) {
+ Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
+ if e.name() == RDF_DESCRIPTION {
+ // attribute case
+ let value = e.attributes().find(|a| {
+ if let Ok(attribute) = a {
+ vec_compare(attribute.key, key.as_bytes())
+ } else {
+ false
+ }
+ });
+ if let Some(Ok(attribute)) = value {
+ if let Ok(s) = String::from_utf8(attribute.value.to_vec()) {
+ return Some(s);
+ }
+ }
+ } else if e.name() == key.as_bytes() {
+ // tag case
+ let mut buf: Vec<u8> = Vec::new();
+ if let Ok(s) = reader.read_text(e.name(), &mut buf) {
+ return Some(s);
+ }
+ }
+ }
+ Ok(Event::Eof) => break,
+ _ => {}
+ }
+ buf.clear();
+ }
+ None
+}
+
+/// Add a value to XMP using a key, replaces the value if the key exists
+fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result<String> {
+ let mut reader = Reader::from_str(xmp);
+ reader.trim_text(true);
+ let mut writer = Writer::new(Cursor::new(Vec::new()));
+ let mut buf = Vec::new();
+ let mut added = false;
+ loop {
+ match reader.read_event(&mut buf) {
+ Ok(Event::Start(ref e)) if e.name() == RDF_DESCRIPTION => {
+ // creates a new element
+ let mut elem = BytesStart::owned(RDF_DESCRIPTION.to_vec(), RDF_DESCRIPTION.len());
+ for attr in e.attributes() {
+ if let Ok(attr) = attr {
+ if attr.key == key.as_bytes() {
+ // replace the key/value if it exists
+ elem.push_attribute((key, value));
+ added = true;
+ } else {
+ // add all other existing elements
+ elem.extend_attributes([attr]);
+ }
+ } else {
+ error!("Error at position {}", reader.buffer_position());
+ return Err(Error::XmpReadError);
+ }
+ }
+ if !added {
+ // didn't exist, so add it
+ elem.push_attribute((key, value));
+ }
+ // writes the event to the writer
+ assert!(writer.write_event(Event::Start(elem)).is_ok());
+ }
+ Ok(Event::End(ref e)) if e.name() == b"this_tag" => {
+ assert!(writer
+ .write_event(Event::End(BytesEnd::borrowed(b"my_elem")))
+ .is_ok());
+ }
+ Ok(Event::Eof) => break,
+ Ok(e) => assert!(writer.write_event(e).is_ok()),
+ Err(e) => {
+ error!("Error at position {}: {:?}", reader.buffer_position(), e);
+ return Err(Error::XmpWriteError);
+ }
+ }
+ }
+ buf.clear();
+ let result = writer.into_inner().into_inner();
+ String::from_utf8(result).map_err(|_e| Error::XmpWriteError)
+}
+
+/// extract the dc:provenance value from xmp
+pub fn extract_provenance(xmp: &str) -> Option<String> {
+ extract_xmp_key(xmp, "dcterms:provenance")
+}
+
+/// extract the xmpMM:InstanceID value from xmp
+fn extract_instance_id(xmp: &str) -> Option<String> {
+ extract_xmp_key(xmp, "xmpMM:InstanceID")
+}
+
+/// extract the "xmpMM:DocumentID" value from xmp
+fn extract_document_id(xmp: &str) -> Option<String> {
+ extract_xmp_key(xmp, "xmpMM:DocumentID")
+}
+
+/// add or replace a dc:provenance value to xmp, including dc:terms if needed
+#[allow(dead_code)] // keep for future
+fn add_provenance(xmp: &str, provenance: &str) -> Result<String> {
+ let xmp = add_xmp_key(xmp, "xmlns:dcterms", "http://purl.org/dc/terms/")?;
+ add_xmp_key(&xmp, "dcterms:provenance", provenance)
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ //use env_logger;
+ use super::*;
+
+ const XMP_DATA: &str = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
+ <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="contentauth">
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <rdf:Description rdf:about=""
+ xmlns:xmp="http://ns.adobe.com/xap/1.0/"
+ xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:dcterms="http://purl.org/dc/terms/"
+ xmpMM:DocumentID="xmp.did:cb9f5498-bb58-4572-8043-8c369e6bfb9b"
+ xmpMM:InstanceID="xmp.iid:cb9f5498-bb58-4572-8043-8c369e6bfb9b"
+ dcterms:provenance="self#jumbf=c2pa/contentauth:urn:uuid:a58065fb-79ae-4eb3-87b9-a19830860059/c2pa.claim"
+ dc:format="image/jpeg">
+ </rdf:Description>
+ </rdf:RDF>
+ </x:xmpmeta>"#;
+
+ const MIN_XMP: &str = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
+ <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <rdf:Description rdf:about="" > </rdf:Description>
+ </rdf:RDF> </x:xmpmeta> "#;
+
+ const PROVENANCE: &str =
+ "self#jumbf=c2pa/contentauth:urn:uuid:a58065fb-79ae-4eb3-87b9-a19830860059/c2pa.claim";
+
+ #[test]
+ fn read_xmp() {
+ let provenance = extract_provenance(XMP_DATA);
+ assert_eq!(provenance, Some("self#jumbf=c2pa/contentauth:urn:uuid:a58065fb-79ae-4eb3-87b9-a19830860059/c2pa.claim".to_owned()));
+ let document_id = extract_document_id(XMP_DATA);
+ assert_eq!(
+ document_id,
+ Some("xmp.did:cb9f5498-bb58-4572-8043-8c369e6bfb9b".to_owned())
+ );
+ let instance_id = extract_instance_id(XMP_DATA);
+ assert_eq!(
+ instance_id,
+ Some("xmp.iid:cb9f5498-bb58-4572-8043-8c369e6bfb9b".to_owned())
+ );
+ let unicorn = extract_xmp_key(XMP_DATA, "unicorn");
+ assert_eq!(unicorn, None);
+ let bad_xmp = extract_xmp_key("bad xmp", "unicorn");
+ assert_eq!(bad_xmp, None);
+ }
+
+ #[test]
+ fn add_xmp() {
+ let xmp = add_provenance(XMP_DATA, PROVENANCE).expect("adding provenance");
+ let unicorn = extract_provenance(&xmp);
+ println!("{}", xmp);
+ assert_eq!(unicorn, Some(PROVENANCE.to_string()));
+
+ let xmp = add_provenance(MIN_XMP, PROVENANCE).expect("adding provenance");
+ let unicorn = extract_provenance(&xmp);
+ println!("{}", xmp);
+ assert_eq!(unicorn, Some(PROVENANCE.to_string()));
+ }
+}
diff --git a/sdk/src/validation_status.rs b/sdk/src/validation_status.rs
@@ -0,0 +1,427 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! Implements validation status for specific parts of a manifest.
+//!
+//! See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_existing_manifests>.
+
+#![deny(missing_docs)]
+
+use log::debug;
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ assertion::AssertionBase,
+ assertions::Ingredient,
+ error::Error,
+ jumbf,
+ status_tracker::{LogItem, StatusTracker},
+ store::Store,
+};
+
+/// A `ValidationStatus` struct describes the validation status of a
+/// specific part of a manifest.
+///
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_existing_manifests>.
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct ValidationStatus {
+ code: String,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ url: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ explanation: Option<String>,
+}
+
+impl ValidationStatus {
+ pub(crate) fn new(code: String) -> Self {
+ Self {
+ code,
+ url: None,
+ explanation: None,
+ }
+ }
+
+ /// Returns the validation status code.
+ ///
+ /// Validation status codes are the labels from the "Value"
+ /// column in <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_existing_manifests>.
+ ///
+ /// These are also defined as constants in the
+ /// [`validation_status`](crate::validation_status) mod.
+ pub fn code(&self) -> &str {
+ &self.code
+ }
+
+ /// Returns the internal JUMBF reference to the entity that was validated.
+ pub fn url(&self) -> Option<&str> {
+ self.url.as_deref()
+ }
+
+ /// Returns a human-readable description of the validation that was performed.
+ pub fn explanation(&self) -> Option<&str> {
+ self.explanation.as_deref()
+ }
+
+ /// Sets the internal JUMBF reference to the entity was validated.
+ pub(crate) fn set_url(mut self, url: String) -> Self {
+ self.url = Some(url);
+ self
+ }
+
+ /// Sets the human-readable description of the validation that was performed.
+ pub(crate) fn set_explanation(mut self, explanation: String) -> Self {
+ self.explanation = Some(explanation);
+ self
+ }
+
+ /// Returns `true` if this has a successful validation code.
+ pub fn passed(&self) -> bool {
+ is_success(&self.code)
+ }
+
+ // Maps errors into validation_status codes.
+ fn code_from_error(error: &Error) -> &str {
+ match error {
+ Error::ClaimMissing { .. } => CLAIM_MISSING,
+ Error::AssertionMissing { .. } => ASSERTION_MISSING,
+ Error::AssertionDecoding(_code) => STATUS_ASSERTION_MALFORMED, // todo: no code for invalid assertion format
+ Error::HashMismatch(_) => ASSERTION_DATAHASH_MATCH,
+ Error::PrereleaseError => STATUS_PRERELEASE,
+ _ => STATUS_OTHER,
+ }
+ }
+
+ /// Creates a ValidationStatus from an error code.
+ pub(crate) fn from_error(error: &Error) -> Self {
+ // We need to create error codes here for client processing.
+ let code = Self::code_from_error(error);
+ debug!("ValidationStatus {} from error {:#?}", code, error);
+ Self::new(code.to_string()).set_explanation(error.to_string())
+ }
+
+ /// Creates a ValidationStatus from a validation_log item.
+ pub(crate) fn from_validation_item(item: &LogItem) -> Option<Self> {
+ match item.validation_status.as_ref() {
+ Some(status) => Some(
+ Self::new(status.to_string())
+ .set_url(item.label.to_string())
+ .set_explanation(item.description.to_string()),
+ ),
+ // If we don't have a validation_status, then make one from the err_val
+ // using the description plus error text explanation.
+ None => item.err_val.as_ref().map(|e| {
+ let code = Self::code_from_error(e);
+ Self::new(code.to_string())
+ .set_url(item.label.to_string())
+ .set_explanation(format!("{}: {}", item.description, e))
+ }),
+ }
+ }
+}
+
+impl PartialEq for ValidationStatus {
+ fn eq(&self, other: &Self) -> bool {
+ self.code == other.code && self.url == other.url
+ }
+}
+
+// TODO: Does this still need to be public? (I do see one reference in the JS SDK.)
+
+/// Given a `Store` and a `StatusTracker`, return `ValidationStatus` items for each
+/// item in the tracker which reflect errors in the active manifest or which would not
+/// be reported as a validation error for any ingredient.
+pub fn status_for_store(
+ store: &Store,
+ validation_log: &mut impl StatusTracker,
+) -> Vec<ValidationStatus> {
+ let statuses: Vec<ValidationStatus> = validation_log
+ .get_log()
+ .iter()
+ .filter_map(ValidationStatus::from_validation_item)
+ .filter(|s| !is_success(&s.code))
+ .collect();
+
+ // Filter out any status that is already captured in an ingredient assertion.
+ if let Some(claim) = store.provenance_claim() {
+ let active_manifest = Some(claim.label().to_string());
+
+ // This closure returns true if the URI references the store's active manifest.
+ let is_active_manifest = |uri: Option<&str>| {
+ uri.filter(|uri| jumbf::labels::manifest_label_from_uri(uri) == active_manifest)
+ .is_some()
+ };
+
+ // We only need to do the more detailed filtering if there are any status
+ // reports that reference ingredients.
+ if statuses
+ .iter()
+ .any(|s| !is_active_manifest(s.url.as_deref()))
+ {
+ // Collect all the ValidationStatus records from all the ingredients in the store.
+ let ingredient_statuses: Vec<ValidationStatus> = claim
+ .ingredient_assertions()
+ .iter()
+ .filter_map(|a| Ingredient::from_assertion(a).ok())
+ .filter_map(|i| i.validation_status)
+ .flat_map(|x| x.into_iter())
+ .collect();
+
+ // Filter to only contain the active statuses and nested statuses not found in active.
+ return statuses
+ .iter()
+ .filter(|s| {
+ is_active_manifest(s.url.as_deref())
+ || !ingredient_statuses.iter().any(|i| s == &i)
+ })
+ .map(|s| s.to_owned())
+ .collect();
+ }
+ }
+
+ statuses
+}
+
+// -- success codes --
+
+/// The claim signature referenced in the ingredient's claim validated.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const CLAIM_SIGNATURE_VALIDATED: &str = "claimSignature.validated";
+
+/// The signing credential is listed on the validator's trust list.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const SIGNING_CREDENTIAL_TRUSTED: &str = "signingCredential.trusted";
+
+/// The time-stamp credential is listed on the validator's trust list.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const TIMESTAMP_TRUSTED: &str = "timeStamp.trusted";
+
+/// The hash of the the referenced assertion in the ingredient's manifest
+/// matches the corresponding hash in the assertion's hashed URI in the claim.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_HASHEDURI_MATCH: &str = "assertion.hashedURI.match";
+
+/// Hash of a byte range of the asset matches the hash declared in the
+/// data hash assertion.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_DATAHASH_MATCH: &str = "assertion.dataHash.match";
+
+/// Hash of a box-based asset matches the hash declared in the BMFF
+/// hash assertion.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_BMFFHASH_MATCH: &str = "assertion.bmffHash.match";
+
+/// A non-embedded (remote) assertion was accessible at the time of
+/// validation.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_ACCESSIBLE: &str = "assertion.accessible";
+
+// -- failure codes --
+
+/// The referenced claim in the ingredient's manifest cannot be found.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box.
+pub const CLAIM_MISSING: &str = "claim.missing";
+
+/// More than one claim box is present in the manifest.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box.
+pub const CLAIM_MULTIPLE: &str = "claim.multiple";
+
+/// No hard bindings are present in the claim.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box.
+pub const HARD_BINDINGS_MISSING: &str = "claim.hardBindings.missing";
+
+/// The hash of the the referenced ingredient claim in the manifest
+/// does not match the corresponding hash in the ingredient's hashed
+/// URI in the claim.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const INGREDIENT_HASHEDURI_MISMATCH: &str = "ingredient.hashedURI.mismatch";
+
+/// The claim signature referenced in the ingredient's claim
+/// cannot be found in its manifest.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const CLAIM_SIGNATURE_MISSING: &str = "claimSignature.missing";
+
+/// The claim signature referenced in the ingredient's claim
+/// failed to validate.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const CLAIM_SIGNATURE_MISMATCH: &str = "claimSignature.mismatch";
+
+/// The manifest has more than one ingredient whose `relationship`
+/// is `parentOf`.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box.
+pub const MANIFEST_MULTIPLE_PARENTS: &str = "manifest.multipleParents";
+
+/// The manifest is an update manifest, but it contains hard binding
+/// or actions assertions.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box.
+pub const MANIFEST_UPDATE_INVALID: &str = "manifest.update.invalid";
+
+/// The manifest is an update manifest, but it contains either zero
+/// or multiple `parentOf` ingredients.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box.
+pub const MANIFEST_UPDATE_WRONG_PARENTS: &str = "manifest.update.wrongParents";
+
+/// The signing credential is not listed on the validator's trust list.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const SIGNING_CREDENTIAL_UNTRUSTED: &str = "signingCredential.untrusted";
+
+/// The signing credential is not valid for signing.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const SIGNING_CREDENTIAL_INVALID: &str = "signingCredential.invalid";
+
+/// The signing credential has been revoked by the issuer.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const SIGNING_CREDENTIAL_REVOKED: &str = "signingCredential.revoked";
+
+/// The signing credential has expired.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const SIGNING_CREDENTIAL_EXPIRED: &str = "signingCredential.expired";
+
+/// The time-stamp does not correspond to the contents of the claim.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const TIMESTAMP_MISMATCH: &str = "timeStamp.mismatch";
+
+/// The time-stamp credential is not listed on the validator's trust list.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const TIMESTAMP_UNTRUSTED: &str = "timeStamp.untrusted";
+
+/// The signed time-stamp attribute in the signature falls outside the
+/// validity window of the signing certificate or the TSA's certificate.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim signature box.
+pub const TIMESTAMP_OUTSIDE_VALIDITY: &str = "timeStamp.outsideValidity";
+
+/// The hash of the the referenced assertion in the manifest does not
+/// match the corresponding hash in the assertion's hashed URI in the claim.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_HASHEDURI_MISMATCH: &str = "assertion.hashedURI.mismatch";
+
+/// An assertion listed in the ingredient's claim is missing from the
+/// ingredient's manifest.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box.
+pub const ASSERTION_MISSING: &str = "assertion.missing";
+
+/// An assertion was found in the ingredient's manifest that was not
+/// explicitly declared in the ingredient's claim.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box or assertion.
+pub const ASSERTION_UNDECLARED: &str = "assertion.undeclared";
+
+/// A non-embedded (remote) assertion was inaccessible at the time of validation.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_INACCESSIBLE: &str = "assertion.inaccessible";
+
+/// An assertion was declared as redacted in the ingredient's claim
+/// but is still present in the ingredient's manifest.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_NOT_REDACTED: &str = "assertion.notRedacted";
+
+/// An assertion was declared as redacted by its own claim.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box.
+pub const ASSERTION_SELF_REDACTED: &str = "assertion.selfRedacted";
+
+/// An `action` assertion was redacted when the ingredient's
+/// claim was created.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ACTION_ASSERTION_REDACTED: &str = "assertion.action.redacted";
+
+/// The hash of a byte range of the asset does not match the
+/// hash declared in the data hash assertion.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_DATAHASH_MISMATCH: &str = "assertion.dataHash.mismatch";
+
+/// The hash of a box-based asset does not match the hash declared
+/// in the BMFF hash assertion.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_BMFFHASH_MISMATCH: &str = "assertion.bmffHash.mismatch";
+
+/// A hard binding assertion is in a cloud data assertion.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_CLOUDDATA_HARD_BINDING: &str = "assertion.clouddata.hardBinding";
+
+/// An update manifest contains a cloud data assertion referencing
+/// an actions assertion.
+///
+/// `ValidationStatus.url()` will point to a C2PA assertion.
+pub const ASSERTION_CLOUDDATA_ACTIONS: &str = "assertion.clouddata.actions";
+
+/// The value of an `alg` header, or other header that specifies an
+/// algorithm used to compute the value of another field, is unknown
+/// or unsupported.
+///
+/// `ValidationStatus.url()` will point to a C2PA claim box or C2PA assertion.
+pub const ALGORITHM_UNSUPPORTED: &str = "algorithm.unsupported";
+
+// -- unofficial status codes --
+
+pub(crate) const STATUS_OTHER: &str = "com.adobe.other";
+pub(crate) const STATUS_PRERELEASE: &str = "com.adobe.prerelease";
+pub(crate) const STATUS_ASSERTION_MALFORMED: &str = "com.adobe.assertion.malformed";
+
+/// Returns `true` if the status code is a known C2PA success status code.
+///
+/// Returns `false` if the status code is a known C2PA failure status
+/// code or is unknown.
+///
+/// # Examples
+///
+/// ```
+/// use c2pa::validation_status::*;
+///
+/// assert!(is_success(CLAIM_SIGNATURE_VALIDATED));
+/// assert!(!is_success(SIGNING_CREDENTIAL_REVOKED));
+/// ```
+pub fn is_success(status_code: &str) -> bool {
+ matches!(
+ status_code,
+ CLAIM_SIGNATURE_VALIDATED
+ | SIGNING_CREDENTIAL_TRUSTED
+ | TIMESTAMP_TRUSTED
+ | ASSERTION_HASHEDURI_MATCH
+ | ASSERTION_DATAHASH_MATCH
+ | ASSERTION_BMFFHASH_MATCH
+ | ASSERTION_ACCESSIBLE
+ )
+}
diff --git a/sdk/src/validator.rs b/sdk/src/validator.rs
@@ -0,0 +1,87 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#[cfg(feature = "file_io")]
+use crate::openssl::{EcValidator, EdValidator, RsaValidator};
+use crate::Result;
+
+use chrono::{DateTime, Utc};
+
+#[derive(Debug)]
+pub struct ValidationInfo {
+ pub alg: String, // validation algorithm
+ pub date: Option<DateTime<Utc>>,
+ pub issuer_org: Option<String>,
+ pub validated: bool, // claim signature is valid
+}
+
+impl Default for ValidationInfo {
+ fn default() -> Self {
+ ValidationInfo {
+ alg: "".to_owned(),
+ date: None,
+ issuer_org: None,
+ validated: false,
+ }
+ }
+}
+
+/// Trait to support validating a signature against the provided data
+pub(crate) trait CoseValidator {
+ /// validate signature "sig" for given "data using provided public key"
+ fn validate(&self, sig: &[u8], data: &[u8], pkey: &[u8]) -> Result<bool>;
+}
+
+pub struct DummyValidator;
+impl CoseValidator for DummyValidator {
+ fn validate(&self, _sig: &[u8], _data: &[u8], _pkey: &[u8]) -> Result<bool> {
+ println!("This signature verified by DummyValidator. Results not valid!");
+ Ok(true)
+ }
+}
+
+// C2PA Supported Signature type
+// • ES256 (ECDSA using P-256 and SHA-256)
+// • ES384 (ECDSA using P-384 and SHA-384)
+// • ES512 (ECDSA using P-521 and SHA-512)
+// • PS256 (RSASSA-PSS using SHA-256 and MGF1 with SHA-256)
+// • PS384 (RSASSA-PSS using SHA-384 and MGF1 with SHA-384)
+// • PS512 (RSASSA-PSS using SHA-512 and MGF1 with SHA-512)
+// • RS256 RSASSA-PKCS1-v1_5 using SHA-256
+// • RS384 RSASSA-PKCS1-v1_5 using SHA-384
+// • RS512 RSASSA-PKCS1-v1_5 using SHA-512
+// • ED25519 Edwards Curve ED25519
+
+/// return validator for supported C2PA algorthms
+#[cfg(feature = "file_io")]
+pub(crate) fn get_validator(alg: &str) -> Option<Box<dyn CoseValidator>> {
+ match alg.to_lowercase().as_str() {
+ "es256" => Some(Box::new(EcValidator::new("es256"))),
+ "es384" => Some(Box::new(EcValidator::new("es384"))),
+ "es512" => Some(Box::new(EcValidator::new("es512"))),
+ "ps256" => Some(Box::new(RsaValidator::new("ps256"))),
+ "ps384" => Some(Box::new(RsaValidator::new("ps384"))),
+ "ps512" => Some(Box::new(RsaValidator::new("ps512"))),
+ "rs256" => Some(Box::new(RsaValidator::new("rs256"))),
+ "rs384" => Some(Box::new(RsaValidator::new("rs384"))),
+ "rs512" => Some(Box::new(RsaValidator::new("rs512"))),
+ "ed25519" => Some(Box::new(EdValidator::new("ed25519"))),
+ _ => None,
+ }
+}
+
+#[cfg(not(feature = "file_io"))]
+#[allow(dead_code)]
+pub(crate) fn get_validator(_alg: &str) -> Option<Box<dyn CoseValidator>> {
+ Some(Box::new(DummyValidator))
+}
diff --git a/sdk/src/wasm/context.rs b/sdk/src/wasm/context.rs
@@ -0,0 +1,63 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::{Error, Result};
+use wasm_bindgen::prelude::*;
+use wasm_bindgen::{JsCast, JsValue};
+use web_sys::{SubtleCrypto, Window, WorkerGlobalScope};
+
+// Adapted from gloo's implementation, since there doesn't seem to be a great way to do context checking using
+// wasm-bindgen/web-sys without using something like `js_sys::eval`. References:
+// - Issue: https://github.com/rustwasm/wasm-bindgen/issues/1046
+// - Issue: https://github.com/rustwasm/wasm-bindgen/issues/2148#issuecomment-638606446
+// - Code reference: https://git.io/J9crn
+
+pub enum WindowOrWorker {
+ Window(Window),
+ Worker(WorkerGlobalScope),
+}
+
+impl WindowOrWorker {
+ pub fn new() -> Result<Self> {
+ #[wasm_bindgen]
+ extern "C" {
+ type Global;
+
+ #[wasm_bindgen(method, getter, js_name = Window)]
+ fn window(this: &Global) -> JsValue;
+
+ #[wasm_bindgen(method, getter, js_name = WorkerGlobalScope)]
+ fn worker(this: &Global) -> JsValue;
+ }
+
+ let global: Global = js_sys::global().unchecked_into();
+
+ if !global.window().is_undefined() {
+ Ok(Self::Window(global.unchecked_into()))
+ } else if !global.worker().is_undefined() {
+ Ok(Self::Worker(global.unchecked_into()))
+ } else {
+ Err(Error::WasmInvalidContext)
+ }
+ }
+
+ pub fn subtle_crypto(&self) -> Result<SubtleCrypto> {
+ let crypto = match self {
+ Self::Window(window) => window.crypto(),
+ Self::Worker(worker) => worker.crypto(),
+ };
+ let subtle_crypto = crypto.map_err(|_err| Error::WasmNoCrypto)?.subtle();
+
+ Ok(subtle_crypto)
+ }
+}
diff --git a/sdk/src/wasm/mod.rs b/sdk/src/wasm/mod.rs
@@ -0,0 +1,19 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#[cfg(target_arch = "wasm32")]
+pub(crate) mod context;
+#[cfg(target_arch = "wasm32")]
+pub(crate) mod webcrypto_validator;
+#[cfg(target_arch = "wasm32")]
+pub use webcrypto_validator::validate_async;
diff --git a/sdk/src/wasm/webcrypto_validator.rs b/sdk/src/wasm/webcrypto_validator.rs
@@ -0,0 +1,484 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use crate::wasm::context::WindowOrWorker;
+use crate::{Error, Result};
+use js_sys::{Array, ArrayBuffer, Object, Reflect, Uint8Array};
+use wasm_bindgen::prelude::*;
+use wasm_bindgen_futures::JsFuture;
+use web_sys::{CryptoKey, SubtleCrypto};
+pub struct RsaHashedImportParams {
+ name: String,
+ hash: String,
+}
+
+impl RsaHashedImportParams {
+ pub fn new(name: &str, hash: &str) -> Self {
+ RsaHashedImportParams {
+ name: name.to_owned(),
+ hash: hash.to_owned(),
+ }
+ }
+
+ pub fn as_js_object(&self) -> Object {
+ let obj = Object::new();
+ Reflect::set(&obj, &"name".into(), &self.name.clone().into()).expect("not valid name");
+
+ let inner_obj = Object::new();
+ Reflect::set(&inner_obj, &"name".into(), &self.hash.clone().into())
+ .expect("not valid name");
+
+ Reflect::set(&obj, &"hash".into(), &inner_obj).expect("not valid name");
+
+ obj
+ }
+}
+
+pub struct EcKeyImportParams {
+ name: String,
+ named_curve: String,
+ hash: String,
+}
+
+impl EcKeyImportParams {
+ pub fn new(name: &str, hash: &str, named_curve: &str) -> Self {
+ EcKeyImportParams {
+ name: name.to_owned(),
+ named_curve: named_curve.to_owned(),
+ hash: hash.to_owned(),
+ }
+ }
+
+ pub fn as_js_object(&self) -> Object {
+ let obj = Object::new();
+ Reflect::set(&obj, &"name".into(), &self.name.clone().into()).expect("not valid name");
+ Reflect::set(&obj, &"namedCurve".into(), &self.named_curve.clone().into())
+ .expect("not valid name");
+
+ let inner_obj = Object::new();
+ Reflect::set(&inner_obj, &"name".into(), &self.hash.clone().into())
+ .expect("not valid name");
+
+ Reflect::set(&obj, &"hash".into(), &inner_obj).expect("not valid name");
+
+ obj
+ }
+}
+
+pub struct RsaPssParams {
+ name: String,
+ salt_length: u32,
+}
+
+impl RsaPssParams {
+ pub fn new(name: &str, salt_length: u32) -> Self {
+ RsaPssParams {
+ name: name.to_owned(),
+ salt_length,
+ }
+ }
+
+ pub fn as_js_object(&self) -> Object {
+ let obj = Object::new();
+ Reflect::set(&obj, &"name".into(), &self.name.clone().into()).expect("not valid name");
+ Reflect::set(&obj, &"saltLength".into(), &self.salt_length.into()).expect("not valid name");
+ obj
+ }
+}
+pub struct EcdsaParams {
+ name: String,
+ hash: String,
+}
+
+impl EcdsaParams {
+ pub fn new(name: &str, hash: &str) -> Self {
+ EcdsaParams {
+ name: name.to_owned(),
+ hash: hash.to_owned(),
+ }
+ }
+
+ pub fn as_js_object(&self) -> Object {
+ let obj = Object::new();
+ Reflect::set(&obj, &"name".into(), &self.name.clone().into()).expect("not valid name");
+
+ let inner_obj = Object::new();
+ Reflect::set(&inner_obj, &"name".into(), &self.hash.clone().into())
+ .expect("not valid name");
+
+ Reflect::set(&obj, &"hash".into(), &inner_obj).expect("not valid name");
+
+ obj
+ }
+}
+
+fn data_as_array_buffer(data: &[u8]) -> ArrayBuffer {
+ let typed_array = Uint8Array::new_with_length(data.len() as u32);
+ typed_array.copy_from(data);
+ typed_array.buffer()
+}
+
+// Alternate salt length computation function for signed data that doesn't adhere to the conventional
+// salt length in the RSA-PSS spec, which should equal the length of the hash function in bytes
+fn alternate_salt_length(crypto_key: &CryptoKey, salt_len: &u32) -> Result<u32> {
+ let algo: Object = crypto_key
+ .algorithm()
+ .map_err(|_err| Error::WasmKey)?
+ .into();
+ let key_size: f64 = js_sys::Reflect::get(&algo, &"modulusLength".into())
+ .map_err(|_err| Error::WasmKey)?
+ .as_f64()
+ .ok_or(Error::WasmKey)?
+ .into();
+ let key_byte_len: f32 = (key_size as f32 - 1.0) / 8.0;
+ Ok((key_byte_len.ceil() as u32) - salt_len - 2)
+}
+
+async fn crypto_is_verified(
+ subtle_crypto: &SubtleCrypto,
+ alg: &Object,
+ key: &CryptoKey,
+ sig: &Object,
+ data: &Object,
+) -> Result<bool> {
+ let promise = subtle_crypto
+ .verify_with_object_and_buffer_source_and_buffer_source(alg, key, sig, data)
+ .map_err(|_err| Error::WasmVerifier)?;
+ let verified: JsValue = JsFuture::from(promise)
+ .await
+ .map_err(|_err| Error::WasmVerifier)?
+ .into();
+ let result = verified.is_truthy();
+ web_sys::console::debug_2(&"verified".into(), &result.into());
+ Ok(result)
+}
+
+async fn async_validate(
+ algo: String,
+ hash: String,
+ salt_len: u32,
+ pkey: Vec<u8>,
+ sig: Vec<u8>,
+ data: Vec<u8>,
+) -> Result<bool> {
+ let context = WindowOrWorker::new();
+ let subtle_crypto = context?.subtle_crypto()?;
+ let sig_array_buf = data_as_array_buffer(&sig);
+ let data_array_buf = data_as_array_buffer(&data);
+
+ match algo.as_ref() {
+ "RSA-PSS" => {
+ // Create key
+ let mut algorithm = RsaHashedImportParams::new(&algo, &hash).as_js_object();
+ let key_array_buf = data_as_array_buffer(&pkey);
+ let usages = Array::new();
+ usages.push(&"verify".into());
+
+ let promise = subtle_crypto
+ .import_key_with_object("spki", &key_array_buf, &algorithm, true, &usages)
+ .map_err(|_err| Error::WasmKey)?;
+ let crypto_key: CryptoKey = JsFuture::from(promise)
+ .await
+ .map_err(|_err| Error::WasmKey)?
+ .into();
+ web_sys::console::debug_2(&"CryptoKey".into(), &crypto_key);
+
+ // Create verifier
+ // WebCrypto requires us to pass in the salt length to validate the signature unlike some other implementations.
+ // Certain beta images don't use the conventional salt length in the RSA-PSS specification, which should equal
+ // the length of the output of the hash function in bytes.
+ // First, let's try to validate with the conventional salt length:
+ algorithm = RsaPssParams::new(&algo, salt_len).as_js_object();
+ web_sys::console::debug_2(
+ &"Attempting verification with salt length".into(),
+ &salt_len.into(),
+ );
+ let verified = crypto_is_verified(
+ &subtle_crypto,
+ &algorithm,
+ &crypto_key,
+ &sig_array_buf,
+ &data_array_buf,
+ )
+ .await?;
+ if verified {
+ Ok(verified)
+ } else {
+ // If this doesn't work, we can try validating against an alternate salt length:
+ let salt_len = alternate_salt_length(&crypto_key, &salt_len)?;
+ web_sys::console::debug_2(
+ &"Attempting fallback verification with salt length".into(),
+ &salt_len.into(),
+ );
+ algorithm = RsaPssParams::new(&algo, salt_len).as_js_object();
+ crypto_is_verified(
+ &subtle_crypto,
+ &algorithm,
+ &crypto_key,
+ &sig_array_buf,
+ &data_array_buf,
+ )
+ .await
+ }
+ }
+ "RSASSA-PKCS1-v1_5" => {
+ // Create Key
+ let algorithm = RsaHashedImportParams::new(&algo, &hash).as_js_object();
+ let key_array_buf = data_as_array_buffer(&pkey);
+ let usages = Array::new();
+ usages.push(&"verify".into());
+
+ let promise = subtle_crypto
+ .import_key_with_object("spki", &key_array_buf, &algorithm, true, &usages)
+ .map_err(|_err| Error::WasmKey)?;
+ let crypto_key: CryptoKey = JsFuture::from(promise)
+ .await
+ .map_err(|_err| Error::WasmKey)?
+ .into();
+ web_sys::console::debug_2(&"CryptoKey".into(), &crypto_key);
+
+ // Create verifier
+ crypto_is_verified(
+ &subtle_crypto,
+ &algorithm,
+ &crypto_key,
+ &sig_array_buf,
+ &data_array_buf,
+ )
+ .await
+ }
+ "ECDSA" => {
+ // Create Key
+ let named_curve = match hash.as_ref() {
+ "SHA-256" => "P-256".to_string(),
+ "SHA-384" => "P-384".to_string(),
+ "SHA-512" => "P-521".to_string(),
+ _ => return Err(Error::UnsupportedType),
+ };
+ let mut algorithm = EcKeyImportParams::new(&algo, &hash, &named_curve).as_js_object();
+ let key_array_buf = data_as_array_buffer(&pkey);
+ let usages = Array::new();
+ usages.push(&"verify".into());
+
+ let promise = subtle_crypto
+ .import_key_with_object("spki", &key_array_buf, &algorithm, true, &usages)
+ .map_err(|_err| Error::WasmKey)?;
+ let crypto_key: CryptoKey = JsFuture::from(promise).await.unwrap().into();
+ web_sys::console::debug_2(&"CryptoKey".into(), &crypto_key);
+
+ // Create verifier
+ algorithm = EcdsaParams::new(&algo, &hash).as_js_object();
+ crypto_is_verified(
+ &subtle_crypto,
+ &algorithm,
+ &crypto_key,
+ &sig_array_buf,
+ &data_array_buf,
+ )
+ .await
+ }
+ _ => Err(Error::UnsupportedType),
+ }
+}
+
+pub async fn validate_async(alg: &str, sig: &[u8], data: &[u8], pkey: &[u8]) -> Result<bool> {
+ web_sys::console::debug_2(
+ &"Validating with algorithm".into(),
+ &String::from(alg).into(),
+ );
+
+ match alg {
+ "ps256" => {
+ async_validate(
+ "RSA-PSS".to_string(),
+ "SHA-256".to_string(),
+ 32,
+ pkey.to_vec(),
+ sig.to_vec(),
+ data.to_vec(),
+ )
+ .await
+ }
+ "ps384" => {
+ async_validate(
+ "RSA-PSS".to_string(),
+ "SHA-384".to_string(),
+ 48,
+ pkey.to_vec(),
+ sig.to_vec(),
+ data.to_vec(),
+ )
+ .await
+ }
+ "ps512" => {
+ async_validate(
+ "RSA-PSS".to_string(),
+ "SHA-512".to_string(),
+ 64,
+ pkey.to_vec(),
+ sig.to_vec(),
+ data.to_vec(),
+ )
+ .await
+ }
+ "rs256" => {
+ async_validate(
+ "RSASSA-PKCS1-v1_5".to_string(),
+ "SHA-256".to_string(),
+ 0,
+ pkey.to_vec(),
+ sig.to_vec(),
+ data.to_vec(),
+ )
+ .await
+ }
+ "rs384" => {
+ async_validate(
+ "RSASSA-PKCS1-v1_5".to_string(),
+ "SHA-384".to_string(),
+ 0,
+ pkey.to_vec(),
+ sig.to_vec(),
+ data.to_vec(),
+ )
+ .await
+ }
+ "rs512" => {
+ async_validate(
+ "RSASSA-PKCS1-v1_5".to_string(),
+ "SHA-512".to_string(),
+ 0,
+ pkey.to_vec(),
+ sig.to_vec(),
+ data.to_vec(),
+ )
+ .await
+ }
+ "es256" => {
+ async_validate(
+ "ECDSA".to_string(),
+ "SHA-256".to_string(),
+ 0,
+ pkey.to_vec(),
+ sig.to_vec(),
+ data.to_vec(),
+ )
+ .await
+ }
+ "es384" => {
+ async_validate(
+ "ECDSA".to_string(),
+ "SHA-384".to_string(),
+ 0,
+ pkey.to_vec(),
+ sig.to_vec(),
+ data.to_vec(),
+ )
+ .await
+ }
+ "es512" => {
+ async_validate(
+ "ECDSA".to_string(),
+ "SHA-512".to_string(),
+ 0,
+ pkey.to_vec(),
+ sig.to_vec(),
+ data.to_vec(),
+ )
+ .await
+ }
+ _ => return Err(Error::UnsupportedType),
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ #[cfg(target_arch = "wasm32")]
+ use wasm_bindgen_test::*;
+
+ #[cfg(target_arch = "wasm32")]
+ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ #[wasm_bindgen_test]
+ async fn test_async_verify_good() {
+ // PS signatures
+ let sig_bytes = include_bytes!("../../tests/fixtures/sig.data");
+ let data_bytes = include_bytes!("../../tests/fixtures/data.data");
+ let key_bytes = include_bytes!("../../tests/fixtures/key.data");
+
+ let mut validated = validate_async("ps256", sig_bytes, data_bytes, key_bytes)
+ .await
+ .unwrap();
+
+ assert_eq!(validated, true);
+
+ // EC signatures
+ let sig_es384_bytes = include_bytes!("../../tests/fixtures/sig_es384.data");
+ let data_es384_bytes = include_bytes!("../../tests/fixtures/data_es384.data");
+ let key_es384_bytes = include_bytes!("../../tests/fixtures/key_es384.data");
+
+ validated = validate_async("es384", sig_es384_bytes, data_es384_bytes, key_es384_bytes)
+ .await
+ .unwrap();
+
+ assert_eq!(validated, true);
+
+ let sig_es512_bytes = include_bytes!("../../tests/fixtures/sig_es512.data");
+ let data_es512_bytes = include_bytes!("../../tests/fixtures/data_es512.data");
+ let key_es512_bytes = include_bytes!("../../tests/fixtures/key_es512.data");
+
+ validated = validate_async("es512", sig_es512_bytes, data_es512_bytes, key_es512_bytes)
+ .await
+ .unwrap();
+
+ assert_eq!(validated, true);
+
+ let sig_es256_bytes = include_bytes!("../../tests/fixtures/sig_es256.data");
+ let data_es256_bytes = include_bytes!("../../tests/fixtures/data_es256.data");
+ let key_es256_bytes = include_bytes!("../../tests/fixtures/key_es256.data");
+
+ let validated = validate_async("es256", sig_es256_bytes, data_es256_bytes, key_es256_bytes)
+ .await
+ .unwrap();
+
+ assert_eq!(validated, true);
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ #[wasm_bindgen_test]
+ #[ignore]
+ async fn test_async_verify_bad() {
+ let sig_bytes = include_bytes!("../../tests/fixtures/sig.data");
+ let data_bytes = include_bytes!("../../tests/fixtures/data.data");
+ let key_bytes = include_bytes!("../../tests/fixtures/key.data");
+
+ let mut bad_bytes = data_bytes.to_vec();
+ bad_bytes[0] = b'c';
+ bad_bytes[1] = b'2';
+ bad_bytes[2] = b'p';
+ bad_bytes[3] = b'a';
+
+ let validated = validate_async("ps256", sig_bytes, &bad_bytes, key_bytes)
+ .await
+ .unwrap();
+
+ assert_eq!(validated, false);
+ }
+}
diff --git a/sdk/tests/fixtures/08manifest.jpg b/sdk/tests/fixtures/08manifest.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/C.jpg b/sdk/tests/fixtures/C.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/CAICAI.jpg b/sdk/tests/fixtures/CAICAI.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/CAICAI_BAD_SIG.jpg b/sdk/tests/fixtures/CAICAI_BAD_SIG.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/CAICAI_NO_XMP.jpg b/sdk/tests/fixtures/CAICAI_NO_XMP.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/CIE-sig-CA.jpg b/sdk/tests/fixtures/CIE-sig-CA.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/IMG_0003.jpg b/sdk/tests/fixtures/IMG_0003.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/P1000827.jpg b/sdk/tests/fixtures/P1000827.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/Purple Square.psd b/sdk/tests/fixtures/Purple Square.psd
Binary files differ.
diff --git a/sdk/tests/fixtures/bad_verify.jpeg b/sdk/tests/fixtures/bad_verify.jpeg
Binary files differ.
diff --git a/sdk/tests/fixtures/bigjumbf.jpg b/sdk/tests/fixtures/bigjumbf.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/bob.key b/sdk/tests/fixtures/bob.key
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIL2yf62xPPFBKbDcBEKU5HIickmG2DEDcZt0lD3N4aOLoAoGCCqGSM49
+AwEHoUQDQgAEL/vKhxGKIRCcFSykCKiV+rHtBPgipPPW8EWEUXwrJh3mbhS3QXCd
+PNiOu5qUjM8xTdXkl/NjHaYW+/rf7iCj5A==
+-----END EC PRIVATE KEY-----
diff --git a/sdk/tests/fixtures/bob.pem b/sdk/tests/fixtures/bob.pem
@@ -0,0 +1,56 @@
+Bag Attributes
+ localKeyID: 21 9D 38 2E 7C 25 38 78 94 3F CA DA 5A A7 BC BA 3F 7F 24 21
+subject=/O=Media Publisher Company/CN=Bob
+issuer=/O=Media Publisher Company/CN=Media Publisher Company Intermediate CA
+-----BEGIN CERTIFICATE-----
+MIICQDCCAaGgAwIBAgIUXsGqKw4Bw9PJBv1BcL3SLGiyCT4wCgYIKoZIzj0EAwIw
+VDEgMB4GA1UECgwXTWVkaWEgUHVibGlzaGVyIENvbXBhbnkxMDAuBgNVBAMMJ01l
+ZGlhIFB1Ymxpc2hlciBDb21wYW55IEludGVybWVkaWF0ZSBDQTAeFw0yMjA0MDQx
+NDE1NDhaFw0yMzA0MDQxNDE1NDhaMDAxIDAeBgNVBAoMF01lZGlhIFB1Ymxpc2hl
+ciBDb21wYW55MQwwCgYDVQQDDANCb2IwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AAQv+8qHEYohEJwVLKQIqJX6se0E+CKk89bwRYRRfCsmHeZuFLdBcJ082I67mpSM
+zzFN1eSX82Mdphb7+t/uIKPko3UwczAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/
+BAwwCgYIKwYBBQUHAwQwCQYDVR0TBAIwADAdBgNVHQ4EFgQUTqgwjgSAknNN8T+e
+bNXm+mbtlfIwHwYDVR0jBBgwFoAUXF8IatqTvlwmGnVmM2L6+v1IQMcwCgYIKoZI
+zj0EAwIDgYwAMIGIAkIAkZ0LAaJ209QLyiSn/hIMfbBReg+d61gX8U+9OqBWYiD2
+i6u59mJrKdwCuj8po8jh7ntkcXHc1v+3ztHWCHCI9R0CQgErPKUhrxei5mbKU0Xx
+NUsTBB6oHMZccZCn1FS0R7YaCFume2mscC1rGGNXqu/Skgsq6FPkFHJqyFTZhtcW
+pJWKaw==
+-----END CERTIFICATE-----
+Bag Attributes: <No Attributes>
+subject=/O=Media Publisher Company/CN=Media Publisher Company Intermediate CA
+issuer=/CN=Media Provenance Intermediate CA 1
+-----BEGIN CERTIFICATE-----
+MIICbTCCAc+gAwIBAgIUA7qQpsd9jsBL7dahNfBx+ftJ5VQwCgYIKoZIzj0EAwQw
+LTErMCkGA1UEAwwiTWVkaWEgUHJvdmVuYW5jZSBJbnRlcm1lZGlhdGUgQ0EgMTAe
+Fw0yMjA0MDQxNDE1MDRaFw0zMjAzMzExNDE1MDRaMFQxIDAeBgNVBAoMF01lZGlh
+IFB1Ymxpc2hlciBDb21wYW55MTAwLgYDVQQDDCdNZWRpYSBQdWJsaXNoZXIgQ29t
+cGFueSBJbnRlcm1lZGlhdGUgQ0EwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABABo
+g4jSfIvYPwpADEOiQjWOSD5KXTJl9k/gz0vVpE1D3gdn5TK2UiuEiKiiZvND45pi
+U/TW0jVs6Rfns7mBTKEAygFGbcjEVqMZTfXcWYIi2AvLARe/HCeVMO3x5g4AmDmr
+CgshTHWNwrit6u/ae9YdOv5QdqBLKW6NRdvv4jvpZpK/QKNjMGEwHQYDVR0OBBYE
+FFxfCGrak75cJhp1ZjNi+vr9SEDHMB8GA1UdIwQYMBaAFP59tM4KPiaHG81hkWz1
+1CoFfPYiMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49
+BAMEA4GLADCBhwJCAeYlt/gDEOc0Y5NFLTcf3tOrwYR+m4Gn+bbgrdKznU2ygm0P
+PqKHjCT+bMZmO7NJHHcwRg245AeKp1tCJ/nM/3+FAkFMQq5CaD6l7meYE+8wFtlD
+vXlpFWg3ryuroc/DDBFMyrTZKWCz3wl5bLfx4GoWvdsrFBdClrhTO2srJmmQKH6t
+QA==
+-----END CERTIFICATE-----
+Bag Attributes: <No Attributes>
+subject=/CN=Media Provenance Intermediate CA 1
+issuer=/CN=Media Provenance Root CA
+-----BEGIN CERTIFICATE-----
+MIICPTCCAZ6gAwIBAgIUX/NDsdeWFPusKDEHeNizDZm8recwCgYIKoZIzj0EAwQw
+IzEhMB8GA1UEAwwYTWVkaWEgUHJvdmVuYW5jZSBSb290IENBMB4XDTIyMDQwNDE0
+MTQ0OVoXDTMyMDQwMTE0MTQ0OVowLTErMCkGA1UEAwwiTWVkaWEgUHJvdmVuYW5j
+ZSBJbnRlcm1lZGlhdGUgQ0EgMTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEALfl
+FbX5HaCQLNFaHCFKcH3AaGoRrgIt+CVFc3llHE+rywqHg9NPw7giiIXkEo6+U6iB
+V6s+UcqrwBM83oyBp5WQAONSy5MAGyLvJ3z/NxKehzAq9E3a5V6ngZEoirgOaFnj
+BpTLRjsE63drSwbv+JIJV226Xvpvmf9lYdJh9nCfnYXuo2MwYTAdBgNVHQ4EFgQU
+/n20zgo+JocbzWGRbPXUKgV89iIwHwYDVR0jBBgwFoAU5vDmMCAcP+Z9yrh88G2C
+gpE29xUwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0E
+AwQDgYwAMIGIAkIA+QQbxd56X+fKFwMMcVgXfUAfGZvcQUmMI4/mD2/1RDKOeh5z
+9A6rAlg+mid1EJKWYHer0521eMnUljR9FH70Rb8CQgHE0RXl1x6vavNK7jK5ZaJl
+d/m9xB6wqtIy8OYt7NvCnx5ZvSLcyHyQfOM81hhku/ovwpoh3MeCngLBLpNrvThq
+Xw==
+-----END CERTIFICATE-----
diff --git a/sdk/tests/fixtures/claim.json b/sdk/tests/fixtures/claim.json
@@ -0,0 +1,61 @@
+{
+ "vendor": "myvendor",
+ "claim_generator": "My Application",
+ "title": "My Title",
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "https://schema.org",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "Joe Bloggs"
+ }
+ ],
+ "url": "https://contentauthenticity.org/"
+ }
+ },
+ {
+ "label": "adobe.dictionary",
+ "data": {
+ "url": "https://cai-assertions.adobe.com/photoshop/dictionary.json"
+ }
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "when": "2021-03-25T01:37:04.872Z"
+ },
+ {
+ "action": "c2pa.edited",
+ "parameters": {
+ "name": "brightnesscontrast"
+ },
+ "when": "2021-03-25T01:37:22.536Z"
+ }
+ ],
+ "metadata": {
+ "dateTime": "2021-03-25T01:37:22.536Z",
+ "reviewRatings": [
+ {
+ "code": "unknownActionsPerformed",
+ "explanation": "Something untracked happened",
+ "value": 4
+ }
+ ]
+ }
+ }
+ },
+ {
+ "label": "my.assertion",
+ "data": {
+ "any_tag": "whatever I want"
+ }
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/sdk/tests/fixtures/data.data b/sdk/tests/fixtures/data.data
@@ -0,0 +1,4 @@
+jSignature1D8$@Y֦idc:format`jinstanceID`oclaim_generatoroadobe unit testisignaturexRself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.signaturejassertionscurl x`self#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.actionscalgfsha256dhashX WKVn|jO#;~a/*O+curl xcself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.cloud-datacalgfsha256dhashX Ѕ@}4&snxcV
+HP2#curl xgself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.location.broadcalgfsha256dhashX ku}kB
+r^:d$+KO{pCxEcurl xiself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.location.precisecalgfsha256dhashX $s`}m
+Q!%\Kpcurl xrself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.thumbnail.ingredient.jpegcalgfsha256dhashX S1RCyH%DZFȽZq>쭣curl xuself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.thumbnail.ingredient__1.jpegcalgfsha256dhashX S1RCyH%DZFȽZq>쭣curl xuself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.thumbnail.ingredient__2.jpegcalgfsha256dhashX S1RCyH%DZFȽZq>쭣curl xbself#jumbf=c2pa/adobe:urn:uuid:30843618-ff1c-4783-a106-1a51d396a6a8/c2pa.assertions/c2pa.hash.datacalgfsha256dhashX brN>)c7pa[רHf)lcalgfsha256
+\ No newline at end of file
diff --git a/sdk/tests/fixtures/data_es256.data b/sdk/tests/fixtures/data_es256.data
Binary files differ.
diff --git a/sdk/tests/fixtures/data_es384.data b/sdk/tests/fixtures/data_es384.data
Binary files differ.
diff --git a/sdk/tests/fixtures/data_es512.data b/sdk/tests/fixtures/data_es512.data
Binary files differ.
diff --git a/sdk/tests/fixtures/earth_apollo17.jpg b/sdk/tests/fixtures/earth_apollo17.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/key.data b/sdk/tests/fixtures/key.data
Binary files differ.
diff --git a/sdk/tests/fixtures/key_es256.data b/sdk/tests/fixtures/key_es256.data
Binary files differ.
diff --git a/sdk/tests/fixtures/key_es384.data b/sdk/tests/fixtures/key_es384.data
Binary files differ.
diff --git a/sdk/tests/fixtures/key_es512.data b/sdk/tests/fixtures/key_es512.data
Binary files differ.
diff --git a/sdk/tests/fixtures/libpng-test.png b/sdk/tests/fixtures/libpng-test.png
Binary files differ.
diff --git a/sdk/tests/fixtures/prerelease.jpg b/sdk/tests/fixtures/prerelease.jpg
Binary files differ.
diff --git a/sdk/tests/fixtures/rsa-pss256-expired.pem b/sdk/tests/fixtures/rsa-pss256-expired.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDC/FlR6NR3F5EO
+4D+92j/vQH2RPLjatLBSBwfYzRfRzeVGAjy32K+L+lyXdSNcJMoMqcWYAbCOVFHy
+mLjA4Yo3+fjm8b+dmPuBIp+faClLjOged2mogr8EZkx6djHjqreZ05Ibwmq4XSYq
+Z7Z/szS8xhgHV7tI5kv4eHR9JzzdKiXX5Yg7ZD0kb06gBHzgD3xmrP5U1l//G2lY
+OnDjJy7qWEdO/O1hOGOp+Mp0S4MLDI/CthgiBxXHh0DIRPJTIwupbZpkfHW5wjsT
+DEAwfEmS8jBCh1sPIzOn8+UVVvy+wdJHMMP7T6hODgwVNZOkmSLYvA0+zjaYGrn8
+xyeb45hDAgMBAAECggEABgTSIQQl4tM4aBQmA49EH/eGqMAOGLoxIycSZ+/Ux2Yq
+qKGTDD3FFXYR+57jC4obLo7jCZEryQjzSqDKOzH7GUU/GKnnt06snMzbzojhlXJx
+C9e1zDzU9qbNv438dkGjzzzHqtGyh6RgdDilWoXGn/khbsCXiGWLfM0DE4XtjAoe
+JOGrhr3LUE3lXGBwvNqGQJ0dPcpF0vwzRKITLu1zH1WUStLMg31nS4NV5P9SLZks
+KsmdB9DnPJ8aWglj+u7+08H//ETQIHpU3nV+/sRgDsw6MYrJ5VCJLt+nxw4Aa2+9
+KFIZVEvrcabkUEaji5LT/fVXVU4Wrf0zoL8cx0j5qQKBgQD6ZooLt/Y5U42ss2P4
+f904AqDEqcMzO8CWJhCn1jZQzoJkmEbWW06Dq9RObjJeNgRlsxEmhGwJ9Di7OHFu
+SJ7Y+ZGpT9L+4wtSok5ieVfxPgA2uO9tjW+fvm/mxpiM1cpZWukupX7Swn/xPDNK
+2oY8nwGFCsYrOnseSu4I7Qw+ZwKBgQDHWJQAEV9UKM2uSoxhyra+/sP8jyuFL2u/
++3PWVmIea7kUpd6KpgDOikY4aCSjIk5tIob5yWYEa17ZcXz1ASe5XJ1Vplco+hbI
+9Q+u3Eybwh4Db+g6bsqsKCz5iRvkyCvpyxeqzDrpVx+CVw2yLJrdA8dGsqY6LWzJ
+IX5SIun1xQKBgH0Hvl5jmRq+0bsuR/jJP9i71zLb4ZAvgdZ3Y1Gq8KwgsZMxRg26
+wdWVcwlGlPfd2Qw/AY1OCfRecgVqBZmfwVFuLIFyTlTfYcP9L06UcIkRAGJSrZry
+SI5nNNDy0TFhfwxnDJAyKsVqQSBfgu0ZeHXEn6mi25iyNs/Fcl4an0Z5AoGAcCFj
+Hqam+K/7Ag7s6BUetlCX7XibAk+qTFMk1WvBxVrSwMqMY3D9AzDETvZFpX2mn7zM
+L7UZrWK395fesfH0Zk+yMHtgi6whJOiz6agBb5vBRi2sczHezvfKVJLLHeV6zgdv
+SKOYf6iCEM7m5VIxyiBV926GEjN6/afZZlo9QAECgYEAw3QkkJU+9ekQ0mYu2UNf
+Hnf6DY8sLe17Ap1nbOR/NTPcm/wBeUABgMtGixeIboXlVIJOjUbOrpTkeltuGrIL
+9z63D1X59clCmhljuGsgPY2uTg2/EIMFX1nXFcw2uq2qAjz5NR8+g5hLAZE1ZsHL
+lWCmGkSQWunFvUeClZK74Aw=
+-----END PRIVATE KEY-----
diff --git a/sdk/tests/fixtures/rsa-pss256_key-expired.pub b/sdk/tests/fixtures/rsa-pss256_key-expired.pub
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEATCCAumgAwIBAgIUAMXK1m2iDtQ165Ruxogp1TjplcUwDQYJKoZIhvcNAQEL
+BQAwfzELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAmNhMREwDwYDVQQHDAhTYW4gSm9z
+ZTEUMBIGA1UECgwLQWRvYmUsIEluYy4xGDAWBgNVBAsMD0NBSSAoVGVtcG9yYXJ5
+KTEgMB4GA1UEAwwXY29udGVudGF1dGhlbnRpY2l0eS5vcmcwHhcNMjIwMjAyMTMy
+NDI4WhcNMjIwMjAzMTMyNDI4WjB/MQswCQYDVQQGEwJ1czELMAkGA1UECAwCY2Ex
+ETAPBgNVBAcMCFNhbiBKb3NlMRQwEgYDVQQKDAtBZG9iZSwgSW5jLjEYMBYGA1UE
+CwwPQ0FJIChUZW1wb3JhcnkpMSAwHgYDVQQDDBdjb250ZW50YXV0aGVudGljaXR5
+Lm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAML8WVHo1HcXkQ7g
+P73aP+9AfZE8uNq0sFIHB9jNF9HN5UYCPLfYr4v6XJd1I1wkygypxZgBsI5UUfKY
+uMDhijf5+Obxv52Y+4Ein59oKUuM6B53aaiCvwRmTHp2MeOqt5nTkhvCarhdJipn
+tn+zNLzGGAdXu0jmS/h4dH0nPN0qJdfliDtkPSRvTqAEfOAPfGas/lTWX/8baVg6
+cOMnLupYR0787WE4Y6n4ynRLgwsMj8K2GCIHFceHQMhE8lMjC6ltmmR8dbnCOxMM
+QDB8SZLyMEKHWw8jM6fz5RVW/L7B0kcww/tPqE4ODBU1k6SZIti8DT7ONpgaufzH
+J5vjmEMCAwEAAaN1MHMwHQYDVR0OBBYEFJNnwVuf7Gh/xPkAvGGmK+Q6gB3NMB8G
+A1UdIwQYMBaAFJNnwVuf7Gh/xPkAvGGmK+Q6gB3NMA8GA1UdEwEB/wQFMAMBAf8w
+CwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMEMA0GCSqGSIb3DQEBCwUA
+A4IBAQAVBKqok5V5SxnfAE5Mt9WyQjfMWu19MwrquPo0I40KyYJI+2coLyo2tkuE
+bXezJTP/RY7v2hqOwlk+2kK82m2idRZcRT3r8yAUwKkg/tX5jlVt4WTNQgdamVtd
+mpCb97ozXdjGGv/FlSji5ufs7u8hNP9jhO0/LFzHoeJDRJc13HBJCdOmGd0topWZ
+NW4UtOdmke5ZPxaWkjxDwtfJxsplsinD3bcaiYGfhQPSNyacYB0UnimdUQaVpO6i
+U9t08t4+zBoFpCoC9XgEAOs9DP7WOGmKPhafL7dbSUhqyFHwacGY3PxoWZPqHwpH
+jsseAnInq8Ves5Bhnd6l6sc4GjiR
+-----END CERTIFICATE-----
diff --git a/sdk/tests/fixtures/sig.data b/sdk/tests/fixtures/sig.data
Binary files differ.
diff --git a/sdk/tests/fixtures/sig_es256.data b/sdk/tests/fixtures/sig_es256.data
@@ -0,0 +1 @@
+ GKbs<P*dA5.J-UQi&PC<ʭM^ҩ}6
+\ No newline at end of file
diff --git a/sdk/tests/fixtures/sig_es384.data b/sdk/tests/fixtures/sig_es384.data
@@ -0,0 +1 @@
+9Oa;^~mO`[Q&QBݯ_?`,:]Mg|θ,[RG: gwp v]ߢ
+\ No newline at end of file
diff --git a/sdk/tests/fixtures/sig_es512.data b/sdk/tests/fixtures/sig_es512.data
Binary files differ.
diff --git a/sdk/tests/fixtures/temp_cert.data b/sdk/tests/fixtures/temp_cert.data
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFVjCCAz4CCQC71oHMXkJ32zANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJV
+UzELMAkGA1UECAwCTkMxFDASBgNVBAcMC1dha2UgRm9yZXN0MRIwEAYDVQQKDAlB
+ZG9iZSBJbmMxEjAQBgNVBAsMCUFkb2JlIEluYzESMBAGA1UEAwwJYWRvYmUuY29t
+MCAXDTIxMTEyMzE2MTMwOFoYDzMwMjEwMzI2MTYxMzA4WjBsMQswCQYDVQQGEwJV
+UzELMAkGA1UECAwCTkMxFDASBgNVBAcMC1dha2UgRm9yZXN0MRIwEAYDVQQKDAlB
+ZG9iZSBJbmMxEjAQBgNVBAsMCUFkb2JlIEluYzESMBAGA1UEAwwJYWRvYmUuY29t
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxJTVkC2Yogu/Ltse0NpE
+yBJpTF0bLMkHAuLzxkYyxp/g8fqO5TmGENhABsyQEQFhFCf8hpvblwnxrkaZ57+K
+oA1cMRdqmYfvWcrlAkve65Rfv8SUFTl63Z20O0bW/NfZzlRb1UQ7PSkdecDJU4wI
+FYkLldHJrEm9BiarEF7E8e2TU348dIdQtn0Q4lnV7Ar6J41oFjY/VCNV/q1ah1VE
+YAmuArurbdGF6QcCHSL8GQawMaABsb+5arQgApWuynO/2H2Jl+EFg2lX0KUM2P2a
+jWBlyizOdjqqECD95ZI5n4BNvUl9xfaMtmAhLODP7TL6rNS3dc9lfLDgjksv9eME
+MVtY9+DtuUo2td2bMBx/hMXeNE3Pg8X1zwrKybvBgCjJF/YTWXSzwT1QUjDhmce5
+npByrxMBY6GzmNXu9OVhhy6Ed2tzQUpzX2TgghVNkhbyKYbLLaDghS9okw2yyv/9
+YWfsbmejhWnZkIfa0d1r8UtveiAd+Hi7ftW8elS2o/t+I9n+XdAfPpKkdMpEM0FT
+VsFn16demNWjACdFAZ2/g8NCSg3K6NyAfOHVS7sby8nrC9gsG0zephcqipcbBvgJ
+Ojvvlvp1YTdDOh/Yl8ZYj/6XpDocehyRLScF2tFw/qMBVBE7zVXD7hlWeWaAf/hp
+wYq4Jz23NcZiOCAaoAU2TScCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAiMPNfx81
+v1B2aTGIeJYdekzH8IZdKfqwXxbS4//UO52MOywfPNknyy3BmWaOapRwY1J8PAo/
+USa187s77Z2Gkuvk8cZUf5uUl7s7/O/uZej/GRtS59PQEYHXvCT3o2ljBRnIXWyK
+SMJSaLZvuaUxgLH/orHsErTTf4HutZJUt0U8C1YwqblWelnXDLHqnYdl5gi3HC6/
+JuPMtsdoEIRvB9MAt4bQZCOtifjk+hwpGiZKrqZlgJ+x0mxmG6K5ZER6CnI9U67m
+MSHI3O+Z+UXe3n4AO6bysSEp7XCd7wzH7pvBDBYmBCItOoKQKluxFrCaod3+4kiu
+ZRear8U120mCh7/yWCmPI4vvnQ0fCqoR79sASgu768alVN0bkFtD4cgG6a998Gp9
+GAPC58TPH2m7EMLeDvdeigvpy6qPjBuZ5VVB8hP0pavQI88mRJJSZVtgyIXt3y/3
+dmgOR6hnyMte2hbuJCHlW53+PLzAb54NezP+cqxTxvcZZ5jMhnceiTh6ua+wqHfS
+OMINa0QkeP/dL9Z1T9trHB4tsFeh3eGnyKV7a11LIluR25b+8BBoU8ZPGiBRlivn
+dCcnk2V6viTDrO/oU/oMNyfsJW01bgDrZtPKCSvgrWj8mF1AFvw/FBSrKFXQ5VDN
+wMZPLqUetGGpI0mFU1wDdTmfJ7oN3/eYTdM=
+-----END CERTIFICATE-----
diff --git a/sdk/tests/fixtures/temp_priv_key.data b/sdk/tests/fixtures/temp_priv_key.data
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDElNWQLZiiC78u
+2x7Q2kTIEmlMXRssyQcC4vPGRjLGn+Dx+o7lOYYQ2EAGzJARAWEUJ/yGm9uXCfGu
+Rpnnv4qgDVwxF2qZh+9ZyuUCS97rlF+/xJQVOXrdnbQ7Rtb819nOVFvVRDs9KR15
+wMlTjAgViQuV0cmsSb0GJqsQXsTx7ZNTfjx0h1C2fRDiWdXsCvonjWgWNj9UI1X+
+rVqHVURgCa4Cu6tt0YXpBwIdIvwZBrAxoAGxv7lqtCACla7Kc7/YfYmX4QWDaVfQ
+pQzY/ZqNYGXKLM52OqoQIP3lkjmfgE29SX3F9oy2YCEs4M/tMvqs1Ld1z2V8sOCO
+Sy/14wQxW1j34O25Sja13ZswHH+Exd40Tc+DxfXPCsrJu8GAKMkX9hNZdLPBPVBS
+MOGZx7mekHKvEwFjobOY1e705WGHLoR3a3NBSnNfZOCCFU2SFvIphsstoOCFL2iT
+DbLK//1hZ+xuZ6OFadmQh9rR3WvxS296IB34eLt+1bx6VLaj+34j2f5d0B8+kqR0
+ykQzQVNWwWfXp16Y1aMAJ0UBnb+Dw0JKDcro3IB84dVLuxvLyesL2CwbTN6mFyqK
+lxsG+Ak6O++W+nVhN0M6H9iXxliP/pekOhx6HJEtJwXa0XD+owFUETvNVcPuGVZ5
+ZoB/+GnBirgnPbc1xmI4IBqgBTZNJwIDAQABAoICAEip/1tBD3duwieuxXBMDjqq
+W5PnoqDmWhoeDCARWLmE5aBsjp96qOzXEquwt9whV2Ic+cJEIGJvQY+69y7r5XEO
+HzLiRfwkfDwDvK0hoHASM2ZuYpKJ4bnDjpiOse9aVl9nXV6yzvbnmMallYW1cFON
+iePCXTq7MwzwBDGT5lbLC7dEJVKpphMenbwdmu6ajCdMpNn5CWWvfh0atSjrQtAB
+I/pMpqx8U3R0q7pbcTZK+5wMWjP6+63OkTqDW83oBVUf6lliyyftK5GrQNmAvL1f
+kaCu+U5Ilw5JM7DFYVltEs9SOBSR8yeC1yCApTs3qeNy6bG5jPkPZKIUdtD8LlLt
++u8h9aD60MJ4OCOTAyRf0KRHJwQaaHvhCaYRNiFEcWUw/gTBm/E4iS+nC/LYlJRe
+SYgNwxjxRwvNkJvj7c3GtntepyckvkM3aoOrXfUTIylxnOtrwWzWqQMySb74Fbxs
+RMA2ryXRdYLwdIhhap88HHgI6R70JIHWwe1mGjbcJuv/8M0eK3q41SvF+qIS4HsQ
+fillfTpo3drp90Qopec8DiJG/JSgrTXPKTr3G0OLOLsfRQdjaXMOYU2vBs5BOPhs
+lXIyNFLXl5cXvbS7xwZCUx7IwmBen5v4ydy5qIcdAQoXW1cAxvnOFuqpkB8gJyPt
+4uTBKyDMmZW0wj05jjfZAoIBAQDxIAwrMHCde6pwLMFht6KwLmK5yWZOt4HMVneW
+NRC160mHA2jFeLzjHrG0IYaf/NHbp2WRc7cW0E+mRB8WrlTHEeV9QRpUPLsTOypP
+F2VmuDpmPH6/m/Y4mGRKyOHwd+qwj/HqBA4nAIUszTA1Q9m2DXnZNZXgws6Szj7h
+/NX+0Z+r9HDP6htDm8det2BQR0niVv40jdp5mUcULn/O7UyDau6yuaxlB2eNiO/H
+SNo3R20zlzj4q3cDfFDfll+7smUM8pNFwZL5fiVsq7KzncI7eRdb1R0GG+M3F8ue
+Tu0OLhsTvckJmM5DPC6tmWCXH6jeRnF2sBZARNV6ZGmGndOjAoIBAQDQtVT0J3UI
+ZY9y21AKKuW91pRiKbGI0lcTroat/sr1QMPk00jq0wtSZrdtVh1boWX9mGaK75NI
+MiPzOhWuz2pdV5nFKr8YDqIlOPilYc2x/RRpewGC7ARQ/PLRnSUIlOBhsBz3MwQR
+Y1PkWmAFpenhqbCq0p4ImJORUBuOCVue4NMGr+kPNQ31eYMFdnvJBY/5JO3Tljtc
+0+eNgEQoTpDiWIgZAA6vZf48NCh2q2dFAbW+YB+rTL3xJwXhG2hY/wJaqmOGpK7T
+IK8hmSpWsUV6kZvtqdotJaJye9bhKFZx2vUD5vhoYlgum/gvLXlvJryAtO8wx2wJ
+QSyvf3kP3RitAoIBAQDo+NbpD9dvQaIu2f9Kg9xLr7Wx3jbcTY+6t7y5w7HOo3Qa
+YB/l1D9kji6SZWYmxGabfUS0YYKAJ2sdsWn8RxogcVkzSSjARSFXdm3tlyRhOBUs
+3Lx2M6GVkyAr5aXv6l02lQ3e1mY6JtWFsQcoH9OFg8W8KXsTfAWNcHrvbJPyreVB
+hl9Nc9s92pCqIjGLvyVS7EyWyIxTutYjWL4iV8L6ouHYi29W+e2CodFS0kAM8xU+
+qJjiIwFNu9qD/U2oE7hw5xuGZX61Ur2kHsz9oKqfPeNk1idB1MYXYAVbgtSmQ8Pj
+shZBEIQSEF7lWnEp8uv9P0C3tXD6TceZhtUyvOFTAoIBAQCmdIkd9T49BSskks/C
+XDZBcxuDMveaRRAX7XES4oyikBXssaqNRubXwH544F6nzxDV8i+OjcbspLjRazkR
+4/FUIuLMQuyH1lvxzf9Zf6ibVVprSiSJc415vlQ3Y9q+fmZ64wgnC/Qpnguxq3a7
+6nifd0Qk6bgK6Q2AruBbJxGApJUJ11DMxSat6kzaFYZibGJIdfCMeVw9C/C4hV1W
+vFqHe86ICeiscW8UOxEunEPP4K4/+NApQhqXc+UXhVFy8WfCQfUA1OVuE7o51LvU
+yqu8Ntei9H3C1io6npRhnVdclSInC5aBQjZMbi7CCcW5Ja7gA+5k/BgpRWbLTaKT
+SWY9AoIBAEY7V4d721yHCI/0krjxk6dgLmgD0ieJayyIlTo/EMOJEBmUVM+PowvA
+v4yotggh/XxjE4rXuWUFLQyERibVEOlniaI1MPA9gqY9nge5Awl2bRJ+nEJFr8Cx
+zIDbMPM5uYYqGpU3T12vZBvkih2snAbSGMhlwCc8HmlciG8xNQC7nVILVMUu9iqh
+J3rX/ViqREIUzpFAv4by6wnk/9rOuVo1KqPj4b5b3fjeeQ/2XnG15L84yezeSc5E
+WiFr77y1u3yHds3QmKbAdi4F/Ml8pUc6lZCn/ZR1HamJdEgEM/9e85WXi0L9cbxv
+HTOH8tFyE+ucA5wi7HPrDDj/a6A0YwY=
+-----END PRIVATE KEY-----
diff --git a/sdk/tests/integration.rs b/sdk/tests/integration.rs
@@ -0,0 +1,114 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+/// complete functional integration test with acquisitions and ingredients
+// isolate from wasm by wrapping in module
+#[cfg(feature = "file_io")]
+mod integration_1 {
+
+ use c2pa::{
+ assertions::{c2pa_action, Action, Actions},
+ openssl::temp_signer::get_signer,
+ Ingredient, Manifest, ManifestStore, Result,
+ };
+ use std::path::PathBuf;
+ use tempfile::tempdir;
+
+ const GENERATOR: &str = "app";
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_embed_manifest() -> Result<()> {
+ // set up parent and destination paths
+ let dir = tempdir()?;
+ let output_path = dir.path().join("test_file.jpg");
+ let mut parent_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ parent_path.push("tests/fixtures/earth_apollo17.jpg");
+ let mut ingredient_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ ingredient_path.push("tests/fixtures/libpng-test.png");
+
+ // create a new Manifest
+ let mut manifest = Manifest::new(GENERATOR.to_owned());
+
+ // allocate actions so we can add them
+ let mut actions = Actions::new();
+
+ // add a parent ingredient
+ let parent = Ingredient::from_file(&parent_path)?;
+ // add an action assertion stating that we imported this file
+ actions.add_action(
+ Action::new(c2pa_action::EDITED)
+ .set_parameter("name".to_owned(), "import")?
+ .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?,
+ );
+
+ // set the parent ingredient
+ manifest.set_parent(parent)?;
+
+ // edit our image
+ let mut img = image::open(&parent_path)?;
+ img = img.brighten(50); // brighten the image
+
+ actions.add_action(
+ Action::new("c2pa.edit").set_parameter("name".to_owned(), "brightnesscontrast")?,
+ );
+
+ // add an ingredient
+ let ingredient = Ingredient::from_file(&ingredient_path)?;
+
+ // now place an image in the image
+ let img_ingredient = image::open(&ingredient_path)?;
+ let img_small = img_ingredient.thumbnail(500, 500);
+ image::imageops::overlay(&mut img, &img_small, 0, 0);
+
+ // add an action assertion stating that we imported this file
+ actions.add_action(
+ Action::new(c2pa_action::EDITED)
+ .set_parameter("name".to_owned(), "import")?
+ .set_parameter("identifier".to_owned(), ingredient.instance_id().to_owned())?,
+ // could add other parameters for position and size here
+ );
+
+ manifest.add_ingredient(ingredient);
+
+ manifest.add_assertion(&actions)?;
+
+ // now place an image in the image
+ let img_ingredient = image::open(&ingredient_path)?;
+ let img_small = img_ingredient.thumbnail(500, 500);
+ image::imageops::overlay(&mut img, &img_small, 0, 0);
+
+ // save the edited image to our output path
+ img.save(&output_path)?;
+
+ // sign and embed into the target file
+ let temp_dir = tempdir().unwrap();
+ let (signer, _) = get_signer(&temp_dir.path());
+
+ manifest.embed(&output_path, &output_path, &signer)?;
+
+ // read our new file with embedded manifest
+ let manifest_store = ManifestStore::from_file(&output_path)?;
+
+ println!("{}", manifest_store);
+
+ assert!(manifest_store.get_active().is_some());
+ if let Some(manifest) = manifest_store.get_active() {
+ assert!(manifest.asset().is_some());
+ assert_eq!(manifest.ingredients().len(), 2);
+ } else {
+ panic!("no manifest in store");
+ }
+ Ok(())
+ }
+}