├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── GOVERNANCE.md ├── LICENSE ├── MAINTAINERS.txt ├── Makefile ├── README.md ├── examples ├── example_link.json ├── in_toto_run_test.rs ├── serialize_keys.rs └── write_link.rs ├── rustfmt.toml ├── src ├── crypto.rs ├── error.rs ├── format_hex.rs ├── interchange │ ├── cjson │ │ ├── mod.rs │ │ ├── pretty.rs │ │ └── shims.rs │ └── mod.rs ├── lib.rs ├── models │ ├── envelope │ │ ├── envelope_file.rs │ │ ├── mod.rs │ │ └── pae_v1.rs │ ├── helpers.rs │ ├── layout │ │ ├── inspection.rs │ │ ├── metadata.rs │ │ ├── mod.rs │ │ ├── rule.rs │ │ ├── step.rs │ │ └── supply_chain_item.rs │ ├── link │ │ ├── byproducts.rs │ │ ├── metadata.rs │ │ └── mod.rs │ ├── metadata.rs │ ├── mod.rs │ ├── predicate │ │ ├── link_v02.rs │ │ ├── mod.rs │ │ ├── slsa_provenance_v01.rs │ │ └── slsa_provenance_v02.rs │ └── statement │ │ ├── mod.rs │ │ ├── state_naive.rs │ │ └── state_v01.rs ├── rulelib.rs ├── runlib.rs └── verifylib.rs └── tests ├── ecdsa ├── ec ├── ec.pk8.der ├── ec.pub ├── ec.spki.der └── gen.sh ├── ed25519 ├── ed25519-1 ├── ed25519-1.pk8.der ├── ed25519-1.pub ├── ed25519-1.spki.der ├── ed25519-2.pk8.der ├── ed25519-3.pk8.der ├── ed25519-4.pk8.der ├── ed25519-5.pk8.der └── ed25519-6.pk8.der ├── rsa ├── alice.pub ├── gen.sh ├── rsa-2048 ├── rsa-2048.der ├── rsa-2048.pk8.der ├── rsa-2048.pkcs1.der ├── rsa-2048.spki.der ├── rsa-4096.der ├── rsa-4096.pk8.der ├── rsa-4096.pkcs1.der └── rsa-4096.spki.der ├── runlib.rs ├── test_link └── foo.tar.gz ├── test_metadata ├── demo.layout ├── demo.link └── owner.der ├── test_prefix ├── left │ └── world └── right │ └── world ├── test_runlib ├── .hidden │ ├── .bar │ └── foo └── hello. │ └── world └── test_verifylib ├── links ├── clone.776a00e2.link ├── package.2f89b927.link └── update-version.776a00e2.link └── workdir ├── alice.pub ├── demo-project.tar.gz └── root.layout /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Ignore calls to `cargo fmt` 2 | # 3 | # To learn more, see 4 | # https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view 5 | 6 | # Run cargo fmt changing max width to 80 7 | e66afe2850768076ce4f4c5f101e9dcc61697ba2 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | commit-message: 8 | prefix: "chore" 9 | include: "scope" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | commit-message: 15 | prefix: "chore" 16 | include: "scope" 17 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Rust CI 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 14 | - uses: dtolnay/rust-toolchain@stable 15 | - name: Run tests 16 | run: cargo test --verbose 17 | fmt: 18 | name: Rustfmt 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 22 | - uses: dtolnay/rust-toolchain@stable 23 | with: 24 | components: rustfmt 25 | - name: Enforce formatting 26 | run: cargo fmt --all --check 27 | clippy: 28 | name: Run Clippy 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 32 | - uses: dtolnay/rust-toolchain@stable 33 | with: 34 | components: clippy 35 | - name: Linting 36 | run: cargo clippy -- -D warnings 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /Cargo.lock 3 | /tests/hello_intoto 4 | 5 | # verifylib tempfiles 6 | tests/test_verifylib/workdir/untar.link 7 | tests/test_verifylib/workdir/demo-product -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "in-toto" 3 | edition = "2021" 4 | version = "0.4.0" 5 | authors = ["Santiago Torres-Arias ", 6 | "Qijia 'Joy' Liu "] 7 | description = "Library for in-toto" 8 | homepage = "https://in-toto.io" 9 | repository = "https://github.com/in-toto/in-toto-rs" 10 | documentation = "https://docs.rs/in-toto" 11 | readme = "README.md" 12 | license = "MIT" 13 | keywords = [ "security" ] 14 | categories = [ "cryptography" ] 15 | 16 | [lib] 17 | name = "in_toto" 18 | path = "./src/lib.rs" 19 | 20 | [dependencies] 21 | chrono = { version = "0.4", features = [ "clock", "serde" ], default-features = false } 22 | data-encoding = "2" 23 | derp = "0.0.15" 24 | itoa = "1" 25 | log = "0.4" 26 | ring = { version = "0.17" } 27 | serde = { version = "1", features = ["derive"] } 28 | serde_json = "1" 29 | untrusted = "0.9" 30 | thiserror = "2.0" 31 | walkdir = "2" 32 | path-clean = "1.0.1" 33 | strum = "0.26" 34 | strum_macros = "0.26" 35 | pem = "3.0.0" 36 | glob = "0.3.0" 37 | 38 | [dev-dependencies] 39 | assert-json-diff = "2.0.2" 40 | lazy_static = "1" 41 | maplit = "1" 42 | matches = "0.1.8" 43 | once_cell = "1.10.0" 44 | pretty_assertions = "1.3" 45 | rstest = "0.25.0" 46 | tempfile = "3" 47 | 48 | [features] 49 | 50 | -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # in-toto Governance 2 | 3 | in-toto's 4 | [governance](https://github.com/in-toto/community/blob/main/GOVERNANCE.md) and 5 | [code of conduct](https://github.com/in-toto/community/blob/main/CODE-OF-CONDUCT.md) 6 | are described in the [in-toto/community](https://github.com/in-toto/community) 7 | repository. 8 | 9 | ## in-toto-java Contributions 10 | 11 | This implementation adheres to 12 | [in-toto's contributing guidelines](https://github.com/in-toto/community/blob/main/CONTRIBUTING.md). 13 | Pull requests must be submitted to the `master` branch where they undergo 14 | review and automated testing. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 heartsucker, Advanced Telematic Systems GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /MAINTAINERS.txt: -------------------------------------------------------------------------------- 1 | The project is managed by the 2 | in-toto Steering Committee . 3 | 4 | Maintainers: 5 | Santiago Torres 6 | Email: santiagotorres@purdue.edu 7 | GitHub username: @SantiagoTorres 8 | PGP fingerprint: 903B AB73 640E B6D6 5533 EFF3 468F 122C E816 2295 9 | 10 | Aditya Sirish A Yelgundhalli 11 | Email: aditya.sirish@nyu.edu 12 | GitHub username: @adityasaky 13 | PGP fingerprint: E329 4129 9CB8 C0D9 3DCF 27AC B831 10D0 1254 5604 14 | 15 | Alan A. Chung Ma 16 | Email: alanchunggt@gmail.com 17 | GitHub username: @alanssitis 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help clean dev-docs 2 | .DEFAULT_GOAL := help 3 | 4 | clean: ## Remove temp/useless files 5 | @find . -name '*.rs.bk' -type f -delete 6 | 7 | dev-docs: ## Generate the documentation for all modules (dev friendly) 8 | @cargo rustdoc --all-features --open -- --no-defaults --passes "collapse-docs" --passes "unindent-comments" 9 | 10 | help: ## Print this message 11 | @awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {printf "\033[36m%16s\033[0m : %s\n", $$1, $$2}' $(MAKEFILE_LIST) 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # in-toto-rs 2 | 3 | A Rust implementation of [in-toto](https://in-toto.io). 4 | 5 | ## Warning: Beta Software 6 | 7 | This is under active development and may not suitable for production use. 8 | Further, the API is unstable and you should be prepared to refactor on even 9 | patch releases. 10 | 11 | ## Contributing 12 | 13 | Please make all pull requests to the `master` branch. 14 | 15 | ### Bugs 16 | 17 | This project has a **full disclosure** policy on security related errors. Please 18 | treat these errors like all other bugs and file a public issue. Errors communicated 19 | via other channels will be immediately made public. 20 | 21 | ## Legal 22 | 23 | ### License 24 | 25 | This work is dual licensed under the MIT license 26 | -------------------------------------------------------------------------------- /examples/example_link.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554", 5 | "sig": "9239111d2f81c106d83a087eb1bcb393e6c8ea588cb3a39a71d3dea560717250044095bd5d811f1ea8f8d882961c39c8a00ba32fddb14219ab36c4cdcd229007" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "example", 10 | "byproducts": { 11 | "return-value": "0", 12 | "stderr": "", 13 | "stdout": "" 14 | }, 15 | "env": {}, 16 | "materials": { 17 | "tests/test_runlib/.hidden/.bar": { 18 | "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", 19 | "sha512": "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" 20 | }, 21 | "tests/test_runlib/.hidden/foo": { 22 | "sha256": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", 23 | "sha512": "cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063" 24 | }, 25 | "tests/test_runlib/hello./symbolic_to_nonparent_folder/.bar": { 26 | "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", 27 | "sha512": "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" 28 | }, 29 | "tests/test_runlib/hello./symbolic_to_nonparent_folder/foo": { 30 | "sha256": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", 31 | "sha512": "cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063" 32 | }, 33 | "tests/test_runlib/hello./world": { 34 | "sha256": "25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", 35 | "sha512": "9b589a5fc9f43c3279e3a184f78b62738794913d976d74cd72f862864b8bc09b61f882273e9a64cbfea53c18782583f4bdc842ace58411824a4b15cd0d0c6ff9" 36 | }, 37 | "tests/test_runlib/symbolic_to_file": { 38 | "sha256": "25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", 39 | "sha512": "9b589a5fc9f43c3279e3a184f78b62738794913d976d74cd72f862864b8bc09b61f882273e9a64cbfea53c18782583f4bdc842ace58411824a4b15cd0d0c6ff9" 40 | }, 41 | "tests/test_runlib/symbolic_to_license_file": { 42 | "sha256": "61ed40687d2656636a04680013dffe41d5c724201edaa84045e0677b8e2064d6", 43 | "sha512": "95df79b6a38f7e7c6b2c0393fcbc433b5d9f5f5b865467de992ff886965816bdf5e9f564390c9d38e3265396f338970a3335b1f3d6a67556e54100898af2e462" 44 | } 45 | }, 46 | "products": { 47 | "tests/test_runlib/.hidden/.bar": { 48 | "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", 49 | "sha512": "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" 50 | }, 51 | "tests/test_runlib/.hidden/foo": { 52 | "sha256": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", 53 | "sha512": "cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063" 54 | }, 55 | "tests/test_runlib/hello./symbolic_to_nonparent_folder/.bar": { 56 | "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", 57 | "sha512": "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" 58 | }, 59 | "tests/test_runlib/hello./symbolic_to_nonparent_folder/foo": { 60 | "sha256": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", 61 | "sha512": "cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063" 62 | }, 63 | "tests/test_runlib/hello./world": { 64 | "sha256": "25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", 65 | "sha512": "9b589a5fc9f43c3279e3a184f78b62738794913d976d74cd72f862864b8bc09b61f882273e9a64cbfea53c18782583f4bdc842ace58411824a4b15cd0d0c6ff9" 66 | }, 67 | "tests/test_runlib/symbolic_to_file": { 68 | "sha256": "25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", 69 | "sha512": "9b589a5fc9f43c3279e3a184f78b62738794913d976d74cd72f862864b8bc09b61f882273e9a64cbfea53c18782583f4bdc842ace58411824a4b15cd0d0c6ff9" 70 | }, 71 | "tests/test_runlib/symbolic_to_license_file": { 72 | "sha256": "61ed40687d2656636a04680013dffe41d5c724201edaa84045e0677b8e2064d6", 73 | "sha512": "95df79b6a38f7e7c6b2c0393fcbc433b5d9f5f5b865467de992ff886965816bdf5e9f564390c9d38e3265396f338970a3335b1f3d6a67556e54100898af2e462" 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /examples/in_toto_run_test.rs: -------------------------------------------------------------------------------- 1 | use in_toto::crypto::PrivateKey; 2 | use in_toto::runlib::in_toto_run; 3 | 4 | const ED25519_1_PRIVATE_KEY: &'static [u8] = 5 | include_bytes!("../tests/ed25519/ed25519-1"); 6 | 7 | fn main() { 8 | let key = PrivateKey::from_ed25519(ED25519_1_PRIVATE_KEY).unwrap(); 9 | 10 | let link = in_toto_run( 11 | "example", 12 | Some("tests"), 13 | &["tests/test_runlib"], 14 | &["tests/test_runlib"], 15 | &["sh", "-c", "echo 'in_toto says hi' >> hello_intoto"], 16 | Some(&key), 17 | Some(&["sha512", "sha256"]), 18 | None, 19 | ) 20 | .unwrap(); 21 | let json = serde_json::to_value(&link).unwrap(); 22 | 23 | println!("Generated link: {json:#}") 24 | } 25 | -------------------------------------------------------------------------------- /examples/serialize_keys.rs: -------------------------------------------------------------------------------- 1 | use in_toto::crypto::{KeyType, PrivateKey, SignatureScheme}; 2 | use std::fs; 3 | use std::fs::OpenOptions; 4 | use std::io::prelude::*; 5 | use std::os::unix::fs::OpenOptionsExt; 6 | 7 | fn main() { 8 | // Generate a new Ed25519 signing key 9 | let key = PrivateKey::new(KeyType::Ed25519).unwrap(); 10 | let mut privkey = 11 | PrivateKey::from_pkcs8(&key, SignatureScheme::Ed25519).unwrap(); 12 | println!("Generated keypair {:?}", &privkey.public()); 13 | 14 | let mut target = OpenOptions::new() 15 | .mode(0o640) 16 | .write(true) 17 | .create(true) 18 | .open("test-key") 19 | .unwrap(); 20 | target.write_all(&key).unwrap(); 21 | 22 | let loaded_key = fs::read("test-key").unwrap(); 23 | privkey = 24 | PrivateKey::from_pkcs8(&loaded_key, SignatureScheme::Ed25519).unwrap(); 25 | 26 | println!("loaded keypair: {:?}", &privkey.public()) 27 | } 28 | -------------------------------------------------------------------------------- /examples/write_link.rs: -------------------------------------------------------------------------------- 1 | use in_toto::crypto::{KeyType, PrivateKey, SignatureScheme}; 2 | use in_toto::interchange::Json; 3 | use in_toto::models::{LinkMetadataBuilder, VirtualTargetPath}; 4 | use serde_json; 5 | 6 | fn main() { 7 | // Generate a new Ed25519 signing key 8 | let key = PrivateKey::new(KeyType::Ed25519).unwrap(); 9 | println!("Generated keypair: {:?}", key); 10 | let privkey = 11 | PrivateKey::from_pkcs8(&key, SignatureScheme::Ed25519).unwrap(); 12 | 13 | let link = LinkMetadataBuilder::new() 14 | .name(String::from("test")) 15 | .add_material(VirtualTargetPath::new("LICENSE".to_string()).unwrap()) 16 | .add_product(VirtualTargetPath::new("Makefile".to_string()).unwrap()) 17 | .signed::(&privkey) 18 | .unwrap(); 19 | 20 | let json = serde_json::to_value(&link).unwrap(); 21 | 22 | println!("Generated link: {}", json) 23 | } 24 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | max_width = 80 3 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types and converters. 2 | 3 | use data_encoding::DecodeError; 4 | use std::io; 5 | use std::path::Path; 6 | use std::str; 7 | use thiserror::Error; 8 | 9 | /// Error type for all in-toto related errors. 10 | #[derive(Error, Debug, PartialEq, Eq)] 11 | pub enum Error { 12 | /// The metadata had a bad signature. 13 | #[error("bad signature")] 14 | BadSignature, 15 | 16 | /// There was a problem encoding or decoding. 17 | #[error("encoding: {0}")] 18 | Encoding(String), 19 | 20 | /// An illegal argument was passed into a function. 21 | #[error("illegal argument: {0}")] 22 | IllegalArgument(String), 23 | 24 | /// There were no available hash algorithms. 25 | #[error("no supported hash algorithm")] 26 | NoSupportedHashAlgorithm, 27 | 28 | /// The metadata or target was not found. 29 | #[error("not found")] 30 | NotFound, 31 | 32 | /// Opaque error type, to be interpreted similar to HTTP 500. Something went wrong, and you may 33 | /// or may not be able to do anything about it. 34 | #[error("opaque: {0}")] 35 | Opaque(String), 36 | 37 | /// There was a library internal error. These errors are *ALWAYS* bugs and should be reported. 38 | #[error("programming: {0}")] 39 | Programming(String), 40 | 41 | /// The target is unavailable. This may mean it is either not in the metadata or the metadata 42 | /// chain to the target cannot be fully verified. 43 | #[error("target unavailable")] 44 | TargetUnavailable, 45 | 46 | /// There is no known or available hash algorithm. 47 | #[error("unknown hash algorithm: {0}")] 48 | UnknownHashAlgorithm(String), 49 | 50 | /// There is no known or available key type. 51 | #[error("unknown key type: {0}")] 52 | UnknownKeyType(String), 53 | 54 | /// The metadata or target failed to verify. 55 | #[error("verification failure: {0}")] 56 | VerificationFailure(String), 57 | 58 | #[error("prefix selection failure: {0}")] 59 | LinkGatheringError(String), 60 | 61 | #[error("do Pre-Authentication Encoding failed: {0}")] 62 | PAEParseFailed(String), 63 | 64 | #[error("runlib failed: {0}")] 65 | RunLibError(String), 66 | 67 | #[error("attestation state and predicate version dismatch: {0} and {1}")] 68 | AttestationFormatDismatch(String, String), 69 | 70 | #[error("convertion from string failed: {0}")] 71 | StringConvertFailed(String), 72 | 73 | #[error("artifact rule error: {0}")] 74 | ArtifactRuleError(String), 75 | } 76 | 77 | impl From for Error { 78 | fn from(err: serde_json::error::Error) -> Error { 79 | Error::Encoding(format!("JSON: {:?}", err)) 80 | } 81 | } 82 | 83 | impl Error { 84 | /// Helper to include the path that causd the error for FS I/O errors. 85 | pub fn from_io(err: &io::Error, path: &Path) -> Error { 86 | Error::Opaque(format!("Path {:?} : {:?}", path, err)) 87 | } 88 | } 89 | 90 | impl From for Error { 91 | fn from(err: io::Error) -> Error { 92 | match err.kind() { 93 | std::io::ErrorKind::NotFound => Error::NotFound, 94 | _ => Error::Opaque(format!("IO: {:?}", err)), 95 | } 96 | } 97 | } 98 | 99 | impl From for Error { 100 | fn from(err: DecodeError) -> Error { 101 | Error::Encoding(format!("{:?}", err)) 102 | } 103 | } 104 | 105 | impl From for Error { 106 | fn from(err: derp::Error) -> Error { 107 | Error::Encoding(format!("DER: {:?}", err)) 108 | } 109 | } 110 | 111 | impl From for Error { 112 | fn from(err: str::Utf8Error) -> Error { 113 | Error::Opaque(format!("Parse utf8: {:?}", err)) 114 | } 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use super::*; 120 | 121 | #[test] 122 | fn verify_io_error_display_string() { 123 | let err = Error::from(io::Error::from(std::io::ErrorKind::NotFound)); 124 | assert_eq!(err.to_string(), "not found"); 125 | assert_eq!(Error::NotFound.to_string(), "not found"); 126 | 127 | let err = 128 | Error::from(io::Error::from(std::io::ErrorKind::PermissionDenied)); 129 | assert_eq!(err.to_string(), "opaque: IO: Kind(PermissionDenied)"); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/format_hex.rs: -------------------------------------------------------------------------------- 1 | use data_encoding::HEXLOWER; 2 | use serde::{self, Deserialize, Deserializer, Serializer}; 3 | use std::result::Result; 4 | 5 | pub fn serialize(value: &[u8], serializer: S) -> Result 6 | where 7 | S: Serializer, 8 | { 9 | serializer.serialize_str(&HEXLOWER.encode(value)) 10 | } 11 | 12 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 13 | where 14 | D: Deserializer<'de>, 15 | { 16 | let s = String::deserialize(deserializer)?; 17 | HEXLOWER 18 | .decode(s.as_bytes()) 19 | .map_err(serde::de::Error::custom) 20 | } 21 | -------------------------------------------------------------------------------- /src/interchange/cjson/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | use serde::ser::Serialize; 3 | use std::collections::BTreeMap; 4 | use std::io::{Read, Write}; 5 | 6 | use crate::error::Error; 7 | use crate::interchange::DataInterchange; 8 | use crate::Result; 9 | 10 | pub(crate) mod pretty; 11 | pub(crate) mod shims; 12 | 13 | pub use pretty::JsonPretty; 14 | 15 | /// JSON data interchange. 16 | /// 17 | /// # Schema 18 | /// 19 | /// This doesn't use JSON Schema because that specification language is rage inducing. Here's 20 | /// something else instead. 21 | /// 22 | /// ## Common Entities 23 | /// 24 | /// `NATURAL_NUMBER` is an integer in the range `[1, 2**32)`. 25 | /// 26 | /// `EXPIRES` is an ISO-8601 date time in format `YYYY-MM-DD'T'hh:mm:ss'Z'`. 27 | /// 28 | /// `KEY_ID` is the hex encoded value of `sha256(cjson(pub_key))`. 29 | /// 30 | /// `PUB_KEY` is the following: 31 | /// 32 | /// ```bash 33 | /// { 34 | /// "type": KEY_TYPE, 35 | /// "scheme": SCHEME, 36 | /// "value": PUBLIC 37 | /// } 38 | /// ``` 39 | /// 40 | /// `PUBLIC` is a base64url encoded `SubjectPublicKeyInfo` DER public key. 41 | /// 42 | /// `KEY_TYPE` is a string (either `rsa` or `ed25519`). 43 | /// 44 | /// `SCHEME` is a string (either `ed25519`, `rsassa-pss-sha256`, or `rsassa-pss-sha512` 45 | /// 46 | /// `HASH_VALUE` is a hex encoded hash value. 47 | /// 48 | /// `SIG_VALUE` is a hex encoded signature value. 49 | /// 50 | /// `METADATA_DESCRIPTION` is the following: 51 | /// 52 | /// ```bash 53 | /// { 54 | /// "version": NATURAL_NUMBER, 55 | /// "length": NATURAL_NUMBER, 56 | /// "hashes": { 57 | /// HASH_ALGORITHM: HASH_VALUE 58 | /// ... 59 | /// } 60 | /// } 61 | /// ``` 62 | /// 63 | /// ## `Metablock` 64 | /// 65 | /// ```bash 66 | /// { 67 | /// "signatures": [SIGNATURE], 68 | /// "signed": SIGNED 69 | /// } 70 | /// ``` 71 | /// 72 | /// `SIGNATURE` is: 73 | /// 74 | /// ```bash 75 | /// { 76 | /// "keyid": KEY_ID, 77 | /// "signature": SIG_VALUE 78 | /// } 79 | /// ``` 80 | /// 81 | /// `SIGNED` is one of: 82 | /// 83 | /// - `RootMetadata` 84 | /// - `SnapshotMetadata` 85 | /// - `TargetsMetadata` 86 | /// - `TimestampMetadata` 87 | /// 88 | /// The the elements of `signatures` must have unique `key_id`s. 89 | /// 90 | /// ## `RootMetadata` 91 | /// 92 | /// ```bash 93 | /// { 94 | /// "_type": "root", 95 | /// "version": NATURAL_NUMBER, 96 | /// "expires": EXPIRES, 97 | /// "keys": [PUB_KEY, ...] 98 | /// "roles": { 99 | /// "root": ROLE_DESCRIPTION, 100 | /// "snapshot": ROLE_DESCRIPTION, 101 | /// "targets": ROLE_DESCRIPTION, 102 | /// "timestamp": ROLE_DESCRIPTION 103 | /// } 104 | /// } 105 | /// ``` 106 | /// 107 | /// `ROLE_DESCRIPTION` is the following: 108 | /// 109 | /// ```bash 110 | /// { 111 | /// "threshold": NATURAL_NUMBER, 112 | /// "keyids": [KEY_ID, ...] 113 | /// } 114 | /// ``` 115 | /// 116 | /// ## `SnapshotMetadata` 117 | /// 118 | /// ```bash 119 | /// { 120 | /// "_type": "snapshot", 121 | /// "version": NATURAL_NUMBER, 122 | /// "expires": EXPIRES, 123 | /// "meta": { 124 | /// META_PATH: METADATA_DESCRIPTION 125 | /// } 126 | /// } 127 | /// ``` 128 | /// 129 | /// `META_PATH` is a string. 130 | /// 131 | /// 132 | /// ## `TargetsMetadata` 133 | /// 134 | /// ```bash 135 | /// { 136 | /// "_type": "timestamp", 137 | /// "version": NATURAL_NUMBER, 138 | /// "expires": EXPIRES, 139 | /// "targets": { 140 | /// TARGET_PATH: TARGET_DESCRIPTION 141 | /// ... 142 | /// }, 143 | /// } 144 | /// ``` 145 | /// 146 | /// `ROLE` is a string, 147 | /// 148 | /// `PATH` is a string. 149 | /// 150 | /// ## `TimestampMetadata` 151 | /// 152 | /// ```bash 153 | /// { 154 | /// "_type": "timestamp", 155 | /// "version": NATURAL_NUMBER, 156 | /// "expires": EXPIRES, 157 | /// "snapshot": METADATA_DESCRIPTION 158 | /// } 159 | /// ``` 160 | #[derive(Debug, Clone, PartialEq, Eq)] 161 | pub struct Json; 162 | 163 | impl DataInterchange for Json { 164 | type RawData = serde_json::Value; 165 | 166 | /// ``` 167 | /// # use in_toto::interchange::{DataInterchange, Json}; 168 | /// assert_eq!(Json::extension(), "json"); 169 | /// ``` 170 | fn extension() -> &'static str { 171 | "json" 172 | } 173 | 174 | /// ``` 175 | /// # use in_toto::interchange::{DataInterchange, Json}; 176 | /// # use std::collections::HashMap; 177 | /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; 178 | /// let raw = Json::from_reader(jsn).unwrap(); 179 | /// let out = Json::canonicalize(&raw).unwrap(); 180 | /// assert_eq!(out, br#"{"baz":"quux","foo":"bar"}"#); 181 | /// ``` 182 | fn canonicalize(raw_data: &Self::RawData) -> Result> { 183 | canonicalize(raw_data).map_err(Error::Opaque) 184 | } 185 | 186 | /// ``` 187 | /// # use serde::Deserialize; 188 | /// # use serde_json::json; 189 | /// # use std::collections::HashMap; 190 | /// # use in_toto::interchange::{DataInterchange, Json}; 191 | /// # 192 | /// #[derive(Deserialize, Debug, PartialEq)] 193 | /// struct Thing { 194 | /// foo: String, 195 | /// bar: String, 196 | /// } 197 | /// 198 | /// # fn main() { 199 | /// let jsn = json!({"foo": "wat", "bar": "lol"}); 200 | /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; 201 | /// let de: Thing = Json::deserialize(&jsn).unwrap(); 202 | /// assert_eq!(de, thing); 203 | /// # } 204 | /// ``` 205 | fn deserialize(raw_data: &Self::RawData) -> Result 206 | where 207 | T: DeserializeOwned, 208 | { 209 | Ok(serde_json::from_value(raw_data.clone())?) 210 | } 211 | 212 | /// ``` 213 | /// # use serde::Serialize; 214 | /// # use serde_json::json; 215 | /// # use std::collections::HashMap; 216 | /// # use in_toto::interchange::{DataInterchange, Json}; 217 | /// # 218 | /// #[derive(Serialize)] 219 | /// struct Thing { 220 | /// foo: String, 221 | /// bar: String, 222 | /// } 223 | /// 224 | /// # fn main() { 225 | /// let jsn = json!({"foo": "wat", "bar": "lol"}); 226 | /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; 227 | /// let se: serde_json::Value = Json::serialize(&thing).unwrap(); 228 | /// assert_eq!(se, jsn); 229 | /// # } 230 | /// ``` 231 | fn serialize(data: &T) -> Result 232 | where 233 | T: Serialize, 234 | { 235 | Ok(serde_json::to_value(data)?) 236 | } 237 | 238 | /// ``` 239 | /// # use serde_json::json; 240 | /// # use in_toto::interchange::{DataInterchange, Json}; 241 | /// let json = json!({ 242 | /// "o": { 243 | /// "a": [1, 2, 3], 244 | /// "s": "string", 245 | /// "n": 123, 246 | /// "t": true, 247 | /// "f": false, 248 | /// "0": null, 249 | /// }, 250 | /// }); 251 | /// let mut buf = Vec::new(); 252 | /// Json::to_writer(&mut buf, &json).unwrap(); 253 | /// assert_eq!( 254 | /// &String::from_utf8(buf).unwrap(), 255 | /// r#"{"o":{"0":null,"a":[1,2,3],"f":false,"n":123,"s":"string","t":true}}"# 256 | /// ); 257 | /// ``` 258 | fn to_writer(mut writer: W, value: &T) -> Result<()> 259 | where 260 | W: Write, 261 | T: Serialize + Sized, 262 | { 263 | let bytes = Self::canonicalize(&Self::serialize(value)?)?; 264 | writer.write_all(&bytes)?; 265 | Ok(()) 266 | } 267 | 268 | /// ``` 269 | /// # use in_toto::interchange::{DataInterchange, Json}; 270 | /// # use std::collections::HashMap; 271 | /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; 272 | /// let _: HashMap = Json::from_reader(jsn).unwrap(); 273 | /// ``` 274 | fn from_reader(rdr: R) -> Result 275 | where 276 | R: Read, 277 | T: DeserializeOwned, 278 | { 279 | Ok(serde_json::from_reader(rdr)?) 280 | } 281 | 282 | /// ``` 283 | /// # use in_toto::interchange::{DataInterchange, Json}; 284 | /// # use std::collections::HashMap; 285 | /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; 286 | /// let _: HashMap = Json::from_slice(&jsn).unwrap(); 287 | /// ``` 288 | fn from_slice(slice: &[u8]) -> Result 289 | where 290 | T: DeserializeOwned, 291 | { 292 | Ok(serde_json::from_slice(slice)?) 293 | } 294 | } 295 | 296 | fn canonicalize( 297 | jsn: &serde_json::Value, 298 | ) -> std::result::Result, String> { 299 | let converted = convert(jsn)?; 300 | let mut buf = Vec::new(); 301 | let _ = converted.write(&mut buf); // Vec impl always succeeds (or panics). 302 | Ok(buf) 303 | } 304 | 305 | enum Value { 306 | Array(Vec), 307 | Bool(bool), 308 | Null, 309 | Number(Number), 310 | Object(BTreeMap), 311 | String(String), 312 | } 313 | 314 | impl Value { 315 | fn write(&self, buf: &mut Vec) -> std::result::Result<(), String> { 316 | match *self { 317 | Value::Null => { 318 | buf.extend(b"null"); 319 | Ok(()) 320 | } 321 | Value::Bool(true) => { 322 | buf.extend(b"true"); 323 | Ok(()) 324 | } 325 | Value::Bool(false) => { 326 | buf.extend(b"false"); 327 | Ok(()) 328 | } 329 | Value::Number(Number::I64(n)) => { 330 | let mut buffer = itoa::Buffer::new(); 331 | let txt = buffer.format(n); 332 | buf.extend(txt.as_bytes()); 333 | Ok(()) 334 | } 335 | Value::Number(Number::U64(n)) => { 336 | let mut buffer = itoa::Buffer::new(); 337 | let txt = buffer.format(n); 338 | buf.extend(txt.as_bytes()); 339 | Ok(()) 340 | } 341 | Value::String(ref s) => { 342 | // this mess is abusing serde_json to get json escaping 343 | let s = serde_json::Value::String(s.clone()); 344 | let s = serde_json::to_string(&s) 345 | .map_err(|e| format!("{:?}", e))?; 346 | buf.extend(s.as_bytes()); 347 | Ok(()) 348 | } 349 | Value::Array(ref arr) => { 350 | buf.push(b'['); 351 | let mut first = true; 352 | for a in arr.iter() { 353 | if !first { 354 | buf.push(b','); 355 | } 356 | a.write(buf)?; 357 | first = false; 358 | } 359 | buf.push(b']'); 360 | Ok(()) 361 | } 362 | Value::Object(ref obj) => { 363 | buf.push(b'{'); 364 | let mut first = true; 365 | for (k, v) in obj.iter() { 366 | if !first { 367 | buf.push(b','); 368 | } 369 | first = false; 370 | 371 | // this mess is abusing serde_json to get json escaping 372 | let k = serde_json::Value::String(k.clone()); 373 | let k = serde_json::to_string(&k) 374 | .map_err(|e| format!("{:?}", e))?; 375 | buf.extend(k.as_bytes()); 376 | 377 | buf.push(b':'); 378 | v.write(buf)?; 379 | } 380 | buf.push(b'}'); 381 | Ok(()) 382 | } 383 | } 384 | } 385 | } 386 | 387 | enum Number { 388 | I64(i64), 389 | U64(u64), 390 | } 391 | 392 | fn convert(jsn: &serde_json::Value) -> std::result::Result { 393 | match *jsn { 394 | serde_json::Value::Null => Ok(Value::Null), 395 | serde_json::Value::Bool(b) => Ok(Value::Bool(b)), 396 | serde_json::Value::Number(ref n) => n 397 | .as_i64() 398 | .map(Number::I64) 399 | .or_else(|| n.as_u64().map(Number::U64)) 400 | .map(Value::Number) 401 | .ok_or_else(|| String::from("only i64 and u64 are supported")), 402 | serde_json::Value::Array(ref arr) => { 403 | let mut out = Vec::new(); 404 | for res in arr.iter().map(convert) { 405 | out.push(res?) 406 | } 407 | Ok(Value::Array(out)) 408 | } 409 | serde_json::Value::Object(ref obj) => { 410 | let mut out = BTreeMap::new(); 411 | for (k, v) in obj.iter() { 412 | let _ = out.insert(k.clone(), convert(v)?); 413 | } 414 | Ok(Value::Object(out)) 415 | } 416 | serde_json::Value::String(ref s) => Ok(Value::String(s.clone())), 417 | } 418 | } 419 | 420 | #[cfg(test)] 421 | mod test { 422 | use super::*; 423 | 424 | #[test] 425 | fn write_str() { 426 | let jsn = Value::String(String::from("wat")); 427 | let mut out = Vec::new(); 428 | jsn.write(&mut out).unwrap(); 429 | assert_eq!(&out, b"\"wat\""); 430 | } 431 | 432 | #[test] 433 | fn write_arr() { 434 | let jsn = Value::Array(vec![ 435 | Value::String(String::from("wat")), 436 | Value::String(String::from("lol")), 437 | Value::String(String::from("no")), 438 | ]); 439 | let mut out = Vec::new(); 440 | jsn.write(&mut out).unwrap(); 441 | assert_eq!(&out, b"[\"wat\",\"lol\",\"no\"]"); 442 | } 443 | 444 | #[test] 445 | fn write_obj() { 446 | let mut map = BTreeMap::new(); 447 | let arr = Value::Array(vec![ 448 | Value::String(String::from("haha")), 449 | Value::String(String::from("new\nline")), 450 | ]); 451 | let _ = map.insert(String::from("lol"), arr); 452 | let jsn = Value::Object(map); 453 | let mut out = Vec::new(); 454 | jsn.write(&mut out).unwrap(); 455 | assert_eq!(&out, &b"{\"lol\":[\"haha\",\"new\\nline\"]}"); 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /src/interchange/cjson/pretty.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | use serde::ser::Serialize; 3 | use std::io::{Read, Write}; 4 | 5 | use super::Json; 6 | use crate::interchange::DataInterchange; 7 | use crate::Result; 8 | 9 | /// Pretty JSON data interchange. 10 | /// 11 | /// This is identical to [Json] in all manners except for the `to_writer` method. Instead of 12 | /// writing the metadata in the canonical format, it instead pretty prints the metadata. 13 | #[derive(Debug, Clone, PartialEq, Eq)] 14 | pub struct JsonPretty; 15 | 16 | impl DataInterchange for JsonPretty { 17 | type RawData = serde_json::Value; 18 | 19 | /// ``` 20 | /// # use in_toto::interchange::{DataInterchange, JsonPretty}; 21 | /// assert_eq!(JsonPretty::extension(), "json"); 22 | /// ``` 23 | fn extension() -> &'static str { 24 | Json::extension() 25 | } 26 | 27 | /// ``` 28 | /// # use in_toto::interchange::{DataInterchange, JsonPretty}; 29 | /// # use std::collections::HashMap; 30 | /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; 31 | /// let raw = JsonPretty::from_reader(jsn).unwrap(); 32 | /// let out = JsonPretty::canonicalize(&raw).unwrap(); 33 | /// assert_eq!(out, br#"{"baz":"quux","foo":"bar"}"#); 34 | /// ``` 35 | fn canonicalize(raw_data: &Self::RawData) -> Result> { 36 | Json::canonicalize(raw_data) 37 | } 38 | 39 | /// ``` 40 | /// # use serde::Deserialize; 41 | /// # use serde_json::json; 42 | /// # use std::collections::HashMap; 43 | /// # use in_toto::interchange::{DataInterchange, JsonPretty}; 44 | /// # 45 | /// #[derive(Deserialize, Debug, PartialEq)] 46 | /// struct Thing { 47 | /// foo: String, 48 | /// bar: String, 49 | /// } 50 | /// 51 | /// # fn main() { 52 | /// let jsn = json!({"foo": "wat", "bar": "lol"}); 53 | /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; 54 | /// let de: Thing = JsonPretty::deserialize(&jsn).unwrap(); 55 | /// assert_eq!(de, thing); 56 | /// # } 57 | /// ``` 58 | fn deserialize(raw_data: &Self::RawData) -> Result 59 | where 60 | T: DeserializeOwned, 61 | { 62 | Json::deserialize(raw_data) 63 | } 64 | 65 | /// ``` 66 | /// # use serde::Serialize; 67 | /// # use serde_json::json; 68 | /// # use std::collections::HashMap; 69 | /// # use in_toto::interchange::{DataInterchange, JsonPretty}; 70 | /// # 71 | /// #[derive(Serialize)] 72 | /// struct Thing { 73 | /// foo: String, 74 | /// bar: String, 75 | /// } 76 | /// 77 | /// # fn main() { 78 | /// let jsn = json!({"foo": "wat", "bar": "lol"}); 79 | /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; 80 | /// let se: serde_json::Value = JsonPretty::serialize(&thing).unwrap(); 81 | /// assert_eq!(se, jsn); 82 | /// # } 83 | /// ``` 84 | fn serialize(data: &T) -> Result 85 | where 86 | T: Serialize, 87 | { 88 | Json::serialize(data) 89 | } 90 | 91 | /// ``` 92 | /// # use serde_json::json; 93 | /// # use in_toto::interchange::{DataInterchange, JsonPretty}; 94 | /// let json = json!({ 95 | /// "o": { 96 | /// "a": [1, 2, 3], 97 | /// "s": "string", 98 | /// "n": 123, 99 | /// "t": true, 100 | /// "f": false, 101 | /// "0": null, 102 | /// }, 103 | /// }); 104 | /// let mut buf = Vec::new(); 105 | /// JsonPretty::to_writer(&mut buf, &json).unwrap(); 106 | /// assert_eq!(&String::from_utf8(buf).unwrap(), r#"{ 107 | /// "o": { 108 | /// "0": null, 109 | /// "a": [ 110 | /// 1, 111 | /// 2, 112 | /// 3 113 | /// ], 114 | /// "f": false, 115 | /// "n": 123, 116 | /// "s": "string", 117 | /// "t": true 118 | /// } 119 | /// }"#); 120 | /// ``` 121 | fn to_writer(writer: W, value: &T) -> Result<()> 122 | where 123 | W: Write, 124 | T: Serialize + Sized, 125 | { 126 | Ok(serde_json::to_writer_pretty( 127 | writer, 128 | &Self::serialize(value)?, 129 | )?) 130 | } 131 | 132 | /// ``` 133 | /// # use in_toto::interchange::{DataInterchange, JsonPretty}; 134 | /// # use std::collections::HashMap; 135 | /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; 136 | /// let _: HashMap = JsonPretty::from_reader(jsn).unwrap(); 137 | /// ``` 138 | fn from_reader(rdr: R) -> Result 139 | where 140 | R: Read, 141 | T: DeserializeOwned, 142 | { 143 | Json::from_reader(rdr) 144 | } 145 | 146 | /// ``` 147 | /// # use in_toto::interchange::{DataInterchange, JsonPretty}; 148 | /// # use std::collections::HashMap; 149 | /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; 150 | /// let _: HashMap = JsonPretty::from_slice(&jsn).unwrap(); 151 | /// ``` 152 | fn from_slice(slice: &[u8]) -> Result 153 | where 154 | T: DeserializeOwned, 155 | { 156 | Json::from_slice(slice) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/interchange/cjson/shims.rs: -------------------------------------------------------------------------------- 1 | // FIXME: imports will be relevant for layout expiration 2 | //use chrono::offset::Utc; 3 | //use chrono::prelude::*; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::crypto; 8 | 9 | #[derive(Serialize, Deserialize)] 10 | pub struct PublicKey { 11 | keytype: crypto::KeyType, 12 | scheme: crypto::SignatureScheme, 13 | #[serde(skip_serializing_if = "Option::is_none")] 14 | keyid_hash_algorithms: Option>, 15 | keyval: PublicKeyValue, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | keyid: Option, 18 | } 19 | 20 | impl PublicKey { 21 | pub fn new( 22 | keytype: crypto::KeyType, 23 | scheme: crypto::SignatureScheme, 24 | keyid_hash_algorithms: Option>, 25 | public_key: String, 26 | keyid: Option<&str>, 27 | private_key: Option<&str>, 28 | ) -> Self { 29 | PublicKey { 30 | keytype, 31 | scheme, 32 | keyid_hash_algorithms, 33 | keyval: PublicKeyValue { 34 | public: public_key, 35 | private: private_key.map(|k| k.to_string()), 36 | }, 37 | keyid: keyid.map(|id| id.to_string()), 38 | } 39 | } 40 | 41 | pub fn public_key(&self) -> &str { 42 | &self.keyval.public 43 | } 44 | 45 | pub fn scheme(&self) -> &crypto::SignatureScheme { 46 | &self.scheme 47 | } 48 | 49 | pub fn keytype(&self) -> &crypto::KeyType { 50 | &self.keytype 51 | } 52 | 53 | pub fn keyid_hash_algorithms(&self) -> &Option> { 54 | &self.keyid_hash_algorithms 55 | } 56 | } 57 | 58 | #[derive(Serialize, Deserialize)] 59 | pub struct PublicKeyValue { 60 | public: String, 61 | #[serde(skip_serializing_if = "Option::is_none")] 62 | private: Option, 63 | } 64 | -------------------------------------------------------------------------------- /src/interchange/mod.rs: -------------------------------------------------------------------------------- 1 | //! Structures and functions to aid in various in-toto data interchange formats. 2 | 3 | pub(crate) mod cjson; 4 | pub use cjson::{Json, JsonPretty}; 5 | 6 | use serde::de::DeserializeOwned; 7 | use serde::ser::Serialize; 8 | use std::fmt::Debug; 9 | use std::io::{Read, Write}; 10 | 11 | use crate::Result; 12 | 13 | /// The format used for data interchange, serialization, and deserialization. 14 | pub trait DataInterchange: Debug + PartialEq + Clone { 15 | /// The type of data that is contained in the `signed` portion of metadata. 16 | type RawData: Serialize + DeserializeOwned + Clone + PartialEq; 17 | 18 | /// The data interchange's extension. 19 | fn extension() -> &'static str; 20 | 21 | /// A function that canonicalizes data to allow for deterministic signatures. 22 | fn canonicalize(raw_data: &Self::RawData) -> Result>; 23 | 24 | /// Deserialize from `RawData`. 25 | fn deserialize(raw_data: &Self::RawData) -> Result 26 | where 27 | T: DeserializeOwned; 28 | 29 | /// Serialize into `RawData`. 30 | fn serialize(data: &T) -> Result 31 | where 32 | T: Serialize; 33 | 34 | /// Write a struct to a stream. 35 | #[allow(clippy::wrong_self_convention)] 36 | fn to_writer(writer: W, value: &T) -> Result<()> 37 | where 38 | W: Write, 39 | T: Serialize + Sized; 40 | 41 | /// Read a struct from a stream. 42 | fn from_reader(rdr: R) -> Result 43 | where 44 | R: Read, 45 | T: DeserializeOwned; 46 | 47 | /// Read a struct from a stream. 48 | fn from_slice(slice: &[u8]) -> Result 49 | where 50 | T: DeserializeOwned; 51 | } 52 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides an API for talking to repositories that implements in-toto 2 | 3 | //#![deny(missing_docs)] 4 | #![allow( 5 | clippy::collapsible_if, 6 | clippy::implicit_hasher, 7 | clippy::new_ret_no_self, 8 | clippy::op_ref, 9 | clippy::too_many_arguments, 10 | clippy::borrowed_box 11 | )] 12 | 13 | pub mod crypto; 14 | pub mod error; 15 | pub mod interchange; 16 | pub mod models; 17 | mod rulelib; 18 | pub mod runlib; 19 | pub mod verifylib; 20 | 21 | mod format_hex; 22 | 23 | pub use crate::error::*; 24 | 25 | /// Alias for `Result`. 26 | pub type Result = std::result::Result; 27 | -------------------------------------------------------------------------------- /src/models/envelope/envelope_file.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::interchange::DataInterchange; 4 | use crate::Result; 5 | use crate::{crypto::Signature, interchange::Json}; 6 | 7 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 8 | #[serde(deny_unknown_fields)] 9 | pub struct EnvelopeFile { 10 | payload: String, 11 | payload_type: String, 12 | signatures: Vec, 13 | } 14 | 15 | impl EnvelopeFile { 16 | #[allow(dead_code)] 17 | // TODO: remove #[allow(dead_code)] after metadata deploy 18 | pub fn new( 19 | payload: String, 20 | payload_type: String, 21 | signatures: Vec, 22 | ) -> Self { 23 | Self { 24 | payload, 25 | payload_type, 26 | signatures, 27 | } 28 | } 29 | 30 | /// standard serialize for EnvelopeFile 31 | #[allow(dead_code)] 32 | // TODO: remove #[allow(dead_code)] after metadata deploy 33 | pub fn to_bytes(&self) -> Result> { 34 | Json::canonicalize(&Json::serialize(self)?) 35 | } 36 | 37 | /// standard deserialize for EnvelopeFile 38 | #[allow(dead_code)] 39 | // TODO: remove #[allow(dead_code)] after metadata deploy 40 | pub fn from_bytes(bytes: &[u8]) -> Result { 41 | let ret: Self = serde_json::from_slice(bytes)?; 42 | Ok(ret) 43 | } 44 | 45 | #[allow(dead_code)] 46 | // TODO: remove #[allow(dead_code)] after metadata deploy 47 | pub fn signatures(&self) -> &Vec { 48 | &self.signatures 49 | } 50 | 51 | #[allow(dead_code)] 52 | // TODO: remove #[allow(dead_code)] after metadata deploy 53 | pub fn payload(&self) -> &String { 54 | &self.payload 55 | } 56 | 57 | #[allow(dead_code)] 58 | // TODO: remove #[allow(dead_code)] after metadata deploy 59 | pub fn payload_type(&self) -> &String { 60 | &self.payload_type 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod test_envelope_file { 66 | use std::str; 67 | 68 | use once_cell::sync::Lazy; 69 | 70 | use super::EnvelopeFile; 71 | // TODO: change to mock test use mockall 72 | use crate::crypto::Signature; 73 | 74 | pub struct EnvelopeFileTuple<'a> { 75 | name: String, 76 | payload: String, 77 | payload_type: String, 78 | signatures: Vec, 79 | packet: &'a str, 80 | } 81 | 82 | pub static SERIALIZE_DATAS: Lazy> = Lazy::new( 83 | || { 84 | vec![ 85 | EnvelopeFileTuple { 86 | name: "blank_test".to_string(), 87 | payload: "114514".to_string(), 88 | payload_type: "link".to_string(), 89 | signatures: Vec::new(), 90 | packet: "{\"payload\":\"114514\",\"payload_type\":\"link\",\"signatures\":[]}", 91 | }, 92 | EnvelopeFileTuple { 93 | name: "blank_test".to_string(), 94 | payload: "in-toto-rs".to_string(), 95 | payload_type: "https://in-toto.io/statement/v0.1".to_string(), 96 | signatures: Vec::new(), 97 | packet: "{\"payload\":\"in-toto-rs\",\"payload_type\":\"https://in-toto.io/statement/v0.1\",\"signatures\":[]}", 98 | }, 99 | ] 100 | }, 101 | ); 102 | 103 | #[test] 104 | fn serialize_link() { 105 | for item in SERIALIZE_DATAS.iter() { 106 | let envelope_file = EnvelopeFile::new( 107 | item.payload.clone(), 108 | item.payload_type.clone(), 109 | item.signatures.clone(), 110 | ); 111 | let bytes = envelope_file.to_bytes().unwrap(); 112 | 113 | let real = str::from_utf8(&bytes).unwrap(); 114 | let right = item.packet; 115 | assert_eq!(real, right, "Assert serialize unequal {}", item.name); 116 | } 117 | } 118 | 119 | #[test] 120 | fn deserialize_link() { 121 | for item in SERIALIZE_DATAS.iter() { 122 | let envelope_file = 123 | EnvelopeFile::from_bytes(item.packet.as_bytes()).unwrap(); 124 | assert_eq!( 125 | envelope_file.payload(), 126 | &item.payload, 127 | "Assert deserialize unequal {} for {} and {}", 128 | item.name, 129 | envelope_file.payload(), 130 | &item.payload 131 | ); 132 | assert_eq!( 133 | envelope_file.payload_type(), 134 | &item.payload_type, 135 | "Assert deserialize unequal {} for {} and {}", 136 | item.name, 137 | envelope_file.payload_type(), 138 | &item.payload_type, 139 | ); 140 | assert_eq!( 141 | envelope_file.signatures(), 142 | &item.signatures, 143 | "Assert deserialize unequal {} for {:?} and {:?}", 144 | item.name, 145 | envelope_file.signatures(), 146 | &item.signatures, 147 | ); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/models/envelope/mod.rs: -------------------------------------------------------------------------------- 1 | use strum::IntoEnumIterator; 2 | use strum_macros::EnumIter; 3 | 4 | use self::pae_v1::PaeV1; 5 | use crate::{Error, Result}; 6 | 7 | mod envelope_file; 8 | mod pae_v1; 9 | 10 | pub trait DSSEParser { 11 | fn pae_pack(payload_ver: String, payload: &[u8]) -> Vec; 12 | fn pae_unpack(bytes: &[u8]) -> Result<(Vec, String)>; 13 | } 14 | 15 | /// DSSE global packer and unpacker. 16 | #[derive(EnumIter, PartialEq, Eq, Hash, Clone, Copy)] 17 | pub enum DSSEVersion { 18 | V1, 19 | } 20 | 21 | impl DSSEVersion { 22 | /// Use Pre-Authentication Encoding to pack payload for any version. 23 | #[allow(dead_code)] 24 | // TODO: remove #[allow(dead_code)] after metadata deploy 25 | pub fn pack(&self, payload: &[u8], payload_ver: String) -> Vec { 26 | let payload = payload.to_vec(); 27 | 28 | match self { 29 | DSSEVersion::V1 => PaeV1::pae_pack(payload_ver, &payload), 30 | } 31 | } 32 | 33 | /// Use Pre-Authentication Encoding to unpack payload for any version. 34 | pub fn unpack(&self, bytes: &[u8]) -> Result<(Vec, String)> { 35 | // Note: if two of the versions is compatible with each other 36 | // `try_unpack` may failed to find the right version. 37 | let (payload, payload_ver) = match self { 38 | DSSEVersion::V1 => PaeV1::pae_unpack(bytes)?, 39 | }; 40 | Ok((payload, payload_ver)) 41 | } 42 | 43 | /// Use Pre-Authentication Encoding to auto unpack a possible version. 44 | #[allow(dead_code)] 45 | // TODO: remove #[allow(dead_code)] after metadata deploy 46 | pub fn try_unpack(bytes: &[u8]) -> Result<(Vec, String)> { 47 | let mut file: Result<(Vec, String)> = 48 | Err(Error::Programming("no available DSSE parser".to_string())); 49 | for method in DSSEVersion::iter() { 50 | file = method.unpack(bytes); 51 | if file.is_ok() { 52 | break; 53 | } 54 | } 55 | file 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod pae_test { 61 | use once_cell::sync::Lazy; 62 | 63 | pub struct EnvelopeFileTuple { 64 | pub(crate) name: String, 65 | pub(crate) inner_payload: &'static str, 66 | pub(crate) payload_ver: String, 67 | } 68 | 69 | pub static SERIALIZE_SRC_DATAS: Lazy> = Lazy::new( 70 | || { 71 | vec![ 72 | EnvelopeFileTuple { 73 | name: "blank_test".to_string(), 74 | inner_payload: "", 75 | payload_ver: "link".to_string(), 76 | }, 77 | EnvelopeFileTuple { 78 | name: "blank_envelope_naive_test".to_string(), 79 | inner_payload: "{\"payload\":[],\"payload_type\":\"link\",\"signatures\":[]}", 80 | payload_ver: "link".to_string(), 81 | }, 82 | EnvelopeFileTuple { 83 | name: "blank_envelope_v01_test".to_string(), 84 | inner_payload: "{\"payload\":[],\"payload_type\":\"link\",\"signatures\":[]}", 85 | payload_ver: "https://in-toto.io/statement/v0.1".to_string(), 86 | }, 87 | ] 88 | }, 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/models/envelope/pae_v1.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | use super::DSSEParser; 4 | use crate::Error; 5 | use crate::Result; 6 | 7 | const PREFIX: &str = "DSSEv1"; 8 | const SPLIT: &str = " "; 9 | const SPLIT_U8: u8 = 0x20; 10 | 11 | pub struct PaeV1; 12 | 13 | /// Extract length and payload from bytes and consume them 14 | /// 15 | /// length, SPLIT, next -> (length, next) 16 | fn consume_load_len(raw: &[u8]) -> Result<(usize, &[u8])> { 17 | let mut iter = raw.splitn(2, |num| *num == SPLIT_U8); 18 | let length_raw = iter.next().ok_or_else(|| { 19 | Error::PAEParseFailed(format!( 20 | "split '{}' failed for {:?}", 21 | SPLIT, 22 | raw.to_owned() 23 | )) 24 | })?; 25 | 26 | let length = 27 | str::from_utf8(length_raw)?.parse::().map_err(|_| { 28 | Error::PAEParseFailed(format!( 29 | "parse to int failed for {:?}", 30 | length_raw 31 | )) 32 | })?; 33 | let next = iter.next().ok_or_else(|| { 34 | Error::PAEParseFailed(format!( 35 | "prefix {} strip failed for {:?}", 36 | PREFIX, 37 | raw.to_owned() 38 | )) 39 | })?; 40 | Ok((length, next)) 41 | } 42 | 43 | impl DSSEParser for PaeV1 { 44 | /// Use Pre-Authentication Encoding to pack payload for DSSE v1. 45 | fn pae_pack(payload_ver: String, payload: &[u8]) -> Vec { 46 | let sig_header: String = format!( 47 | "{prefix}{split}{payload_ver_len}{split}{payload_ver}{split}{payload_len}{split}", 48 | prefix = PREFIX, 49 | split = SPLIT, 50 | payload_ver_len = payload_ver.len(), 51 | payload_ver = payload_ver.as_str(), 52 | payload_len = payload.len(), 53 | ); 54 | let sig = [sig_header.as_bytes(), payload].concat(); 55 | sig 56 | } 57 | 58 | /// Use Pre-Authentication Encoding to unpack payload for DSSE v1. 59 | fn pae_unpack(bytes: &[u8]) -> Result<(Vec, String)> { 60 | // Strip prefix + split "DSSEv1 " 61 | let raw = bytes 62 | .strip_prefix(format!("{}{}", PREFIX, SPLIT).as_bytes()) 63 | .ok_or_else(|| { 64 | Error::PAEParseFailed(format!( 65 | "prefix {} strip failed for {:?}", 66 | PREFIX, 67 | bytes.to_owned() 68 | )) 69 | })?; 70 | 71 | // Extract payload_ver from bytes 72 | let (payload_ver_len, raw) = consume_load_len(raw)?; 73 | let payload_ver = str::from_utf8(&raw[0..payload_ver_len])? 74 | .parse::() 75 | .map_err(|_| { 76 | Error::PAEParseFailed(format!( 77 | "parse to string failed for {:?}", 78 | raw 79 | )) 80 | })?; 81 | 82 | // Extract payload from bytes 83 | let (payload_len, raw) = 84 | consume_load_len(&raw[(payload_ver_len + 1)..])?; 85 | let payload = raw[0..payload_len].to_vec(); 86 | 87 | Ok((payload, payload_ver)) 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod pae_test { 93 | use std::collections::HashMap; 94 | use std::str; 95 | 96 | use once_cell::sync::Lazy; 97 | 98 | use crate::models::envelope::{pae_test::SERIALIZE_SRC_DATAS, DSSEVersion}; 99 | 100 | static SERIALIZE_RESULT_DATAS: Lazy> = Lazy::new( 101 | || { 102 | let real_serialized_file = HashMap::from([ 103 | ("blank_test".to_string(), "DSSEv1 4 link 0 "), 104 | ( 105 | "blank_envelope_naive_test".to_string(), 106 | "DSSEv1 4 link 52 {\"payload\":[],\"payload_type\":\"link\",\"signatures\":[]}", 107 | ), 108 | ( 109 | "blank_envelope_v01_test".to_string(), 110 | "DSSEv1 33 https://in-toto.io/statement/v0.1 52 {\"payload\":[],\"payload_type\":\"link\",\"signatures\":[]}", 111 | ), 112 | ]); 113 | real_serialized_file 114 | }, 115 | ); 116 | 117 | #[test] 118 | fn test_pack() { 119 | for file_tuple in SERIALIZE_SRC_DATAS.iter() { 120 | let outer = DSSEVersion::V1.pack( 121 | file_tuple.inner_payload.as_bytes(), 122 | file_tuple.payload_ver.to_owned(), 123 | ); 124 | 125 | let real = std::str::from_utf8(&outer).unwrap(); 126 | let right = *SERIALIZE_RESULT_DATAS.get(&file_tuple.name).unwrap(); 127 | assert_eq!( 128 | real, right, 129 | "pack assert failed for {}", 130 | file_tuple.name 131 | ); 132 | } 133 | } 134 | 135 | #[test] 136 | fn test_unpack() { 137 | for file_tuple in SERIALIZE_SRC_DATAS.iter() { 138 | let outer = SERIALIZE_RESULT_DATAS 139 | .get(&file_tuple.name) 140 | .unwrap() 141 | .as_bytes() 142 | .to_vec(); 143 | 144 | let (inner, _) = DSSEVersion::V1.unpack(&outer).unwrap(); 145 | let real = str::from_utf8(&inner).unwrap(); 146 | let right = file_tuple.inner_payload; 147 | 148 | assert_eq!( 149 | real, right, 150 | "unpack assert failed for {}", 151 | file_tuple.name 152 | ); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/models/helpers.rs: -------------------------------------------------------------------------------- 1 | //! Supporting Functions and Types (VirtualTargetPath) 2 | use std::collections::HashMap; 3 | use std::fmt; 4 | use std::fmt::Debug; 5 | use std::str; 6 | 7 | use serde::de::{Deserialize, Deserializer, Error as DeserializeError}; 8 | use serde::Serialize; 9 | 10 | use crate::crypto::{HashAlgorithm, HashValue}; 11 | use crate::{Error, Result}; 12 | 13 | /// Description of a target, used in verification. 14 | pub type TargetDescription = HashMap; 15 | 16 | /// Wrapper for the Virtual path to a target. 17 | #[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord, Serialize)] 18 | pub struct VirtualTargetPath(String); 19 | 20 | impl VirtualTargetPath { 21 | /// Create a new `VirtualTargetPath` from a `String`. 22 | /// 23 | pub fn new(path: String) -> Result { 24 | Ok(VirtualTargetPath(path)) 25 | } 26 | 27 | /// The string value of the path. 28 | pub fn value(&self) -> &str { 29 | &self.0 30 | } 31 | 32 | /// Judge if this [`VirtualTargetPath`] matches the given pattern 33 | pub(crate) fn matches(&self, pattern: &str) -> Result { 34 | let matcher = glob::Pattern::new(pattern).map_err(|e| { 35 | Error::IllegalArgument(format!( 36 | "Pattern matcher creation failed: {}", 37 | e 38 | )) 39 | })?; 40 | Ok(matcher.matches(self.value())) 41 | } 42 | } 43 | 44 | impl fmt::Display for VirtualTargetPath { 45 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 46 | write!(f, "{}", self.0) 47 | } 48 | } 49 | 50 | impl From<&str> for VirtualTargetPath { 51 | fn from(s: &str) -> Self { 52 | Self(s.to_string()) 53 | } 54 | } 55 | 56 | impl AsRef for VirtualTargetPath { 57 | fn as_ref(&self) -> &str { 58 | &self.0 59 | } 60 | } 61 | 62 | impl<'de> Deserialize<'de> for VirtualTargetPath { 63 | fn deserialize>( 64 | de: D, 65 | ) -> ::std::result::Result { 66 | let s: String = Deserialize::deserialize(de)?; 67 | VirtualTargetPath::new(s) 68 | .map_err(|e| DeserializeError::custom(format!("{:?}", e))) 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use crate::models::VirtualTargetPath; 75 | 76 | #[test] 77 | fn serialize_virtual_target_path() { 78 | let path = VirtualTargetPath::from("foo.py"); 79 | let serialized = 80 | serde_json::to_string(&path).expect("serialize failed"); 81 | let expected = "\"foo.py\""; 82 | assert!(serialized == expected); 83 | } 84 | 85 | #[test] 86 | fn deserialize_virtual_target_path() { 87 | let path = VirtualTargetPath::from("foo.py"); 88 | let deserialized: VirtualTargetPath = 89 | serde_json::from_str("\"foo.py\"").expect("serialize failed"); 90 | assert!(path == deserialized); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/models/layout/inspection.rs: -------------------------------------------------------------------------------- 1 | //! in-toto layout's Inspection 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::supply_chain_item_derive; 6 | 7 | use super::{ 8 | rule::ArtifactRule, step::Command, supply_chain_item::SupplyChainItem, 9 | }; 10 | 11 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 12 | pub struct Inspection { 13 | #[serde(rename = "_type")] 14 | pub typ: String, 15 | #[serde(rename = "name")] 16 | pub name: String, 17 | pub expected_materials: Vec, 18 | pub expected_products: Vec, 19 | pub run: Command, 20 | } 21 | 22 | impl Inspection { 23 | pub fn new(name: &str) -> Self { 24 | Inspection { 25 | run: Command::default(), 26 | name: name.into(), 27 | expected_materials: Vec::new(), 28 | expected_products: Vec::new(), 29 | typ: "inspection".into(), 30 | } 31 | } 32 | 33 | /// Set expected command for this Inspection 34 | pub fn run(mut self, command: Command) -> Self { 35 | self.run = command; 36 | self 37 | } 38 | 39 | // Derive operations on `materials`/`products` and `name` 40 | supply_chain_item_derive!(); 41 | } 42 | 43 | impl SupplyChainItem for Inspection { 44 | fn name(&self) -> &str { 45 | &self.name 46 | } 47 | 48 | fn expected_materials(&self) -> &Vec { 49 | &self.expected_materials 50 | } 51 | 52 | fn expected_products(&self) -> &Vec { 53 | &self.expected_products 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod test { 59 | use assert_json_diff::assert_json_eq; 60 | use serde_json::json; 61 | 62 | use super::Inspection; 63 | use crate::models::rule::test::{ 64 | generate_materials_rule, generate_products_rule, 65 | }; 66 | 67 | #[test] 68 | fn serialize_inspection() { 69 | let json = json!({ 70 | "_type": "inspection", 71 | "name": "test_inspect", 72 | "expected_materials" : [ 73 | [ 74 | "MATCH", 75 | "pattern/", 76 | "IN", 77 | "src", 78 | "WITH", 79 | "MATERIALS", 80 | "IN", 81 | "dst", 82 | "FROM", 83 | "test_step" 84 | ] 85 | ], 86 | "expected_products" : [ 87 | [ 88 | "MATCH", 89 | "pattern/", 90 | "IN", 91 | "src", 92 | "WITH", 93 | "PRODUCTS", 94 | "IN", 95 | "dst", 96 | "FROM", 97 | "test_step" 98 | ] 99 | ], 100 | "run" : ["ls", "-al"] 101 | }); 102 | let inspection = Inspection::new("test_inspect") 103 | .add_expected_material(generate_materials_rule()) 104 | .add_expected_product(generate_products_rule()) 105 | .run("ls -al".into()); 106 | 107 | let json_serialized = serde_json::to_value(&inspection).unwrap(); 108 | assert_json_eq!(json, json_serialized); 109 | } 110 | 111 | #[test] 112 | fn deserialize_inspection() { 113 | let json = r#"{ 114 | "_type": "inspection", 115 | "name": "test_inspect", 116 | "expected_materials" : [ 117 | [ 118 | "MATCH", 119 | "pattern/", 120 | "IN", 121 | "src", 122 | "WITH", 123 | "MATERIALS", 124 | "IN", 125 | "dst", 126 | "FROM", 127 | "test_step" 128 | ] 129 | ], 130 | "expected_products" : [ 131 | [ 132 | "MATCH", 133 | "pattern/", 134 | "IN", 135 | "src", 136 | "WITH", 137 | "PRODUCTS", 138 | "IN", 139 | "dst", 140 | "FROM", 141 | "test_step" 142 | ] 143 | ], 144 | "run" : ["ls", "-al"] 145 | }"#; 146 | 147 | let inspection = Inspection::new("test_inspect") 148 | .add_expected_material(generate_materials_rule()) 149 | .add_expected_product(generate_products_rule()) 150 | .run("ls -al".into()); 151 | let inspection_parsed: Inspection = serde_json::from_str(json).unwrap(); 152 | assert_eq!(inspection_parsed, inspection); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/models/layout/metadata.rs: -------------------------------------------------------------------------------- 1 | //! in-toto layoput metadata. 2 | 3 | use chrono::{DateTime, Duration, Utc}; 4 | use serde::de::{Deserialize, Deserializer, Error as DeserializeError}; 5 | use serde::ser::{Error as SerializeError, Serialize, Serializer}; 6 | 7 | use std::collections::HashMap; 8 | 9 | use crate::crypto::KeyId; 10 | use crate::crypto::PublicKey; 11 | use crate::interchange::{DataInterchange, Json}; 12 | use crate::models::{Metadata, MetadataType, MetadataWrapper}; 13 | use crate::Result; 14 | 15 | use super::Layout; 16 | use super::{inspection::Inspection, step::Step}; 17 | 18 | /// Helper to construct `LayoutMetadata` 19 | pub struct LayoutMetadataBuilder { 20 | expires: DateTime, 21 | readme: String, 22 | keys: HashMap, 23 | steps: Vec, 24 | inspect: Vec, 25 | } 26 | 27 | impl Default for LayoutMetadataBuilder { 28 | fn default() -> Self { 29 | LayoutMetadataBuilder::new() 30 | } 31 | } 32 | 33 | impl LayoutMetadataBuilder { 34 | /// Create a new `LayoutMetadataBuilder`. It defaults to: 35 | /// 36 | /// * expires: 365 days from the current time. 37 | /// * readme: "" 38 | pub fn new() -> Self { 39 | LayoutMetadataBuilder { 40 | steps: Vec::new(), 41 | inspect: Vec::new(), 42 | keys: HashMap::new(), 43 | expires: Utc::now() + Duration::days(365), 44 | readme: String::new(), 45 | } 46 | } 47 | 48 | /// Set expire time for this layout 49 | pub fn expires(mut self, expires: DateTime) -> Self { 50 | self.expires = expires; 51 | self 52 | } 53 | 54 | /// Set readme field fot this layout 55 | pub fn readme(mut self, readme: String) -> Self { 56 | self.readme = readme; 57 | self 58 | } 59 | 60 | /// Add new step to this layout 61 | pub fn add_step(mut self, step: Step) -> Self { 62 | self.steps.push(step); 63 | self 64 | } 65 | 66 | /// Add new steps to this layout 67 | pub fn add_steps(mut self, mut steps: Vec) -> Self { 68 | self.steps.append(&mut steps); 69 | self 70 | } 71 | 72 | /// Set steps to this layout 73 | pub fn steps(mut self, steps: Vec) -> Self { 74 | self.steps = steps; 75 | self 76 | } 77 | 78 | /// Add new inspect to this layout 79 | pub fn add_inspect(mut self, inspect: Inspection) -> Self { 80 | self.inspect.push(inspect); 81 | self 82 | } 83 | 84 | /// Add new inspects to this layout 85 | pub fn add_inspects(mut self, mut inspects: Vec) -> Self { 86 | self.inspect.append(&mut inspects); 87 | self 88 | } 89 | 90 | /// Set inspects to this layout 91 | pub fn inspects(mut self, step: Vec) -> Self { 92 | self.inspect = step; 93 | self 94 | } 95 | 96 | /// Add a new pubkey to this layout 97 | pub fn add_key(mut self, key: PublicKey) -> Self { 98 | self.keys.insert(key.key_id().clone(), key); 99 | self 100 | } 101 | 102 | pub fn build(self) -> Result { 103 | Ok(LayoutMetadata::new( 104 | self.expires, 105 | self.readme, 106 | self.keys, 107 | self.steps, 108 | self.inspect, 109 | )) 110 | } 111 | } 112 | 113 | /// layout metadata 114 | #[derive(Debug, Clone, PartialEq, Eq)] 115 | pub struct LayoutMetadata { 116 | pub steps: Vec, 117 | pub inspect: Vec, 118 | pub keys: HashMap, 119 | pub expires: DateTime, 120 | pub readme: String, 121 | } 122 | 123 | impl LayoutMetadata { 124 | pub fn new( 125 | expires: DateTime, 126 | readme: String, 127 | keys: HashMap, 128 | steps: Vec, 129 | inspect: Vec, 130 | ) -> Self { 131 | LayoutMetadata { 132 | steps, 133 | inspect, 134 | keys, 135 | expires, 136 | readme, 137 | } 138 | } 139 | } 140 | 141 | impl Metadata for LayoutMetadata { 142 | fn typ(&self) -> MetadataType { 143 | MetadataType::Layout 144 | } 145 | 146 | fn into_enum(self: Box) -> MetadataWrapper { 147 | MetadataWrapper::Layout(*self) 148 | } 149 | 150 | fn to_bytes(&self) -> Result> { 151 | Json::canonicalize(&Json::serialize(self)?) 152 | } 153 | } 154 | 155 | impl Serialize for LayoutMetadata { 156 | fn serialize(&self, ser: S) -> ::std::result::Result 157 | where 158 | S: Serializer, 159 | { 160 | Layout::from(self) 161 | .map_err(|e| SerializeError::custom(format!("{:?}", e)))? 162 | .serialize(ser) 163 | } 164 | } 165 | 166 | impl<'de> Deserialize<'de> for LayoutMetadata { 167 | fn deserialize>( 168 | de: D, 169 | ) -> ::std::result::Result { 170 | let intermediate: Layout = Deserialize::deserialize(de)?; 171 | intermediate 172 | .try_into() 173 | .map_err(|e| DeserializeError::custom(format!("{:?}", e))) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/models/layout/mod.rs: -------------------------------------------------------------------------------- 1 | //! in-toto layout: used by the project owner to generate a desired supply chain layout file. 2 | 3 | use std::collections::BTreeMap; 4 | 5 | use chrono::prelude::*; 6 | use chrono::{DateTime, Utc}; 7 | use log::warn; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::crypto::{KeyId, PublicKey}; 11 | use crate::{Error, Result}; 12 | 13 | use self::{inspection::Inspection, step::Step}; 14 | 15 | pub mod inspection; 16 | mod metadata; 17 | pub mod rule; 18 | pub mod step; 19 | pub mod supply_chain_item; 20 | 21 | pub use metadata::{LayoutMetadata, LayoutMetadataBuilder}; 22 | 23 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 24 | pub struct Layout { 25 | #[serde(rename = "_type")] 26 | typ: String, 27 | expires: String, 28 | readme: String, 29 | keys: BTreeMap, 30 | steps: Vec, 31 | inspect: Vec, 32 | } 33 | 34 | impl Layout { 35 | pub fn from(meta: &LayoutMetadata) -> Result { 36 | Ok(Layout { 37 | typ: String::from("layout"), 38 | expires: format_datetime(&meta.expires), 39 | readme: meta.readme.to_string(), 40 | keys: meta 41 | .keys 42 | .iter() 43 | .map(|(id, key)| (id.clone(), key.clone())) 44 | .collect(), 45 | steps: meta.steps.clone(), 46 | inspect: meta.inspect.clone(), 47 | }) 48 | } 49 | 50 | pub fn try_into(self) -> Result { 51 | // Ignore all keys with incorrect key IDs. 52 | // If a malformed key is used, there will be a warning 53 | let keys_with_correct_key_id = self 54 | .keys 55 | .into_iter() 56 | .filter(|(key_id, pkey)| { 57 | if key_id == pkey.key_id() { 58 | true 59 | } else { 60 | warn!("Malformed key of ID {:?}", key_id); 61 | false 62 | } 63 | }) 64 | .collect(); 65 | 66 | Ok(LayoutMetadata::new( 67 | parse_datetime(&self.expires)?, 68 | self.readme, 69 | keys_with_correct_key_id, 70 | self.steps, 71 | self.inspect, 72 | )) 73 | } 74 | } 75 | 76 | fn parse_datetime(ts: &str) -> Result> { 77 | let dt = DateTime::parse_from_rfc3339(ts).map_err(|e| { 78 | Error::Encoding(format!("Can't parse DateTime: {:?}", e)) 79 | })?; 80 | Ok(dt.with_timezone(&Utc)) 81 | } 82 | 83 | fn format_datetime(ts: &DateTime) -> String { 84 | ts.to_rfc3339_opts(SecondsFormat::Secs, true) 85 | } 86 | 87 | #[cfg(test)] 88 | mod test { 89 | use assert_json_diff::assert_json_eq; 90 | use chrono::DateTime; 91 | use serde_json::json; 92 | 93 | use crate::{crypto::PublicKey, models::layout::format_datetime}; 94 | 95 | use super::{ 96 | inspection::Inspection, 97 | parse_datetime, 98 | rule::{Artifact, ArtifactRule}, 99 | step::Step, 100 | Layout, LayoutMetadataBuilder, 101 | }; 102 | 103 | const ALICE_PUB_KEY: &'static [u8] = 104 | include_bytes!("../../../tests/ed25519/ed25519-1.pub"); 105 | const BOB_PUB_KEY: &'static [u8] = 106 | include_bytes!("../../../tests/rsa/rsa-4096.spki.der"); 107 | 108 | #[test] 109 | fn parse_datetime_test() { 110 | let time_str = "1970-01-01T00:00:00Z".to_string(); 111 | let parsed_dt = parse_datetime(&time_str[..]).unwrap(); 112 | let dt = DateTime::UNIX_EPOCH; 113 | assert_eq!(parsed_dt, dt); 114 | } 115 | 116 | #[test] 117 | fn format_datetime_test() { 118 | let dt = DateTime::UNIX_EPOCH; 119 | let generated_dt_str = format_datetime(&dt); 120 | let dt_str = "1970-01-01T00:00:00Z".to_string(); 121 | assert_eq!(dt_str, generated_dt_str); 122 | } 123 | 124 | fn get_example_layout_metadata() -> Layout { 125 | let alice_key = PublicKey::from_ed25519(ALICE_PUB_KEY).unwrap(); 126 | let bob_key = PublicKey::from_spki( 127 | BOB_PUB_KEY, 128 | crate::crypto::SignatureScheme::RsaSsaPssSha256, 129 | ) 130 | .unwrap(); 131 | let metadata = LayoutMetadataBuilder::new() 132 | .expires(DateTime::UNIX_EPOCH) 133 | .add_key(alice_key.clone()) 134 | .add_key(bob_key.clone()) 135 | .add_step( 136 | Step::new("write-code") 137 | .threshold(1) 138 | .add_expected_product(ArtifactRule::Create("foo.py".into())) 139 | .add_key(alice_key.key_id().to_owned()) 140 | .expected_command("vi".into()), 141 | ) 142 | .add_step( 143 | Step::new("package") 144 | .threshold(1) 145 | .add_expected_material(ArtifactRule::Match { 146 | pattern: "foo.py".into(), 147 | in_src: None, 148 | with: Artifact::Products, 149 | in_dst: None, 150 | from: "write-code".into(), 151 | }) 152 | .add_expected_product(ArtifactRule::Create( 153 | "foo.tar.gz".into(), 154 | )) 155 | .add_key(bob_key.key_id().to_owned()) 156 | .expected_command("tar zcvf foo.tar.gz foo.py".into()), 157 | ) 158 | .add_inspect( 159 | Inspection::new("inspect_tarball") 160 | .add_expected_material(ArtifactRule::Match { 161 | pattern: "foo.tar.gz".into(), 162 | in_src: None, 163 | with: Artifact::Products, 164 | in_dst: None, 165 | from: "package".into(), 166 | }) 167 | .add_expected_product(ArtifactRule::Match { 168 | pattern: "foo.py".into(), 169 | in_src: None, 170 | with: Artifact::Products, 171 | in_dst: None, 172 | from: "write-code".into(), 173 | }) 174 | .run("inspect_tarball.sh foo.tar.gz".into()), 175 | ) 176 | .readme("".into()) 177 | .build() 178 | .unwrap(); 179 | Layout::from(&metadata).unwrap() 180 | } 181 | 182 | #[test] 183 | fn serialize_layout() { 184 | let layout = get_example_layout_metadata(); 185 | let json = json!({ 186 | "_type": "layout", 187 | "expires": "1970-01-01T00:00:00Z", 188 | "readme": "", 189 | "keys": { 190 | "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": { 191 | "keyid": "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf", 192 | "keytype": "rsa", 193 | "scheme": "rsassa-pss-sha256", 194 | "keyid_hash_algorithms": [ 195 | "sha256", 196 | "sha512" 197 | ], 198 | "keyval": { 199 | "private": "", 200 | "public": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA91+6CJmBzrb6ODSXPvVK\nh9IVvDkD63d5/wHawj1ZB22Y0R7A7b8lRl7IqJJ3TcZO8W2zFfeRuPFlghQs+O7h\nA6XiRr4mlD1dLItk+p93E0vgY+/Jj4I09LObgA2ncGw/bUlYt3fB5tbmnojQyhrQ\nwUQvBxOqI3nSglg02mCdQRWpPzerGxItOIQkmU2TsqTg7TZ8lnSUbAsFuMebnA2d\nJ2hzeou7ZGsyCJj/6O0ORVF37nLZiOFF8EskKVpUJuoLWopEA2c09YDgFWHEPTIo\nGNWB2l/qyX7HTk1wf+WK/Wnn3nerzdEhY9dH+U0uH7tOBBVCyEKxUqXDGpzuLSxO\nGBpJXa3TTqLHJWIOzhIjp5J3rV93aeSqemU38KjguZzdwOMO5lRsFco5gaFS9aNL\nLXtLd4ZgXaxB3vYqFDhvZCx4IKrsYEc/Nr8ubLwyQ8WHeS7v8FpIT7H9AVNDo9BM\nZpnmdTc5Lxi15/TulmswIIgjDmmIqujUqyHN27u7l6bZJlcn8lQdYMm4eJr2o+Jt\ndloTwm7Cv/gKkhZ5tdO5c/219UYBnKaGF8No1feEHirm5mdvwpngCxdFMZMbfmUA\nfzPeVPkXE+LR0lsLGnMlXKG5vKFcQpCXW9iwJ4pZl7j12wLwiWyLDQtsIxiG6Sds\nALPkWf0mnfBaVj/Q4FNkJBECAwEAAQ==\n-----END PUBLIC KEY-----" 201 | } 202 | }, 203 | "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": { 204 | "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554", 205 | "keytype": "ed25519", 206 | "scheme": "ed25519", 207 | "keyval": { 208 | "private": "", 209 | "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6" 210 | } 211 | } 212 | }, 213 | "steps": [ 214 | { 215 | "_type": "step", 216 | "name": "write-code", 217 | "threshold": 1, 218 | "expected_materials": [ ], 219 | "expected_products": [ 220 | ["CREATE", "foo.py"] 221 | ], 222 | "pubkeys": [ 223 | "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554" 224 | ], 225 | "expected_command": ["vi"] 226 | }, 227 | { 228 | "_type": "step", 229 | "name": "package", 230 | "threshold": 1, 231 | "expected_materials": [ 232 | ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] 233 | ], 234 | "expected_products": [ 235 | ["CREATE", "foo.tar.gz"] 236 | ], 237 | "pubkeys": [ 238 | "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf" 239 | ], 240 | "expected_command": ["tar", "zcvf", "foo.tar.gz", "foo.py"] 241 | }], 242 | "inspect": [ 243 | { 244 | "_type": "inspection", 245 | "name": "inspect_tarball", 246 | "expected_materials": [ 247 | ["MATCH", "foo.tar.gz", "WITH", "PRODUCTS", "FROM", "package"] 248 | ], 249 | "expected_products": [ 250 | ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] 251 | ], 252 | "run": ["inspect_tarball.sh", "foo.tar.gz"] 253 | } 254 | ] 255 | }); 256 | 257 | let json_serialize = serde_json::to_value(&layout).unwrap(); 258 | assert_json_eq!(json, json_serialize); 259 | } 260 | 261 | #[test] 262 | fn deserialize_layout() { 263 | let json = r#"{ 264 | "_type": "layout", 265 | "expires": "1970-01-01T00:00:00Z", 266 | "readme": "", 267 | "keys": { 268 | "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": { 269 | "keytype": "rsa", 270 | "scheme": "rsassa-pss-sha256", 271 | "keyid_hash_algorithms": [ 272 | "sha256", 273 | "sha512" 274 | ], 275 | "keyval": { 276 | "public": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA91+6CJmBzrb6ODSXPvVK\nh9IVvDkD63d5/wHawj1ZB22Y0R7A7b8lRl7IqJJ3TcZO8W2zFfeRuPFlghQs+O7h\nA6XiRr4mlD1dLItk+p93E0vgY+/Jj4I09LObgA2ncGw/bUlYt3fB5tbmnojQyhrQ\nwUQvBxOqI3nSglg02mCdQRWpPzerGxItOIQkmU2TsqTg7TZ8lnSUbAsFuMebnA2d\nJ2hzeou7ZGsyCJj/6O0ORVF37nLZiOFF8EskKVpUJuoLWopEA2c09YDgFWHEPTIo\nGNWB2l/qyX7HTk1wf+WK/Wnn3nerzdEhY9dH+U0uH7tOBBVCyEKxUqXDGpzuLSxO\nGBpJXa3TTqLHJWIOzhIjp5J3rV93aeSqemU38KjguZzdwOMO5lRsFco5gaFS9aNL\nLXtLd4ZgXaxB3vYqFDhvZCx4IKrsYEc/Nr8ubLwyQ8WHeS7v8FpIT7H9AVNDo9BM\nZpnmdTc5Lxi15/TulmswIIgjDmmIqujUqyHN27u7l6bZJlcn8lQdYMm4eJr2o+Jt\ndloTwm7Cv/gKkhZ5tdO5c/219UYBnKaGF8No1feEHirm5mdvwpngCxdFMZMbfmUA\nfzPeVPkXE+LR0lsLGnMlXKG5vKFcQpCXW9iwJ4pZl7j12wLwiWyLDQtsIxiG6Sds\nALPkWf0mnfBaVj/Q4FNkJBECAwEAAQ==\n-----END PUBLIC KEY-----" 277 | } 278 | }, 279 | "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": { 280 | "keytype": "ed25519", 281 | "scheme": "ed25519", 282 | "keyval": { 283 | "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6" 284 | } 285 | } 286 | }, 287 | "steps": [ 288 | { 289 | "_type": "step", 290 | "name": "write-code", 291 | "threshold": 1, 292 | "expected_materials": [], 293 | "expected_products": [ 294 | [ 295 | "CREATE", 296 | "foo.py" 297 | ] 298 | ], 299 | "pubkeys": [ 300 | "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554" 301 | ], 302 | "expected_command": [ 303 | "vi" 304 | ] 305 | }, 306 | { 307 | "_type": "step", 308 | "name": "package", 309 | "threshold": 1, 310 | "expected_materials": [ 311 | [ 312 | "MATCH", 313 | "foo.py", 314 | "WITH", 315 | "PRODUCTS", 316 | "FROM", 317 | "write-code" 318 | ] 319 | ], 320 | "expected_products": [ 321 | [ 322 | "CREATE", 323 | "foo.tar.gz" 324 | ] 325 | ], 326 | "pubkeys": [ 327 | "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf" 328 | ], 329 | "expected_command": [ 330 | "tar", 331 | "zcvf", 332 | "foo.tar.gz", 333 | "foo.py" 334 | ] 335 | } 336 | ], 337 | "inspect": [ 338 | { 339 | "_type": "inspection", 340 | "name": "inspect_tarball", 341 | "expected_materials": [ 342 | [ 343 | "MATCH", 344 | "foo.tar.gz", 345 | "WITH", 346 | "PRODUCTS", 347 | "FROM", 348 | "package" 349 | ] 350 | ], 351 | "expected_products": [ 352 | [ 353 | "MATCH", 354 | "foo.py", 355 | "WITH", 356 | "PRODUCTS", 357 | "FROM", 358 | "write-code" 359 | ] 360 | ], 361 | "run": [ 362 | "inspect_tarball.sh", 363 | "foo.tar.gz" 364 | ] 365 | } 366 | ] 367 | }"#; 368 | 369 | let layout = get_example_layout_metadata(); 370 | let layout_parse: Layout = serde_json::from_str(json).unwrap(); 371 | assert_eq!(layout, layout_parse); 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/models/layout/step.rs: -------------------------------------------------------------------------------- 1 | //! in-toto layout's Step 2 | 3 | use std::str::FromStr; 4 | 5 | use serde::ser::Serializer; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::crypto::KeyId; 9 | use crate::{supply_chain_item_derive, Error, Result}; 10 | 11 | use super::rule::ArtifactRule; 12 | use super::supply_chain_item::SupplyChainItem; 13 | 14 | /// Wrapper type for a command in step. 15 | #[derive( 16 | Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Deserialize, 17 | )] 18 | pub struct Command(Vec); 19 | 20 | impl Command { 21 | pub fn is_empty(&self) -> bool { 22 | self.0.is_empty() 23 | } 24 | } 25 | 26 | impl AsRef<[String]> for Command { 27 | fn as_ref(&self) -> &[String] { 28 | &self.0 29 | } 30 | } 31 | 32 | impl From for Command { 33 | fn from(str: String) -> Self { 34 | let paras: Vec = str 35 | .split_whitespace() 36 | .collect::>() 37 | .iter() 38 | .map(|s| s.to_string()) 39 | .collect(); 40 | Command(paras) 41 | } 42 | } 43 | 44 | impl From> for Command { 45 | fn from(strs: Vec) -> Self { 46 | Command::from(&strs[..]) 47 | } 48 | } 49 | 50 | impl From<&[String]> for Command { 51 | fn from(strs: &[String]) -> Self { 52 | Command(strs.to_vec()) 53 | } 54 | } 55 | 56 | impl From<&str> for Command { 57 | fn from(str: &str) -> Self { 58 | let paras: Vec = str 59 | .split_whitespace() 60 | .collect::>() 61 | .iter() 62 | .map(|s| s.to_string()) 63 | .collect(); 64 | Command(paras) 65 | } 66 | } 67 | 68 | impl FromStr for Command { 69 | type Err = Error; 70 | 71 | /// Parse a Command from a string. 72 | fn from_str(string: &str) -> Result { 73 | let paras: Vec = string 74 | .split_whitespace() 75 | .collect::>() 76 | .iter() 77 | .map(|s| s.to_string()) 78 | .collect(); 79 | Ok(Command(paras)) 80 | } 81 | } 82 | 83 | impl Serialize for Command { 84 | fn serialize(&self, ser: S) -> ::std::result::Result 85 | where 86 | S: Serializer, 87 | { 88 | self.0.serialize(ser) 89 | } 90 | } 91 | 92 | /// Step represents an in-toto step of the supply chain performed by a functionary. 93 | /// During final product verification in-toto looks for corresponding Link 94 | /// metadata, which is used as signed evidence that the step was performed 95 | /// according to the supply chain definition. 96 | /// Materials and products used/produced by the step are constrained by the 97 | /// artifact rules in the step's supply_chain_item's expected_materials and 98 | /// expected_products fields. 99 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 100 | pub struct Step { 101 | #[serde(rename = "_type")] 102 | pub typ: String, 103 | pub threshold: u32, 104 | pub name: String, 105 | pub expected_materials: Vec, 106 | pub expected_products: Vec, 107 | #[serde(rename = "pubkeys")] 108 | pub pub_keys: Vec, 109 | pub expected_command: Command, 110 | } 111 | 112 | impl Step { 113 | pub fn new(name: &str) -> Self { 114 | Step { 115 | pub_keys: Vec::new(), 116 | expected_command: Command::default(), 117 | threshold: 0, 118 | name: name.into(), 119 | expected_materials: Vec::new(), 120 | expected_products: Vec::new(), 121 | typ: "step".into(), 122 | } 123 | } 124 | 125 | /// Add a pub key for this Step 126 | pub fn add_key(mut self, key: KeyId) -> Self { 127 | self.pub_keys.push(key); 128 | self 129 | } 130 | 131 | /// Set expected command for this Step 132 | pub fn expected_command(mut self, command: Command) -> Self { 133 | self.expected_command = command; 134 | self 135 | } 136 | 137 | /// Set threshold for this Step 138 | pub fn threshold(mut self, threshold: u32) -> Self { 139 | self.threshold = threshold; 140 | self 141 | } 142 | 143 | // Derive operations on `materials`/`products` and `name` 144 | supply_chain_item_derive!(); 145 | } 146 | 147 | impl SupplyChainItem for Step { 148 | fn name(&self) -> &str { 149 | &self.name 150 | } 151 | 152 | fn expected_materials(&self) -> &Vec { 153 | &self.expected_materials 154 | } 155 | 156 | fn expected_products(&self) -> &Vec { 157 | &self.expected_products 158 | } 159 | } 160 | 161 | #[cfg(test)] 162 | mod test { 163 | use std::str::FromStr; 164 | 165 | use serde_json::json; 166 | 167 | use crate::{ 168 | crypto::KeyId, 169 | models::rule::{Artifact, ArtifactRule}, 170 | Result, 171 | }; 172 | 173 | use super::Step; 174 | 175 | #[test] 176 | fn serialize_step() -> Result<()> { 177 | let step = Step::new("package") 178 | .add_expected_material(ArtifactRule::Match { 179 | pattern: "foo.py".into(), 180 | in_src: None, 181 | with: Artifact::Products, 182 | in_dst: None, 183 | from: "write-code".into(), 184 | }) 185 | .add_expected_product(ArtifactRule::Create("foo.tar.gz".into())) 186 | .expected_command("tar zcvf foo.tar.gz foo.py".into()) 187 | .add_key(KeyId::from_str( 188 | "70ca5750c2eda80b18f41f4ec5f92146789b5d68dd09577be422a0159bd13680", 189 | )?) 190 | .threshold(1); 191 | 192 | let json_serialize = serde_json::to_value(&step)?; 193 | let json = json!( 194 | { 195 | "_type": "step", 196 | "name": "package", 197 | "expected_materials": [ 198 | ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] 199 | ], 200 | "expected_products": [ 201 | ["CREATE", "foo.tar.gz"] 202 | ], 203 | "expected_command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], 204 | "pubkeys": [ 205 | "70ca5750c2eda80b18f41f4ec5f92146789b5d68dd09577be422a0159bd13680" 206 | ], 207 | "threshold": 1 208 | } 209 | ); 210 | assert_eq!( 211 | json, json_serialize, 212 | "{:#?} != {:#?}", 213 | json, json_serialize 214 | ); 215 | Ok(()) 216 | } 217 | 218 | #[test] 219 | fn deserialize_step() -> Result<()> { 220 | let json = r#" 221 | { 222 | "_type": "step", 223 | "name": "package", 224 | "expected_materials": [ 225 | ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] 226 | ], 227 | "expected_products": [ 228 | ["CREATE", "foo.tar.gz"] 229 | ], 230 | "expected_command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], 231 | "pubkeys": [ 232 | "70ca5750c2eda80b18f41f4ec5f92146789b5d68dd09577be422a0159bd13680" 233 | ], 234 | "threshold": 1 235 | }"#; 236 | let step_parsed: Step = serde_json::from_str(json)?; 237 | let step = Step::new("package") 238 | .add_expected_material(ArtifactRule::Match { 239 | pattern: "foo.py".into(), 240 | in_src: None, 241 | with: Artifact::Products, 242 | in_dst: None, 243 | from: "write-code".into(), 244 | }) 245 | .add_expected_product(ArtifactRule::Create("foo.tar.gz".into())) 246 | .expected_command("tar zcvf foo.tar.gz foo.py".into()) 247 | .add_key(KeyId::from_str( 248 | "70ca5750c2eda80b18f41f4ec5f92146789b5d68dd09577be422a0159bd13680", 249 | )?) 250 | .threshold(1); 251 | assert_eq!(step_parsed, step); 252 | Ok(()) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/models/layout/supply_chain_item.rs: -------------------------------------------------------------------------------- 1 | //! SupplyChainItem summarizes common fields of the two available supply chain 2 | //! item types in Inspection and Step. 3 | 4 | use super::rule::ArtifactRule; 5 | 6 | pub trait SupplyChainItem { 7 | /// Get the name of this item 8 | fn name(&self) -> &str; 9 | 10 | /// Get the expected material 11 | fn expected_materials(&self) -> &Vec; 12 | 13 | /// Get the expected products 14 | fn expected_products(&self) -> &Vec; 15 | } 16 | 17 | #[macro_export] 18 | macro_rules! supply_chain_item_derive { 19 | () => { 20 | /// Add an expected material artifact rule to this Step 21 | pub fn add_expected_material( 22 | mut self, 23 | expected_material: ArtifactRule, 24 | ) -> Self { 25 | self.expected_materials.push(expected_material); 26 | self 27 | } 28 | 29 | /// Set expected materials for this Step 30 | pub fn expected_materials( 31 | mut self, 32 | expected_materials: Vec, 33 | ) -> Self { 34 | self.expected_materials = expected_materials; 35 | self 36 | } 37 | 38 | /// Add an expected product artifact rule to this Step 39 | pub fn add_expected_product( 40 | mut self, 41 | expected_product: ArtifactRule, 42 | ) -> Self { 43 | self.expected_products.push(expected_product); 44 | self 45 | } 46 | 47 | /// Set expected products for this Step 48 | pub fn expected_products( 49 | mut self, 50 | expected_products: Vec, 51 | ) -> Self { 52 | self.expected_products = expected_products; 53 | self 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/models/link/byproducts.rs: -------------------------------------------------------------------------------- 1 | //! in-toto link's byproducts 2 | //! 3 | use std::collections::BTreeMap; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// byproducts of a link file 8 | /// # Example 9 | /// ``` 10 | /// use in_toto::models::byproducts::ByProducts; 11 | /// // let other_byproducts: BTreeMap = BTreeMap::new(); 12 | /// // ... 13 | /// // insert some other byproducts to other_byproducts 14 | /// let byproducts = ByProducts::new() 15 | /// .set_return_value(0) 16 | /// .set_stderr("".into()) 17 | /// .set_stdout("".into()); 18 | /// // .set_other_fields(other_byproducts); 19 | /// ``` 20 | /// 21 | /// Also, can directly set a whole BTree as other_fields 22 | /// 23 | /// ``` 24 | /// use std::collections::BTreeMap; 25 | /// use in_toto::models::byproducts::ByProducts; 26 | /// let mut other_byproducts: BTreeMap = BTreeMap::new(); 27 | /// other_byproducts.insert("key".into(), "value".into()); 28 | /// 29 | /// let byproducts = ByProducts::new() 30 | /// .set_return_value(0) 31 | /// .set_stderr("".into()) 32 | /// .set_stdout("".into()) 33 | /// .set_other_fields(other_byproducts); 34 | /// ``` 35 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)] 36 | pub struct ByProducts { 37 | #[serde(rename = "return-value", skip_serializing_if = "Option::is_none")] 38 | return_value: Option, 39 | #[serde(skip_serializing_if = "Option::is_none")] 40 | stderr: Option, 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | stdout: Option, 43 | #[serde(flatten)] 44 | other_fields: BTreeMap, 45 | } 46 | 47 | impl ByProducts { 48 | pub fn new() -> Self { 49 | ByProducts { 50 | return_value: None, 51 | stderr: None, 52 | stdout: None, 53 | other_fields: BTreeMap::new(), 54 | } 55 | } 56 | 57 | /// Set return-value 58 | pub fn set_return_value(mut self, return_value: i32) -> Self { 59 | self.return_value = Some(return_value); 60 | self 61 | } 62 | 63 | /// Set stderr 64 | pub fn set_stderr(mut self, stderr: String) -> Self { 65 | self.stderr = Some(stderr); 66 | self 67 | } 68 | 69 | /// Set stdout 70 | pub fn set_stdout(mut self, stdout: String) -> Self { 71 | self.stdout = Some(stdout); 72 | self 73 | } 74 | 75 | /// Set other fields. 76 | /// Warning: This operation will overwrite all the present other-field 77 | /// set by `set_other_field` or `set_other_fields` before. 78 | pub fn set_other_fields( 79 | mut self, 80 | other_fields: BTreeMap, 81 | ) -> Self { 82 | self.other_fields = other_fields; 83 | self 84 | } 85 | 86 | /// Insert another field 87 | pub fn set_other_field(mut self, key: String, value: String) -> Self { 88 | self.other_fields.insert(key, value); 89 | self 90 | } 91 | 92 | /// Get return-value 93 | pub fn return_value(&self) -> Option { 94 | self.return_value 95 | } 96 | 97 | /// Get stderr 98 | pub fn stderr(&self) -> &Option { 99 | &self.stderr 100 | } 101 | 102 | /// Get stdout 103 | pub fn stdout(&self) -> &Option { 104 | &self.stdout 105 | } 106 | 107 | /// Get other fields 108 | pub fn other_fields(&self) -> &BTreeMap { 109 | &self.other_fields 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use std::collections::BTreeMap; 116 | 117 | use serde_json::json; 118 | 119 | use super::ByProducts; 120 | 121 | #[test] 122 | fn serialize_byproducts_other_field() { 123 | let byproducts = ByProducts::new() 124 | .set_return_value(0) 125 | .set_stderr("a foo.py\n".into()) 126 | .set_stdout("".into()) 127 | .set_other_field("key1".into(), "value1".into()) 128 | .set_other_field("key2".into(), "value2".into()); 129 | 130 | let serialized_byproducts = serde_json::to_value(byproducts).unwrap(); 131 | let json = json!({ 132 | "return-value": 0, 133 | "stderr": "a foo.py\n", 134 | "stdout": "", 135 | "key1": "value1", 136 | "key2": "value2" 137 | }); 138 | assert_eq!(json, serialized_byproducts); 139 | } 140 | 141 | #[test] 142 | fn serialize_byproducts_other_fields() { 143 | let mut other_fields = BTreeMap::new(); 144 | other_fields.insert("key1".into(), "value1".into()); 145 | other_fields.insert("key2".into(), "value2".into()); 146 | 147 | let byproducts = ByProducts::new() 148 | .set_return_value(0) 149 | .set_stderr("a foo.py\n".into()) 150 | .set_stdout("".into()) 151 | .set_other_fields(other_fields); 152 | 153 | let serialized_byproducts = serde_json::to_value(byproducts).unwrap(); 154 | let json = json!({ 155 | "return-value": 0, 156 | "stderr": "a foo.py\n", 157 | "stdout": "", 158 | "key1": "value1", 159 | "key2": "value2" 160 | }); 161 | assert_eq!(json, serialized_byproducts); 162 | } 163 | 164 | #[test] 165 | fn deserialize_byproducts_other_field() { 166 | let json = r#"{ 167 | "return-value": 0, 168 | "stderr": "a foo.py\n", 169 | "stdout": "", 170 | "key1": "value1", 171 | "key2": "value2" 172 | }"#; 173 | 174 | let byproducts = ByProducts::new() 175 | .set_return_value(0) 176 | .set_stderr("a foo.py\n".into()) 177 | .set_stdout("".into()) 178 | .set_other_field("key1".into(), "value1".into()) 179 | .set_other_field("key2".into(), "value2".into()); 180 | 181 | let deserialized_byproducts: ByProducts = 182 | serde_json::from_str(json).unwrap(); 183 | assert_eq!(byproducts, deserialized_byproducts); 184 | } 185 | 186 | #[test] 187 | fn deserialize_byproducts_other_fields() { 188 | let json = r#"{ 189 | "return-value": 0, 190 | "stderr": "a foo.py\n", 191 | "stdout": "", 192 | "key1": "value1", 193 | "key2": "value2" 194 | }"#; 195 | 196 | let mut other_fields = BTreeMap::new(); 197 | other_fields.insert("key1".into(), "value1".into()); 198 | other_fields.insert("key2".into(), "value2".into()); 199 | 200 | let byproducts = ByProducts::new() 201 | .set_return_value(0) 202 | .set_stderr("a foo.py\n".into()) 203 | .set_stdout("".into()) 204 | .set_other_fields(other_fields); 205 | 206 | let deserialized_byproducts: ByProducts = 207 | serde_json::from_str(json).unwrap(); 208 | assert_eq!(byproducts, deserialized_byproducts); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/models/link/metadata.rs: -------------------------------------------------------------------------------- 1 | //! in-toto link metadata. 2 | 3 | use serde::de::{Deserialize, Deserializer, Error as DeserializeError}; 4 | use serde::ser::{Error as SerializeError, Serialize, Serializer}; 5 | use std::collections::BTreeMap; 6 | use std::fmt::Debug; 7 | use std::fs::File; 8 | use std::io::BufReader; 9 | 10 | use crate::crypto::{self, PrivateKey}; 11 | use crate::interchange::{DataInterchange, Json}; 12 | use crate::Result; 13 | 14 | use crate::models::step::Command; 15 | use crate::models::{ 16 | Link, Metablock, Metadata, MetadataType, MetadataWrapper, 17 | TargetDescription, VirtualTargetPath, 18 | }; 19 | 20 | use super::byproducts::ByProducts; 21 | 22 | /// Helper to construct `LinkMetadata`. 23 | pub struct LinkMetadataBuilder { 24 | name: String, 25 | materials: BTreeMap, 26 | products: BTreeMap, 27 | env: Option>, 28 | byproducts: ByProducts, 29 | command: Command, 30 | } 31 | 32 | impl Default for LinkMetadataBuilder { 33 | fn default() -> Self { 34 | LinkMetadataBuilder::new() 35 | } 36 | } 37 | 38 | impl LinkMetadataBuilder { 39 | pub fn new() -> Self { 40 | LinkMetadataBuilder { 41 | name: String::new(), 42 | materials: BTreeMap::new(), 43 | products: BTreeMap::new(), 44 | env: None, 45 | byproducts: ByProducts::new(), 46 | command: Command::default(), 47 | } 48 | } 49 | 50 | /// Set the name number for this link 51 | pub fn name(mut self, name: String) -> Self { 52 | self.name = name; 53 | self 54 | } 55 | 56 | /// Set the materials for this metadata 57 | pub fn materials( 58 | mut self, 59 | materials: BTreeMap, 60 | ) -> Self { 61 | self.materials = materials; 62 | self 63 | } 64 | 65 | /// Set the products for this metadata 66 | pub fn products( 67 | mut self, 68 | products: BTreeMap, 69 | ) -> Self { 70 | self.products = products; 71 | self 72 | } 73 | 74 | pub fn add_material(mut self, material_path: VirtualTargetPath) -> Self { 75 | let file = File::open(material_path.to_string()).unwrap(); 76 | let mut reader = BufReader::new(file); 77 | let (_length, hashes) = crypto::calculate_hashes( 78 | &mut reader, 79 | &[crypto::HashAlgorithm::Sha256], 80 | ) 81 | .unwrap(); 82 | self.materials.insert(material_path, hashes); 83 | self 84 | } 85 | 86 | pub fn add_product(mut self, material_path: VirtualTargetPath) -> Self { 87 | let file = File::open(material_path.to_string()).unwrap(); 88 | let mut reader = BufReader::new(file); 89 | let (_length, hashes) = crypto::calculate_hashes( 90 | &mut reader, 91 | &[crypto::HashAlgorithm::Sha256], 92 | ) 93 | .unwrap(); 94 | self.products.insert(material_path, hashes); 95 | self 96 | } 97 | 98 | /// Set the products for this metadata 99 | pub fn env(mut self, env: Option>) -> Self { 100 | self.env = env; 101 | self 102 | } 103 | 104 | /// Set the products for this metadata 105 | pub fn byproducts(mut self, byproducts: ByProducts) -> Self { 106 | self.byproducts = byproducts; 107 | self 108 | } 109 | 110 | /// Set the command for this metadata 111 | pub fn command(mut self, command: Command) -> Self { 112 | self.command = command; 113 | self 114 | } 115 | 116 | pub fn build(self) -> Result { 117 | LinkMetadata::new( 118 | self.name, 119 | self.materials, 120 | self.products, 121 | self.env, 122 | self.byproducts, 123 | self.command, 124 | ) 125 | } 126 | 127 | /// Construct a new `Metablock`. 128 | pub fn signed(self, private_key: &PrivateKey) -> Result 129 | where 130 | D: DataInterchange, 131 | { 132 | Metablock::new(Box::new(self.build()?).into_enum(), &[private_key]) 133 | } 134 | 135 | /// Construct a new `Metablock`. 136 | pub fn unsigned(self) -> Result 137 | where 138 | D: DataInterchange, 139 | { 140 | Metablock::new(Box::new(self.build()?).into_enum(), &[]) 141 | } 142 | } 143 | 144 | /// link metadata 145 | #[derive(Debug, Clone, PartialEq, Eq)] 146 | pub struct LinkMetadata { 147 | pub name: String, 148 | pub materials: BTreeMap, 149 | pub products: BTreeMap, 150 | pub env: Option>, 151 | pub byproducts: ByProducts, 152 | pub command: Command, 153 | } 154 | 155 | impl LinkMetadata { 156 | /// Create new `LinkMetadata`. 157 | pub fn new( 158 | name: String, 159 | materials: BTreeMap, 160 | products: BTreeMap, 161 | env: Option>, 162 | byproducts: ByProducts, 163 | command: Command, 164 | ) -> Result { 165 | Ok(LinkMetadata { 166 | name, 167 | materials, 168 | products, 169 | env, 170 | byproducts, 171 | command, 172 | }) 173 | } 174 | } 175 | 176 | impl Metadata for LinkMetadata { 177 | fn typ(&self) -> MetadataType { 178 | MetadataType::Link 179 | } 180 | 181 | fn into_enum(self: Box) -> MetadataWrapper { 182 | MetadataWrapper::Link(*self) 183 | } 184 | 185 | fn to_bytes(&self) -> Result> { 186 | Json::canonicalize(&Json::serialize(self)?) 187 | } 188 | } 189 | 190 | impl Serialize for LinkMetadata { 191 | fn serialize(&self, ser: S) -> ::std::result::Result 192 | where 193 | S: Serializer, 194 | { 195 | Link::from(self) 196 | .map_err(|e| SerializeError::custom(format!("{:?}", e)))? 197 | .serialize(ser) 198 | } 199 | } 200 | 201 | impl<'de> Deserialize<'de> for LinkMetadata { 202 | fn deserialize>( 203 | de: D, 204 | ) -> ::std::result::Result { 205 | let intermediate: Link = Deserialize::deserialize(de)?; 206 | intermediate 207 | .try_into() 208 | .map_err(|e| DeserializeError::custom(format!("{:?}", e))) 209 | } 210 | } 211 | 212 | #[cfg(test)] 213 | mod test { 214 | use serde_json::json; 215 | 216 | use crate::models::{ 217 | byproducts::ByProducts, step::Command, LinkMetadata, 218 | LinkMetadataBuilder, VirtualTargetPath, 219 | }; 220 | 221 | #[test] 222 | fn serialize_linkmetadata() { 223 | let link_metadata = LinkMetadataBuilder::new() 224 | .name("".into()) 225 | .add_product( 226 | VirtualTargetPath::new("tests/test_link/foo.tar.gz".into()) 227 | .unwrap(), 228 | ) 229 | .byproducts( 230 | ByProducts::new() 231 | .set_return_value(0) 232 | .set_stderr("a foo.py\n".into()) 233 | .set_stdout("".into()), 234 | ) 235 | .command(Command::from("tar zcvf foo.tar.gz foo.py")) 236 | .build() 237 | .unwrap(); 238 | 239 | let serialized_linkmetadata = 240 | serde_json::to_value(link_metadata).unwrap(); 241 | let json = json!({ 242 | "_type": "link", 243 | "name": "", 244 | "materials": {}, 245 | "products": { 246 | "tests/test_link/foo.tar.gz": { 247 | "sha256": "52947cb78b91ad01fe81cd6aef42d1f6817e92b9e6936c1e5aabb7c98514f355" 248 | } 249 | }, 250 | "byproducts": { 251 | "return-value": 0, 252 | "stderr": "a foo.py\n", 253 | "stdout": "" 254 | }, 255 | "command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], 256 | "environment": null 257 | }); 258 | assert_eq!(json, serialized_linkmetadata); 259 | } 260 | 261 | #[test] 262 | fn deserialize_linkmetadata() { 263 | let json = r#"{ 264 | "_type": "link", 265 | "name": "", 266 | "materials": {}, 267 | "products": { 268 | "tests/test_link/foo.tar.gz": { 269 | "sha256": "52947cb78b91ad01fe81cd6aef42d1f6817e92b9e6936c1e5aabb7c98514f355" 270 | } 271 | }, 272 | "byproducts": { 273 | "return-value": 0, 274 | "stderr": "a foo.py\n", 275 | "stdout": "" 276 | }, 277 | "command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], 278 | "environment": null 279 | }"#; 280 | 281 | let link_metadata = LinkMetadataBuilder::new() 282 | .name("".into()) 283 | .add_product( 284 | VirtualTargetPath::new("tests/test_link/foo.tar.gz".into()) 285 | .unwrap(), 286 | ) 287 | .byproducts( 288 | ByProducts::new() 289 | .set_return_value(0) 290 | .set_stderr("a foo.py\n".into()) 291 | .set_stdout("".into()), 292 | ) 293 | .command(Command::from("tar zcvf foo.tar.gz foo.py")) 294 | .build() 295 | .unwrap(); 296 | 297 | let deserialized_link_metadata: LinkMetadata = 298 | serde_json::from_str(json).unwrap(); 299 | assert_eq!(link_metadata, deserialized_link_metadata); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/models/link/mod.rs: -------------------------------------------------------------------------------- 1 | //! in-toto link 2 | 3 | use std::collections::BTreeMap; 4 | use std::fmt::Debug; 5 | use std::str; 6 | 7 | use crate::Result; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | pub mod byproducts; 11 | mod metadata; 12 | pub use metadata::{LinkMetadata, LinkMetadataBuilder}; 13 | 14 | use crate::models::{TargetDescription, VirtualTargetPath}; 15 | 16 | use self::byproducts::ByProducts; 17 | 18 | use super::step::Command; 19 | 20 | // FIXME, we need to tag a spec 21 | //const SPEC_VERSION: &str = "0.9-dev"; 22 | 23 | #[derive(Debug, Serialize, Deserialize)] 24 | pub struct Link { 25 | #[serde(rename = "_type")] 26 | typ: String, 27 | name: String, 28 | materials: BTreeMap, 29 | products: BTreeMap, 30 | #[serde(rename = "environment")] 31 | env: Option>, 32 | byproducts: ByProducts, 33 | command: Command, 34 | } 35 | 36 | impl Link { 37 | pub fn from(meta: &LinkMetadata) -> Result { 38 | Ok(Link { 39 | typ: String::from("link"), 40 | name: meta.name.clone(), 41 | materials: meta.materials.clone(), 42 | products: meta.products.clone(), 43 | env: meta.env.clone(), 44 | byproducts: meta.byproducts.clone(), 45 | command: meta.command.clone(), 46 | }) 47 | } 48 | 49 | pub fn try_into(self) -> Result { 50 | LinkMetadata::new( 51 | self.name, 52 | self.materials, 53 | self.products, 54 | self.env, 55 | self.byproducts, 56 | self.command, 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | //! Models used in in-toto 2 | 3 | mod envelope; 4 | mod helpers; 5 | mod layout; 6 | mod link; 7 | mod metadata; 8 | mod predicate; 9 | mod statement; 10 | 11 | pub use helpers::*; 12 | pub use layout::*; 13 | pub use link::*; 14 | pub use metadata::*; 15 | pub use predicate::{PredicateLayout, PredicateVer, PredicateWrapper}; 16 | pub use statement::{StatementVer, StatementWrapper}; 17 | 18 | #[cfg(test)] 19 | mod test { 20 | use once_cell::sync::Lazy; 21 | 22 | use super::{LinkMetadata, LinkMetadataBuilder}; 23 | 24 | pub static BLANK_META: Lazy = Lazy::new(|| { 25 | let builder = LinkMetadataBuilder::default(); 26 | builder.build().unwrap() 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/models/predicate/link_v02.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::{PredicateLayout, PredicateVer, PredicateWrapper}; 6 | use crate::interchange::{DataInterchange, Json}; 7 | use crate::models::byproducts::ByProducts; 8 | use crate::models::step::Command; 9 | use crate::models::{TargetDescription, VirtualTargetPath}; 10 | use crate::Result; 11 | 12 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 13 | #[serde(deny_unknown_fields)] 14 | /// Predicate `LinkV02` means the predicate of original compatible format. 15 | /// 16 | /// [LinkV02](https://in-toto.io/Link/v0.2) 17 | /// can be used together with most states. 18 | pub struct LinkV02 { 19 | name: String, 20 | materials: BTreeMap, 21 | env: Option>, 22 | command: Command, 23 | byproducts: ByProducts, 24 | } 25 | 26 | impl PredicateLayout for LinkV02 { 27 | fn to_bytes(&self) -> Result> { 28 | Json::canonicalize(&Json::serialize(self)?) 29 | } 30 | 31 | fn into_enum(self: Box) -> PredicateWrapper { 32 | PredicateWrapper::LinkV0_2(*self) 33 | } 34 | 35 | fn version(&self) -> PredicateVer { 36 | PredicateVer::LinkV0_2 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | pub mod test { 42 | use std::collections::BTreeMap; 43 | use std::str; 44 | 45 | use once_cell::sync::Lazy; 46 | use serde_json::{json, Value}; 47 | use strum::IntoEnumIterator; 48 | 49 | use super::LinkV02; 50 | use crate::{ 51 | interchange::{DataInterchange, Json}, 52 | models::{ 53 | byproducts::ByProducts, PredicateLayout, PredicateVer, 54 | PredicateWrapper, 55 | }, 56 | }; 57 | 58 | pub static STR_PREDICATE_LINK_V02: Lazy = Lazy::new(|| { 59 | let raw_data = json!( 60 | { 61 | "byproducts": { 62 | "return-value": 0, 63 | "stderr": "", 64 | "stdout": "" 65 | }, 66 | "command": [], 67 | "env": null, 68 | "materials": {}, 69 | "name": "" 70 | }); 71 | let value = serde_json::value::to_value(raw_data).unwrap(); 72 | let bytes = Json::canonicalize(&value).unwrap(); 73 | let data = str::from_utf8(&bytes).unwrap(); 74 | data.to_string() 75 | }); 76 | 77 | pub static PREDICATE_LINK_V02: Lazy = Lazy::new(|| LinkV02 { 78 | name: "".to_string(), 79 | materials: BTreeMap::new(), 80 | env: None, 81 | command: "".into(), 82 | byproducts: ByProducts::new() 83 | .set_return_value(0) 84 | .set_stderr("".into()) 85 | .set_stdout("".into()), 86 | }); 87 | 88 | #[test] 89 | fn into_trait_equal() { 90 | let predicate = PredicateWrapper::LinkV0_2(PREDICATE_LINK_V02.clone()); 91 | let real = Box::new(PREDICATE_LINK_V02.clone()).into_enum(); 92 | 93 | assert_eq!(predicate, real); 94 | } 95 | 96 | #[test] 97 | fn create_predicate_from_meta() { 98 | // TODO: convert from metadata is no supported recentely 99 | } 100 | 101 | #[test] 102 | fn serialize_predicate() { 103 | let predicate = Box::new(PREDICATE_LINK_V02.clone()).into_enum(); 104 | let buf = predicate.into_trait().to_bytes().unwrap(); 105 | let predicate_serialized = str::from_utf8(&buf).unwrap(); 106 | 107 | assert_eq!(predicate_serialized, *STR_PREDICATE_LINK_V02); 108 | } 109 | 110 | #[test] 111 | fn deserialize_predicate() { 112 | let value: Value = 113 | serde_json::from_str(&STR_PREDICATE_LINK_V02).unwrap(); 114 | let predicate = 115 | PredicateWrapper::from_value(value, PredicateVer::LinkV0_2) 116 | .unwrap(); 117 | let real = Box::new(PREDICATE_LINK_V02.clone()).into_enum(); 118 | 119 | assert_eq!(predicate, real); 120 | } 121 | 122 | #[test] 123 | fn deserialize_auto() { 124 | let value: Value = 125 | serde_json::from_str(&STR_PREDICATE_LINK_V02).unwrap(); 126 | let predicate = PredicateWrapper::try_from_value(value).unwrap(); 127 | let real = Box::new(PREDICATE_LINK_V02.clone()).into_enum(); 128 | 129 | assert_eq!(predicate, real); 130 | } 131 | 132 | #[test] 133 | fn deserialize_dismatch() { 134 | let value: Value = 135 | serde_json::from_str(&STR_PREDICATE_LINK_V02).unwrap(); 136 | for version in PredicateVer::iter() { 137 | if version == PredicateVer::LinkV0_2 { 138 | continue; 139 | } 140 | let predicate = 141 | PredicateWrapper::from_value(value.clone(), version); 142 | 143 | assert!(predicate.is_err()); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/models/predicate/mod.rs: -------------------------------------------------------------------------------- 1 | //! in-toto link 2 | 3 | pub mod link_v02; 4 | pub mod slsa_provenance_v01; 5 | pub mod slsa_provenance_v02; 6 | use std::convert::TryFrom; 7 | 8 | pub use link_v02::LinkV02; 9 | use serde_json::Value; 10 | pub use slsa_provenance_v01::SLSAProvenanceV01; 11 | pub use slsa_provenance_v02::SLSAProvenanceV02; 12 | 13 | use serde::de::{Deserializer, Error as DeserializeError}; 14 | use serde::ser::Serializer; 15 | use serde::{Deserialize, Serialize}; 16 | use strum::IntoEnumIterator; 17 | use strum_macros::EnumIter; 18 | 19 | use crate::{Error, Result}; 20 | 21 | #[derive(Debug, Hash, PartialEq, Eq, EnumIter, Clone, Copy)] 22 | pub enum PredicateVer { 23 | LinkV0_2, 24 | SLSAProvenanceV0_1, 25 | SLSAProvenanceV0_2, 26 | } 27 | 28 | impl TryFrom for PredicateVer { 29 | type Error = crate::Error; 30 | 31 | fn try_from(target: String) -> Result { 32 | match target.as_str() { 33 | "https://in-toto.io/Link/v0.2" => Ok(PredicateVer::LinkV0_2), 34 | "https://slsa.dev/provenance/v0.1" => { 35 | Ok(PredicateVer::SLSAProvenanceV0_1) 36 | } 37 | "https://slsa.dev/provenance/v0.2" => { 38 | Ok(PredicateVer::SLSAProvenanceV0_2) 39 | } 40 | _ => Err(Error::StringConvertFailed(target)), 41 | } 42 | } 43 | } 44 | 45 | impl From for String { 46 | fn from(value: PredicateVer) -> Self { 47 | match value { 48 | PredicateVer::LinkV0_2 => { 49 | "https://in-toto.io/Link/v0.2".to_string() 50 | } 51 | PredicateVer::SLSAProvenanceV0_1 => { 52 | "https://slsa.dev/provenance/v0.1".to_string() 53 | } 54 | PredicateVer::SLSAProvenanceV0_2 => { 55 | "https://slsa.dev/provenance/v0.2".to_string() 56 | } 57 | } 58 | } 59 | } 60 | 61 | impl Serialize for PredicateVer { 62 | fn serialize(&self, ser: S) -> ::std::result::Result 63 | where 64 | S: Serializer, 65 | { 66 | let target: String = (*self).into(); 67 | ser.serialize_str(&target) 68 | } 69 | } 70 | 71 | impl<'de> Deserialize<'de> for PredicateVer { 72 | fn deserialize>( 73 | de: D, 74 | ) -> ::std::result::Result { 75 | let target: String = Deserialize::deserialize(de)?; 76 | PredicateVer::try_from(target) 77 | .map_err(|e| DeserializeError::custom(format!("{:?}", e))) 78 | } 79 | } 80 | 81 | #[derive(Debug, Serialize, PartialEq, Eq, Clone)] 82 | #[serde(untagged)] 83 | pub enum PredicateWrapper { 84 | LinkV0_2(LinkV02), 85 | SLSAProvenanceV0_1(SLSAProvenanceV01), 86 | SLSAProvenanceV0_2(SLSAProvenanceV02), 87 | } 88 | 89 | impl<'de> Deserialize<'de> for PredicateWrapper { 90 | fn deserialize>( 91 | de: D, 92 | ) -> ::std::result::Result { 93 | let value = Value::deserialize(de)?; 94 | PredicateWrapper::try_from_value(value) 95 | .map_err(|e| DeserializeError::custom(format!("{:?}", e))) 96 | } 97 | } 98 | 99 | impl PredicateWrapper { 100 | /// Convert from enum `PredicateWrapper` to trait `PredicateLayout` 101 | pub fn into_trait(self) -> Box { 102 | match self { 103 | PredicateWrapper::LinkV0_2(link) => Box::new(link), 104 | PredicateWrapper::SLSAProvenanceV0_1(proven) => Box::new(proven), 105 | PredicateWrapper::SLSAProvenanceV0_2(proven) => Box::new(proven), 106 | } 107 | } 108 | 109 | /// Deserialize method for `PredicateWrapper` from `serde:Value` by its version 110 | fn from_value(value: Value, version: PredicateVer) -> Result { 111 | match version { 112 | PredicateVer::LinkV0_2 => serde_json::from_value(value) 113 | .map(Self::LinkV0_2) 114 | .map_err(|e| e.into()), 115 | PredicateVer::SLSAProvenanceV0_1 => serde_json::from_value(value) 116 | .map(Self::SLSAProvenanceV0_1) 117 | .map_err(|e| e.into()), 118 | PredicateVer::SLSAProvenanceV0_2 => serde_json::from_value(value) 119 | .map(Self::SLSAProvenanceV0_2) 120 | .map_err(|e| e.into()), 121 | } 122 | } 123 | 124 | /// Auto judge the `PredicateWrapper` version from `serde:Value` 125 | pub fn judge_from_value(value: &Value) -> Result { 126 | println!("{:?}", value); 127 | for version in PredicateVer::iter() { 128 | let wrapper = PredicateWrapper::from_value(value.clone(), version); 129 | if wrapper.is_ok() { 130 | return Ok(version); 131 | } 132 | } 133 | Err(Error::Programming("no available value parser".to_string())) 134 | } 135 | 136 | /// Auto deserialize for `PredicateWrapper` by any possible version. 137 | pub fn try_from_value(value: Value) -> Result { 138 | let version = Self::judge_from_value(&value)?; 139 | PredicateWrapper::from_value(value, version) 140 | } 141 | } 142 | 143 | pub trait PredicateLayout { 144 | /// The version of predicate 145 | fn version(&self) -> PredicateVer; 146 | /// Convert from trait `PredicateLayout` to enum `PredicateWrapper` 147 | fn into_enum(self: Box) -> PredicateWrapper; 148 | /// Standard serialize for PredicateLayout 149 | fn to_bytes(&self) -> Result>; 150 | } 151 | -------------------------------------------------------------------------------- /src/models/predicate/slsa_provenance_v01.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use chrono::{DateTime, FixedOffset, SecondsFormat}; 4 | use serde::de::{Deserializer, Error as DeserializeError}; 5 | use serde::ser::Serializer; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use super::{PredicateLayout, PredicateVer, PredicateWrapper}; 9 | use crate::interchange::{DataInterchange, Json}; 10 | use crate::Result; 11 | 12 | #[derive(Debug, PartialEq, Eq, Clone)] 13 | pub struct TimeStamp(pub DateTime); 14 | 15 | impl Serialize for TimeStamp { 16 | fn serialize(&self, ser: S) -> ::std::result::Result 17 | where 18 | S: Serializer, 19 | { 20 | let form = self.0.to_rfc3339_opts(SecondsFormat::Secs, true); 21 | form.serialize(ser) 22 | } 23 | } 24 | 25 | impl<'de> Deserialize<'de> for TimeStamp { 26 | fn deserialize>( 27 | de: D, 28 | ) -> ::std::result::Result { 29 | let form: &str = Deserialize::deserialize(de)?; 30 | DateTime::parse_from_rfc3339(form) 31 | .map(TimeStamp) 32 | .map_err(|e| DeserializeError::custom(format!("{:?}", e))) 33 | } 34 | } 35 | 36 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 37 | #[serde(deny_unknown_fields)] 38 | pub struct TypeURI(pub String); 39 | 40 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 41 | #[serde(deny_unknown_fields)] 42 | pub struct Builder { 43 | pub id: TypeURI, 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 47 | #[serde(deny_unknown_fields)] 48 | pub struct Completeness { 49 | #[serde(skip_serializing_if = "Option::is_none")] 50 | pub arguments: Option, 51 | #[serde(skip_serializing_if = "Option::is_none")] 52 | pub environment: Option, 53 | #[serde(skip_serializing_if = "Option::is_none")] 54 | pub materials: Option, 55 | } 56 | 57 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 58 | #[serde(deny_unknown_fields)] 59 | pub struct Recipe { 60 | #[serde(rename = "type")] 61 | pub typ: TypeURI, 62 | #[serde(rename = "definedInMaterial")] 63 | #[serde(skip_serializing_if = "Option::is_none")] 64 | pub defined_in_material: Option, 65 | #[serde(rename = "entryPoint")] 66 | #[serde(skip_serializing_if = "Option::is_none")] 67 | pub entry_point: Option, 68 | #[serde(skip_serializing_if = "Option::is_none")] 69 | pub arguments: Option, 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub environment: Option, 72 | } 73 | 74 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 75 | #[serde(deny_unknown_fields)] 76 | pub struct ProvenanceMetadata { 77 | #[serde(rename = "buildInvocationId")] 78 | #[serde(skip_serializing_if = "Option::is_none")] 79 | pub build_invocation_id: Option, 80 | #[serde(rename = "buildStartedOn")] 81 | #[serde(skip_serializing_if = "Option::is_none")] 82 | pub build_started_on: Option, 83 | #[serde(rename = "buildFinishedOn")] 84 | #[serde(skip_serializing_if = "Option::is_none")] 85 | pub build_finished_on: Option, 86 | #[serde(skip_serializing_if = "Option::is_none")] 87 | pub completeness: Option, 88 | #[serde(skip_serializing_if = "Option::is_none")] 89 | pub reproducible: Option, 90 | } 91 | 92 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 93 | #[serde(deny_unknown_fields)] 94 | pub struct Material { 95 | #[serde(skip_serializing_if = "Option::is_none")] 96 | pub(crate) uri: Option, 97 | #[serde(skip_serializing_if = "Option::is_none")] 98 | pub(crate) digest: Option>, 99 | } 100 | 101 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 102 | #[serde(deny_unknown_fields)] 103 | /// Predicate `SLSAProvenanceV01` means the predicate of SLSA format. 104 | /// 105 | /// [SLSAProvenanceV01](https://slsa.dev/provenance/v0.1) 106 | /// can be used together with most states. 107 | pub struct SLSAProvenanceV01 { 108 | builder: Builder, 109 | #[serde(skip_serializing_if = "Option::is_none")] 110 | recipe: Option, 111 | #[serde(skip_serializing_if = "Option::is_none")] 112 | metadata: Option, 113 | #[serde(skip_serializing_if = "Option::is_none")] 114 | materials: Option>, 115 | } 116 | 117 | impl PredicateLayout for SLSAProvenanceV01 { 118 | fn to_bytes(&self) -> Result> { 119 | Json::canonicalize(&Json::serialize(self)?) 120 | } 121 | 122 | fn into_enum(self: Box) -> PredicateWrapper { 123 | PredicateWrapper::SLSAProvenanceV0_1(*self) 124 | } 125 | 126 | fn version(&self) -> PredicateVer { 127 | PredicateVer::SLSAProvenanceV0_1 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | pub mod test { 133 | use std::{collections::HashMap, str}; 134 | 135 | use chrono::DateTime; 136 | use once_cell::sync::Lazy; 137 | use serde_json::{json, Value}; 138 | use strum::IntoEnumIterator; 139 | 140 | use super::{ 141 | Builder, Completeness, Material, ProvenanceMetadata, Recipe, 142 | SLSAProvenanceV01, TimeStamp, TypeURI, 143 | }; 144 | use crate::{ 145 | interchange::{DataInterchange, Json}, 146 | models::{PredicateLayout, PredicateVer, PredicateWrapper}, 147 | }; 148 | 149 | pub static STR_PREDICATE_PROVEN_V01: Lazy = Lazy::new(|| { 150 | let raw_data = json!({ 151 | "builder": { 152 | "id": "https://github.com/Attestations/GitHubHostedActions@v1" 153 | }, 154 | "recipe": { 155 | "type": "https://github.com/Attestations/GitHubActionsWorkflow@v1", 156 | "definedInMaterial": 0, 157 | "entryPoint": "build.yaml:maketgz" 158 | }, 159 | "metadata": { 160 | "buildInvocationId":"test_invocation_id", 161 | "completeness": { 162 | "environment": true 163 | } 164 | }, 165 | "materials": [{ 166 | "uri": "git+https://github.com/curl/curl-docker@master", 167 | "digest": { 168 | "sha1": "d6525c840a62b398424a78d792f457477135d0cf" 169 | } 170 | }, { 171 | "uri": "github_hosted_vm:ubuntu-18.04:20210123.1" 172 | }] 173 | }); 174 | let value = serde_json::value::to_value(raw_data).unwrap(); 175 | let bytes = Json::canonicalize(&value).unwrap(); 176 | let data = str::from_utf8(&bytes).unwrap(); 177 | data.to_string() 178 | }); 179 | 180 | pub static PREDICATE_PROVEN_V01: Lazy = 181 | Lazy::new(|| SLSAProvenanceV01 { 182 | builder: Builder { 183 | id: TypeURI( 184 | "https://github.com/Attestations/GitHubHostedActions@v1" 185 | .to_string(), 186 | ), 187 | }, 188 | recipe: Some(Recipe { 189 | typ: TypeURI( 190 | "https://github.com/Attestations/GitHubActionsWorkflow@v1" 191 | .to_string(), 192 | ), 193 | defined_in_material: Some(0), 194 | entry_point: Some("build.yaml:maketgz".to_string()), 195 | arguments: None, 196 | environment: None, 197 | }), 198 | metadata: Some(ProvenanceMetadata { 199 | build_invocation_id: Some("test_invocation_id".to_string()), 200 | build_started_on: None, 201 | build_finished_on: None, 202 | completeness: Some(Completeness { 203 | arguments: None, 204 | environment: Some(true), 205 | materials: None, 206 | }), 207 | reproducible: None, 208 | }), 209 | materials: Some(vec![ 210 | Material { 211 | uri: Some(TypeURI( 212 | "git+https://github.com/curl/curl-docker@master" 213 | .to_string(), 214 | )), 215 | digest: Some(HashMap::from([( 216 | "sha1".to_string(), 217 | "d6525c840a62b398424a78d792f457477135d0cf".to_string(), 218 | )])), 219 | }, 220 | Material { 221 | uri: Some(TypeURI( 222 | "github_hosted_vm:ubuntu-18.04:20210123.1".to_string(), 223 | )), 224 | digest: None, 225 | }, 226 | ]), 227 | }); 228 | 229 | #[test] 230 | fn serialize_deserialize_datetime() { 231 | let datetime = TimeStamp( 232 | DateTime::parse_from_rfc3339("2020-08-19T08:38:00Z").unwrap(), 233 | ); 234 | let datetime_raw = "\"2020-08-19T08:38:00Z\""; 235 | 236 | // serialize 237 | let buf = 238 | Json::canonicalize(&Json::serialize(&datetime).unwrap()).unwrap(); 239 | let datetime_serialized = str::from_utf8(&buf).unwrap(); 240 | assert_eq!(datetime_raw, datetime_serialized); 241 | 242 | // deserialize 243 | let datetime_deserialized: TimeStamp = 244 | serde_json::from_slice(datetime_raw.as_bytes()).unwrap(); 245 | assert_eq!(datetime_deserialized, datetime); 246 | } 247 | 248 | #[test] 249 | fn into_trait_equal() { 250 | let predicate = 251 | PredicateWrapper::SLSAProvenanceV0_1(PREDICATE_PROVEN_V01.clone()); 252 | let real = Box::new(PREDICATE_PROVEN_V01.clone()).into_enum(); 253 | 254 | assert_eq!(predicate, real); 255 | } 256 | 257 | #[test] 258 | fn create_predicate_from_meta() { 259 | // TODO: convert from metadata is no supported recentely 260 | } 261 | 262 | #[test] 263 | fn serialize_predicate() { 264 | let predicate = Box::new(PREDICATE_PROVEN_V01.clone()).into_enum(); 265 | let buf = predicate.into_trait().to_bytes().unwrap(); 266 | let predicate_serialized = str::from_utf8(&buf).unwrap(); 267 | 268 | assert_eq!(predicate_serialized, *STR_PREDICATE_PROVEN_V01); 269 | } 270 | 271 | #[test] 272 | fn deserialize_predicate() { 273 | let value: Value = 274 | serde_json::from_str(&STR_PREDICATE_PROVEN_V01).unwrap(); 275 | let predicate = PredicateWrapper::from_value( 276 | value, 277 | PredicateVer::SLSAProvenanceV0_1, 278 | ) 279 | .unwrap(); 280 | let real = Box::new(PREDICATE_PROVEN_V01.clone()).into_enum(); 281 | 282 | assert_eq!(predicate, real); 283 | } 284 | 285 | #[test] 286 | fn deserialize_auto() { 287 | let value: Value = 288 | serde_json::from_str(&STR_PREDICATE_PROVEN_V01).unwrap(); 289 | let predicate = PredicateWrapper::try_from_value(value).unwrap(); 290 | let real = Box::new(PREDICATE_PROVEN_V01.clone()).into_enum(); 291 | 292 | assert_eq!(predicate, real); 293 | } 294 | 295 | #[test] 296 | fn deserialize_dismatch() { 297 | let value: Value = 298 | serde_json::from_str(&STR_PREDICATE_PROVEN_V01).unwrap(); 299 | for version in PredicateVer::iter() { 300 | if version == PredicateVer::SLSAProvenanceV0_1 { 301 | continue; 302 | } 303 | let predicate = 304 | PredicateWrapper::from_value(value.clone(), version); 305 | 306 | assert!(predicate.is_err()); 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/models/predicate/slsa_provenance_v02.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::slsa_provenance_v01::{ 6 | Builder, Material, ProvenanceMetadata, TypeURI, 7 | }; 8 | use super::{PredicateLayout, PredicateVer, PredicateWrapper}; 9 | use crate::interchange::{DataInterchange, Json}; 10 | use crate::Result; 11 | 12 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 13 | #[serde(deny_unknown_fields)] 14 | pub struct ConfigSource { 15 | pub uri: Option, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub digest: Option>, 18 | #[serde(rename = "entryPoint")] 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub entry_point: Option, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 24 | #[serde(deny_unknown_fields)] 25 | pub struct Invocation { 26 | #[serde(rename = "configSource")] 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub config_source: Option, 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub parameters: Option, 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub environment: Option, 33 | } 34 | 35 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 36 | #[serde(deny_unknown_fields)] 37 | /// Predicate `SLSAProvenanceV02` means the predicate of SLSA format. 38 | /// 39 | /// [SLSAProvenanceV02](https://slsa.dev/provenance/v0.2) 40 | /// can be used together with most states. 41 | pub struct SLSAProvenanceV02 { 42 | pub builder: Builder, 43 | #[serde(rename = "buildType")] 44 | pub build_type: TypeURI, 45 | #[serde(skip_serializing_if = "Option::is_none")] 46 | pub invocation: Option, 47 | #[serde(rename = "buildConfig")] 48 | #[serde(skip_serializing_if = "Option::is_none")] 49 | pub build_config: Option, 50 | #[serde(skip_serializing_if = "Option::is_none")] 51 | pub metadata: Option, 52 | #[serde(skip_serializing_if = "Option::is_none")] 53 | pub materials: Option>, 54 | } 55 | 56 | impl PredicateLayout for SLSAProvenanceV02 { 57 | fn to_bytes(&self) -> Result> { 58 | Json::canonicalize(&Json::serialize(self)?) 59 | } 60 | 61 | fn into_enum(self: Box) -> PredicateWrapper { 62 | PredicateWrapper::SLSAProvenanceV0_2(*self) 63 | } 64 | 65 | fn version(&self) -> PredicateVer { 66 | PredicateVer::SLSAProvenanceV0_2 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | pub mod test { 72 | use std::collections::HashMap; 73 | use std::str; 74 | 75 | use once_cell::sync::Lazy; 76 | use serde_json::{json, Value}; 77 | use strum::IntoEnumIterator; 78 | 79 | use super::{ConfigSource, Invocation, SLSAProvenanceV02}; 80 | use crate::{ 81 | interchange::{DataInterchange, Json}, 82 | models::{ 83 | predicate::slsa_provenance_v01::{ 84 | Builder, Completeness, Material, ProvenanceMetadata, TypeURI, 85 | }, 86 | PredicateLayout, PredicateVer, PredicateWrapper, 87 | }, 88 | }; 89 | 90 | pub static STR_PREDICATE_PROVEN_V02: Lazy = Lazy::new(|| { 91 | let raw_data = json!({ 92 | "builder": { 93 | "id": "https://github.com/Attestations/GitHubHostedActions@v1" 94 | }, 95 | "buildType": "https://github.com/Attestations/GitHubActionsWorkflow@v1", 96 | "invocation": { 97 | "configSource": { 98 | "uri": "git+https://github.com/curl/curl-docker@master", 99 | "digest": { 100 | "sha1": "d6525c840a62b398424a78d792f457477135d0cf" 101 | }, 102 | "entryPoint": "build.yaml:maketgz" 103 | } 104 | }, 105 | "metadata": { 106 | "buildInvocationId":"test_invocation_id", 107 | "completeness": { 108 | "environment": true 109 | } 110 | }, 111 | "materials": [{ 112 | "uri": "git+https://github.com/curl/curl-docker@master", 113 | "digest": { 114 | "sha1": "d6525c840a62b398424a78d792f457477135d0cf" 115 | } 116 | }, { 117 | "uri": "github_hosted_vm:ubuntu-18.04:20210123.1" 118 | }] 119 | }); 120 | let value = serde_json::value::to_value(raw_data).unwrap(); 121 | let bytes = Json::canonicalize(&value).unwrap(); 122 | let data = str::from_utf8(&bytes).unwrap(); 123 | data.to_string() 124 | }); 125 | 126 | pub static PREDICATE_PROVEN_V02: Lazy = Lazy::new( 127 | || { 128 | let digest = HashMap::from([( 129 | "sha1".to_string(), 130 | "d6525c840a62b398424a78d792f457477135d0cf".to_string(), 131 | )]); 132 | SLSAProvenanceV02 { 133 | builder: Builder { 134 | id: TypeURI("https://github.com/Attestations/GitHubHostedActions@v1".to_string()), 135 | }, 136 | build_type: TypeURI( 137 | "https://github.com/Attestations/GitHubActionsWorkflow@v1".to_string(), 138 | ), 139 | invocation: Some(Invocation { 140 | config_source: Some(ConfigSource { 141 | uri: Some(TypeURI( 142 | "git+https://github.com/curl/curl-docker@master".to_string(), 143 | )), 144 | digest: Some(digest), 145 | entry_point: Some("build.yaml:maketgz".to_string()), 146 | }), 147 | parameters: None, 148 | environment: None, 149 | }), 150 | build_config: None, 151 | metadata: Some(ProvenanceMetadata { 152 | build_invocation_id: Some("test_invocation_id".to_string()), 153 | build_started_on: None, 154 | build_finished_on: None, 155 | completeness: Some(Completeness { 156 | arguments: None, 157 | environment: Some(true), 158 | materials: None, 159 | }), 160 | reproducible: None, 161 | }), 162 | materials: Some(vec![ 163 | Material { 164 | uri: Some(TypeURI( 165 | "git+https://github.com/curl/curl-docker@master".to_string(), 166 | )), 167 | digest: Some(HashMap::from([( 168 | "sha1".to_string(), 169 | "d6525c840a62b398424a78d792f457477135d0cf".to_string(), 170 | )])), 171 | }, 172 | Material { 173 | uri: Some(TypeURI( 174 | "github_hosted_vm:ubuntu-18.04:20210123.1".to_string(), 175 | )), 176 | digest: None, 177 | }, 178 | ]), 179 | } 180 | }, 181 | ); 182 | 183 | #[test] 184 | fn into_trait_equal() { 185 | let predicate = 186 | PredicateWrapper::SLSAProvenanceV0_2(PREDICATE_PROVEN_V02.clone()); 187 | let real = Box::new(PREDICATE_PROVEN_V02.clone()).into_enum(); 188 | 189 | assert_eq!(predicate, real); 190 | } 191 | 192 | #[test] 193 | fn create_predicate_from_meta() { 194 | // TODO: convert from metadata is no supported recentely 195 | } 196 | 197 | #[test] 198 | fn serialize_predicate() { 199 | let predicate = Box::new(PREDICATE_PROVEN_V02.clone()).into_enum(); 200 | let buf = predicate.into_trait().to_bytes().unwrap(); 201 | let predicate_serialized = str::from_utf8(&buf).unwrap(); 202 | 203 | assert_eq!(predicate_serialized, *STR_PREDICATE_PROVEN_V02); 204 | } 205 | 206 | #[test] 207 | fn deserialize_predicate() { 208 | let value: Value = 209 | serde_json::from_str(&STR_PREDICATE_PROVEN_V02).unwrap(); 210 | let predicate = PredicateWrapper::from_value( 211 | value, 212 | PredicateVer::SLSAProvenanceV0_2, 213 | ) 214 | .unwrap(); 215 | let real = Box::new(PREDICATE_PROVEN_V02.clone()).into_enum(); 216 | 217 | assert_eq!(predicate, real); 218 | } 219 | 220 | #[test] 221 | fn deserialize_auto() { 222 | let value: Value = 223 | serde_json::from_str(&STR_PREDICATE_PROVEN_V02).unwrap(); 224 | let predicate = PredicateWrapper::try_from_value(value).unwrap(); 225 | let real = Box::new(PREDICATE_PROVEN_V02.clone()).into_enum(); 226 | 227 | assert_eq!(predicate, real); 228 | } 229 | 230 | #[test] 231 | fn deserialize_dismatch() { 232 | let value: Value = 233 | serde_json::from_str(&STR_PREDICATE_PROVEN_V02).unwrap(); 234 | for version in PredicateVer::iter() { 235 | if version == PredicateVer::SLSAProvenanceV0_2 { 236 | continue; 237 | } 238 | let predicate = 239 | PredicateWrapper::from_value(value.clone(), version); 240 | 241 | assert!(predicate.is_err()); 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/models/statement/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod state_naive; 2 | pub mod state_v01; 3 | use serde_json::Value; 4 | pub use state_naive::StateNaive; 5 | pub use state_v01::StateV01; 6 | 7 | use std::convert::TryFrom; 8 | use std::fmt::{Display, Formatter, Result as FmtResult}; 9 | 10 | use serde::de::{Deserializer, Error as DeserializeError}; 11 | use serde::ser::Serializer; 12 | use serde::{Deserialize, Serialize}; 13 | use strum::IntoEnumIterator; 14 | use strum_macros::EnumIter; 15 | 16 | use super::{LinkMetadata, PredicateLayout}; 17 | use crate::Error; 18 | use crate::Result; 19 | 20 | #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, EnumIter)] 21 | pub enum StatementVer { 22 | Naive, 23 | V0_1, 24 | } 25 | 26 | impl TryFrom for StatementVer { 27 | type Error = crate::Error; 28 | 29 | fn try_from(target: String) -> Result { 30 | match target.as_str() { 31 | "link" => Ok(StatementVer::Naive), 32 | "https://in-toto.io/Statement/v0.1" => Ok(StatementVer::V0_1), 33 | _ => Err(Error::StringConvertFailed(target)), 34 | } 35 | } 36 | } 37 | 38 | impl From for String { 39 | fn from(value: StatementVer) -> Self { 40 | match value { 41 | StatementVer::Naive => "link".to_string(), 42 | StatementVer::V0_1 => { 43 | "https://in-toto.io/Statement/v0.1".to_string() 44 | } 45 | } 46 | } 47 | } 48 | 49 | impl Serialize for StatementVer { 50 | fn serialize(&self, ser: S) -> ::std::result::Result 51 | where 52 | S: Serializer, 53 | { 54 | let target: String = (*self).into(); 55 | ser.serialize_str(&target) 56 | } 57 | } 58 | 59 | impl<'de> Deserialize<'de> for StatementVer { 60 | fn deserialize>( 61 | de: D, 62 | ) -> ::std::result::Result { 63 | let target: String = Deserialize::deserialize(de)?; 64 | StatementVer::try_from(target) 65 | .map_err(|e| DeserializeError::custom(format!("{:?}", e))) 66 | } 67 | } 68 | 69 | impl Display for StatementVer { 70 | fn fmt(&self, fmt: &mut Formatter) -> FmtResult { 71 | match self { 72 | StatementVer::V0_1 => fmt.write_str("v0.1")?, 73 | StatementVer::Naive => fmt.write_str("naive")?, 74 | } 75 | Ok(()) 76 | } 77 | } 78 | 79 | #[derive(Debug, Serialize, PartialEq, Eq)] 80 | pub enum StatementWrapper { 81 | Naive(StateNaive), 82 | V0_1(StateV01), 83 | } 84 | impl<'de> Deserialize<'de> for StatementWrapper { 85 | fn deserialize>( 86 | de: D, 87 | ) -> ::std::result::Result { 88 | let value = Value::deserialize(de)?; 89 | StatementWrapper::try_from_value(value) 90 | .map_err(|e| DeserializeError::custom(format!("{:?}", e))) 91 | } 92 | } 93 | 94 | pub trait FromMerge: Sized { 95 | fn merge( 96 | meta: LinkMetadata, 97 | predicate: Option>, 98 | ) -> Result; 99 | } 100 | 101 | impl StatementWrapper { 102 | pub fn into_trait(self) -> Box { 103 | match self { 104 | StatementWrapper::Naive(link) => Box::new(link), 105 | StatementWrapper::V0_1(link) => Box::new(link), 106 | } 107 | } 108 | 109 | pub fn from_meta( 110 | meta: LinkMetadata, 111 | predicate: Option>, 112 | version: StatementVer, 113 | ) -> Self { 114 | match version { 115 | StatementVer::Naive => { 116 | Self::Naive(StateNaive::merge(meta, predicate).unwrap()) 117 | } 118 | StatementVer::V0_1 => { 119 | Self::V0_1(StateV01::merge(meta, predicate).unwrap()) 120 | } 121 | } 122 | } 123 | 124 | /// Deserialize method for `StatementWrapper` from `serde:Value` by its version 125 | fn from_value(value: Value, version: StatementVer) -> Result { 126 | match version { 127 | StatementVer::Naive => serde_json::from_value(value) 128 | .map(Self::Naive) 129 | .map_err(|e| e.into()), 130 | StatementVer::V0_1 => serde_json::from_value(value) 131 | .map(Self::V0_1) 132 | .map_err(|e| e.into()), 133 | } 134 | } 135 | 136 | /// Auto judge the `PredicateWrapper` version from `serde:Value` 137 | pub fn judge_from_value(value: &Value) -> Result { 138 | for version in StatementVer::iter() { 139 | let wrapper = StatementWrapper::from_value(value.clone(), version); 140 | if wrapper.is_ok() { 141 | return Ok(version); 142 | } 143 | } 144 | Err(Error::Programming("no available value parser".to_string())) 145 | } 146 | 147 | // Auto deserialize for `PredicateWrapper` by any possible version. 148 | pub fn try_from_value(value: Value) -> Result { 149 | let version = Self::judge_from_value(&value)?; 150 | StatementWrapper::from_value(value, version) 151 | } 152 | } 153 | 154 | pub trait StateLayout { 155 | fn version(&self) -> StatementVer; 156 | fn into_enum(self: Box) -> StatementWrapper; 157 | fn to_bytes(&self) -> Result>; 158 | } 159 | -------------------------------------------------------------------------------- /src/models/statement/state_naive.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, fmt::Debug}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::{FromMerge, StateLayout, StatementVer, StatementWrapper}; 6 | use crate::models::{LinkMetadata, TargetDescription, VirtualTargetPath}; 7 | use crate::{ 8 | interchange::{DataInterchange, Json}, 9 | models::{byproducts::ByProducts, step::Command, PredicateLayout}, 10 | Error, Result, 11 | }; 12 | 13 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 14 | #[serde(deny_unknown_fields)] 15 | /// Statement `Naive` means the predicate of original format. 16 | /// 17 | /// Can be used together with no predicate as `None`. 18 | pub struct StateNaive { 19 | #[serde(rename = "_type")] 20 | typ: String, 21 | name: String, 22 | materials: BTreeMap, 23 | products: BTreeMap, 24 | env: Option>, 25 | command: Command, 26 | byproducts: ByProducts, 27 | } 28 | 29 | impl StateLayout for StateNaive { 30 | fn version(&self) -> StatementVer { 31 | StatementVer::Naive 32 | } 33 | 34 | fn into_enum(self: Box) -> StatementWrapper { 35 | StatementWrapper::Naive(*self) 36 | } 37 | 38 | fn to_bytes(&self) -> Result> { 39 | Json::canonicalize(&Json::serialize(self)?) 40 | } 41 | } 42 | 43 | impl FromMerge for StateNaive { 44 | fn merge( 45 | meta: LinkMetadata, 46 | predicate: Option>, 47 | ) -> Result { 48 | if let Some(p) = predicate { 49 | return Err(Error::AttestationFormatDismatch( 50 | "None".to_string(), 51 | p.version().into(), 52 | )); 53 | }; 54 | let version = StatementVer::Naive.into(); 55 | Ok(StateNaive { 56 | typ: version, 57 | name: meta.name, 58 | materials: meta.materials, 59 | products: meta.products, 60 | env: meta.env, 61 | command: meta.command, 62 | byproducts: meta.byproducts, 63 | }) 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | pub mod test { 69 | use std::collections::BTreeMap; 70 | use std::str; 71 | 72 | use once_cell::sync::Lazy; 73 | use serde_json::{json, Value}; 74 | use strum::IntoEnumIterator; 75 | 76 | use super::StateNaive; 77 | use crate::interchange::{DataInterchange, Json}; 78 | use crate::models::byproducts::ByProducts; 79 | use crate::models::statement::{ 80 | StateLayout, StatementVer, StatementWrapper, 81 | }; 82 | use crate::models::test::BLANK_META; 83 | 84 | pub static STR_NAIVE: Lazy = Lazy::new(|| { 85 | let raw_data = json!({ 86 | "_type": "link", 87 | "byproducts": {}, 88 | "command": [], 89 | "env": null, 90 | "materials": {}, 91 | "name": "", 92 | "products": {} 93 | }); 94 | let value = serde_json::value::to_value(raw_data).unwrap(); 95 | let bytes = Json::canonicalize(&value).unwrap(); 96 | let data = str::from_utf8(&bytes).unwrap(); 97 | data.to_string() 98 | }); 99 | 100 | pub static STATE_NAIVE: Lazy = Lazy::new(|| StateNaive { 101 | typ: StatementVer::Naive.into(), 102 | name: "".to_string(), 103 | materials: BTreeMap::new(), 104 | products: BTreeMap::new(), 105 | env: None, 106 | command: "".into(), 107 | byproducts: ByProducts::new(), 108 | }); 109 | 110 | #[test] 111 | fn into_trait_equal() { 112 | let state = StatementWrapper::Naive(STATE_NAIVE.clone()); 113 | let real = Box::new(STATE_NAIVE.clone()).into_enum(); 114 | 115 | assert_eq!(state, real); 116 | } 117 | 118 | #[test] 119 | fn create_statement_from_meta() { 120 | let state = StatementWrapper::from_meta( 121 | BLANK_META.clone(), 122 | None, 123 | StatementVer::Naive, 124 | ); 125 | let real = Box::new(STATE_NAIVE.clone()).into_enum(); 126 | 127 | assert_eq!(state, real); 128 | } 129 | 130 | #[test] 131 | fn serialize_statement() { 132 | let state = Box::new(STATE_NAIVE.clone()).into_enum(); 133 | let buf = state.into_trait().to_bytes().unwrap(); 134 | let link_serialized = str::from_utf8(&buf).unwrap(); 135 | 136 | assert_eq!(link_serialized, *STR_NAIVE); 137 | } 138 | 139 | #[test] 140 | fn deserialize_statement() { 141 | let value: Value = serde_json::from_str(&STR_NAIVE).unwrap(); 142 | let link = 143 | StatementWrapper::from_value(value, StatementVer::Naive).unwrap(); 144 | let real = Box::new(STATE_NAIVE.clone()).into_enum(); 145 | 146 | assert_eq!(link, real); 147 | } 148 | 149 | #[test] 150 | fn deserialize_auto() { 151 | let value: Value = serde_json::from_str(&STR_NAIVE).unwrap(); 152 | let link = StatementWrapper::try_from_value(value).unwrap(); 153 | let real = Box::new(STATE_NAIVE.clone()).into_enum(); 154 | 155 | assert_eq!(link, real); 156 | } 157 | 158 | #[test] 159 | fn deserialize_dismatch() { 160 | let value: Value = serde_json::from_str(&STR_NAIVE).unwrap(); 161 | for version in StatementVer::iter() { 162 | if version == StatementVer::Naive { 163 | continue; 164 | } 165 | let state = StatementWrapper::from_value(value.clone(), version); 166 | 167 | assert!(state.is_err()); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/models/statement/state_v01.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{ 6 | interchange::{DataInterchange, Json}, 7 | models::{ 8 | LinkMetadata, PredicateLayout, PredicateVer, PredicateWrapper, 9 | TargetDescription, VirtualTargetPath, 10 | }, 11 | Error, 12 | }; 13 | 14 | use super::{FromMerge, StateLayout, StatementVer, StatementWrapper}; 15 | use crate::Result; 16 | 17 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] 18 | #[serde(deny_unknown_fields)] 19 | /// Statement `V0_1` means the statement of contains a predicate for SLSA format. 20 | /// 21 | /// Can be used together with most predicate. 22 | pub struct StateV01 { 23 | #[serde(rename = "_type")] 24 | typ: String, 25 | subject: BTreeMap, 26 | #[serde(rename = "predicateType")] 27 | predicate_type: PredicateVer, 28 | predicate: PredicateWrapper, 29 | } 30 | 31 | impl StateLayout for StateV01 { 32 | fn version(&self) -> StatementVer { 33 | StatementVer::V0_1 34 | } 35 | 36 | fn into_enum(self: Box) -> StatementWrapper { 37 | StatementWrapper::V0_1(*self) 38 | } 39 | 40 | fn to_bytes(&self) -> Result> { 41 | Json::canonicalize(&Json::serialize(self)?) 42 | } 43 | } 44 | 45 | impl FromMerge for StateV01 { 46 | fn merge( 47 | meta: LinkMetadata, 48 | predicate: Option>, 49 | ) -> Result { 50 | if predicate.is_none() { 51 | return Err(Error::AttestationFormatDismatch( 52 | StatementVer::V0_1.to_string(), 53 | "None".to_string(), 54 | )); 55 | } 56 | let p = predicate.ok_or_else(|| { 57 | Error::Programming("match rules failed for StateV01".to_string()) 58 | })?; 59 | let version = StatementVer::V0_1.into(); 60 | Ok(StateV01 { 61 | typ: version, 62 | subject: meta.products, 63 | predicate_type: p.version(), 64 | predicate: p.into_enum(), 65 | }) 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | pub mod test { 71 | use crate::{ 72 | interchange::{DataInterchange, Json}, 73 | models::{ 74 | predicate::link_v02::test::PREDICATE_LINK_V02, 75 | statement::{StateLayout, StatementVer, StatementWrapper}, 76 | test::BLANK_META, 77 | PredicateLayout, PredicateVer, 78 | }, 79 | }; 80 | use std::collections::BTreeMap; 81 | use std::str; 82 | 83 | use once_cell::sync::Lazy; 84 | use serde_json::{json, Value}; 85 | use strum::IntoEnumIterator; 86 | 87 | use super::StateV01; 88 | 89 | pub static STR_V01: Lazy = Lazy::new(|| { 90 | let raw_data = json!({ 91 | "_type": "https://in-toto.io/Statement/v0.1", 92 | "predicateType": "https://in-toto.io/Link/v0.2", 93 | "predicate": { 94 | "byproducts": { 95 | "return-value": 0, 96 | "stderr": "", 97 | "stdout": "" 98 | }, 99 | "command": [], 100 | "env": null, 101 | "materials": {}, 102 | "name": "" 103 | }, 104 | "subject": {} 105 | }); 106 | let value = serde_json::value::to_value(raw_data).unwrap(); 107 | let bytes = Json::canonicalize(&value).unwrap(); 108 | let data = str::from_utf8(&bytes).unwrap(); 109 | data.to_string() 110 | }); 111 | 112 | pub static STATE_V01: Lazy = Lazy::new(|| StateV01 { 113 | typ: StatementVer::V0_1.into(), 114 | subject: BTreeMap::new(), 115 | predicate_type: PredicateVer::LinkV0_2, 116 | predicate: Box::new(PREDICATE_LINK_V02.clone()).into_enum(), 117 | }); 118 | 119 | #[test] 120 | fn into_trait_equal() { 121 | let link = StatementWrapper::V0_1(STATE_V01.clone()); 122 | let real = Box::new(STATE_V01.clone()).into_enum(); 123 | 124 | assert_eq!(link, real); 125 | } 126 | 127 | #[test] 128 | fn create_statement_from_meta() { 129 | let link = StatementWrapper::from_meta( 130 | BLANK_META.clone(), 131 | Some(Box::new(PREDICATE_LINK_V02.clone())), 132 | StatementVer::V0_1, 133 | ); 134 | let real = Box::new(STATE_V01.clone()).into_enum(); 135 | 136 | assert_eq!(link, real); 137 | } 138 | 139 | #[test] 140 | fn serialize_statement() { 141 | let state = Box::new(STATE_V01.clone()).into_enum(); 142 | let buf = state.into_trait().to_bytes().unwrap(); 143 | let link_serialized = str::from_utf8(&buf).unwrap(); 144 | 145 | assert_eq!(link_serialized, *STR_V01); 146 | } 147 | 148 | #[test] 149 | fn deserialize_statement() { 150 | let value: Value = serde_json::from_str(&STR_V01).unwrap(); 151 | let link = 152 | StatementWrapper::from_value(value, StatementVer::V0_1).unwrap(); 153 | let real = Box::new(STATE_V01.clone()).into_enum(); 154 | 155 | assert_eq!(link, real); 156 | } 157 | 158 | #[test] 159 | fn deserialize_auto() { 160 | let value: Value = serde_json::from_str(&STR_V01).unwrap(); 161 | let link = StatementWrapper::try_from_value(value).unwrap(); 162 | let real = Box::new(STATE_V01.clone()).into_enum(); 163 | 164 | assert_eq!(link, real); 165 | } 166 | 167 | #[test] 168 | fn deserialize_dismatch() { 169 | let value: Value = serde_json::from_str(&STR_V01).unwrap(); 170 | for version in StatementVer::iter() { 171 | if version == StatementVer::V0_1 { 172 | continue; 173 | } 174 | let state = StatementWrapper::from_value(value.clone(), version); 175 | 176 | assert!(state.is_err()); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/rulelib.rs: -------------------------------------------------------------------------------- 1 | //! Helper for ArtifactRule to apply on LinkMetadata 2 | 3 | use std::collections::{BTreeMap, BTreeSet, HashMap}; 4 | use std::path::PathBuf; 5 | 6 | use log::warn; 7 | 8 | use crate::models::rule::Artifact; 9 | use crate::models::supply_chain_item::SupplyChainItem; 10 | use crate::models::{rule::ArtifactRule, LinkMetadata}; 11 | use crate::models::{TargetDescription, VirtualTargetPath}; 12 | use crate::{Error, Result}; 13 | 14 | /// Canonicalize a given [`VirtualTargetPath`]. For example 15 | /// `/test/1/2/../3` -> `/test/1/3`. If any error 16 | /// occurs, just warn it and return None. 17 | fn canonicalize_path(path: &VirtualTargetPath) -> Option { 18 | let path = path_clean::clean(path.value()); 19 | VirtualTargetPath::new(path.into_os_string().into_string().unwrap()).ok() 20 | } 21 | 22 | /// Apply match rule. The parameters: 23 | /// * `rule`: MATCH rule to be applied (if not a MATCH rule, will be paniced) 24 | /// * `src_artifacts`: artifacts of a given link (either Products or Materials) 25 | /// * `src_artifact_queue`: artifact paths (canonicalized) of the same link (either Products or Materials) 26 | /// * `items_metadata`: a to hashmap 27 | /// 28 | /// This function will match the artifact paths of `src_artifact_queue` 29 | /// and the `dst_artifacts`. Here `dst_artifacts` can be calculated 30 | /// by indexing the step name from `items_metadata`. Return value is 31 | /// the matched artifact paths. 32 | fn verify_match_rule( 33 | rule: &ArtifactRule, 34 | src_artifacts: &BTreeMap, 35 | src_artifact_queue: &BTreeSet, 36 | items_metadata: &HashMap, 37 | ) -> BTreeSet { 38 | let mut consumed = BTreeSet::new(); 39 | 40 | match rule { 41 | ArtifactRule::Match { 42 | pattern, 43 | in_src, 44 | with, 45 | in_dst, 46 | from, 47 | } => { 48 | let dst_link = match items_metadata.get(from) { 49 | Some(lm) => lm, 50 | None => { 51 | warn!("no link metadata {} found.", from); 52 | return consumed; 53 | } 54 | }; 55 | 56 | let dst_artifact = match with { 57 | Artifact::Materials => &dst_link.materials, 58 | Artifact::Products => &dst_link.products, 59 | }; 60 | 61 | let src_artifacts: BTreeMap = 62 | src_artifacts 63 | .iter() 64 | .map(|(path, value)| { 65 | ( 66 | canonicalize_path(path) 67 | .unwrap_or_else(|| path.clone()), 68 | value.clone(), 69 | ) 70 | }) 71 | .collect(); 72 | 73 | let dst_artifacts: BTreeMap = 74 | dst_artifact 75 | .iter() 76 | .map(|(path, value)| { 77 | ( 78 | canonicalize_path(path) 79 | .unwrap_or_else(|| path.clone()), 80 | value.clone(), 81 | ) 82 | }) 83 | .collect(); 84 | 85 | let dst_prefix = { 86 | match in_dst { 87 | None => String::new(), 88 | Some(dst_dir) => { 89 | let mut res = PathBuf::new(); 90 | res.push(dst_dir); 91 | let mut res = res.to_string_lossy().to_string(); 92 | res.push('/'); 93 | res 94 | } 95 | } 96 | }; 97 | 98 | let src_prefix = { 99 | match in_src { 100 | None => String::new(), 101 | Some(src_dir) => { 102 | let mut res = PathBuf::new(); 103 | res.push(src_dir); 104 | let mut res = res.to_string_lossy().to_string(); 105 | res.push('/'); 106 | res 107 | } 108 | } 109 | }; 110 | 111 | for src_path in src_artifact_queue { 112 | let src_base_path = src_path 113 | .value() 114 | .strip_prefix(&src_prefix) 115 | .unwrap_or_else(|| src_path.value()); 116 | let src_base_path = 117 | VirtualTargetPath::new(src_base_path.to_string()) 118 | .expect("Unexpected VirtualTargetPath creation failed"); 119 | 120 | if let Err(e) = src_base_path.matches(pattern.value()) { 121 | warn!("match failed: {}", e.to_string()); 122 | continue; 123 | } 124 | 125 | let dst_path = { 126 | let mut res = PathBuf::new(); 127 | res.push(&dst_prefix); 128 | res.push(src_base_path.value()); 129 | VirtualTargetPath::new(res.to_string_lossy().to_string()) 130 | .expect("Unexpected VirtualTargetPath creation failed") 131 | }; 132 | 133 | if let Some(dst_artifact) = dst_artifacts.get(&dst_path) { 134 | if src_artifacts[src_path] == *dst_artifact { 135 | consumed.insert(src_path.clone()); 136 | } 137 | } 138 | } 139 | } 140 | _ => panic!("Unexpected rule type"), 141 | } 142 | 143 | consumed 144 | } 145 | 146 | /// Apply rules of the given [`SupplyChainItem`] onto the [`LinkMetadata`] 147 | pub(crate) fn apply_rules_on_link( 148 | item: &Box, 149 | reduced_link_files: &HashMap, 150 | ) -> Result<()> { 151 | // name of the given item 152 | let item_name = item.name(); 153 | 154 | // get the LinkMetadata for the given SupplyChainItem (`step` or `inspection`) 155 | let src_link = reduced_link_files.get(item_name).ok_or_else(|| { 156 | Error::VerificationFailure(format!( 157 | "can not find link metadata of step {}", 158 | item_name, 159 | )) 160 | })?; 161 | 162 | // materials of this link 163 | let material_paths: BTreeSet = src_link 164 | .materials 165 | .iter() 166 | .filter_map(|(path, _)| canonicalize_path(path)) 167 | .collect(); 168 | 169 | // products of this link 170 | let product_paths: BTreeSet = src_link 171 | .products 172 | .iter() 173 | .filter_map(|(path, _)| canonicalize_path(path)) 174 | .collect(); 175 | 176 | // prepare sets of artifacts for `create`, `delete` and `modify` rules. 177 | // these are calculated from the link's materials and products 178 | let created: BTreeSet<_> = 179 | product_paths.difference(&material_paths).cloned().collect(); 180 | let deleted: BTreeSet<_> = 181 | material_paths.difference(&product_paths).cloned().collect(); 182 | let modified: BTreeSet<_> = material_paths 183 | .intersection(&product_paths) 184 | .cloned() 185 | .filter_map(|name| { 186 | if src_link.materials[&name] != src_link.products[&name] { 187 | Some(name) 188 | } else { 189 | None 190 | } 191 | }) 192 | .collect(); 193 | 194 | #[derive(Debug)] 195 | struct VerificationDataList<'a> { 196 | src_type: Artifact, 197 | rules: &'a Vec, 198 | artifacts: &'a BTreeMap, 199 | artifact_paths: BTreeSet, 200 | } 201 | 202 | let list = [ 203 | // rule expected materials 204 | VerificationDataList { 205 | src_type: Artifact::Materials, 206 | rules: item.expected_materials(), 207 | artifacts: &src_link.materials, 208 | artifact_paths: material_paths, 209 | }, 210 | // rule expected products 211 | VerificationDataList { 212 | src_type: Artifact::Products, 213 | rules: item.expected_products(), 214 | artifacts: &src_link.products, 215 | artifact_paths: product_paths, 216 | }, 217 | ]; 218 | 219 | for verification_data in list { 220 | // rules to apply onto the link metadata 221 | let rules = verification_data.rules; 222 | // artifacts from the link metadata of this step, whose paths are all canonicalized 223 | let mut queue = verification_data.artifact_paths; 224 | // artifacts from the link metadata of this step and their digests 225 | let artifacts = verification_data.artifacts; 226 | 227 | // for every rule, we choose those items whose path matches the given pattern 228 | // rule in the queue as a set named `filtered`. and use the set to filter 229 | // items in `queue` using rule CREATE, DELETE, MODIFY, ALLOW, REQUIRE and DISALLOW. 230 | // besides, use MATCH rule to filter other items. 231 | for rule in rules { 232 | let filtered: BTreeSet<_> = queue 233 | .iter() 234 | .filter(|p| p.matches(rule.pattern().value()).unwrap_or(false)) 235 | .cloned() 236 | .collect(); 237 | let consumed = match rule { 238 | ArtifactRule::Create(_) => { 239 | filtered.intersection(&created).cloned().collect() 240 | } 241 | ArtifactRule::Delete(_) => { 242 | filtered.intersection(&deleted).cloned().collect() 243 | } 244 | ArtifactRule::Modify(_) => { 245 | filtered.intersection(&modified).cloned().collect() 246 | } 247 | ArtifactRule::Allow(_) => filtered, 248 | ArtifactRule::Require(_) => { 249 | if !queue.contains(rule.pattern()) { 250 | return Err(Error::ArtifactRuleError(format!( 251 | r#"artifact verification failed for {:?} in REQUIRE '{:?}', 252 | because {:?} is not in {:?}"#, 253 | verification_data.src_type, 254 | rule.pattern(), 255 | rule.pattern(), 256 | queue 257 | ))); 258 | } else { 259 | BTreeSet::new() 260 | } 261 | } 262 | ArtifactRule::Disallow(_) => { 263 | if !filtered.is_empty() { 264 | return Err(Error::ArtifactRuleError(format!( 265 | r#"artifact verification failed for {:?} in DISALLOW, because {:?} is disallowed by rule {:?} in {}"#, 266 | verification_data.src_type, 267 | filtered, 268 | rule, 269 | item_name, 270 | ))); 271 | } else { 272 | BTreeSet::new() 273 | } 274 | } 275 | ArtifactRule::Match { .. } => verify_match_rule( 276 | rule, 277 | artifacts, 278 | &queue, 279 | reduced_link_files, 280 | ), 281 | }; 282 | 283 | queue = queue.difference(&consumed).cloned().collect(); 284 | } 285 | } 286 | 287 | Ok(()) 288 | } 289 | 290 | #[cfg(test)] 291 | mod tests { 292 | 293 | use rstest::rstest; 294 | 295 | use crate::models::VirtualTargetPath; 296 | 297 | #[rstest] 298 | #[case("test/../1/1/2", "1/1/2")] 299 | #[case("test/../../1/2", "../1/2")] 300 | #[case("../././../1/2", "../../1/2")] 301 | fn canonicalize_path(#[case] given: &str, #[case] expected: &str) { 302 | let expected = Some( 303 | VirtualTargetPath::new(expected.to_string()) 304 | .expect("Unexpected creation failed"), 305 | ); 306 | let processed = VirtualTargetPath::new(given.to_string()) 307 | .expect("Unexpected creation failed"); 308 | let processed = super::canonicalize_path(&processed); 309 | assert_eq!(expected, processed); 310 | } 311 | 312 | #[rstest] 313 | #[ 314 | case( 315 | r#"["MATCH", "demo-project.tar.gz", "WITH", "PRODUCTS", "FROM", "package"]"#, 316 | r#"{"demo-project.tar.gz": {"sha256": "2989659e6836c941e9015bf38af3cb045365520dbf80460d8a44b2c5b6677fd9"}, "not-deleted.tar": {"sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}"#, 317 | r#"["demo-project.tar.gz", "not-deleted.tar"]"#, 318 | r#"{"package":{"_type":"link","byproducts":{"return-value":0,"stderr":"","stdout":"demo-project/\ndemo-project/foo.py\n"},"command":["tar","--exclude",".git","-zcvf","demo-project.tar.gz","demo-project"],"environment":{},"materials":{"demo-project/foo.py":{"sha256":"c2c0ea54fa94fac3a4e1575d6ed3bbd1b01a6d0b8deb39196bdc31c457ef731b"}},"name":"package","products":{"demo-project.tar.gz":{"sha256":"2989659e6836c941e9015bf38af3cb045365520dbf80460d8a44b2c5b6677fd9"}}}}"#, 319 | r#"["demo-project.tar.gz"]"#, 320 | )] 321 | #[ 322 | case( 323 | r#"["MATCH", "*", "WITH", "PRODUCTS", "IN", "test", "FROM", "package"]"#, 324 | r#"{"demo-project.tar.gz": {"sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}"#, 325 | r#"["demo-project.tar.gz"]"#, 326 | r#"{"package":{"_type":"link","byproducts":{"return-value":0,"stderr":"","stdout":"demo-project/\ndemo-project/foo.py\n"},"command":["tar","--exclude",".git","-zcvf","demo-project.tar.gz","demo-project"],"environment":{},"materials":{"demo-project/foo.py":{"sha256":"c2c0ea54fa94fac3a4e1575d6ed3bbd1b01a6d0b8deb39196bdc31c457ef731b"}},"name":"package","products":{"test/demo-project.tar.gz":{"sha256":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}}}"#, 327 | r#"["demo-project.tar.gz"]"#, 328 | ) 329 | ] 330 | #[ 331 | case( 332 | r#"["MATCH", "test1", "IN", "dir1", "WITH", "PRODUCTS", "IN", "test", "FROM", "package"]"#, 333 | r#"{"dir1/test1": {"sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}"#, 334 | r#"["dir1/test1"]"#, 335 | r#"{"package":{"_type":"link","byproducts":{},"command":[""],"environment":{},"materials":{},"name":"package","products":{"test/test1":{"sha256":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}}}"#, 336 | r#"["dir1/test1"]"#, 337 | ) 338 | ] 339 | fn verify_match_rule( 340 | #[case] rule: &str, 341 | #[case] src_artifacts: &str, 342 | #[case] src_artifact_queue: &str, 343 | #[case] items_metadata: &str, 344 | #[case] expected: &str, 345 | ) { 346 | let rule = 347 | serde_json::from_str(rule).expect("Parse artifact rule failed"); 348 | let src_artifacts = serde_json::from_str(src_artifacts) 349 | .expect("Parse Source Artifacts failed"); 350 | let src_artifact_queue = serde_json::from_str(src_artifact_queue) 351 | .expect("Parse Source Artifact Queue failed"); 352 | let items_metadata = serde_json::from_str(items_metadata) 353 | .expect("Parse Metadata HashMap failed"); 354 | let expected = serde_json::from_str(expected).expect("Parse failed"); 355 | let got = super::verify_match_rule( 356 | &rule, 357 | &src_artifacts, 358 | &src_artifact_queue, 359 | &items_metadata, 360 | ); 361 | assert_eq!(got, expected); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /tests/ecdsa/ec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ecdsa/ec -------------------------------------------------------------------------------- /tests/ecdsa/ec.pk8.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ecdsa/ec.pk8.der -------------------------------------------------------------------------------- /tests/ecdsa/ec.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkvD6ZeE7OAU09FK6ZMrOVD+eYFCe 3 | bIHQg1MoQnHlmxx3gyINRA0f9Grxn1dT4NxFPItqMiymZlo8BYzJThxeqg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /tests/ecdsa/ec.spki.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ecdsa/ec.spki.der -------------------------------------------------------------------------------- /tests/ecdsa/gen.sh: -------------------------------------------------------------------------------- 1 | # Generate EC private key: 2 | openssl ecparam -name prime256v1 -genkey -noout -out ec.pem 3 | 4 | # Extract/generate the public key from the private key: 5 | openssl ec -in ec.pem -pubout -out ec.pub 6 | 7 | # Generate public key in Subject Public-Key Infomation format: 8 | openssl ec -in ec.pem -pubout -outform der -out ec.spki.der 9 | 10 | # Generate the key in PKCS8 format: 11 | openssl pkcs8 -in ec.pem -outform der -out ec.pk8.der -topk8 -nocrypt 12 | 13 | -------------------------------------------------------------------------------- /tests/ed25519/ed25519-1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ed25519/ed25519-1 -------------------------------------------------------------------------------- /tests/ed25519/ed25519-1.pk8.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ed25519/ed25519-1.pk8.der -------------------------------------------------------------------------------- /tests/ed25519/ed25519-1.pub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ed25519/ed25519-1.pub -------------------------------------------------------------------------------- /tests/ed25519/ed25519-1.spki.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ed25519/ed25519-1.spki.der -------------------------------------------------------------------------------- /tests/ed25519/ed25519-2.pk8.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ed25519/ed25519-2.pk8.der -------------------------------------------------------------------------------- /tests/ed25519/ed25519-3.pk8.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ed25519/ed25519-3.pk8.der -------------------------------------------------------------------------------- /tests/ed25519/ed25519-4.pk8.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ed25519/ed25519-4.pk8.der -------------------------------------------------------------------------------- /tests/ed25519/ed25519-5.pk8.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ed25519/ed25519-5.pk8.der -------------------------------------------------------------------------------- /tests/ed25519/ed25519-6.pk8.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/ed25519/ed25519-6.pk8.der -------------------------------------------------------------------------------- /tests/rsa/alice.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxPX3kFs/z645x4UOC3KF 3 | Y3V80YQtKrp6YS3qU+Jlvx/XzK53lb4sCDRU9jqBBx3We45TmFUibroMd8tQXCUS 4 | e8gYCBUBqBmmz0dEHJYbW0tYF7IoapMIxhRYn76YqNdl1JoRTcmzIaOJ7QrHxQrS 5 | GpivvTm6kQ9WLeApG1GLYJ3C3Wl4bnsI1bKSv55Zi45/JawHzTzYUAIXX9qCd3Io 6 | HzDucz9IAj9Ookw0va/q9FjoPGrRB80IReVxLVnbo6pYJfu/O37jvEobHFa8ckHd 7 | YxUIg8wvkIOy1O3M74lBDm6CVI0ZO25xPlDB/4nHAE1PbA3aF3lw8JGuxLDsetxm 8 | fzgAleVt4vXLQiCrZaLf+0cM97JcT7wdHcbIvRLsij9LNP+2tWZgeZ/hIAOEdaDq 9 | cYANPDIAxfTvbe9I0sXrCtrLer1SS7GqUmdFCdkdun8erXdNF0ls9Rp4cbYhjdf3 10 | yMxdI/24LUOOQ71cHW3ITIDImm6I8KmrXFM2NewTARKfAgMBAAE= 11 | -----END PUBLIC KEY----- 12 | -------------------------------------------------------------------------------- /tests/rsa/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | 4 | cd "$(dirname "$0")" 5 | 6 | for key_size in 2048 4096; do 7 | key="rsa-$key_size" 8 | pk8="$key.pk8.der" 9 | spki="$key.spki.der" 10 | pkcs1="$key.pkcs1.der" 11 | key="$key.der" 12 | 13 | if [ ! -f "$key" ]; then 14 | openssl genpkey -algorithm RSA \ 15 | -pkeyopt "rsa_keygen_bits:$key_size" \ 16 | -pkeyopt rsa_keygen_pubexp:65537 \ 17 | -outform der \ 18 | -out "$key" 19 | fi 20 | 21 | openssl rsa -in "$key" \ 22 | -inform der \ 23 | -RSAPublicKey_out \ 24 | -outform der \ 25 | -out "$pkcs1" 26 | 27 | openssl rsa -in "$key" \ 28 | -inform der \ 29 | -pubout \ 30 | -outform der \ 31 | -out "$spki" 32 | 33 | openssl pkcs8 -topk8 \ 34 | -inform der \ 35 | -in "$key" \ 36 | -outform der \ 37 | -out "$pk8" \ 38 | -nocrypt 39 | done 40 | -------------------------------------------------------------------------------- /tests/rsa/rsa-2048: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/rsa/rsa-2048 -------------------------------------------------------------------------------- /tests/rsa/rsa-2048.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/rsa/rsa-2048.der -------------------------------------------------------------------------------- /tests/rsa/rsa-2048.pk8.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/rsa/rsa-2048.pk8.der -------------------------------------------------------------------------------- /tests/rsa/rsa-2048.pkcs1.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/rsa/rsa-2048.pkcs1.der -------------------------------------------------------------------------------- /tests/rsa/rsa-2048.spki.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/rsa/rsa-2048.spki.der -------------------------------------------------------------------------------- /tests/rsa/rsa-4096.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/rsa/rsa-4096.der -------------------------------------------------------------------------------- /tests/rsa/rsa-4096.pk8.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/rsa/rsa-4096.pk8.der -------------------------------------------------------------------------------- /tests/rsa/rsa-4096.pkcs1.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/rsa/rsa-4096.pkcs1.der -------------------------------------------------------------------------------- /tests/rsa/rsa-4096.spki.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/rsa/rsa-4096.spki.der -------------------------------------------------------------------------------- /tests/runlib.rs: -------------------------------------------------------------------------------- 1 | use in_toto::{ 2 | crypto::{KeyType, PrivateKey, SignatureScheme}, 3 | interchange::Json, 4 | models::{byproducts::ByProducts, LinkMetadataBuilder, VirtualTargetPath}, 5 | runlib::in_toto_run, 6 | }; 7 | use std::fs::{canonicalize, write}; 8 | use std::os::unix::fs; 9 | use tempfile::tempdir; 10 | 11 | #[macro_use] 12 | extern crate lazy_static; 13 | 14 | lazy_static! { 15 | pub static ref TEST_KEY: Vec = 16 | PrivateKey::new(KeyType::Ed25519).unwrap(); 17 | pub static ref TEST_PRIVATE_KEY: PrivateKey = PrivateKey::from_pkcs8( 18 | &PrivateKey::new(KeyType::Ed25519).unwrap(), 19 | SignatureScheme::Ed25519 20 | ) 21 | .unwrap(); 22 | } 23 | 24 | /* TODO ERRORS 25 | - Signature Values don't match up 26 | - "IllegalArgument("Cannot start with \'/\'")', tests/runlib.rs:38:5" error -> workaround added using tempdir 27 | */ 28 | 29 | // Default link generated Metablock like step_name and default key 30 | 31 | /* 32 | Test Cases 33 | - in_toto_run_record_file 34 | - in_toto_run_record_new_file 35 | - in_toto_run_record_modified_file (TODO) 36 | - in_toto_run_record_symlink_file (TODO) 37 | - in_toto_run_record_symlink_cycle (TODO) 38 | - in_toto_run_handle_nonexistent_materials (TODO) 39 | - in_toto_run_test_key_signature (TODO) 40 | - One test where things *fail* 41 | */ 42 | 43 | #[test] 44 | fn in_toto_run_record_file() { 45 | // Initialization 46 | let dir = tempdir().unwrap(); 47 | let dir_canonical = canonicalize(dir.path()).unwrap(); 48 | let dir_path = dir_canonical.to_str().unwrap(); 49 | 50 | // Create file 51 | write(format!("{}/foo.txt", dir_path), "lorem ipsum").unwrap(); 52 | print!("Path: {}\n", dir_path); 53 | 54 | // Expected value 55 | let byproducts = ByProducts::new() 56 | .set_return_value(0) 57 | .set_stderr(String::from("")) 58 | .set_stdout(String::from("in_toto says hi\n")); 59 | let expected = LinkMetadataBuilder::new() 60 | .name(String::from("test")) 61 | .byproducts(byproducts) 62 | .add_material( 63 | VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), 64 | ) 65 | .add_product( 66 | VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), 67 | ) 68 | .signed::(&TEST_PRIVATE_KEY) 69 | .unwrap(); 70 | 71 | // Result value 72 | let result = in_toto_run( 73 | "test", 74 | None, 75 | &vec![dir_path], 76 | &vec![dir_path], 77 | &["sh", "-c", "echo 'in_toto says hi'"], 78 | Some(&TEST_PRIVATE_KEY), 79 | None, 80 | None, 81 | ) 82 | .unwrap(); 83 | 84 | assert_eq!(expected, result); 85 | 86 | // Clean-up 87 | dir.close().unwrap(); 88 | } 89 | 90 | #[test] 91 | fn in_toto_run_record_new_file() { 92 | // Initialization 93 | let dir = tempdir().unwrap(); 94 | let dir_canonical = canonicalize(dir.path()).unwrap(); 95 | let dir_path = dir_canonical.to_str().unwrap(); 96 | 97 | // Create file 98 | write(format!("{}/foo.txt", dir_path), "lorem ipsum").unwrap(); 99 | print!("Path: {}\n", dir_path); 100 | 101 | // Result Value 102 | let result = in_toto_run( 103 | "test", 104 | None, 105 | &vec![dir_path], 106 | &vec![dir_path], 107 | &[ 108 | "sh", 109 | "-c", 110 | &format!("echo 'in_toto says hi' >> {}/bar.txt", dir_path), 111 | ], 112 | Some(&TEST_PRIVATE_KEY), 113 | None, 114 | None, 115 | ) 116 | .unwrap(); 117 | 118 | let byproducts = ByProducts::new() 119 | .set_return_value(0) 120 | .set_stderr(String::from("")) 121 | .set_stdout(String::from("")); 122 | 123 | // Expected value 124 | let expected = LinkMetadataBuilder::new() 125 | .name(String::from("test")) 126 | .add_material( 127 | VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), 128 | ) 129 | .add_product( 130 | VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), 131 | ) 132 | .add_product( 133 | VirtualTargetPath::new(format!("{}/bar.txt", dir_path)).unwrap(), 134 | ) 135 | .byproducts(byproducts) 136 | .signed::(&TEST_PRIVATE_KEY) 137 | .unwrap(); 138 | 139 | assert_eq!(expected, result); 140 | 141 | // Clean-up work 142 | dir.close().unwrap(); 143 | } 144 | 145 | #[test] 146 | fn in_toto_run_new_line_in_stdout() { 147 | // Initialization 148 | let dir = tempdir().unwrap(); 149 | let dir_canonical = canonicalize(dir.path()).unwrap(); 150 | let dir_path = dir_canonical.to_str().unwrap(); 151 | 152 | // Create file 153 | write(format!("{}/foo.txt", dir_path), "lorem ipsum").unwrap(); 154 | 155 | let byproducts = ByProducts::new() 156 | .set_return_value(0) 157 | .set_stderr(String::from("")) 158 | .set_stdout(String::from("Cloning into 'some-project'...\n")); 159 | 160 | let link = LinkMetadataBuilder::new() 161 | .name(String::from("test")) 162 | .add_material( 163 | VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), 164 | ) 165 | .add_product( 166 | VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), 167 | ) 168 | .byproducts(byproducts) 169 | .signed::(&TEST_PRIVATE_KEY) 170 | .unwrap(); 171 | 172 | assert!(link.verify(1, [TEST_PRIVATE_KEY.public()]).is_ok()); 173 | 174 | // Clean-up work 175 | dir.close().unwrap(); 176 | } 177 | 178 | #[test] 179 | fn in_toto_run_record_modified_file() { 180 | // TODO 181 | } 182 | 183 | #[test] 184 | fn in_toto_run_record_symlink_file() { 185 | // Initialization 186 | let dir = tempdir().unwrap(); 187 | let dir_canonical = canonicalize(dir.path()).unwrap(); 188 | let dir_path = dir_canonical.to_str().unwrap(); 189 | 190 | // Create symlink file 191 | write(format!("{}/foo.txt", dir_path), "lorem ipsum").unwrap(); 192 | fs::symlink( 193 | format!("{}/foo.txt", dir_path), 194 | format!("{}/symfile.txt", dir_path), 195 | ) 196 | .unwrap(); 197 | 198 | print!("Path: {}\n", dir_path); 199 | 200 | let byproducts = ByProducts::new() 201 | .set_return_value(0) 202 | .set_stderr(String::from("")) 203 | .set_stdout(String::from("in_toto says hi\n")); 204 | 205 | // Expected value 206 | let expected = LinkMetadataBuilder::new() 207 | .name(String::from("test")) 208 | .add_material( 209 | VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), 210 | ) 211 | .add_material( 212 | VirtualTargetPath::new(format!("{}/symfile.txt", dir_path)) 213 | .unwrap(), 214 | ) 215 | .add_product( 216 | VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), 217 | ) 218 | .add_product( 219 | VirtualTargetPath::new(format!("{}/symfile.txt", dir_path)) 220 | .unwrap(), 221 | ) 222 | .byproducts(byproducts) 223 | .signed::(&TEST_PRIVATE_KEY) 224 | .unwrap(); 225 | 226 | // Result Value 227 | let result = in_toto_run( 228 | "test", 229 | None, 230 | &vec![dir_path], 231 | &vec![dir_path], 232 | &["sh", "-c", "echo 'in_toto says hi'"], 233 | Some(&TEST_PRIVATE_KEY), 234 | None, 235 | None, 236 | ) 237 | .unwrap(); 238 | 239 | assert_eq!(expected, result); 240 | 241 | // Clean-up work 242 | dir.close().unwrap(); 243 | } 244 | 245 | #[test] 246 | fn in_toto_run_record_symlink_cycle() { 247 | // TODO 248 | } 249 | -------------------------------------------------------------------------------- /tests/test_link/foo.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/test_link/foo.tar.gz -------------------------------------------------------------------------------- /tests/test_metadata/demo.layout: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "64786e5921b589af1ca1bf5767087bf201806a9b3ce2e6856c903682132bd1dd", 5 | "sig": "0c2c5bb8fb58ccbb644e17bfbda0b754cc13f71ddb5ae4be1fff7ad7ec5c94543bec3818b0c45c4a9dd17545382b4ec6d9fcc71366be08c131505981ca415d04" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "layout", 10 | "expires": "1970-01-01T00:00:00Z", 11 | "readme": "", 12 | "keys": { 13 | "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": { 14 | "keytype": "rsa", 15 | "scheme": "rsassa-pss-sha256", 16 | "keyid_hash_algorithms": [ 17 | "sha256", 18 | "sha512" 19 | ], 20 | "keyval": { 21 | "public": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA91+6CJmBzrb6ODSXPvVK\nh9IVvDkD63d5/wHawj1ZB22Y0R7A7b8lRl7IqJJ3TcZO8W2zFfeRuPFlghQs+O7h\nA6XiRr4mlD1dLItk+p93E0vgY+/Jj4I09LObgA2ncGw/bUlYt3fB5tbmnojQyhrQ\nwUQvBxOqI3nSglg02mCdQRWpPzerGxItOIQkmU2TsqTg7TZ8lnSUbAsFuMebnA2d\nJ2hzeou7ZGsyCJj/6O0ORVF37nLZiOFF8EskKVpUJuoLWopEA2c09YDgFWHEPTIo\nGNWB2l/qyX7HTk1wf+WK/Wnn3nerzdEhY9dH+U0uH7tOBBVCyEKxUqXDGpzuLSxO\nGBpJXa3TTqLHJWIOzhIjp5J3rV93aeSqemU38KjguZzdwOMO5lRsFco5gaFS9aNL\nLXtLd4ZgXaxB3vYqFDhvZCx4IKrsYEc/Nr8ubLwyQ8WHeS7v8FpIT7H9AVNDo9BM\nZpnmdTc5Lxi15/TulmswIIgjDmmIqujUqyHN27u7l6bZJlcn8lQdYMm4eJr2o+Jt\ndloTwm7Cv/gKkhZ5tdO5c/219UYBnKaGF8No1feEHirm5mdvwpngCxdFMZMbfmUA\nfzPeVPkXE+LR0lsLGnMlXKG5vKFcQpCXW9iwJ4pZl7j12wLwiWyLDQtsIxiG6Sds\nALPkWf0mnfBaVj/Q4FNkJBECAwEAAQ==\n-----END PUBLIC KEY-----" 22 | } 23 | }, 24 | "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": { 25 | "keytype": "ed25519", 26 | "scheme": "ed25519", 27 | "keyval": { 28 | "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6" 29 | } 30 | } 31 | }, 32 | "steps": [ 33 | { 34 | "_type": "step", 35 | "name": "write-code", 36 | "threshold": 1, 37 | "expected_materials": [], 38 | "expected_products": [ 39 | [ 40 | "CREATE", 41 | "foo.py" 42 | ] 43 | ], 44 | "pubkeys": [ 45 | "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554" 46 | ], 47 | "expected_command": [ 48 | "vi" 49 | ] 50 | }, 51 | { 52 | "_type": "step", 53 | "name": "package", 54 | "threshold": 1, 55 | "expected_materials": [ 56 | [ 57 | "MATCH", 58 | "foo.py", 59 | "WITH", 60 | "PRODUCTS", 61 | "FROM", 62 | "write-code" 63 | ] 64 | ], 65 | "expected_products": [ 66 | [ 67 | "CREATE", 68 | "foo.tar.gz" 69 | ] 70 | ], 71 | "pubkeys": [ 72 | "3e26343b3a7907b5652dec86222e8fd60e456ebbb6fe4875a1f4281ffd5bd9ae" 73 | ], 74 | "expected_command": [ 75 | "tar", 76 | "zcvf", 77 | "foo.tar.gz", 78 | "foo.py" 79 | ] 80 | } 81 | ], 82 | "inspect": [ 83 | { 84 | "_type": "inspection", 85 | "name": "inspect_tarball", 86 | "expected_materials": [ 87 | [ 88 | "MATCH", 89 | "foo.tar.gz", 90 | "WITH", 91 | "PRODUCTS", 92 | "FROM", 93 | "package" 94 | ] 95 | ], 96 | "expected_products": [ 97 | [ 98 | "MATCH", 99 | "foo.py", 100 | "WITH", 101 | "PRODUCTS", 102 | "FROM", 103 | "write-code" 104 | ] 105 | ], 106 | "run": [ 107 | "inspect_tarball.sh", 108 | "foo.tar.gz" 109 | ] 110 | } 111 | ] 112 | } 113 | } -------------------------------------------------------------------------------- /tests/test_metadata/demo.link: -------------------------------------------------------------------------------- 1 | { 2 | "signed": { 3 | "_type": "link", 4 | "name": "", 5 | "materials": {}, 6 | "products": { 7 | "tests/test_link/foo.tar.gz": { 8 | "sha256": "52947cb78b91ad01fe81cd6aef42d1f6817e92b9e6936c1e5aabb7c98514f355" 9 | } 10 | }, 11 | "byproducts": { 12 | "return-value": 0, 13 | "stderr": "a foo.py\n", 14 | "stdout": "" 15 | }, 16 | "command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], 17 | "environment": null 18 | }, 19 | "signatures": [] 20 | } -------------------------------------------------------------------------------- /tests/test_metadata/owner.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/test_metadata/owner.der -------------------------------------------------------------------------------- /tests/test_prefix/left/world: -------------------------------------------------------------------------------- 1 | lorem ipsum 2 | -------------------------------------------------------------------------------- /tests/test_prefix/right/world: -------------------------------------------------------------------------------- 1 | lorem ipsum 2 | -------------------------------------------------------------------------------- /tests/test_runlib/.hidden/.bar: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /tests/test_runlib/.hidden/foo: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /tests/test_runlib/hello./world: -------------------------------------------------------------------------------- 1 | lorem ipsum 2 | -------------------------------------------------------------------------------- /tests/test_verifylib/links/clone.776a00e2.link: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5", 5 | "sig": "3b77aba17d665ae408a9df69ef15ee5279d47637dee60b9fea69dc09c08f858fe12ad4c416c1558be3eb09397c3f9ece2a6d0743322d3861cf8bd0827494a0163d4f26590e9ad9a7684ec211daade7f71a64a896f95088bd05401aad50f0b1d8991b8bd25a55117a1493569f12878078ce3dc1242960dacb430b815ca42656e21e0b6df3e525028376e6c452b716221af857a28a6062643f08f37f1225e620182b9a3a8493121643ca7f21a71138cb7d8a5ae3be267680aef7e5de594a950d426bbbfdc3069762a7baec9f475773251b9dd6835f4173ed62340f8690c3e0db3e6435f29b730d2544cf96f1a3e47466ddad94bd745af6a19a04e9edb227866a8f6975abd76099adf5cb2ed54cdbad243e404671f3510be2ce9eed49cba8edbd03ba1b335e5180b78eac596d23021e542615db8607558839ad358001fab85ad0913381db70814388932e56bde067f92c94fc3ff8ff7eb3eb79b95a74eed8b28c716f83764020618214e2131367a7708260c13c3ede27853a9e2e185f1dd181ad0f" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "link", 10 | "byproducts": { 11 | "return-value": 0, 12 | "stderr": "", 13 | "stdout": "" 14 | }, 15 | "command": [ 16 | "git", 17 | "clone", 18 | "https://github.com/in-toto/demo-project.git" 19 | ], 20 | "environment": {}, 21 | "materials": {}, 22 | "name": "clone", 23 | "products": { 24 | "demo-project/foo.py": { 25 | "sha256": "ebebf8778035e0e842a4f1aeb92a601be8ea8e621195f3b972316c60c9e12235" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /tests/test_verifylib/links/package.2f89b927.link: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "2f89b9272acfc8f4a0a0f094d789fdb0ba798b0fe41f2f5f417c12f0085ff498", 5 | "sig": "b61da0bacfe8970bfbb45dcb0809024a840cc426c71e3e72dd35dcf3b9703355ec386a70967fca2891cdbba49dcf8c78619f9ee3e26709b37b0fa657a48352608d70b38c5192ced88a7af5b15e3ea010d6516d7baa7f3cc0f39f4a69f47160d1983ecf916b891a340c0e084fbd73269a91e58397615de56990d046d2ad145583d6dc1c579aa63afc7285be3cbee227b399cb75477be9f29d1ce59886e9a1ee25a73cb40f253e044ad1d2ba57b552f9eb64087cdc0b5f603d787c9264bbe613c6121c5166f707d616ac8bb3a8f88425a6707d58df46c0b6a0e3138117dba20104750090aabb4e26e4ee1c5717e16fc811f1c2669e949287b1022cb2fdae89d6d60a3ac06bd298cede01e848b7d1aa55d2d0737a81ff0a5812fc099b62d26cf02af403938d897c170169e5240c297ff1ee006caefef7c658e44ae6f9f4babb9dc27e01c931af64200d4ddb8cc073b6738fb4dcf4aecb3b6da498645b4ada15e92a9db85424411505cb0643250118f1787b22904047662200eded168fc0d085bf94" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "link", 10 | "byproducts": { 11 | "return-value": 0, 12 | "stderr": "", 13 | "stdout": "demo-project/\ndemo-project/foo.py\n" 14 | }, 15 | "command": [ 16 | "tar", 17 | "--exclude", 18 | ".git", 19 | "-zcvf", 20 | "demo-project.tar.gz", 21 | "demo-project" 22 | ], 23 | "environment": {}, 24 | "materials": { 25 | "demo-project/foo.py": { 26 | "sha256": "c2c0ea54fa94fac3a4e1575d6ed3bbd1b01a6d0b8deb39196bdc31c457ef731b" 27 | } 28 | }, 29 | "name": "package", 30 | "products": { 31 | "demo-project.tar.gz": { 32 | "sha256": "2989659e6836c941e9015bf38af3cb045365520dbf80460d8a44b2c5b6677fd9" 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /tests/test_verifylib/links/update-version.776a00e2.link: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5", 5 | "sig": "60b8f0b42afbcbffda3ea9bfeb4e5bef67d2a6584607aa000dbc95dd7d607047487b619627d56ecb800d1fa6d9749e7f65415bd7117679786a0ab603b3746be3909442ab5607d5bb1cda8bebd951deccfd3cc1b2c11fe4fcfcbf4a2a7a6937339e22dbc61742c6200927a01a0abe798ba39ee2e3bdd39eb814d68072b852869cb557726686aa303b1dd6e4e4d60d46c468ae235120762144c22a6ffc30047dd66fb4e24feb28a1df79f9e0fbcc68228330cbb3f4e45e865aad30ffd49c9084ea81777e81ff5506ad34b7bf12b516ce7e133ea50f27d6848e9594bc35279fefcd80b180c18f3aaf498f599225a0cb54705affa86cdcb4bf25b36655a5d2aa77a5d932a576ced3f2d4fb052fe5a3db1020b6fb6e76e6a000c1757ac8fb2a2de7d42e3288e7afe03d2f780c7bcfbf4a21243dd963e0f3f7c6d17337fb112780de04d0275ef373ff1350d254e1528569cb9245ce8c2da7790a84327fcecad71d18f0561377213dae2ab2d694a8137f159435119c7f15dd9072b9ea13598bc01e6984" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "link", 10 | "byproducts": {}, 11 | "command": [], 12 | "environment": {}, 13 | "materials": { 14 | "demo-project/foo.py": { 15 | "sha256": "ebebf8778035e0e842a4f1aeb92a601be8ea8e621195f3b972316c60c9e12235" 16 | } 17 | }, 18 | "name": "update-version", 19 | "products": { 20 | "demo-project/foo.py": { 21 | "sha256": "c2c0ea54fa94fac3a4e1575d6ed3bbd1b01a6d0b8deb39196bdc31c457ef731b" 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /tests/test_verifylib/workdir/alice.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxPX3kFs/z645x4UOC3KF 3 | Y3V80YQtKrp6YS3qU+Jlvx/XzK53lb4sCDRU9jqBBx3We45TmFUibroMd8tQXCUS 4 | e8gYCBUBqBmmz0dEHJYbW0tYF7IoapMIxhRYn76YqNdl1JoRTcmzIaOJ7QrHxQrS 5 | GpivvTm6kQ9WLeApG1GLYJ3C3Wl4bnsI1bKSv55Zi45/JawHzTzYUAIXX9qCd3Io 6 | HzDucz9IAj9Ookw0va/q9FjoPGrRB80IReVxLVnbo6pYJfu/O37jvEobHFa8ckHd 7 | YxUIg8wvkIOy1O3M74lBDm6CVI0ZO25xPlDB/4nHAE1PbA3aF3lw8JGuxLDsetxm 8 | fzgAleVt4vXLQiCrZaLf+0cM97JcT7wdHcbIvRLsij9LNP+2tWZgeZ/hIAOEdaDq 9 | cYANPDIAxfTvbe9I0sXrCtrLer1SS7GqUmdFCdkdun8erXdNF0ls9Rp4cbYhjdf3 10 | yMxdI/24LUOOQ71cHW3ITIDImm6I8KmrXFM2NewTARKfAgMBAAE= 11 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /tests/test_verifylib/workdir/demo-project.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/in-toto/in-toto-rs/1b3e850c36ae719f2b06fe807fea508e1394fe0b/tests/test_verifylib/workdir/demo-project.tar.gz -------------------------------------------------------------------------------- /tests/test_verifylib/workdir/root.layout: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "556caebdc0877eed53d419b60eddb1e57fa773e4e31d70698b588f3e9cc48b35", 5 | "sig": "a2e420a830389fbe32761b203f87fb2521068a16c60c5780baac13e600b37ffc451b2f7b76546bde211e343ba670e4e87680e0cd2dce46cd8566c2bd23c2cb7c6f78bdd91b5f24480f260d9bdc9c0bb217ca1ba8869873f790d7d2663af42d821a2952951f411d760a38bc9e1367e7048a6c4cc7de245a5b14dfd98e3ed227460b5c305541d88c7f9300d71091211a80d1363f68ba090cd8cee7d552e49a0396b9aff863e562225886d0f33195141d21fb3b0f67ab6b800c40ccac073a354a79e6d797d0aa8b9436799da783e217a3d7527a891f0f2ebf04cd6c6430430e725c8a7b68b2d40978bfe20340374f5b0cbe001dd3c8e3589e9b988fac1c47917391884d4b6bbc83b7b9265eeb6f96764a82fca5a9ea4770d8096e563b43adac50f414b117f497c663d68b17faeac3d4535eeaa973b6cfe6e81b309f7e9c8ef05fceb9b748f712d0691f2ef7def3b0a858c8436b4752687861e69dc94ed3ae8b5023161c6201bdffcaa1e8286b83d3818bf0d3ba4383fefa4c699f493635813e51b0" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "layout", 10 | "expires": "2022-12-02T18:41:07Z", 11 | "inspect": [ 12 | { 13 | "_type": "inspection", 14 | "expected_materials": [ 15 | [ 16 | "MATCH", 17 | "demo-project.tar.gz", 18 | "WITH", 19 | "PRODUCTS", 20 | "FROM", 21 | "package" 22 | ], 23 | [ 24 | "ALLOW", 25 | ".keep" 26 | ], 27 | [ 28 | "ALLOW", 29 | "alice.pub" 30 | ], 31 | [ 32 | "ALLOW", 33 | "root.layout" 34 | ], 35 | [ 36 | "DISALLOW", 37 | "*" 38 | ] 39 | ], 40 | "expected_products": [ 41 | [ 42 | "MATCH", 43 | "demo-project/foo.py", 44 | "WITH", 45 | "PRODUCTS", 46 | "FROM", 47 | "update-version" 48 | ], 49 | [ 50 | "ALLOW", 51 | "demo-project/.git/*" 52 | ], 53 | [ 54 | "ALLOW", 55 | "demo-project.tar.gz" 56 | ], 57 | [ 58 | "ALLOW", 59 | ".keep" 60 | ], 61 | [ 62 | "ALLOW", 63 | "alice.pub" 64 | ], 65 | [ 66 | "ALLOW", 67 | "root.layout" 68 | ], 69 | [ 70 | "DISALLOW", 71 | "*" 72 | ] 73 | ], 74 | "name": "untar", 75 | "run": [ 76 | "tar", 77 | "xzf", 78 | "demo-project.tar.gz" 79 | ] 80 | } 81 | ], 82 | "keys": { 83 | "2f89b9272acfc8f4a0a0f094d789fdb0ba798b0fe41f2f5f417c12f0085ff498": { 84 | "keyid": "2f89b9272acfc8f4a0a0f094d789fdb0ba798b0fe41f2f5f417c12f0085ff498", 85 | "keyid_hash_algorithms": [ 86 | "sha256", 87 | "sha512" 88 | ], 89 | "keytype": "rsa", 90 | "keyval": { 91 | "private": "", 92 | "public": "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAzgLBsMFSgwBiWTBmVsyW\n5KbJwLFSodAzdUhU2Bq6SdRz/W6UOBGdojZXibxupjRtAaEQW/eXDe+1CbKg6ENZ\nGt2D9HGFCQZgQS8ONgNDQGiNxgApMA0T21AaUhru0vEofzdN1DfEF4CAGv5AkcgK\nsalhTyONervFIjFEdXGelFZ7dVMV3Pp5WkZPG0jFQWjnmDZhUrtSxEtqbVghc3kK\nAUj9Ll/3jyi2wS92Z1j5ueN8X62hWX2xBqQ6nViOMzdujkoiYCRSwuMLRqzW2CbT\nL8hF1+S5KWKFzxl5sCVfpPe7V5HkgEHjwCILXTbCn2fCMKlaSbJ/MG2lW7qSY2Ro\nwVXWkp1wDrsJ6Ii9f2dErv9vJeOVZeO9DsooQ5EuzLCfQLEU5mn7ul7bU7rFsb8J\nxYOeudkNBatnNCgVMAkmDPiNA7E33bmL5ARRwU0iZicsqLQR32pmwdap8PjofxqQ\nk7Gtvz/iYzaLrZv33cFWWTsEOqK1gKqigSqgW9T26wO9AgMBAAE=\n-----END PUBLIC KEY-----" 93 | }, 94 | "scheme": "rsassa-pss-sha256" 95 | }, 96 | "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5": { 97 | "keyid": "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5", 98 | "keyid_hash_algorithms": [ 99 | "sha256", 100 | "sha512" 101 | ], 102 | "keytype": "rsa", 103 | "keyval": { 104 | "private": "", 105 | "public": "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA0Zfzonp3/FScaIP+KKuz\nB+OZNFpjbVGWjm3leqnFqHYLqrLcCw5KhlXpycJqoSvZBpO+PFCksUx8U/ryklHG\nVoDiB84pRkvZtBoVaA4b4IHDIhz1K5NqkJgieya4fwReTxmCW0a9gH7AnDicHBCX\nlzMxqEdt6OKMV5g4yjKaxf8lW72O1gSI46GSIToo+Z7UUgs3ofaM5UFIcczgCpUa\n5kEKocB6cSZ9U8PKRLSs0xO0ROjrcOTsfxMs8eV4bsRCWY5mAq1WM9EHDSV9WO8g\nqrRmanC4enNqa8jU4O3zhgJVegP9A01r9AwNt6AqgPSikwhXN/P4v1FMYV+R6N3b\nS1lsVWRAnwBq5RFz5zVvcY88JEkHbrcBqP/A4909NXae1VMXmnoJb4EzGAkyUySB\na+fHXAVJgzwyv3I48d/OIjH8NWcVmM/DQL7FtcJk3tp0YUjY5wNpcbQTnLzURtlU\nsd+MtGuvdlDxUUvtUYCIVKRdS8UzYnTPjI2xzeoSHZ2ZAgMBAAE=\n-----END PUBLIC KEY-----" 106 | }, 107 | "scheme": "rsassa-pss-sha256" 108 | } 109 | }, 110 | "readme": "", 111 | "steps": [ 112 | { 113 | "_type": "step", 114 | "expected_command": [ 115 | "git", 116 | "clone", 117 | "https://github.com/in-toto/demo-project.git" 118 | ], 119 | "expected_materials": [], 120 | "expected_products": [ 121 | [ 122 | "CREATE", 123 | "demo-project/foo.py" 124 | ], 125 | [ 126 | "DISALLOW", 127 | "*" 128 | ] 129 | ], 130 | "name": "clone", 131 | "pubkeys": [ 132 | "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5" 133 | ], 134 | "threshold": 1 135 | }, 136 | { 137 | "_type": "step", 138 | "expected_command": [], 139 | "expected_materials": [ 140 | [ 141 | "MATCH", 142 | "demo-project/*", 143 | "WITH", 144 | "PRODUCTS", 145 | "FROM", 146 | "clone" 147 | ], 148 | [ 149 | "DISALLOW", 150 | "*" 151 | ] 152 | ], 153 | "expected_products": [ 154 | [ 155 | "MODIFY", 156 | "demo-project/foo.py" 157 | ], 158 | [ 159 | "DISALLOW", 160 | "*" 161 | ] 162 | ], 163 | "name": "update-version", 164 | "pubkeys": [ 165 | "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5" 166 | ], 167 | "threshold": 1 168 | }, 169 | { 170 | "_type": "step", 171 | "expected_command": [ 172 | "tar", 173 | "--exclude", 174 | ".git", 175 | "-zcvf", 176 | "demo-project.tar.gz", 177 | "demo-project" 178 | ], 179 | "expected_materials": [ 180 | [ 181 | "MATCH", 182 | "demo-project/*", 183 | "WITH", 184 | "PRODUCTS", 185 | "FROM", 186 | "update-version" 187 | ], 188 | [ 189 | "DISALLOW", 190 | "*" 191 | ] 192 | ], 193 | "expected_products": [ 194 | [ 195 | "CREATE", 196 | "demo-project.tar.gz" 197 | ], 198 | [ 199 | "DISALLOW", 200 | "*" 201 | ] 202 | ], 203 | "name": "package", 204 | "pubkeys": [ 205 | "2f89b9272acfc8f4a0a0f094d789fdb0ba798b0fe41f2f5f417c12f0085ff498" 206 | ], 207 | "threshold": 1 208 | } 209 | ] 210 | } 211 | } --------------------------------------------------------------------------------