in-toto-0.4.0/.cargo_vcs_info.json0000644000000001360000000000100124260ustar { "git": { "sha1": "90b4531688ef3e07717f8458afd0a64aa0f99f89" }, "path_in_vcs": "" }in-toto-0.4.0/.git-blame-ignore-revs000064400000000000000000000004051046102023000153150ustar 00000000000000# Ignore calls to `cargo fmt` # # To learn more, see # https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view # Run cargo fmt changing max width to 80 e66afe2850768076ce4f4c5f101e9dcc61697ba2 in-toto-0.4.0/.github/dependabot.yml000064400000000000000000000005171046102023000154110ustar 00000000000000version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" commit-message: prefix: "chore" include: "scope" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" commit-message: prefix: "chore" include: "scope" in-toto-0.4.0/.github/workflows/rust.yml000064400000000000000000000016031046102023000163330ustar 00000000000000on: [push, pull_request] name: Rust CI env: CARGO_TERM_COLOR: always jobs: test: name: Test runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: dtolnay/rust-toolchain@stable - name: Run tests run: cargo test --verbose fmt: name: Rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - name: Enforce formatting run: cargo fmt --all --check clippy: name: Run Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: dtolnay/rust-toolchain@stable with: components: clippy - name: Linting run: cargo clippy -- -D warnings in-toto-0.4.0/.gitignore000064400000000000000000000002211046102023000132010ustar 00000000000000/target/ /Cargo.lock /tests/hello_intoto # verifylib tempfiles tests/test_verifylib/workdir/untar.link tests/test_verifylib/workdir/demo-productin-toto-0.4.0/Cargo.lock0000644000000577440000000000100104220ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "assert-json-diff" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" dependencies = [ "serde", "serde_json", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cc" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", "windows-targets", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "data-encoding" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "derp" version = "0.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9b84cfd9b6fa437e498215e5625e9e3ae3bf9bb54d623028a181c40820db169" dependencies = [ "untrusted 0.7.1", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "in-toto" version = "0.4.0" dependencies = [ "assert-json-diff", "chrono", "data-encoding", "derp", "glob", "itoa", "lazy_static", "log", "maplit", "matches", "once_cell", "path-clean", "pem", "pretty_assertions", "ring", "rstest", "serde", "serde_json", "strum", "strum_macros", "tempfile", "thiserror", "untrusted 0.7.1", "walkdir", ] [[package]] name = "indexmap" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "matches" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "path-clean" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" [[package]] name = "pem" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ "base64", "serde", ] [[package]] name = "pin-project-lite" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro-crate" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin", "untrusted 0.9.0", "windows-sys 0.52.0", ] [[package]] name = "rstest" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" dependencies = [ "futures", "futures-timer", "rstest_macros", "rustc_version", ] [[package]] name = "rstest_macros" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" dependencies = [ "cfg-if", "glob", "proc-macro-crate", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", "syn", "unicode-ident", ] [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "rustversion" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" [[package]] name = "strum_macros" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", "syn", ] [[package]] name = "syn" version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "thiserror" version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" in-toto-0.4.0/Cargo.toml0000644000000046000000000000100104240ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "in-toto" version = "0.4.0" authors = [ "Santiago Torres-Arias ", "Qijia 'Joy' Liu ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Library for in-toto" homepage = "https://in-toto.io" documentation = "https://docs.rs/in-toto" readme = "README.md" keywords = ["security"] categories = ["cryptography"] license = "MIT" repository = "https://github.com/in-toto/in-toto-rs" [lib] name = "in_toto" path = "src/lib.rs" [[example]] name = "in_toto_run_test" path = "examples/in_toto_run_test.rs" [[example]] name = "serialize_keys" path = "examples/serialize_keys.rs" [[example]] name = "write_link" path = "examples/write_link.rs" [[test]] name = "runlib" path = "tests/runlib.rs" [dependencies.chrono] version = "0.4" features = [ "clock", "serde", ] default-features = false [dependencies.data-encoding] version = "2" [dependencies.derp] version = "0.0.14" [dependencies.glob] version = "0.3.0" [dependencies.itoa] version = "1" [dependencies.log] version = "0.4" [dependencies.path-clean] version = "1.0.1" [dependencies.pem] version = "3.0.0" [dependencies.ring] version = "0.17" [dependencies.serde] version = "1" features = ["derive"] [dependencies.serde_json] version = "1" [dependencies.strum] version = "0.26" [dependencies.strum_macros] version = "0.26" [dependencies.thiserror] version = "2.0" [dependencies.untrusted] version = "0.7" [dependencies.walkdir] version = "2" [dev-dependencies.assert-json-diff] version = "2.0.2" [dev-dependencies.lazy_static] version = "1" [dev-dependencies.maplit] version = "1" [dev-dependencies.matches] version = "0.1.8" [dev-dependencies.once_cell] version = "1.10.0" [dev-dependencies.pretty_assertions] version = "1.3" [dev-dependencies.rstest] version = "0.23.0" [dev-dependencies.tempfile] version = "3" [features] in-toto-0.4.0/Cargo.toml.orig000064400000000000000000000020501046102023000141020ustar 00000000000000[package] name = "in-toto" edition = "2021" version = "0.4.0" authors = ["Santiago Torres-Arias ", "Qijia 'Joy' Liu "] description = "Library for in-toto" homepage = "https://in-toto.io" repository = "https://github.com/in-toto/in-toto-rs" documentation = "https://docs.rs/in-toto" readme = "README.md" license = "MIT" keywords = [ "security" ] categories = [ "cryptography" ] [lib] name = "in_toto" path = "./src/lib.rs" [dependencies] chrono = { version = "0.4", features = [ "clock", "serde" ], default-features = false } data-encoding = "2" derp = "0.0.14" itoa = "1" log = "0.4" ring = { version = "0.17" } serde = { version = "1", features = ["derive"] } serde_json = "1" untrusted = "0.7" thiserror = "2.0" walkdir = "2" path-clean = "1.0.1" strum = "0.26" strum_macros = "0.26" pem = "3.0.0" glob = "0.3.0" [dev-dependencies] assert-json-diff = "2.0.2" lazy_static = "1" maplit = "1" matches = "0.1.8" once_cell = "1.10.0" pretty_assertions = "1.3" rstest = "0.23.0" tempfile = "3" [features] in-toto-0.4.0/GOVERNANCE.md000064400000000000000000000010531046102023000131660ustar 00000000000000# in-toto Governance in-toto's [governance](https://github.com/in-toto/community/blob/main/GOVERNANCE.md) and [code of conduct](https://github.com/in-toto/community/blob/main/CODE-OF-CONDUCT.md) are described in the [in-toto/community](https://github.com/in-toto/community) repository. ## in-toto-java Contributions This implementation adheres to [in-toto's contributing guidelines](https://github.com/in-toto/community/blob/main/CONTRIBUTING.md). Pull requests must be submitted to the `master` branch where they undergo review and automated testing. in-toto-0.4.0/LICENSE000064400000000000000000000021271046102023000122250ustar 00000000000000The MIT License (MIT) Copyright (c) 2017 heartsucker, Advanced Telematic Systems GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. in-toto-0.4.0/MAINTAINERS.txt000064400000000000000000000010521046102023000135270ustar 00000000000000The project is managed by the in-toto Steering Committee . Maintainers: Santiago Torres Email: santiagotorres@purdue.edu GitHub username: @SantiagoTorres PGP fingerprint: 903B AB73 640E B6D6 5533 EFF3 468F 122C E816 2295 Aditya Sirish A Yelgundhalli Email: aditya.sirish@nyu.edu GitHub username: @adityasaky PGP fingerprint: E329 4129 9CB8 C0D9 3DCF 27AC B831 10D0 1254 5604 Alan A. Chung Ma Email: alanchunggt@gmail.com GitHub username: @alanssitis in-toto-0.4.0/Makefile000064400000000000000000000007131046102023000126570ustar 00000000000000.PHONY: help clean dev-docs .DEFAULT_GOAL := help clean: ## Remove temp/useless files @find . -name '*.rs.bk' -type f -delete dev-docs: ## Generate the documentation for all modules (dev friendly) @cargo rustdoc --all-features --open -- --no-defaults --passes "collapse-docs" --passes "unindent-comments" help: ## Print this message @awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {printf "\033[36m%16s\033[0m : %s\n", $$1, $$2}' $(MAKEFILE_LIST) in-toto-0.4.0/README.md000064400000000000000000000011751046102023000125010ustar 00000000000000# in-toto-rs A Rust implementation of [in-toto](https://in-toto.io). ## Warning: Beta Software This is under active development and may not suitable for production use. Further, the API is unstable and you should be prepared to refactor on even patch releases. ## Contributing Please make all pull requests to the `master` branch. ### Bugs This project has a **full disclosure** policy on security related errors. Please treat these errors like all other bugs and file a public issue. Errors communicated via other channels will be immediately made public. ## Legal ### License This work is dual licensed under the MIT license in-toto-0.4.0/examples/example_link.json000064400000000000000000000110001046102023000163670ustar 00000000000000{ "signatures": [ { "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554", "sig": "9239111d2f81c106d83a087eb1bcb393e6c8ea588cb3a39a71d3dea560717250044095bd5d811f1ea8f8d882961c39c8a00ba32fddb14219ab36c4cdcd229007" } ], "signed": { "_type": "example", "byproducts": { "return-value": "0", "stderr": "", "stdout": "" }, "env": {}, "materials": { "tests/test_runlib/.hidden/.bar": { "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", "sha512": "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" }, "tests/test_runlib/.hidden/foo": { "sha256": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", "sha512": "cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063" }, "tests/test_runlib/hello./symbolic_to_nonparent_folder/.bar": { "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", "sha512": "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" }, "tests/test_runlib/hello./symbolic_to_nonparent_folder/foo": { "sha256": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", "sha512": "cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063" }, "tests/test_runlib/hello./world": { "sha256": "25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", "sha512": "9b589a5fc9f43c3279e3a184f78b62738794913d976d74cd72f862864b8bc09b61f882273e9a64cbfea53c18782583f4bdc842ace58411824a4b15cd0d0c6ff9" }, "tests/test_runlib/symbolic_to_file": { "sha256": "25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", "sha512": "9b589a5fc9f43c3279e3a184f78b62738794913d976d74cd72f862864b8bc09b61f882273e9a64cbfea53c18782583f4bdc842ace58411824a4b15cd0d0c6ff9" }, "tests/test_runlib/symbolic_to_license_file": { "sha256": "61ed40687d2656636a04680013dffe41d5c724201edaa84045e0677b8e2064d6", "sha512": "95df79b6a38f7e7c6b2c0393fcbc433b5d9f5f5b865467de992ff886965816bdf5e9f564390c9d38e3265396f338970a3335b1f3d6a67556e54100898af2e462" } }, "products": { "tests/test_runlib/.hidden/.bar": { "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", "sha512": "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" }, "tests/test_runlib/.hidden/foo": { "sha256": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", "sha512": "cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063" }, "tests/test_runlib/hello./symbolic_to_nonparent_folder/.bar": { "sha256": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", "sha512": "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" }, "tests/test_runlib/hello./symbolic_to_nonparent_folder/foo": { "sha256": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", "sha512": "cc06808cbbee0510331aa97974132e8dc296aeb795be229d064bae784b0a87a5cf4281d82e8c99271b75db2148f08a026c1a60ed9cabdb8cac6d24242dac4063" }, "tests/test_runlib/hello./world": { "sha256": "25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", "sha512": "9b589a5fc9f43c3279e3a184f78b62738794913d976d74cd72f862864b8bc09b61f882273e9a64cbfea53c18782583f4bdc842ace58411824a4b15cd0d0c6ff9" }, "tests/test_runlib/symbolic_to_file": { "sha256": "25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", "sha512": "9b589a5fc9f43c3279e3a184f78b62738794913d976d74cd72f862864b8bc09b61f882273e9a64cbfea53c18782583f4bdc842ace58411824a4b15cd0d0c6ff9" }, "tests/test_runlib/symbolic_to_license_file": { "sha256": "61ed40687d2656636a04680013dffe41d5c724201edaa84045e0677b8e2064d6", "sha512": "95df79b6a38f7e7c6b2c0393fcbc433b5d9f5f5b865467de992ff886965816bdf5e9f564390c9d38e3265396f338970a3335b1f3d6a67556e54100898af2e462" } } } } in-toto-0.4.0/examples/in_toto_run_test.rs000064400000000000000000000011751046102023000170040ustar 00000000000000use in_toto::crypto::PrivateKey; use in_toto::runlib::in_toto_run; const ED25519_1_PRIVATE_KEY: &'static [u8] = include_bytes!("../tests/ed25519/ed25519-1"); fn main() { let key = PrivateKey::from_ed25519(ED25519_1_PRIVATE_KEY).unwrap(); let link = in_toto_run( "example", Some("tests"), &["tests/test_runlib"], &["tests/test_runlib"], &["sh", "-c", "echo 'in_toto says hi' >> hello_intoto"], Some(&key), Some(&["sha512", "sha256"]), None, ) .unwrap(); let json = serde_json::to_value(&link).unwrap(); println!("Generated link: {json:#}") } in-toto-0.4.0/examples/serialize_keys.rs000064400000000000000000000014701046102023000164260ustar 00000000000000use in_toto::crypto::{KeyType, PrivateKey, SignatureScheme}; use std::fs; use std::fs::OpenOptions; use std::io::prelude::*; use std::os::unix::fs::OpenOptionsExt; fn main() { // Generate a new Ed25519 signing key let key = PrivateKey::new(KeyType::Ed25519).unwrap(); let mut privkey = PrivateKey::from_pkcs8(&key, SignatureScheme::Ed25519).unwrap(); println!("Generated keypair {:?}", &privkey.public()); let mut target = OpenOptions::new() .mode(0o640) .write(true) .create(true) .open("test-key") .unwrap(); target.write_all(&key).unwrap(); let loaded_key = fs::read("test-key").unwrap(); privkey = PrivateKey::from_pkcs8(&loaded_key, SignatureScheme::Ed25519).unwrap(); println!("loaded keypair: {:?}", &privkey.public()) } in-toto-0.4.0/examples/write_link.rs000064400000000000000000000014501046102023000155510ustar 00000000000000use in_toto::crypto::{KeyType, PrivateKey, SignatureScheme}; use in_toto::interchange::Json; use in_toto::models::{LinkMetadataBuilder, VirtualTargetPath}; use serde_json; fn main() { // Generate a new Ed25519 signing key let key = PrivateKey::new(KeyType::Ed25519).unwrap(); println!("Generated keypair: {:?}", key); let privkey = PrivateKey::from_pkcs8(&key, SignatureScheme::Ed25519).unwrap(); let link = LinkMetadataBuilder::new() .name(String::from("test")) .add_material(VirtualTargetPath::new("LICENSE".to_string()).unwrap()) .add_product(VirtualTargetPath::new("Makefile".to_string()).unwrap()) .signed::(&privkey) .unwrap(); let json = serde_json::to_value(&link).unwrap(); println!("Generated link: {}", json) } in-toto-0.4.0/rustfmt.toml000064400000000000000000000000401046102023000136110ustar 00000000000000edition = "2021" max_width = 80 in-toto-0.4.0/src/crypto.rs000064400000000000000000001721251046102023000137030ustar 00000000000000//! Cryptographic structures and functions. use data_encoding::HEXLOWER; use derp::{self, Der, Tag}; use ring::digest::{self, SHA256, SHA512}; use ring::rand::SystemRandom; use ring::signature::{ EcdsaKeyPair, Ed25519KeyPair, KeyPair, RsaKeyPair, ECDSA_P256_SHA256_ASN1, ECDSA_P256_SHA256_ASN1_SIGNING, ED25519, RSA_PSS_2048_8192_SHA256, RSA_PSS_2048_8192_SHA512, RSA_PSS_SHA256, RSA_PSS_SHA512, }; use serde::de::{Deserializer, Error as DeserializeError}; use serde::ser::{Error as SerializeError, Serializer}; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; use std::fmt::{self, Debug, Display}; use std::hash; use std::io::{Read, Write}; use std::process::{Command, Stdio}; use std::str::FromStr; use std::sync::Arc; use untrusted::Input; use crate::error::Error; use crate::interchange::cjson::shims; use crate::Result; const HASH_ALG_PREFS: &[HashAlgorithm] = &[HashAlgorithm::Sha512, HashAlgorithm::Sha256]; /// 1.2.840.113549.1.1.1 rsaEncryption(PKCS #1) const RSA_SPKI_OID: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]; /// 1.3.101.112 curveEd25519(EdDSA 25519 signature algorithm) const ED25519_SPKI_OID: &[u8] = &[0x2b, 0x65, 0x70]; /// 1.2.840.10045.2.1 ecPublicKey (Elliptic Curve public key cryptography) const ECC_SPKI_OID: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]; /// The length of an ed25519 private key in bytes const ED25519_PRIVATE_KEY_LENGTH: usize = 32; /// The length of an ed25519 public key in bytes const ED25519_PUBLIC_KEY_LENGTH: usize = 32; /// The length of an ed25519 keypair in bytes const ED25519_KEYPAIR_LENGTH: usize = ED25519_PRIVATE_KEY_LENGTH + ED25519_PUBLIC_KEY_LENGTH; /// Pem header of a rsa private key const PEM_PUBLIC_KEY: &str = "PUBLIC KEY"; fn python_sslib_compatibility_keyid_hash_algorithms() -> Option> { Some(vec!["sha256".to_string(), "sha512".to_string()]) } /// Given a map of hash algorithms and their values, get the prefered algorithm and the hash /// calculated by it. Returns an `Err` if there is no match. /// /// ``` /// use std::collections::HashMap; /// use in_toto::crypto::{hash_preference, HashValue, HashAlgorithm}; /// /// let mut map = HashMap::new(); /// assert!(hash_preference(&map).is_err()); /// /// let _ = map.insert(HashAlgorithm::Sha512, HashValue::new(vec![0x00, 0x01])); /// assert_eq!(hash_preference(&map).unwrap().0, &HashAlgorithm::Sha512); /// /// let _ = map.insert(HashAlgorithm::Sha256, HashValue::new(vec![0x02, 0x03])); /// assert_eq!(hash_preference(&map).unwrap().0, &HashAlgorithm::Sha512); /// ``` pub fn hash_preference( hashes: &HashMap, ) -> Result<(&'static HashAlgorithm, &HashValue)> { for alg in HASH_ALG_PREFS { match hashes.get(alg) { Some(v) => return Ok((alg, v)), None => continue, } } Err(Error::NoSupportedHashAlgorithm) } /// Calculate the size and hash digest from a given `Read`. pub fn calculate_hashes( mut read: R, hash_algs: &[HashAlgorithm], ) -> Result<(u64, HashMap)> { if hash_algs.is_empty() { return Err(Error::IllegalArgument( "Cannot provide empty set of hash algorithms".into(), )); } let mut size = 0; let mut hashes = HashMap::new(); for alg in hash_algs { let _ = hashes.insert(alg, alg.digest_context()?); } let mut buf = vec![0; 1024]; loop { match read.read(&mut buf) { Ok(read_bytes) => { if read_bytes == 0 { break; } size += read_bytes as u64; for context in hashes.values_mut() { context.update(&buf[0..read_bytes]); } } e @ Err(_) => e.map(|_| ())?, } } let hashes = hashes .drain() .map(|(k, v)| (k.clone(), HashValue::new(v.finish().as_ref().to_vec()))) .collect(); Ok((size, hashes)) } fn shim_public_key( key_type: &KeyType, signature_scheme: &SignatureScheme, keyid_hash_algorithms: &Option>, public_key: &[u8], private_key: bool, keyid: Option<&str>, ) -> Result { let key = match key_type { KeyType::Ed25519 => HEXLOWER.encode(public_key), KeyType::Rsa => { let contents = write_spki(public_key, key_type)?; let public_pem = pem::Pem::new(PEM_PUBLIC_KEY.to_string(), contents); pem::encode(&public_pem) .replace("\r\n", "\n") .trim() .to_string() } KeyType::Ecdsa => HEXLOWER.encode(public_key), KeyType::Unknown(inner) => { return Err(Error::UnknownKeyType(format!("content: {}", inner))) } }; let private_key = private_key.then_some(""); Ok(shims::PublicKey::new( key_type.clone(), signature_scheme.clone(), keyid_hash_algorithms.clone(), key, keyid, private_key, )) } /// Calculate unique key_id. This function will convert the der bytes /// of the public key into PKIX-encoded pem bytes to keep consistent /// with the python and golang version. fn calculate_key_id( key_type: &KeyType, signature_scheme: &SignatureScheme, keyid_hash_algorithms: &Option>, public_key: &[u8], ) -> Result { use crate::interchange::{DataInterchange, Json}; let public_key = shim_public_key( key_type, signature_scheme, keyid_hash_algorithms, public_key, false, None, )?; let public_key = Json::canonicalize(&Json::serialize(&public_key)?)?; let public_key = String::from_utf8(public_key) .map_err(|e| { Error::Encoding(format!( "public key from bytes to string failed: {}", e, )) })? .replace("\\n", "\n"); let mut context = digest::Context::new(&SHA256); context.update(public_key.as_bytes()); let key_id = HEXLOWER.encode(context.finish().as_ref()); Ok(KeyId(key_id)) } /// Wrapper type for public key's ID. /// /// # Calculating /// A `KeyId` is calculated as the hex digest of the SHA-256 hash of the canonical form of the /// public key, or `hexdigest(sha256(cjson(public_key)))`. #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct KeyId(String); impl KeyId { /// Return the first 8 hex digits of the key id pub fn prefix(&self) -> String { assert!(self.0.len() >= 8); self.0[0..8].to_string() } } impl FromStr for KeyId { type Err = Error; /// Parse a key ID from a string. fn from_str(string: &str) -> Result { if string.len() != 64 { return Err(Error::IllegalArgument( "key ID must be 64 characters long".into(), )); } Ok(KeyId(string.to_owned())) } } impl Serialize for KeyId { fn serialize(&self, ser: S) -> ::std::result::Result where S: Serializer, { self.0.serialize(ser) } } impl<'de> Deserialize<'de> for KeyId { fn deserialize>( de: D, ) -> ::std::result::Result { let string: String = Deserialize::deserialize(de)?; KeyId::from_str(&string) .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } /// Cryptographic signature schemes. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum SignatureScheme { /// [Ed25519](https://ed25519.cr.yp.to/) #[serde(rename = "ed25519")] Ed25519, /// [RSASSA-PSS](https://tools.ietf.org/html/rfc5756) calculated over SHA256 #[serde(rename = "rsassa-pss-sha256")] RsaSsaPssSha256, /// [RSASSA-PSS](https://tools.ietf.org/html/rfc5756) calculated over SHA512 #[serde(rename = "rsassa-pss-sha512")] RsaSsaPssSha512, /// [ECDSA](https://www.rfc-editor.org/rfc/rfc5480) calculated over SHA256 /// Also known as 'prime256v1', 'P-256', and 'sepc256r1'). #[serde(rename = "ecdsa-sha2-nistp256")] EcdsaP256Sha256, /// Placeholder for an unknown scheme. Unknown(String), } /// Wrapper type for the value of a cryptographic signature. #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SignatureValue(#[serde(with = "crate::format_hex")] Vec); impl SignatureValue { /// Create a new `SignatureValue` from the given bytes. /// /// Note: It is unlikely that you ever want to do this manually. pub fn new(bytes: Vec) -> Self { SignatureValue(bytes) } /// Create a new `SignatureValue` from the given hex string. /// /// Note: It is unlikely that you ever want to do this manually. pub fn from_hex(string: &str) -> Result { Ok(SignatureValue(HEXLOWER.decode(string.as_bytes())?)) } /// Return the signature as bytes. pub fn as_bytes(&self) -> &[u8] { &self.0 } } impl Debug for SignatureValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("SignatureValue") .field(&HEXLOWER.encode(&self.0)) .finish() } } /// Types of public keys. #[derive(Clone, PartialEq, Debug, Eq, Hash)] pub enum KeyType { /// [Ed25519](https://ed25519.cr.yp.to/) Ed25519, /// [RSA](https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29) Rsa, /// [ECDSA](https://www.rfc-editor.org/rfc/rfc5480) Ecdsa, /// Placeholder for an unknown key type. Unknown(String), } impl KeyType { pub fn from_oid(oid: &[u8]) -> Result { match oid { RSA_SPKI_OID => Ok(KeyType::Rsa), ED25519_SPKI_OID => Ok(KeyType::Ed25519), ECC_SPKI_OID => Ok(KeyType::Ecdsa), oid => { let oid = HEXLOWER.encode(oid); Err(Error::Encoding(format!("Unknown OID: {}", oid))) } } } pub fn as_oid(&self) -> Result<&'static [u8]> { match *self { KeyType::Rsa => Ok(RSA_SPKI_OID), KeyType::Ed25519 => Ok(ED25519_SPKI_OID), KeyType::Ecdsa => Ok(ECC_SPKI_OID), KeyType::Unknown(ref s) => Err(Error::UnknownKeyType(s.clone())), } } } impl FromStr for KeyType { type Err = Error; fn from_str(s: &str) -> ::std::result::Result { match s { "ed25519" => Ok(KeyType::Ed25519), "rsa" => Ok(KeyType::Rsa), "ecdsa" => Ok(KeyType::Ecdsa), typ => Err(Error::Encoding(typ.into())), } } } impl fmt::Display for KeyType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}", match *self { KeyType::Ed25519 => "ed25519", KeyType::Rsa => "rsa", KeyType::Ecdsa => "ecdsa", KeyType::Unknown(ref s) => s, } ) } } impl Serialize for KeyType { fn serialize(&self, ser: S) -> ::std::result::Result where S: Serializer, { ser.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for KeyType { fn deserialize>( de: D, ) -> ::std::result::Result { let string: String = Deserialize::deserialize(de)?; string .parse() .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } enum PrivateKeyType { Ed25519(Ed25519KeyPair), Rsa(Arc), Ecdsa(EcdsaKeyPair), } impl Debug for PrivateKeyType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = match *self { PrivateKeyType::Ed25519(_) => "Ed25519", PrivateKeyType::Rsa(_) => "Rsa", PrivateKeyType::Ecdsa(_) => "Ecdsa", }; f.debug_tuple(s).field(&"_").finish() } } /// A structure containing information about a private key. pub struct PrivateKey { private: PrivateKeyType, public: PublicKey, } impl PrivateKey { /// Generate a new `PrivateKey` bytes in pkcs8 format. /// /// Note: For RSA keys, `openssl` needs to the on the `$PATH`. pub fn new(key_type: KeyType) -> Result> { match key_type { KeyType::Ed25519 => { Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()) .map(|bytes| bytes.as_ref().to_vec()) .map_err(|_| { Error::Opaque("Failed to generate Ed25519 key".into()) }) } KeyType::Rsa => Self::rsa_gen(), KeyType::Ecdsa => EcdsaKeyPair::generate_pkcs8( &ECDSA_P256_SHA256_ASN1_SIGNING, &SystemRandom::new(), ) .map(|bytes| bytes.as_ref().to_vec()) .map_err(|_| Error::Opaque("Failed to generate Ecdsa key".into())), KeyType::Unknown(s) => { Err(Error::IllegalArgument(format!("Unknown key type: {}", s))) } } } /// Create a new `PrivateKey` from an ed25519 keypair, a 64 byte slice, where the first 32 /// bytes are the ed25519 seed, and the second 32 bytes are the public key. pub fn from_ed25519(key: &[u8]) -> Result { Self::from_ed25519_with_keyid_hash_algorithms(key, None) } fn from_ed25519_with_keyid_hash_algorithms( key: &[u8], keyid_hash_algorithms: Option>, ) -> Result { if key.len() != ED25519_KEYPAIR_LENGTH { return Err(Error::Encoding( "ed25519 private keys must be 64 bytes long".into(), )); } let (private_key_bytes, public_key_bytes) = key.split_at(ED25519_PRIVATE_KEY_LENGTH); let key = Ed25519KeyPair::from_seed_and_public_key( private_key_bytes, public_key_bytes, ) .map_err(|err| Error::Encoding(err.to_string()))?; let public = PublicKey::new( KeyType::Ed25519, SignatureScheme::Ed25519, keyid_hash_algorithms, key.public_key().as_ref().to_vec(), )?; let private = PrivateKeyType::Ed25519(key); Ok(PrivateKey { private, public }) } /// Create a private key from PKCS#8v2 DER bytes. /// /// # Generating Keys /// /// ## Ed25519 /// /// ```bash /// $ touch ed25519-private-key.pk8 /// $ chmod 0600 ed25519-private-key.pk8 /// ``` /// /// ```no_run /// # use ring::rand::SystemRandom; /// # use ring::signature::Ed25519KeyPair; /// # use std::fs::File; /// # use std::io::Write; /// # fn main() { /// let mut file = File::open("ed25519-private-key.pk8").unwrap(); /// let key = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap(); /// file.write_all(key.as_ref()).unwrap() /// # } /// ``` /// /// ## RSA /// /// ```bash /// $ umask 077 /// $ openssl genpkey -algorithm RSA \ /// -pkeyopt rsa_keygen_bits:4096 \ /// -pkeyopt rsa_keygen_pubexp:65537 | \ /// openssl pkcs8 -topk8 -nocrypt -outform der > rsa-4096-private-key.pk8 /// ``` /// /// ## Ecdsa /// /// ```bash /// $ openssl ecparam -name prime256v1 -genkey -noout -out ec.pem /// $ openssl pkcs8 -in ec.pem -outform der -out ec.pk8.der -topk8 -nocrypt /// ``` pub fn from_pkcs8(der_key: &[u8], scheme: SignatureScheme) -> Result { let res = Self::ed25519_from_pkcs8(der_key); if res.is_ok() { if scheme != SignatureScheme::Ed25519 { return Err(Error::IllegalArgument(format!( "Cannot use signature scheme {:?} with Ed25519 keys", scheme, ))); } return res; } let res = Self::rsa_from_pkcs8(der_key, scheme.clone()); if res.is_ok() { return res; } let res = Self::ecdsa_from_pkcs8(der_key, scheme); if res.is_ok() { return res; } Err(Error::Opaque( "Key was not Ed25519, RSA, or ECDSA".to_string(), )) } fn ed25519_from_pkcs8(der_key: &[u8]) -> Result { Self::ed25519_from_pkcs8_with_keyid_hash_algorithms( der_key, python_sslib_compatibility_keyid_hash_algorithms(), ) } fn ed25519_from_pkcs8_with_keyid_hash_algorithms( der_key: &[u8], keyid_hash_algorithms: Option>, ) -> Result { let key = Ed25519KeyPair::from_pkcs8(der_key).map_err(|_| { Error::Encoding("Could not parse key as PKCS#8v2".into()) })?; let public = PublicKey::new( KeyType::Ed25519, SignatureScheme::Ed25519, keyid_hash_algorithms, key.public_key().as_ref().to_vec(), )?; let private = PrivateKeyType::Ed25519(key); Ok(PrivateKey { private, public }) } fn rsa_from_pkcs8(der_key: &[u8], scheme: SignatureScheme) -> Result { if SignatureScheme::Ed25519 == scheme { return Err(Error::IllegalArgument( "RSA keys do not support the Ed25519 signing scheme".into(), )); } let key = RsaKeyPair::from_pkcs8(der_key).map_err(|_| { Error::Encoding("Could not parse key as PKCS#8v2".into()) })?; if key.public().modulus_len() < 256 { return Err(Error::IllegalArgument(format!( "RSA public modulus must be 2048 or greater. Found {}", key.public().modulus_len() * 8 ))); } let pub_key = extract_rsa_pub_from_pkcs8(der_key)?; let public = PublicKey::new( KeyType::Rsa, scheme, python_sslib_compatibility_keyid_hash_algorithms(), pub_key, )?; let private = PrivateKeyType::Rsa(Arc::new(key)); Ok(PrivateKey { private, public }) } fn ecdsa_from_pkcs8( der_key: &[u8], scheme: SignatureScheme, ) -> Result { let key_pair = EcdsaKeyPair::from_pkcs8( &ECDSA_P256_SHA256_ASN1_SIGNING, der_key, &SystemRandom::new(), ) .unwrap(); let public = PublicKey::new( KeyType::Ecdsa, scheme, python_sslib_compatibility_keyid_hash_algorithms(), key_pair.public_key().as_ref().to_vec(), )?; let private = PrivateKeyType::Ecdsa(key_pair); Ok(PrivateKey { private, public }) } /// Sign a message. pub fn sign(&self, msg: &[u8]) -> Result { let value = match (&self.private, &self.public.scheme) { (PrivateKeyType::Rsa(rsa), &SignatureScheme::RsaSsaPssSha256) => { let rng = SystemRandom::new(); let mut buf = vec![0; rsa.public().modulus_len()]; rsa.sign(&RSA_PSS_SHA256, &rng, msg, &mut buf).map_err( |_| Error::Opaque("Failed to sign message.".into()), )?; SignatureValue(buf) } (PrivateKeyType::Rsa(rsa), &SignatureScheme::RsaSsaPssSha512) => { let rng = SystemRandom::new(); let mut buf = vec![0; rsa.public().modulus_len()]; rsa.sign(&RSA_PSS_SHA512, &rng, msg, &mut buf).map_err( |_| Error::Opaque("Failed to sign message.".into()), )?; SignatureValue(buf) } (PrivateKeyType::Ed25519(ed), &SignatureScheme::Ed25519) => { SignatureValue(ed.sign(msg).as_ref().into()) } (PrivateKeyType::Ecdsa(ec), &SignatureScheme::EcdsaP256Sha256) => { let rng = SystemRandom::new(); let s = ec.sign(&rng, msg).map_err(|_| { Error::Opaque("Failed to sign message.".into()) })?; SignatureValue(s.as_ref().into()) } (k, s) => { return Err(Error::IllegalArgument(format!( "Key {:?} can't be used with scheme {:?}", k, s ))); } }; Ok(Signature { key_id: self.key_id().clone(), value, }) } fn rsa_gen() -> Result> { let gen = Command::new("openssl") .args([ "genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:4096", "-pkeyopt", "rsa_keygen_pubexp:65537", "-outform", "der", ]) .output()?; let mut pk8 = Command::new("openssl") .args([ "pkcs8", "-inform", "der", "-topk8", "-nocrypt", "-outform", "der", ]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; match pk8.stdin { Some(ref mut stdin) => stdin.write_all(&gen.stdout)?, None => return Err(Error::Opaque("openssl has no stdin".into())), }; Ok(pk8.wait_with_output()?.stdout) } /// Return the public component of the key. pub fn public(&self) -> &PublicKey { &self.public } /// Return the key ID of the public key. pub fn key_id(&self) -> &KeyId { &self.public.key_id } } /// A structure containing information about a public key. #[derive(Clone, Debug)] pub struct PublicKey { typ: KeyType, key_id: KeyId, scheme: SignatureScheme, keyid_hash_algorithms: Option>, value: PublicKeyValue, } impl PublicKey { fn new( typ: KeyType, scheme: SignatureScheme, keyid_hash_algorithms: Option>, value: Vec, ) -> Result { let key_id = calculate_key_id(&typ, &scheme, &keyid_hash_algorithms, &value)?; let value = PublicKeyValue(value); Ok(PublicKey { typ, key_id, scheme, keyid_hash_algorithms, value, }) } /// Parse DER bytes as an SPKI key. /// /// See the documentation on `KeyValue` for more information on SPKI. pub fn from_spki( der_bytes: &[u8], scheme: SignatureScheme, ) -> Result { Self::from_spki_with_keyid_hash_algorithms( der_bytes, scheme, python_sslib_compatibility_keyid_hash_algorithms(), ) } /// Parse PEM as an Subject Public Key Info (SPKI) key. /// /// See the documentation on `KeyValue` for more information on SPKI. pub fn from_pem_spki(pem: &str, scheme: SignatureScheme) -> Result { let der_bytes = pem::parse(pem).unwrap(); Self::from_spki_with_keyid_hash_algorithms( der_bytes.contents(), scheme, python_sslib_compatibility_keyid_hash_algorithms(), ) } /// Parse DER bytes as an SPKI key and the `keyid_hash_algorithms`. /// /// See the documentation on `KeyValue` for more information on SPKI. fn from_spki_with_keyid_hash_algorithms( der_bytes: &[u8], scheme: SignatureScheme, keyid_hash_algorithms: Option>, ) -> Result { let input = Input::from(der_bytes); let (typ, value) = input.read_all(derp::Error::Read, |input| { derp::nested(input, Tag::Sequence, |input| { let typ = derp::nested(input, Tag::Sequence, |input| { let typ = derp::expect_tag_and_get_value(input, Tag::Oid)?; let typ = KeyType::from_oid(typ.as_slice_less_safe()) .map_err(|_| derp::Error::WrongValue)?; if typ == KeyType::Ecdsa { let _alg_oid = derp::expect_tag_and_get_value(input, Tag::Oid)?; } else { // for RSA / ed25519 this is null, so don't both parsing it derp::read_null(input)?; } Ok(typ) })?; let value = derp::bit_string_with_no_unused_bits(input)?; Ok((typ, value.as_slice_less_safe().to_vec())) }) })?; Self::new(typ, scheme, keyid_hash_algorithms, value) } /// Parse ED25519 bytes as a public key. pub fn from_ed25519>>(bytes: T) -> Result { Self::from_ed25519_with_keyid_hash_algorithms(bytes, None) } /// Parse ED25519 bytes as a public key with a custom `keyid_hash_algorithms`. pub fn from_ed25519_with_keyid_hash_algorithms>>( bytes: T, keyid_hash_algorithms: Option>, ) -> Result { let bytes = bytes.into(); if bytes.len() != 32 { return Err(Error::IllegalArgument( "ed25519 keys must be 32 bytes long".into(), )); } Self::new( KeyType::Ed25519, SignatureScheme::Ed25519, keyid_hash_algorithms, bytes, ) } /// Parse Ecdsa bytes as a public key. pub fn from_ecdsa>>(bytes: T) -> Result { Self::from_ecdsa_with_keyid_hash_algorithms(bytes, None) } /// Parse Ecdsa bytes as a public key with a custom `keyid_hash_algorithms`. pub fn from_ecdsa_with_keyid_hash_algorithms>>( bytes: T, keyid_hash_algorithms: Option>, ) -> Result { let bytes = bytes.into(); Self::new( KeyType::Ecdsa, SignatureScheme::EcdsaP256Sha256, keyid_hash_algorithms, bytes, ) } pub fn from_ecdsa_with_keyid_hash_algorithm>>( der_bytes: T, scheme: SignatureScheme, keyid_hash_algorithms: Option>, ) -> Result { let bytes = der_bytes.into(); Self::new(KeyType::Ecdsa, scheme, keyid_hash_algorithms, bytes) } /// Write the public key as SPKI DER bytes. /// /// See the documentation on `KeyValue` for more information on SPKI. pub fn as_spki(&self) -> Result> { Ok(write_spki(&self.value.0, &self.typ)?) } /// An immutable reference to the key's type. pub fn typ(&self) -> &KeyType { &self.typ } /// An immutable referece to the key's authorized signing scheme. pub fn scheme(&self) -> &SignatureScheme { &self.scheme } /// An immutable reference to the key's ID. pub fn key_id(&self) -> &KeyId { &self.key_id } /// Return the public key as bytes. pub fn as_bytes(&self) -> &[u8] { &self.value.0 } /// Use this key to verify a message with a signature. pub fn verify(&self, msg: &[u8], sig: &Signature) -> Result<()> { let alg: &dyn ring::signature::VerificationAlgorithm = match self.scheme { SignatureScheme::Ed25519 => &ED25519, SignatureScheme::RsaSsaPssSha256 => &RSA_PSS_2048_8192_SHA256, SignatureScheme::RsaSsaPssSha512 => &RSA_PSS_2048_8192_SHA512, SignatureScheme::EcdsaP256Sha256 => &ECDSA_P256_SHA256_ASN1, SignatureScheme::Unknown(ref s) => { return Err(Error::IllegalArgument(format!( "Unknown signature scheme: {}", s ))); } }; let key = ring::signature::UnparsedPublicKey::new(alg, &self.value.0); key.verify(msg, &sig.value.0) .map_err(|_| Error::BadSignature) } } impl PartialEq for PublicKey { fn eq(&self, other: &Self) -> bool { // key_id is derived from these fields, so we ignore it. self.typ == other.typ && self.scheme == other.scheme && self.keyid_hash_algorithms == other.keyid_hash_algorithms && self.value == other.value } } impl Eq for PublicKey {} impl Ord for PublicKey { fn cmp(&self, other: &Self) -> Ordering { self.key_id.cmp(&other.key_id) } } impl PartialOrd for PublicKey { fn partial_cmp(&self, other: &Self) -> Option { Some(self.key_id.cmp(&other.key_id)) } } impl hash::Hash for PublicKey { fn hash(&self, state: &mut H) { // key_id is derived from these fields, so we ignore it. self.typ.hash(state); self.scheme.hash(state); self.keyid_hash_algorithms.hash(state); self.value.hash(state); } } impl Serialize for PublicKey { fn serialize(&self, ser: S) -> ::std::result::Result where S: Serializer, { let key = shim_public_key( &self.typ, &self.scheme, &self.keyid_hash_algorithms, &self.value.0, true, Some(&self.key_id.0), ) .map_err(|e| { SerializeError::custom(format!( "Couldn't write key as SPKI: {:?}", e )) })?; key.serialize(ser) } } impl<'de> Deserialize<'de> for PublicKey { fn deserialize>( de: D, ) -> ::std::result::Result { let intermediate: shims::PublicKey = Deserialize::deserialize(de)?; let key = match intermediate.keytype() { KeyType::Ed25519 => { if intermediate.scheme() != &SignatureScheme::Ed25519 { return Err(DeserializeError::custom(format!( "ed25519 key type must be used with the ed25519 signature scheme, not {:?}", intermediate.scheme() ))); } let bytes = HEXLOWER .decode(intermediate.public_key().as_bytes()) .map_err(|e| { DeserializeError::custom(format!( "Couldn't parse key as HEX: {:?}", e )) })?; PublicKey::from_ed25519_with_keyid_hash_algorithms( bytes, intermediate.keyid_hash_algorithms().clone(), ) .map_err(|e| { DeserializeError::custom(format!( "Couldn't parse key as ed25519: {:?}", e )) })? } KeyType::Rsa => { let pub_pem = pem::parse(intermediate.public_key().as_bytes()) .map_err(|e| { DeserializeError::custom(format!( "pem deserialize failed: {:?}", e )) })?; PublicKey::from_spki_with_keyid_hash_algorithms( pub_pem.contents(), intermediate.scheme().clone(), intermediate.keyid_hash_algorithms().clone(), ) .map_err(|e| { DeserializeError::custom(format!( "Couldn't parse key as SPKI: {:?}", e )) })? } KeyType::Ecdsa => { if intermediate.scheme() != &SignatureScheme::EcdsaP256Sha256 { return Err(DeserializeError::custom(format!( "ecdsa key type must be used with the ecdsa signature scheme, not {:?}", intermediate.scheme() ))); } let bytes = HEXLOWER .decode(intermediate.public_key().as_bytes()) .map_err(|e| { DeserializeError::custom(format!( "Couldn't parse key as HEX: {:?}", e )) })?; PublicKey::from_ecdsa_with_keyid_hash_algorithm( bytes, intermediate.scheme().clone(), intermediate.keyid_hash_algorithms().clone(), ) .map_err(|e| { DeserializeError::custom(format!( "Couldn't parse key as SPKI: {:?}", e )) })? } KeyType::Unknown(inner) => { return Err(DeserializeError::custom(format!( "Unknown key type, content: {}", inner ))) } }; if intermediate.keytype() != &key.typ { return Err(DeserializeError::custom(format!( "Key type listed in the metadata did not match the type extrated \ from the key. {:?} vs. {:?}", intermediate.keytype(), key.typ, ))); } Ok(key) } } #[derive(Clone, PartialEq, Hash, Eq)] struct PublicKeyValue(Vec); impl Debug for PublicKeyValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("PublicKeyValue") .field(&HEXLOWER.encode(&self.0)) .finish() } } /// A structure that contains a `Signature` and associated data for verifying it. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Signature { #[serde(rename = "keyid")] key_id: KeyId, #[serde(rename = "sig")] value: SignatureValue, } impl Signature { /// An immutable reference to the `KeyId` of the key that produced the signature. pub fn key_id(&self) -> &KeyId { &self.key_id } /// An immutable reference to the `SignatureValue`. pub fn value(&self) -> &SignatureValue { &self.value } } /// The available hash algorithms. #[derive( Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, )] pub enum HashAlgorithm { /// SHA256 as describe in [RFC-6234](https://tools.ietf.org/html/rfc6234) #[serde(rename = "sha256")] Sha256, /// SHA512 as describe in [RFC-6234](https://tools.ietf.org/html/rfc6234) #[serde(rename = "sha512")] Sha512, /// Placeholder for an unknown hash algorithm. Unknown(String), } impl HashAlgorithm { /// Create a new `digest::Context` suitable for computing the hash of some data using this hash /// algorithm. pub(crate) fn digest_context(&self) -> Result { match self { HashAlgorithm::Sha256 => Ok(digest::Context::new(&SHA256)), HashAlgorithm::Sha512 => Ok(digest::Context::new(&SHA512)), HashAlgorithm::Unknown(ref s) => Err(Error::IllegalArgument( format!("Unknown hash algorithm: {}", s), )), } } pub fn return_all() -> HashMap { let mut map = HashMap::new(); map.insert(String::from("sha256"), HashAlgorithm::Sha256); map.insert(String::from("sha512"), HashAlgorithm::Sha512); map } } /// Wrapper for the value of a hash digest. #[derive(Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct HashValue(#[serde(with = "crate::format_hex")] Vec); impl HashValue { /// Create a new `HashValue` from the given digest bytes. pub fn new(bytes: Vec) -> Self { HashValue(bytes) } /// An immutable reference to the bytes of the hash value. pub fn value(&self) -> &[u8] { &self.0 } } impl Debug for HashValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("HashValue") .field(&HEXLOWER.encode(&self.0)) .finish() } } impl Display for HashValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", HEXLOWER.encode(&self.0)) } } fn write_spki( public: &[u8], key_type: &KeyType, ) -> ::std::result::Result, derp::Error> { let mut output = Vec::new(); { let mut der = Der::new(&mut output); der.sequence(|der| { der.sequence(|der| match key_type.as_oid().ok() { Some(tag) => { der.element(Tag::Oid, tag)?; der.null() } None => Err(derp::Error::WrongValue), })?; der.bit_string(0, public) })?; } Ok(output) } fn extract_rsa_pub_from_pkcs8( der_key: &[u8], ) -> ::std::result::Result, derp::Error> { let input = Input::from(der_key); input.read_all(derp::Error::Read, |input| { derp::nested(input, Tag::Sequence, |input| { if derp::small_nonnegative_integer(input)? != 0 { return Err(derp::Error::WrongValue); } derp::nested(input, Tag::Sequence, |input| { let actual_alg_id = derp::expect_tag_and_get_value(input, Tag::Oid)?; if actual_alg_id.as_slice_less_safe() != RSA_SPKI_OID { return Err(derp::Error::WrongValue); } let _ = derp::expect_tag_and_get_value(input, Tag::Null)?; Ok(()) })?; derp::nested(input, Tag::OctetString, |input| { derp::nested(input, Tag::Sequence, |input| { if derp::small_nonnegative_integer(input)? != 0 { return Err(derp::Error::WrongValue); } let n = derp::positive_integer(input)?; let e = derp::positive_integer(input)?; input.skip_to_end(); write_pkcs1(n.as_slice_less_safe(), e.as_slice_less_safe()) }) }) }) }) } fn write_pkcs1( n: &[u8], e: &[u8], ) -> ::std::result::Result, derp::Error> { let mut output = Vec::new(); { let mut der = Der::new(&mut output); der.sequence(|der| { der.positive_integer(n)?; der.positive_integer(e) })?; } Ok(output) } #[cfg(test)] mod test { use crate::models::Metablock; use super::*; use pretty_assertions::assert_eq; use serde_json::{self, json}; use std::str; const RSA_2048_PK8: &'static [u8] = include_bytes!("../tests/rsa/rsa-2048.pk8.der"); const RSA_2048_SPKI: &'static [u8] = include_bytes!("../tests/rsa/rsa-2048.spki.der"); const RSA_2048_PKCS1: &'static [u8] = include_bytes!("../tests/rsa/rsa-2048.pkcs1.der"); const RSA_4096_PK8: &'static [u8] = include_bytes!("../tests/rsa/rsa-4096.pk8.der"); const RSA_4096_SPKI: &'static [u8] = include_bytes!("../tests/rsa/rsa-4096.spki.der"); const RSA_4096_PKCS1: &'static [u8] = include_bytes!("../tests/rsa/rsa-4096.pkcs1.der"); const ED25519_1_PRIVATE_KEY: &'static [u8] = include_bytes!("../tests/ed25519/ed25519-1"); const ED25519_1_PUBLIC_KEY: &'static [u8] = include_bytes!("../tests/ed25519/ed25519-1.pub"); const ED25519_1_PK8: &'static [u8] = include_bytes!("../tests/ed25519/ed25519-1.pk8.der"); const ED25519_1_SPKI: &'static [u8] = include_bytes!("../tests/ed25519/ed25519-1.spki.der"); const ED25519_2_PK8: &'static [u8] = include_bytes!("../tests/ed25519/ed25519-2.pk8.der"); const ECDSA_PK8: &'static [u8] = include_bytes!("../tests/ecdsa/ec.pk8.der"); const ECDSA_SPKI: &'static [u8] = include_bytes!("../tests/ecdsa/ec.spki.der"); const ECDSA_PUBLIC_KEY: &'static [u8] = include_bytes!("../tests/ecdsa/ec.pub"); const DEMO_KEY_ID: &str = "556caebdc0877eed53d419b60eddb1e57fa773e4e31d70698b588f3e9cc48b35"; const DEMO_PUBLIC_KEY: &'static [u8] = include_bytes!("../tests/rsa/alice.pub"); const DEMO_LAYOUT: &'static [u8] = include_bytes!("../tests/test_verifylib/workdir/root.layout"); #[test] fn parse_public_rsa_2048_spki() { let key = PublicKey::from_spki( RSA_2048_SPKI, SignatureScheme::RsaSsaPssSha256, ) .unwrap(); assert_eq!(key.typ, KeyType::Rsa); assert_eq!(key.scheme, SignatureScheme::RsaSsaPssSha256); } #[test] fn parse_public_rsa_4096_spki() { let key = PublicKey::from_spki( RSA_4096_SPKI, SignatureScheme::RsaSsaPssSha256, ) .unwrap(); assert_eq!(key.typ, KeyType::Rsa); assert_eq!(key.scheme, SignatureScheme::RsaSsaPssSha256); } #[test] fn parse_public_ed25519_spki() { let key = PublicKey::from_spki(ED25519_1_SPKI, SignatureScheme::Ed25519) .unwrap(); assert_eq!(key.typ, KeyType::Ed25519); assert_eq!(key.scheme, SignatureScheme::Ed25519); } #[test] fn parse_public_ecdsa_spki() { let key = PublicKey::from_spki(ECDSA_SPKI, SignatureScheme::EcdsaP256Sha256) .unwrap(); assert_eq!(key.typ, KeyType::Ecdsa); assert_eq!(key.scheme, SignatureScheme::EcdsaP256Sha256); } #[test] fn parse_public_ed25519() { let key = PublicKey::from_ed25519(ED25519_1_PUBLIC_KEY).unwrap(); assert_eq!( key.key_id(), &KeyId::from_str("e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554") .unwrap() ); assert_eq!(key.typ, KeyType::Ed25519); assert_eq!(key.scheme, SignatureScheme::Ed25519); } #[test] fn parse_public_ecdsa() { let key = PublicKey::from_ecdsa(ECDSA_PUBLIC_KEY).unwrap(); assert_eq!( key.key_id(), &KeyId::from_str("d23fafcd03bf36532580dbab48b54f53e280ccb119db5846cc6fbe094c612947") .unwrap() ); assert_eq!(key.typ, KeyType::Ecdsa); assert_eq!(key.scheme, SignatureScheme::EcdsaP256Sha256); } #[test] fn parse_public_ed25519_without_keyid_hash_algo() { let key = PublicKey::from_ed25519_with_keyid_hash_algorithms( ED25519_1_PUBLIC_KEY, None, ) .unwrap(); assert_eq!( key.key_id(), &KeyId::from_str("e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554") .unwrap() ); assert_eq!(key.typ, KeyType::Ed25519); assert_eq!(key.scheme, SignatureScheme::Ed25519); } #[test] fn parse_public_ed25519_with_keyid_hash_algo() { let key = PublicKey::from_ed25519_with_keyid_hash_algorithms( ED25519_1_PUBLIC_KEY, python_sslib_compatibility_keyid_hash_algorithms(), ) .unwrap(); assert_eq!( key.key_id(), &KeyId::from_str("a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a") .unwrap(), ); assert_eq!(key.typ, KeyType::Ed25519); assert_eq!(key.scheme, SignatureScheme::Ed25519); } #[test] fn rsa_2048_read_pkcs8_and_sign() { let msg = b"test"; let key = PrivateKey::from_pkcs8( RSA_2048_PK8, SignatureScheme::RsaSsaPssSha256, ) .unwrap(); let sig = key.sign(msg).unwrap(); key.public.verify(msg, &sig).unwrap(); let key = PrivateKey::from_pkcs8( RSA_2048_PK8, SignatureScheme::RsaSsaPssSha512, ) .unwrap(); let sig = key.sign(msg).unwrap(); key.public.verify(msg, &sig).unwrap(); } #[test] fn rsa_4096_read_pkcs8_and_sign() { let msg = b"test"; let key = PrivateKey::from_pkcs8( RSA_4096_PK8, SignatureScheme::RsaSsaPssSha256, ) .unwrap(); let sig = key.sign(msg).unwrap(); key.public.verify(msg, &sig).unwrap(); let key = PrivateKey::from_pkcs8( RSA_4096_PK8, SignatureScheme::RsaSsaPssSha512, ) .unwrap(); let sig = key.sign(msg).unwrap(); key.public.verify(msg, &sig).unwrap(); } #[test] fn extract_pkcs1_from_rsa_2048_pkcs8() { let res = extract_rsa_pub_from_pkcs8(RSA_2048_PK8).unwrap(); assert_eq!(res.as_slice(), RSA_2048_PKCS1); } #[test] fn extract_pkcs1_from_rsa_4096_pkcs8() { let res = extract_rsa_pub_from_pkcs8(RSA_4096_PK8).unwrap(); assert_eq!(res.as_slice(), RSA_4096_PKCS1); } #[test] fn ed25519_read_pkcs8_and_sign() { let key = PrivateKey::from_pkcs8(ED25519_1_PK8, SignatureScheme::Ed25519) .unwrap(); let msg = b"test"; let sig = key.sign(msg).unwrap(); let pub_key = PublicKey::from_spki( &key.public.as_spki().unwrap(), SignatureScheme::Ed25519, ) .unwrap(); assert_eq!(pub_key.verify(msg, &sig), Ok(())); // Make sure we match what ring expects. let ring_key = ring::signature::Ed25519KeyPair::from_pkcs8(ED25519_1_PK8).unwrap(); assert_eq!(key.public().as_bytes(), ring_key.public_key().as_ref()); assert_eq!(sig.value().as_bytes(), ring_key.sign(msg).as_ref()); // Make sure verification fails with the wrong key. let bad_pub_key = PrivateKey::from_pkcs8(ED25519_2_PK8, SignatureScheme::Ed25519) .unwrap() .public() .clone(); assert_eq!(bad_pub_key.verify(msg, &sig), Err(Error::BadSignature)); } #[test] fn ecdsa_read_pkcs8_and_sign() { let msg = b"test"; let key = PrivateKey::from_pkcs8(ECDSA_PK8, SignatureScheme::EcdsaP256Sha256) .unwrap(); let sig = key.sign(msg).unwrap(); key.public.verify(msg, &sig).unwrap(); let key = PrivateKey::from_pkcs8(ECDSA_PK8, SignatureScheme::EcdsaP256Sha256) .unwrap(); let sig = key.sign(msg).unwrap(); key.public.verify(msg, &sig).unwrap(); } #[test] fn ed25519_read_keypair_and_sign() { let key = PrivateKey::from_ed25519(ED25519_1_PRIVATE_KEY).unwrap(); let pub_key = PublicKey::from_ed25519(ED25519_1_PUBLIC_KEY).unwrap(); assert_eq!(key.public(), &pub_key); let msg = b"test"; let sig = key.sign(msg).unwrap(); assert_eq!(pub_key.verify(msg, &sig), Ok(())); // Make sure we match what ring expects. let ring_key = ring::signature::Ed25519KeyPair::from_pkcs8(ED25519_1_PK8).unwrap(); assert_eq!(key.public().as_bytes(), ring_key.public_key().as_ref()); assert_eq!(sig.value().as_bytes(), ring_key.sign(msg).as_ref()); // Make sure verification fails with the wrong key. let bad_pub_key = PrivateKey::from_pkcs8(ED25519_2_PK8, SignatureScheme::Ed25519) .unwrap() .public() .clone(); assert_eq!(bad_pub_key.verify(msg, &sig), Err(Error::BadSignature)); } #[test] fn ed25519_read_keypair_and_sign_with_keyid_hash_algorithms() { let key = PrivateKey::from_ed25519_with_keyid_hash_algorithms( ED25519_1_PRIVATE_KEY, python_sslib_compatibility_keyid_hash_algorithms(), ) .unwrap(); let pub_key = PublicKey::from_ed25519_with_keyid_hash_algorithms( ED25519_1_PUBLIC_KEY, python_sslib_compatibility_keyid_hash_algorithms(), ) .unwrap(); assert_eq!(key.public(), &pub_key); let msg = b"test"; let sig = key.sign(msg).unwrap(); assert_eq!(pub_key.verify(msg, &sig), Ok(())); // Make sure we match what ring expects. let ring_key = ring::signature::Ed25519KeyPair::from_pkcs8(ED25519_1_PK8).unwrap(); assert_eq!(key.public().as_bytes(), ring_key.public_key().as_ref()); assert_eq!(sig.value().as_bytes(), ring_key.sign(msg).as_ref()); // Make sure verification fails with the wrong key. let bad_pub_key = PrivateKey::from_pkcs8(ED25519_2_PK8, SignatureScheme::Ed25519) .unwrap() .public() .clone(); assert_eq!(bad_pub_key.verify(msg, &sig), Err(Error::BadSignature)); } #[test] fn serde_key_id() { let s = "4750eaf6878740780d6f97b12dbad079fb012bec88c78de2c380add56d3f51db"; let jsn = json!(s); let parsed: KeyId = serde_json::from_str(&format!("\"{}\"", s)).unwrap(); assert_eq!(parsed, KeyId::from_str(s).unwrap()); let encoded = serde_json::to_value(&parsed).unwrap(); assert_eq!(encoded, jsn); } #[test] fn serde_signature_value() { let s = "4750eaf6878740780d6f97b12dbad079fb012bec88c78de2c380add56d3f51db"; let jsn = json!(s); let parsed: SignatureValue = serde_json::from_str(&format!("\"{}\"", s)).unwrap(); assert_eq!(parsed, SignatureValue::from_hex(s).unwrap()); let encoded = serde_json::to_value(&parsed).unwrap(); assert_eq!(encoded, jsn); } #[test] fn serde_rsa_public_key() { let der = RSA_2048_SPKI; let pub_key = PublicKey::from_spki(der, SignatureScheme::RsaSsaPssSha256) .unwrap(); let pem = pem::encode(&pem::Pem::new( PEM_PUBLIC_KEY.to_string(), der.to_vec(), )) .trim() .replace("\r\n", "\n") .to_string(); let encoded = serde_json::to_value(&pub_key).unwrap(); let jsn = json!({ "keyid": "c2620e94b6ff57f433c24436013a89d403fa6934a2ee490f44f897176c2c52e9", "keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": { "private": "", "public": pem, } }); assert_eq!(encoded, jsn); let decoded: PublicKey = serde_json::from_value(encoded).unwrap(); assert_eq!(decoded, pub_key); } #[test] fn de_ser_rsa_public_key_with_keyid_hash_algo() { let pem = pem::encode(&pem::Pem::new( PEM_PUBLIC_KEY.to_string(), RSA_2048_SPKI.to_vec(), )) .trim() .replace("\r\n", "\n") .to_string(); let original = json!({ "keyid": "c2620e94b6ff57f433c24436013a89d403fa6934a2ee490f44f897176c2c52e9", "keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": { "private": "", "public": pem, } }); let decoded: PublicKey = serde_json::from_value(original.clone()).unwrap(); let encoded = serde_json::to_value(&decoded).unwrap(); assert_eq!(original, encoded); } #[test] fn de_ser_rsa_public_key_without_keyid_hash_algo() { let pem = pem::encode(&pem::Pem::new( PEM_PUBLIC_KEY.to_string(), RSA_2048_SPKI.to_vec(), )) .trim() .replace("\r\n", "\n") .to_string(); let original = json!({ "keyid": "3733b56bfa06e9d731b561891d413569e0795c74d9c3434bc6373ff809683dde", "keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": { "private": "", "public": pem, } }); let decoded: PublicKey = serde_json::from_value(original.clone()).unwrap(); let encoded = serde_json::to_value(&decoded).unwrap(); assert_eq!(original, encoded); } #[test] fn serde_ed25519_public_key() { let pub_key = PrivateKey::from_pkcs8(ED25519_1_PK8, SignatureScheme::Ed25519) .unwrap() .public() .clone(); let pub_key = PublicKey::from_ed25519_with_keyid_hash_algorithms( pub_key.as_bytes().to_vec(), python_sslib_compatibility_keyid_hash_algorithms(), ) .unwrap(); let encoded = serde_json::to_value(&pub_key).unwrap(); let jsn = json!({ "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", "keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": { "private": "", "public": HEXLOWER.encode(pub_key.as_bytes()), } }); assert_eq!(encoded, jsn); let decoded: PublicKey = serde_json::from_value(encoded).unwrap(); assert_eq!(decoded, pub_key); } #[test] fn de_ser_ed25519_public_key_with_keyid_hash_algo() { let pub_key = PrivateKey::from_pkcs8(ED25519_1_PK8, SignatureScheme::Ed25519) .unwrap() .public() .clone(); let pub_key = PublicKey::from_ed25519_with_keyid_hash_algorithms( pub_key.as_bytes().to_vec(), python_sslib_compatibility_keyid_hash_algorithms(), ) .unwrap(); let original = json!({ "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", "keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": { "private": "", "public": HEXLOWER.encode(pub_key.as_bytes()), } }); let encoded: PublicKey = serde_json::from_value(original.clone()).unwrap(); let decoded = serde_json::to_value(&encoded).unwrap(); assert_eq!(original, decoded); } #[test] fn de_ser_ed25519_public_key_without_keyid_hash_algo() { let pub_key = PrivateKey::from_pkcs8(ED25519_1_PK8, SignatureScheme::Ed25519) .unwrap() .public() .clone(); let pub_key = PublicKey::from_ed25519_with_keyid_hash_algorithms( pub_key.as_bytes().to_vec(), None, ) .unwrap(); let original = json!({ "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554", "keytype": "ed25519", "scheme": "ed25519", "keyval": { "private": "", "public": HEXLOWER.encode(pub_key.as_bytes()), } }); let encoded: PublicKey = serde_json::from_value(original.clone()).unwrap(); let decoded = serde_json::to_value(&encoded).unwrap(); assert_eq!(original, decoded); } #[test] fn serde_ecdsa_public_key() { let pub_key = PrivateKey::from_pkcs8(ECDSA_PK8, SignatureScheme::EcdsaP256Sha256) .unwrap() .public() .clone(); let pub_key = PublicKey::from_ecdsa_with_keyid_hash_algorithms( pub_key.as_bytes().to_vec(), python_sslib_compatibility_keyid_hash_algorithms(), ) .unwrap(); let encoded = serde_json::to_value(&pub_key).unwrap(); let jsn = json!({ "keyid": "562b12b3f14a84bfe37d9de25c64f2e98eea7ab1918366361a7e37b5ab83b5f3", "keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": { "public": HEXLOWER.encode(pub_key.as_bytes()), "private": "" } }); assert_eq!(encoded, jsn); let decoded: PublicKey = serde_json::from_value(encoded).unwrap(); assert_eq!(decoded, pub_key); } #[test] fn serde_signature() { let key = PrivateKey::from_pkcs8(ED25519_1_PK8, SignatureScheme::Ed25519) .unwrap(); let msg = b"test"; let sig = key.sign(msg).unwrap(); let encoded = serde_json::to_value(&sig).unwrap(); let jsn = json!({ "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", "sig": "fe4d13b2a73c033a1de7f5107b205fc7ba0e1566cb95b92349cae6aa453\ 8956013bfe0f7bf977cb072bb65e8782b5f33a0573fe78816299a017ca5ba55\ 9e390c", }); assert_eq!(encoded, jsn); let decoded: Signature = serde_json::from_value(encoded).unwrap(); assert_eq!(decoded, sig); } #[test] fn serde_signature_without_keyid_hash_algo() { let key = PrivateKey::ed25519_from_pkcs8_with_keyid_hash_algorithms( ED25519_1_PK8, None, ) .unwrap(); let msg = b"test"; let sig = key.sign(msg).unwrap(); let encoded = serde_json::to_value(&sig).unwrap(); let jsn = json!({ "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554", "sig": "fe4d13b2a73c033a1de7f5107b205fc7ba0e1566cb95b92349cae6aa453\ 8956013bfe0f7bf977cb072bb65e8782b5f33a0573fe78816299a017ca5ba55\ 9e390c", }); assert_eq!(encoded, jsn); let decoded: Signature = serde_json::from_value(encoded).unwrap(); assert_eq!(decoded, sig); } #[test] #[cfg(not(any(target_os = "fuchsia", windows)))] fn new_rsa_key() { let bytes = PrivateKey::new(KeyType::Rsa).unwrap(); let _ = PrivateKey::from_pkcs8(&bytes, SignatureScheme::RsaSsaPssSha256) .unwrap(); } #[test] fn new_ed25519_key() { let bytes = PrivateKey::new(KeyType::Ed25519).unwrap(); let _ = PrivateKey::from_pkcs8(&bytes, SignatureScheme::Ed25519).unwrap(); } #[test] fn new_ecdsa_key() { let bytes = PrivateKey::new(KeyType::Ecdsa).unwrap(); let _ = PrivateKey::from_pkcs8(&bytes, SignatureScheme::EcdsaP256Sha256) .unwrap(); } #[test] fn test_public_key_eq() { let key256 = PublicKey::from_spki( RSA_2048_SPKI, SignatureScheme::RsaSsaPssSha256, ) .unwrap(); let key512 = PublicKey::from_spki( RSA_2048_SPKI, SignatureScheme::RsaSsaPssSha512, ) .unwrap(); assert_eq!(key256, key256); assert_ne!(key256, key512); } #[test] fn test_public_key_hash() { use std::hash::{BuildHasher, Hash, Hasher}; let key256 = PublicKey::from_spki( RSA_2048_SPKI, SignatureScheme::RsaSsaPssSha256, ) .unwrap(); let key512 = PublicKey::from_spki( RSA_2048_SPKI, SignatureScheme::RsaSsaPssSha512, ) .unwrap(); let state = std::collections::hash_map::RandomState::new(); let mut hasher256 = state.build_hasher(); key256.hash(&mut hasher256); let mut hasher512 = state.build_hasher(); key512.hash(&mut hasher512); assert_ne!(hasher256.finish(), hasher512.finish()); } #[test] fn parse_public_rsa_from_pem_spki() { let pem = str::from_utf8(&DEMO_PUBLIC_KEY).unwrap(); let key = PublicKey::from_pem_spki(&pem, SignatureScheme::RsaSsaPssSha256) .unwrap(); assert_eq!(key.typ, KeyType::Rsa); assert_eq!(key.scheme, SignatureScheme::RsaSsaPssSha256); } #[test] fn parse_public_ed25519_from_pem_spki() { let pem = pubkey_as_pem( &PublicKey::from_ed25519(ED25519_1_PUBLIC_KEY).unwrap(), ); let key = PublicKey::from_pem_spki(&pem, SignatureScheme::Ed25519).unwrap(); assert_eq!(key.typ, KeyType::Ed25519); assert_eq!(key.scheme, SignatureScheme::Ed25519); } #[test] fn parse_public_key_ecdsa_from_pem_spki() { let pem = str::from_utf8(&ECDSA_PUBLIC_KEY).unwrap(); let public_key = PublicKey::from_pem_spki(&pem, SignatureScheme::EcdsaP256Sha256) .unwrap(); assert_eq!(public_key.typ(), &KeyType::Ecdsa); assert_eq!(public_key.scheme(), &SignatureScheme::EcdsaP256Sha256); } #[test] fn compatibility_keyid_with_python_in_toto() { let der = pem::parse(DEMO_PUBLIC_KEY) .expect("parse alice.pub in pem format failed"); let key = PublicKey::from_spki( der.contents(), SignatureScheme::RsaSsaPssSha256, ) .expect("create PublicKey failed"); assert_eq!(key.key_id.0, DEMO_KEY_ID); } #[test] fn compatibility_rsa_verify_with_python_in_toto() { let der = pem::parse(DEMO_PUBLIC_KEY) .expect("parse alice.pub in pem format failed"); let key = PublicKey::from_spki( der.contents(), SignatureScheme::RsaSsaPssSha256, ) .expect("create PublicKey failed"); let meta: Metablock = serde_json::from_slice(DEMO_LAYOUT).expect("failed to deserialize"); let msg = meta.metadata.to_bytes().expect("failed to canonicalize"); let msg = String::from_utf8(msg) .expect("failed to parse metadata string") .replace("\\n", "\n"); let sig = &meta.signatures[0]; let res = key.verify(msg.as_bytes(), &sig); assert!(res.is_ok(), "{:?}", res); } fn pubkey_as_pem(key: &PublicKey) -> String { pem::encode(&pem::Pem::new( PEM_PUBLIC_KEY.to_string(), key.as_spki().unwrap(), )) .trim() .replace("\r\n", "\n") .to_string() } } in-toto-0.4.0/src/error.rs000064400000000000000000000072221046102023000135070ustar 00000000000000//! Error types and converters. use data_encoding::DecodeError; use std::io; use std::path::Path; use std::str; use thiserror::Error; /// Error type for all in-toto related errors. #[derive(Error, Debug, PartialEq, Eq)] pub enum Error { /// The metadata had a bad signature. #[error("bad signature")] BadSignature, /// There was a problem encoding or decoding. #[error("encoding: {0}")] Encoding(String), /// An illegal argument was passed into a function. #[error("illegal argument: {0}")] IllegalArgument(String), /// There were no available hash algorithms. #[error("no supported hash algorithm")] NoSupportedHashAlgorithm, /// The metadata or target was not found. #[error("not found")] NotFound, /// Opaque error type, to be interpreted similar to HTTP 500. Something went wrong, and you may /// or may not be able to do anything about it. #[error("opaque: {0}")] Opaque(String), /// There was a library internal error. These errors are *ALWAYS* bugs and should be reported. #[error("programming: {0}")] Programming(String), /// The target is unavailable. This may mean it is either not in the metadata or the metadata /// chain to the target cannot be fully verified. #[error("target unavailable")] TargetUnavailable, /// There is no known or available hash algorithm. #[error("unknown hash algorithm: {0}")] UnknownHashAlgorithm(String), /// There is no known or available key type. #[error("unknown key type: {0}")] UnknownKeyType(String), /// The metadata or target failed to verify. #[error("verification failure: {0}")] VerificationFailure(String), #[error("prefix selection failure: {0}")] LinkGatheringError(String), #[error("do Pre-Authentication Encoding failed: {0}")] PAEParseFailed(String), #[error("runlib failed: {0}")] RunLibError(String), #[error("attestation state and predicate version dismatch: {0} and {1}")] AttestationFormatDismatch(String, String), #[error("convertion from string failed: {0}")] StringConvertFailed(String), #[error("artifact rule error: {0}")] ArtifactRuleError(String), } impl From for Error { fn from(err: serde_json::error::Error) -> Error { Error::Encoding(format!("JSON: {:?}", err)) } } impl Error { /// Helper to include the path that causd the error for FS I/O errors. pub fn from_io(err: &io::Error, path: &Path) -> Error { Error::Opaque(format!("Path {:?} : {:?}", path, err)) } } impl From for Error { fn from(err: io::Error) -> Error { match err.kind() { std::io::ErrorKind::NotFound => Error::NotFound, _ => Error::Opaque(format!("IO: {:?}", err)), } } } impl From for Error { fn from(err: DecodeError) -> Error { Error::Encoding(format!("{:?}", err)) } } impl From for Error { fn from(err: derp::Error) -> Error { Error::Encoding(format!("DER: {:?}", err)) } } impl From for Error { fn from(err: str::Utf8Error) -> Error { Error::Opaque(format!("Parse utf8: {:?}", err)) } } #[cfg(test)] mod tests { use super::*; #[test] fn verify_io_error_display_string() { let err = Error::from(io::Error::from(std::io::ErrorKind::NotFound)); assert_eq!(err.to_string(), "not found"); assert_eq!(Error::NotFound.to_string(), "not found"); let err = Error::from(io::Error::from(std::io::ErrorKind::PermissionDenied)); assert_eq!(err.to_string(), "opaque: IO: Kind(PermissionDenied)"); } } in-toto-0.4.0/src/format_hex.rs000064400000000000000000000010041046102023000145020ustar 00000000000000use data_encoding::HEXLOWER; use serde::{self, Deserialize, Deserializer, Serializer}; use std::result::Result; pub fn serialize(value: &[u8], serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&HEXLOWER.encode(value)) } pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; HEXLOWER .decode(s.as_bytes()) .map_err(serde::de::Error::custom) } in-toto-0.4.0/src/interchange/cjson/mod.rs000064400000000000000000000272261046102023000165460ustar 00000000000000use serde::de::DeserializeOwned; use serde::ser::Serialize; use std::collections::BTreeMap; use std::io::{Read, Write}; use crate::error::Error; use crate::interchange::DataInterchange; use crate::Result; pub(crate) mod pretty; pub(crate) mod shims; pub use pretty::JsonPretty; /// JSON data interchange. /// /// # Schema /// /// This doesn't use JSON Schema because that specification language is rage inducing. Here's /// something else instead. /// /// ## Common Entities /// /// `NATURAL_NUMBER` is an integer in the range `[1, 2**32)`. /// /// `EXPIRES` is an ISO-8601 date time in format `YYYY-MM-DD'T'hh:mm:ss'Z'`. /// /// `KEY_ID` is the hex encoded value of `sha256(cjson(pub_key))`. /// /// `PUB_KEY` is the following: /// /// ```bash /// { /// "type": KEY_TYPE, /// "scheme": SCHEME, /// "value": PUBLIC /// } /// ``` /// /// `PUBLIC` is a base64url encoded `SubjectPublicKeyInfo` DER public key. /// /// `KEY_TYPE` is a string (either `rsa` or `ed25519`). /// /// `SCHEME` is a string (either `ed25519`, `rsassa-pss-sha256`, or `rsassa-pss-sha512` /// /// `HASH_VALUE` is a hex encoded hash value. /// /// `SIG_VALUE` is a hex encoded signature value. /// /// `METADATA_DESCRIPTION` is the following: /// /// ```bash /// { /// "version": NATURAL_NUMBER, /// "length": NATURAL_NUMBER, /// "hashes": { /// HASH_ALGORITHM: HASH_VALUE /// ... /// } /// } /// ``` /// /// ## `Metablock` /// /// ```bash /// { /// "signatures": [SIGNATURE], /// "signed": SIGNED /// } /// ``` /// /// `SIGNATURE` is: /// /// ```bash /// { /// "keyid": KEY_ID, /// "signature": SIG_VALUE /// } /// ``` /// /// `SIGNED` is one of: /// /// - `RootMetadata` /// - `SnapshotMetadata` /// - `TargetsMetadata` /// - `TimestampMetadata` /// /// The the elements of `signatures` must have unique `key_id`s. /// /// ## `RootMetadata` /// /// ```bash /// { /// "_type": "root", /// "version": NATURAL_NUMBER, /// "expires": EXPIRES, /// "keys": [PUB_KEY, ...] /// "roles": { /// "root": ROLE_DESCRIPTION, /// "snapshot": ROLE_DESCRIPTION, /// "targets": ROLE_DESCRIPTION, /// "timestamp": ROLE_DESCRIPTION /// } /// } /// ``` /// /// `ROLE_DESCRIPTION` is the following: /// /// ```bash /// { /// "threshold": NATURAL_NUMBER, /// "keyids": [KEY_ID, ...] /// } /// ``` /// /// ## `SnapshotMetadata` /// /// ```bash /// { /// "_type": "snapshot", /// "version": NATURAL_NUMBER, /// "expires": EXPIRES, /// "meta": { /// META_PATH: METADATA_DESCRIPTION /// } /// } /// ``` /// /// `META_PATH` is a string. /// /// /// ## `TargetsMetadata` /// /// ```bash /// { /// "_type": "timestamp", /// "version": NATURAL_NUMBER, /// "expires": EXPIRES, /// "targets": { /// TARGET_PATH: TARGET_DESCRIPTION /// ... /// }, /// } /// ``` /// /// `ROLE` is a string, /// /// `PATH` is a string. /// /// ## `TimestampMetadata` /// /// ```bash /// { /// "_type": "timestamp", /// "version": NATURAL_NUMBER, /// "expires": EXPIRES, /// "snapshot": METADATA_DESCRIPTION /// } /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub struct Json; impl DataInterchange for Json { type RawData = serde_json::Value; /// ``` /// # use in_toto::interchange::{DataInterchange, Json}; /// assert_eq!(Json::extension(), "json"); /// ``` fn extension() -> &'static str { "json" } /// ``` /// # use in_toto::interchange::{DataInterchange, Json}; /// # use std::collections::HashMap; /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; /// let raw = Json::from_reader(jsn).unwrap(); /// let out = Json::canonicalize(&raw).unwrap(); /// assert_eq!(out, br#"{"baz":"quux","foo":"bar"}"#); /// ``` fn canonicalize(raw_data: &Self::RawData) -> Result> { canonicalize(raw_data).map_err(Error::Opaque) } /// ``` /// # use serde::Deserialize; /// # use serde_json::json; /// # use std::collections::HashMap; /// # use in_toto::interchange::{DataInterchange, Json}; /// # /// #[derive(Deserialize, Debug, PartialEq)] /// struct Thing { /// foo: String, /// bar: String, /// } /// /// # fn main() { /// let jsn = json!({"foo": "wat", "bar": "lol"}); /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; /// let de: Thing = Json::deserialize(&jsn).unwrap(); /// assert_eq!(de, thing); /// # } /// ``` fn deserialize(raw_data: &Self::RawData) -> Result where T: DeserializeOwned, { Ok(serde_json::from_value(raw_data.clone())?) } /// ``` /// # use serde::Serialize; /// # use serde_json::json; /// # use std::collections::HashMap; /// # use in_toto::interchange::{DataInterchange, Json}; /// # /// #[derive(Serialize)] /// struct Thing { /// foo: String, /// bar: String, /// } /// /// # fn main() { /// let jsn = json!({"foo": "wat", "bar": "lol"}); /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; /// let se: serde_json::Value = Json::serialize(&thing).unwrap(); /// assert_eq!(se, jsn); /// # } /// ``` fn serialize(data: &T) -> Result where T: Serialize, { Ok(serde_json::to_value(data)?) } /// ``` /// # use serde_json::json; /// # use in_toto::interchange::{DataInterchange, Json}; /// let json = json!({ /// "o": { /// "a": [1, 2, 3], /// "s": "string", /// "n": 123, /// "t": true, /// "f": false, /// "0": null, /// }, /// }); /// let mut buf = Vec::new(); /// Json::to_writer(&mut buf, &json).unwrap(); /// assert_eq!( /// &String::from_utf8(buf).unwrap(), /// r#"{"o":{"0":null,"a":[1,2,3],"f":false,"n":123,"s":"string","t":true}}"# /// ); /// ``` fn to_writer(mut writer: W, value: &T) -> Result<()> where W: Write, T: Serialize + Sized, { let bytes = Self::canonicalize(&Self::serialize(value)?)?; writer.write_all(&bytes)?; Ok(()) } /// ``` /// # use in_toto::interchange::{DataInterchange, Json}; /// # use std::collections::HashMap; /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; /// let _: HashMap = Json::from_reader(jsn).unwrap(); /// ``` fn from_reader(rdr: R) -> Result where R: Read, T: DeserializeOwned, { Ok(serde_json::from_reader(rdr)?) } /// ``` /// # use in_toto::interchange::{DataInterchange, Json}; /// # use std::collections::HashMap; /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; /// let _: HashMap = Json::from_slice(&jsn).unwrap(); /// ``` fn from_slice(slice: &[u8]) -> Result where T: DeserializeOwned, { Ok(serde_json::from_slice(slice)?) } } fn canonicalize( jsn: &serde_json::Value, ) -> std::result::Result, String> { let converted = convert(jsn)?; let mut buf = Vec::new(); let _ = converted.write(&mut buf); // Vec impl always succeeds (or panics). Ok(buf) } enum Value { Array(Vec), Bool(bool), Null, Number(Number), Object(BTreeMap), String(String), } impl Value { fn write(&self, buf: &mut Vec) -> std::result::Result<(), String> { match *self { Value::Null => { buf.extend(b"null"); Ok(()) } Value::Bool(true) => { buf.extend(b"true"); Ok(()) } Value::Bool(false) => { buf.extend(b"false"); Ok(()) } Value::Number(Number::I64(n)) => { let mut buffer = itoa::Buffer::new(); let txt = buffer.format(n); buf.extend(txt.as_bytes()); Ok(()) } Value::Number(Number::U64(n)) => { let mut buffer = itoa::Buffer::new(); let txt = buffer.format(n); buf.extend(txt.as_bytes()); Ok(()) } Value::String(ref s) => { // this mess is abusing serde_json to get json escaping let s = serde_json::Value::String(s.clone()); let s = serde_json::to_string(&s) .map_err(|e| format!("{:?}", e))?; buf.extend(s.as_bytes()); Ok(()) } Value::Array(ref arr) => { buf.push(b'['); let mut first = true; for a in arr.iter() { if !first { buf.push(b','); } a.write(buf)?; first = false; } buf.push(b']'); Ok(()) } Value::Object(ref obj) => { buf.push(b'{'); let mut first = true; for (k, v) in obj.iter() { if !first { buf.push(b','); } first = false; // this mess is abusing serde_json to get json escaping let k = serde_json::Value::String(k.clone()); let k = serde_json::to_string(&k) .map_err(|e| format!("{:?}", e))?; buf.extend(k.as_bytes()); buf.push(b':'); v.write(buf)?; } buf.push(b'}'); Ok(()) } } } } enum Number { I64(i64), U64(u64), } fn convert(jsn: &serde_json::Value) -> std::result::Result { match *jsn { serde_json::Value::Null => Ok(Value::Null), serde_json::Value::Bool(b) => Ok(Value::Bool(b)), serde_json::Value::Number(ref n) => n .as_i64() .map(Number::I64) .or_else(|| n.as_u64().map(Number::U64)) .map(Value::Number) .ok_or_else(|| String::from("only i64 and u64 are supported")), serde_json::Value::Array(ref arr) => { let mut out = Vec::new(); for res in arr.iter().map(convert) { out.push(res?) } Ok(Value::Array(out)) } serde_json::Value::Object(ref obj) => { let mut out = BTreeMap::new(); for (k, v) in obj.iter() { let _ = out.insert(k.clone(), convert(v)?); } Ok(Value::Object(out)) } serde_json::Value::String(ref s) => Ok(Value::String(s.clone())), } } #[cfg(test)] mod test { use super::*; #[test] fn write_str() { let jsn = Value::String(String::from("wat")); let mut out = Vec::new(); jsn.write(&mut out).unwrap(); assert_eq!(&out, b"\"wat\""); } #[test] fn write_arr() { let jsn = Value::Array(vec![ Value::String(String::from("wat")), Value::String(String::from("lol")), Value::String(String::from("no")), ]); let mut out = Vec::new(); jsn.write(&mut out).unwrap(); assert_eq!(&out, b"[\"wat\",\"lol\",\"no\"]"); } #[test] fn write_obj() { let mut map = BTreeMap::new(); let arr = Value::Array(vec![ Value::String(String::from("haha")), Value::String(String::from("new\nline")), ]); let _ = map.insert(String::from("lol"), arr); let jsn = Value::Object(map); let mut out = Vec::new(); jsn.write(&mut out).unwrap(); assert_eq!(&out, &b"{\"lol\":[\"haha\",\"new\\nline\"]}"); } } in-toto-0.4.0/src/interchange/cjson/pretty.rs000064400000000000000000000106411046102023000173070ustar 00000000000000use serde::de::DeserializeOwned; use serde::ser::Serialize; use std::io::{Read, Write}; use super::Json; use crate::interchange::DataInterchange; use crate::Result; /// Pretty JSON data interchange. /// /// This is identical to [Json] in all manners except for the `to_writer` method. Instead of /// writing the metadata in the canonical format, it instead pretty prints the metadata. #[derive(Debug, Clone, PartialEq, Eq)] pub struct JsonPretty; impl DataInterchange for JsonPretty { type RawData = serde_json::Value; /// ``` /// # use in_toto::interchange::{DataInterchange, JsonPretty}; /// assert_eq!(JsonPretty::extension(), "json"); /// ``` fn extension() -> &'static str { Json::extension() } /// ``` /// # use in_toto::interchange::{DataInterchange, JsonPretty}; /// # use std::collections::HashMap; /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; /// let raw = JsonPretty::from_reader(jsn).unwrap(); /// let out = JsonPretty::canonicalize(&raw).unwrap(); /// assert_eq!(out, br#"{"baz":"quux","foo":"bar"}"#); /// ``` fn canonicalize(raw_data: &Self::RawData) -> Result> { Json::canonicalize(raw_data) } /// ``` /// # use serde::Deserialize; /// # use serde_json::json; /// # use std::collections::HashMap; /// # use in_toto::interchange::{DataInterchange, JsonPretty}; /// # /// #[derive(Deserialize, Debug, PartialEq)] /// struct Thing { /// foo: String, /// bar: String, /// } /// /// # fn main() { /// let jsn = json!({"foo": "wat", "bar": "lol"}); /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; /// let de: Thing = JsonPretty::deserialize(&jsn).unwrap(); /// assert_eq!(de, thing); /// # } /// ``` fn deserialize(raw_data: &Self::RawData) -> Result where T: DeserializeOwned, { Json::deserialize(raw_data) } /// ``` /// # use serde::Serialize; /// # use serde_json::json; /// # use std::collections::HashMap; /// # use in_toto::interchange::{DataInterchange, JsonPretty}; /// # /// #[derive(Serialize)] /// struct Thing { /// foo: String, /// bar: String, /// } /// /// # fn main() { /// let jsn = json!({"foo": "wat", "bar": "lol"}); /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; /// let se: serde_json::Value = JsonPretty::serialize(&thing).unwrap(); /// assert_eq!(se, jsn); /// # } /// ``` fn serialize(data: &T) -> Result where T: Serialize, { Json::serialize(data) } /// ``` /// # use serde_json::json; /// # use in_toto::interchange::{DataInterchange, JsonPretty}; /// let json = json!({ /// "o": { /// "a": [1, 2, 3], /// "s": "string", /// "n": 123, /// "t": true, /// "f": false, /// "0": null, /// }, /// }); /// let mut buf = Vec::new(); /// JsonPretty::to_writer(&mut buf, &json).unwrap(); /// assert_eq!(&String::from_utf8(buf).unwrap(), r#"{ /// "o": { /// "0": null, /// "a": [ /// 1, /// 2, /// 3 /// ], /// "f": false, /// "n": 123, /// "s": "string", /// "t": true /// } /// }"#); /// ``` fn to_writer(writer: W, value: &T) -> Result<()> where W: Write, T: Serialize + Sized, { Ok(serde_json::to_writer_pretty( writer, &Self::serialize(value)?, )?) } /// ``` /// # use in_toto::interchange::{DataInterchange, JsonPretty}; /// # use std::collections::HashMap; /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; /// let _: HashMap = JsonPretty::from_reader(jsn).unwrap(); /// ``` fn from_reader(rdr: R) -> Result where R: Read, T: DeserializeOwned, { Json::from_reader(rdr) } /// ``` /// # use in_toto::interchange::{DataInterchange, JsonPretty}; /// # use std::collections::HashMap; /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; /// let _: HashMap = JsonPretty::from_slice(&jsn).unwrap(); /// ``` fn from_slice(slice: &[u8]) -> Result where T: DeserializeOwned, { Json::from_slice(slice) } } in-toto-0.4.0/src/interchange/cjson/shims.rs000064400000000000000000000031001046102023000170730ustar 00000000000000// FIXME: imports will be relevant for layout expiration //use chrono::offset::Utc; //use chrono::prelude::*; use serde::{Deserialize, Serialize}; use crate::crypto; #[derive(Serialize, Deserialize)] pub struct PublicKey { keytype: crypto::KeyType, scheme: crypto::SignatureScheme, #[serde(skip_serializing_if = "Option::is_none")] keyid_hash_algorithms: Option>, keyval: PublicKeyValue, #[serde(skip_serializing_if = "Option::is_none")] keyid: Option, } impl PublicKey { pub fn new( keytype: crypto::KeyType, scheme: crypto::SignatureScheme, keyid_hash_algorithms: Option>, public_key: String, keyid: Option<&str>, private_key: Option<&str>, ) -> Self { PublicKey { keytype, scheme, keyid_hash_algorithms, keyval: PublicKeyValue { public: public_key, private: private_key.map(|k| k.to_string()), }, keyid: keyid.map(|id| id.to_string()), } } pub fn public_key(&self) -> &str { &self.keyval.public } pub fn scheme(&self) -> &crypto::SignatureScheme { &self.scheme } pub fn keytype(&self) -> &crypto::KeyType { &self.keytype } pub fn keyid_hash_algorithms(&self) -> &Option> { &self.keyid_hash_algorithms } } #[derive(Serialize, Deserialize)] pub struct PublicKeyValue { public: String, #[serde(skip_serializing_if = "Option::is_none")] private: Option, } in-toto-0.4.0/src/interchange/mod.rs000064400000000000000000000027371046102023000154320ustar 00000000000000//! Structures and functions to aid in various in-toto data interchange formats. pub(crate) mod cjson; pub use cjson::{Json, JsonPretty}; use serde::de::DeserializeOwned; use serde::ser::Serialize; use std::fmt::Debug; use std::io::{Read, Write}; use crate::Result; /// The format used for data interchange, serialization, and deserialization. pub trait DataInterchange: Debug + PartialEq + Clone { /// The type of data that is contained in the `signed` portion of metadata. type RawData: Serialize + DeserializeOwned + Clone + PartialEq; /// The data interchange's extension. fn extension() -> &'static str; /// A function that canonicalizes data to allow for deterministic signatures. fn canonicalize(raw_data: &Self::RawData) -> Result>; /// Deserialize from `RawData`. fn deserialize(raw_data: &Self::RawData) -> Result where T: DeserializeOwned; /// Serialize into `RawData`. fn serialize(data: &T) -> Result where T: Serialize; /// Write a struct to a stream. #[allow(clippy::wrong_self_convention)] fn to_writer(writer: W, value: &T) -> Result<()> where W: Write, T: Serialize + Sized; /// Read a struct from a stream. fn from_reader(rdr: R) -> Result where R: Read, T: DeserializeOwned; /// Read a struct from a stream. fn from_slice(slice: &[u8]) -> Result where T: DeserializeOwned; } in-toto-0.4.0/src/lib.rs000064400000000000000000000010241046102023000131160ustar 00000000000000//! This crate provides an API for talking to repositories that implements in-toto //#![deny(missing_docs)] #![allow( clippy::collapsible_if, clippy::implicit_hasher, clippy::new_ret_no_self, clippy::op_ref, clippy::too_many_arguments, clippy::borrowed_box )] pub mod crypto; pub mod error; pub mod interchange; pub mod models; mod rulelib; pub mod runlib; pub mod verifylib; mod format_hex; pub use crate::error::*; /// Alias for `Result`. pub type Result = std::result::Result; in-toto-0.4.0/src/models/envelope/envelope_file.rs000064400000000000000000000106101046102023000202650ustar 00000000000000use serde::{Deserialize, Serialize}; use crate::interchange::DataInterchange; use crate::Result; use crate::{crypto::Signature, interchange::Json}; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct EnvelopeFile { payload: String, payload_type: String, signatures: Vec, } impl EnvelopeFile { #[allow(dead_code)] // TODO: remove #[allow(dead_code)] after metadata deploy pub fn new( payload: String, payload_type: String, signatures: Vec, ) -> Self { Self { payload, payload_type, signatures, } } /// standard serialize for EnvelopeFile #[allow(dead_code)] // TODO: remove #[allow(dead_code)] after metadata deploy pub fn to_bytes(&self) -> Result> { Json::canonicalize(&Json::serialize(self)?) } /// standard deserialize for EnvelopeFile #[allow(dead_code)] // TODO: remove #[allow(dead_code)] after metadata deploy pub fn from_bytes(bytes: &[u8]) -> Result { let ret: Self = serde_json::from_slice(bytes)?; Ok(ret) } #[allow(dead_code)] // TODO: remove #[allow(dead_code)] after metadata deploy pub fn signatures(&self) -> &Vec { &self.signatures } #[allow(dead_code)] // TODO: remove #[allow(dead_code)] after metadata deploy pub fn payload(&self) -> &String { &self.payload } #[allow(dead_code)] // TODO: remove #[allow(dead_code)] after metadata deploy pub fn payload_type(&self) -> &String { &self.payload_type } } #[cfg(test)] mod test_envelope_file { use std::str; use once_cell::sync::Lazy; use super::EnvelopeFile; // TODO: change to mock test use mockall use crate::crypto::Signature; pub struct EnvelopeFileTuple<'a> { name: String, payload: String, payload_type: String, signatures: Vec, packet: &'a str, } pub static SERIALIZE_DATAS: Lazy> = Lazy::new( || { vec![ EnvelopeFileTuple { name: "blank_test".to_string(), payload: "114514".to_string(), payload_type: "link".to_string(), signatures: Vec::new(), packet: "{\"payload\":\"114514\",\"payload_type\":\"link\",\"signatures\":[]}", }, EnvelopeFileTuple { name: "blank_test".to_string(), payload: "in-toto-rs".to_string(), payload_type: "https://in-toto.io/statement/v0.1".to_string(), signatures: Vec::new(), packet: "{\"payload\":\"in-toto-rs\",\"payload_type\":\"https://in-toto.io/statement/v0.1\",\"signatures\":[]}", }, ] }, ); #[test] fn serialize_link() { for item in SERIALIZE_DATAS.iter() { let envelope_file = EnvelopeFile::new( item.payload.clone(), item.payload_type.clone(), item.signatures.clone(), ); let bytes = envelope_file.to_bytes().unwrap(); let real = str::from_utf8(&bytes).unwrap(); let right = item.packet; assert_eq!(real, right, "Assert serialize unequal {}", item.name); } } #[test] fn deserialize_link() { for item in SERIALIZE_DATAS.iter() { let envelope_file = EnvelopeFile::from_bytes(item.packet.as_bytes()).unwrap(); assert_eq!( envelope_file.payload(), &item.payload, "Assert deserialize unequal {} for {} and {}", item.name, envelope_file.payload(), &item.payload ); assert_eq!( envelope_file.payload_type(), &item.payload_type, "Assert deserialize unequal {} for {} and {}", item.name, envelope_file.payload_type(), &item.payload_type, ); assert_eq!( envelope_file.signatures(), &item.signatures, "Assert deserialize unequal {} for {:?} and {:?}", item.name, envelope_file.signatures(), &item.signatures, ); } } } in-toto-0.4.0/src/models/envelope/mod.rs000064400000000000000000000054741046102023000162440ustar 00000000000000use strum::IntoEnumIterator; use strum_macros::EnumIter; use self::pae_v1::PaeV1; use crate::{Error, Result}; mod envelope_file; mod pae_v1; pub trait DSSEParser { fn pae_pack(payload_ver: String, payload: &[u8]) -> Vec; fn pae_unpack(bytes: &[u8]) -> Result<(Vec, String)>; } /// DSSE global packer and unpacker. #[derive(EnumIter, PartialEq, Eq, Hash, Clone, Copy)] pub enum DSSEVersion { V1, } impl DSSEVersion { /// Use Pre-Authentication Encoding to pack payload for any version. #[allow(dead_code)] // TODO: remove #[allow(dead_code)] after metadata deploy pub fn pack(&self, payload: &[u8], payload_ver: String) -> Vec { let payload = payload.to_vec(); match self { DSSEVersion::V1 => PaeV1::pae_pack(payload_ver, &payload), } } /// Use Pre-Authentication Encoding to unpack payload for any version. pub fn unpack(&self, bytes: &[u8]) -> Result<(Vec, String)> { // Note: if two of the versions is compatible with each other // `try_unpack` may failed to find the right version. let (payload, payload_ver) = match self { DSSEVersion::V1 => PaeV1::pae_unpack(bytes)?, }; Ok((payload, payload_ver)) } /// Use Pre-Authentication Encoding to auto unpack a possible version. #[allow(dead_code)] // TODO: remove #[allow(dead_code)] after metadata deploy pub fn try_unpack(bytes: &[u8]) -> Result<(Vec, String)> { let mut file: Result<(Vec, String)> = Err(Error::Programming("no available DSSE parser".to_string())); for method in DSSEVersion::iter() { file = method.unpack(bytes); if file.is_ok() { break; } } file } } #[cfg(test)] mod pae_test { use once_cell::sync::Lazy; pub struct EnvelopeFileTuple { pub(crate) name: String, pub(crate) inner_payload: &'static str, pub(crate) payload_ver: String, } pub static SERIALIZE_SRC_DATAS: Lazy> = Lazy::new( || { vec![ EnvelopeFileTuple { name: "blank_test".to_string(), inner_payload: "", payload_ver: "link".to_string(), }, EnvelopeFileTuple { name: "blank_envelope_naive_test".to_string(), inner_payload: "{\"payload\":[],\"payload_type\":\"link\",\"signatures\":[]}", payload_ver: "link".to_string(), }, EnvelopeFileTuple { name: "blank_envelope_v01_test".to_string(), inner_payload: "{\"payload\":[],\"payload_type\":\"link\",\"signatures\":[]}", payload_ver: "https://in-toto.io/statement/v0.1".to_string(), }, ] }, ); } in-toto-0.4.0/src/models/envelope/pae_v1.rs000064400000000000000000000112601046102023000166260ustar 00000000000000use std::str; use super::DSSEParser; use crate::Error; use crate::Result; const PREFIX: &str = "DSSEv1"; const SPLIT: &str = " "; const SPLIT_U8: u8 = 0x20; pub struct PaeV1; /// Extract length and payload from bytes and consume them /// /// length, SPLIT, next -> (length, next) fn consume_load_len(raw: &[u8]) -> Result<(usize, &[u8])> { let mut iter = raw.splitn(2, |num| *num == SPLIT_U8); let length_raw = iter.next().ok_or_else(|| { Error::PAEParseFailed(format!( "split '{}' failed for {:?}", SPLIT, raw.to_owned() )) })?; let length = str::from_utf8(length_raw)?.parse::().map_err(|_| { Error::PAEParseFailed(format!( "parse to int failed for {:?}", length_raw )) })?; let next = iter.next().ok_or_else(|| { Error::PAEParseFailed(format!( "prefix {} strip failed for {:?}", PREFIX, raw.to_owned() )) })?; Ok((length, next)) } impl DSSEParser for PaeV1 { /// Use Pre-Authentication Encoding to pack payload for DSSE v1. fn pae_pack(payload_ver: String, payload: &[u8]) -> Vec { let sig_header: String = format!( "{prefix}{split}{payload_ver_len}{split}{payload_ver}{split}{payload_len}{split}", prefix = PREFIX, split = SPLIT, payload_ver_len = payload_ver.as_bytes().len(), payload_ver = payload_ver.as_str(), payload_len = payload.len(), ); let sig = [sig_header.as_bytes(), payload].concat(); sig } /// Use Pre-Authentication Encoding to unpack payload for DSSE v1. fn pae_unpack(bytes: &[u8]) -> Result<(Vec, String)> { // Strip prefix + split "DSSEv1 " let raw = bytes .strip_prefix(format!("{}{}", PREFIX, SPLIT).as_bytes()) .ok_or_else(|| { Error::PAEParseFailed(format!( "prefix {} strip failed for {:?}", PREFIX, bytes.to_owned() )) })?; // Extract payload_ver from bytes let (payload_ver_len, raw) = consume_load_len(raw)?; let payload_ver = str::from_utf8(&raw[0..payload_ver_len])? .parse::() .map_err(|_| { Error::PAEParseFailed(format!( "parse to string failed for {:?}", raw )) })?; // Extract payload from bytes let (payload_len, raw) = consume_load_len(&raw[(payload_ver_len + 1)..])?; let payload = raw[0..payload_len].to_vec(); Ok((payload, payload_ver)) } } #[cfg(test)] mod pae_test { use std::collections::HashMap; use std::str; use once_cell::sync::Lazy; use crate::models::envelope::{pae_test::SERIALIZE_SRC_DATAS, DSSEVersion}; static SERIALIZE_RESULT_DATAS: Lazy> = Lazy::new( || { let real_serialized_file = HashMap::from([ ("blank_test".to_string(), "DSSEv1 4 link 0 "), ( "blank_envelope_naive_test".to_string(), "DSSEv1 4 link 52 {\"payload\":[],\"payload_type\":\"link\",\"signatures\":[]}", ), ( "blank_envelope_v01_test".to_string(), "DSSEv1 33 https://in-toto.io/statement/v0.1 52 {\"payload\":[],\"payload_type\":\"link\",\"signatures\":[]}", ), ]); real_serialized_file }, ); #[test] fn test_pack() { for file_tuple in SERIALIZE_SRC_DATAS.iter() { let outer = DSSEVersion::V1.pack( file_tuple.inner_payload.as_bytes(), file_tuple.payload_ver.to_owned(), ); let real = std::str::from_utf8(&outer).unwrap(); let right = *SERIALIZE_RESULT_DATAS.get(&file_tuple.name).unwrap(); assert_eq!( real, right, "pack assert failed for {}", file_tuple.name ); } } #[test] fn test_unpack() { for file_tuple in SERIALIZE_SRC_DATAS.iter() { let outer = SERIALIZE_RESULT_DATAS .get(&file_tuple.name) .unwrap() .as_bytes() .to_vec(); let (inner, _) = DSSEVersion::V1.unpack(&outer).unwrap(); let real = str::from_utf8(&inner).unwrap(); let right = file_tuple.inner_payload; assert_eq!( real, right, "unpack assert failed for {}", file_tuple.name ); } } } in-toto-0.4.0/src/models/helpers.rs000064400000000000000000000047731046102023000153130ustar 00000000000000//! Supporting Functions and Types (VirtualTargetPath) use std::collections::HashMap; use std::fmt; use std::fmt::Debug; use std::str; use serde::de::{Deserialize, Deserializer, Error as DeserializeError}; use serde::Serialize; use crate::crypto::{HashAlgorithm, HashValue}; use crate::{Error, Result}; /// Description of a target, used in verification. pub type TargetDescription = HashMap; /// Wrapper for the Virtual path to a target. #[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord, Serialize)] pub struct VirtualTargetPath(String); impl VirtualTargetPath { /// Create a new `VirtualTargetPath` from a `String`. /// pub fn new(path: String) -> Result { Ok(VirtualTargetPath(path)) } /// The string value of the path. pub fn value(&self) -> &str { &self.0 } /// Judge if this [`VirtualTargetPath`] matches the given pattern pub(crate) fn matches(&self, pattern: &str) -> Result { let matcher = glob::Pattern::new(pattern).map_err(|e| { Error::IllegalArgument(format!( "Pattern matcher creation failed: {}", e )) })?; Ok(matcher.matches(self.value())) } } impl fmt::Display for VirtualTargetPath { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } impl From<&str> for VirtualTargetPath { fn from(s: &str) -> Self { Self(s.to_string()) } } impl AsRef for VirtualTargetPath { fn as_ref(&self) -> &str { &self.0 } } impl<'de> Deserialize<'de> for VirtualTargetPath { fn deserialize>( de: D, ) -> ::std::result::Result { let s: String = Deserialize::deserialize(de)?; VirtualTargetPath::new(s) .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } #[cfg(test)] mod tests { use crate::models::VirtualTargetPath; #[test] fn serialize_virtual_target_path() { let path = VirtualTargetPath::from("foo.py"); let serialized = serde_json::to_string(&path).expect("serialize failed"); let expected = "\"foo.py\""; assert!(serialized == expected); } #[test] fn deserialize_virtual_target_path() { let path = VirtualTargetPath::from("foo.py"); let deserialized: VirtualTargetPath = serde_json::from_str("\"foo.py\"").expect("serialize failed"); assert!(path == deserialized); } } in-toto-0.4.0/src/models/layout/inspection.rs000064400000000000000000000100641046102023000173270ustar 00000000000000//! in-toto layout's Inspection use serde::{Deserialize, Serialize}; use crate::supply_chain_item_derive; use super::{ rule::ArtifactRule, step::Command, supply_chain_item::SupplyChainItem, }; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Inspection { #[serde(rename = "_type")] pub typ: String, #[serde(rename = "name")] pub name: String, pub expected_materials: Vec, pub expected_products: Vec, pub run: Command, } impl Inspection { pub fn new(name: &str) -> Self { Inspection { run: Command::default(), name: name.into(), expected_materials: Vec::new(), expected_products: Vec::new(), typ: "inspection".into(), } } /// Set expected command for this Inspection pub fn run(mut self, command: Command) -> Self { self.run = command; self } // Derive operations on `materials`/`products` and `name` supply_chain_item_derive!(); } impl SupplyChainItem for Inspection { fn name(&self) -> &str { &self.name } fn expected_materials(&self) -> &Vec { &self.expected_materials } fn expected_products(&self) -> &Vec { &self.expected_products } } #[cfg(test)] mod test { use assert_json_diff::assert_json_eq; use serde_json::json; use super::Inspection; use crate::models::rule::test::{ generate_materials_rule, generate_products_rule, }; #[test] fn serialize_inspection() { let json = json!({ "_type": "inspection", "name": "test_inspect", "expected_materials" : [ [ "MATCH", "pattern/", "IN", "src", "WITH", "MATERIALS", "IN", "dst", "FROM", "test_step" ] ], "expected_products" : [ [ "MATCH", "pattern/", "IN", "src", "WITH", "PRODUCTS", "IN", "dst", "FROM", "test_step" ] ], "run" : ["ls", "-al"] }); let inspection = Inspection::new("test_inspect") .add_expected_material(generate_materials_rule()) .add_expected_product(generate_products_rule()) .run("ls -al".into()); let json_serialized = serde_json::to_value(&inspection).unwrap(); assert_json_eq!(json, json_serialized); } #[test] fn deserialize_inspection() { let json = r#"{ "_type": "inspection", "name": "test_inspect", "expected_materials" : [ [ "MATCH", "pattern/", "IN", "src", "WITH", "MATERIALS", "IN", "dst", "FROM", "test_step" ] ], "expected_products" : [ [ "MATCH", "pattern/", "IN", "src", "WITH", "PRODUCTS", "IN", "dst", "FROM", "test_step" ] ], "run" : ["ls", "-al"] }"#; let inspection = Inspection::new("test_inspect") .add_expected_material(generate_materials_rule()) .add_expected_product(generate_products_rule()) .run("ls -al".into()); let inspection_parsed: Inspection = serde_json::from_str(json).unwrap(); assert_eq!(inspection_parsed, inspection); } } in-toto-0.4.0/src/models/layout/metadata.rs000064400000000000000000000105041046102023000167330ustar 00000000000000//! in-toto layoput metadata. use chrono::{DateTime, Duration, Utc}; use serde::de::{Deserialize, Deserializer, Error as DeserializeError}; use serde::ser::{Error as SerializeError, Serialize, Serializer}; use std::collections::HashMap; use crate::crypto::KeyId; use crate::crypto::PublicKey; use crate::interchange::{DataInterchange, Json}; use crate::models::{Metadata, MetadataType, MetadataWrapper}; use crate::Result; use super::Layout; use super::{inspection::Inspection, step::Step}; /// Helper to construct `LayoutMetadata` pub struct LayoutMetadataBuilder { expires: DateTime, readme: String, keys: HashMap, steps: Vec, inspect: Vec, } impl Default for LayoutMetadataBuilder { fn default() -> Self { LayoutMetadataBuilder::new() } } impl LayoutMetadataBuilder { /// Create a new `LayoutMetadataBuilder`. It defaults to: /// /// * expires: 365 days from the current time. /// * readme: "" pub fn new() -> Self { LayoutMetadataBuilder { steps: Vec::new(), inspect: Vec::new(), keys: HashMap::new(), expires: Utc::now() + Duration::days(365), readme: String::new(), } } /// Set expire time for this layout pub fn expires(mut self, expires: DateTime) -> Self { self.expires = expires; self } /// Set readme field fot this layout pub fn readme(mut self, readme: String) -> Self { self.readme = readme; self } /// Add new step to this layout pub fn add_step(mut self, step: Step) -> Self { self.steps.push(step); self } /// Add new steps to this layout pub fn add_steps(mut self, mut steps: Vec) -> Self { self.steps.append(&mut steps); self } /// Set steps to this layout pub fn steps(mut self, steps: Vec) -> Self { self.steps = steps; self } /// Add new inspect to this layout pub fn add_inspect(mut self, inspect: Inspection) -> Self { self.inspect.push(inspect); self } /// Add new inspects to this layout pub fn add_inspects(mut self, mut inspects: Vec) -> Self { self.inspect.append(&mut inspects); self } /// Set inspects to this layout pub fn inspects(mut self, step: Vec) -> Self { self.inspect = step; self } /// Add a new pubkey to this layout pub fn add_key(mut self, key: PublicKey) -> Self { self.keys.insert(key.key_id().clone(), key); self } pub fn build(self) -> Result { Ok(LayoutMetadata::new( self.expires, self.readme, self.keys, self.steps, self.inspect, )) } } /// layout metadata #[derive(Debug, Clone, PartialEq, Eq)] pub struct LayoutMetadata { pub steps: Vec, pub inspect: Vec, pub keys: HashMap, pub expires: DateTime, pub readme: String, } impl LayoutMetadata { pub fn new( expires: DateTime, readme: String, keys: HashMap, steps: Vec, inspect: Vec, ) -> Self { LayoutMetadata { steps, inspect, keys, expires, readme, } } } impl Metadata for LayoutMetadata { fn typ(&self) -> MetadataType { MetadataType::Layout } fn into_enum(self: Box) -> MetadataWrapper { MetadataWrapper::Layout(*self) } fn to_bytes(&self) -> Result> { Json::canonicalize(&Json::serialize(self)?) } } impl Serialize for LayoutMetadata { fn serialize(&self, ser: S) -> ::std::result::Result where S: Serializer, { Layout::from(self) .map_err(|e| SerializeError::custom(format!("{:?}", e)))? .serialize(ser) } } impl<'de> Deserialize<'de> for LayoutMetadata { fn deserialize>( de: D, ) -> ::std::result::Result { let intermediate: Layout = Deserialize::deserialize(de)?; intermediate .try_into() .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } in-toto-0.4.0/src/models/layout/mod.rs000064400000000000000000000337471046102023000157500ustar 00000000000000//! in-toto layout: used by the project owner to generate a desired supply chain layout file. use std::collections::BTreeMap; use chrono::prelude::*; use chrono::{DateTime, Utc}; use log::warn; use serde::{Deserialize, Serialize}; use crate::crypto::{KeyId, PublicKey}; use crate::{Error, Result}; use self::{inspection::Inspection, step::Step}; pub mod inspection; mod metadata; pub mod rule; pub mod step; pub mod supply_chain_item; pub use metadata::{LayoutMetadata, LayoutMetadataBuilder}; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct Layout { #[serde(rename = "_type")] typ: String, expires: String, readme: String, keys: BTreeMap, steps: Vec, inspect: Vec, } impl Layout { pub fn from(meta: &LayoutMetadata) -> Result { Ok(Layout { typ: String::from("layout"), expires: format_datetime(&meta.expires), readme: meta.readme.to_string(), keys: meta .keys .iter() .map(|(id, key)| (id.clone(), key.clone())) .collect(), steps: meta.steps.clone(), inspect: meta.inspect.clone(), }) } pub fn try_into(self) -> Result { // Ignore all keys with incorrect key IDs. // If a malformed key is used, there will be a warning let keys_with_correct_key_id = self .keys .into_iter() .filter(|(key_id, pkey)| { if key_id == pkey.key_id() { true } else { warn!("Malformed key of ID {:?}", key_id); false } }) .collect(); Ok(LayoutMetadata::new( parse_datetime(&self.expires)?, self.readme, keys_with_correct_key_id, self.steps, self.inspect, )) } } fn parse_datetime(ts: &str) -> Result> { let dt = DateTime::parse_from_rfc3339(ts).map_err(|e| { Error::Encoding(format!("Can't parse DateTime: {:?}", e)) })?; Ok(dt.with_timezone(&Utc)) } fn format_datetime(ts: &DateTime) -> String { ts.to_rfc3339_opts(SecondsFormat::Secs, true) } #[cfg(test)] mod test { use assert_json_diff::assert_json_eq; use chrono::DateTime; use serde_json::json; use crate::{crypto::PublicKey, models::layout::format_datetime}; use super::{ inspection::Inspection, parse_datetime, rule::{Artifact, ArtifactRule}, step::Step, Layout, LayoutMetadataBuilder, }; const ALICE_PUB_KEY: &'static [u8] = include_bytes!("../../../tests/ed25519/ed25519-1.pub"); const BOB_PUB_KEY: &'static [u8] = include_bytes!("../../../tests/rsa/rsa-4096.spki.der"); #[test] fn parse_datetime_test() { let time_str = "1970-01-01T00:00:00Z".to_string(); let parsed_dt = parse_datetime(&time_str[..]).unwrap(); let dt = DateTime::UNIX_EPOCH; assert_eq!(parsed_dt, dt); } #[test] fn format_datetime_test() { let dt = DateTime::UNIX_EPOCH; let generated_dt_str = format_datetime(&dt); let dt_str = "1970-01-01T00:00:00Z".to_string(); assert_eq!(dt_str, generated_dt_str); } fn get_example_layout_metadata() -> Layout { let alice_key = PublicKey::from_ed25519(ALICE_PUB_KEY).unwrap(); let bob_key = PublicKey::from_spki( BOB_PUB_KEY, crate::crypto::SignatureScheme::RsaSsaPssSha256, ) .unwrap(); let metadata = LayoutMetadataBuilder::new() .expires(DateTime::UNIX_EPOCH) .add_key(alice_key.clone()) .add_key(bob_key.clone()) .add_step( Step::new("write-code") .threshold(1) .add_expected_product(ArtifactRule::Create("foo.py".into())) .add_key(alice_key.key_id().to_owned()) .expected_command("vi".into()), ) .add_step( Step::new("package") .threshold(1) .add_expected_material(ArtifactRule::Match { pattern: "foo.py".into(), in_src: None, with: Artifact::Products, in_dst: None, from: "write-code".into(), }) .add_expected_product(ArtifactRule::Create( "foo.tar.gz".into(), )) .add_key(bob_key.key_id().to_owned()) .expected_command("tar zcvf foo.tar.gz foo.py".into()), ) .add_inspect( Inspection::new("inspect_tarball") .add_expected_material(ArtifactRule::Match { pattern: "foo.tar.gz".into(), in_src: None, with: Artifact::Products, in_dst: None, from: "package".into(), }) .add_expected_product(ArtifactRule::Match { pattern: "foo.py".into(), in_src: None, with: Artifact::Products, in_dst: None, from: "write-code".into(), }) .run("inspect_tarball.sh foo.tar.gz".into()), ) .readme("".into()) .build() .unwrap(); Layout::from(&metadata).unwrap() } #[test] fn serialize_layout() { let layout = get_example_layout_metadata(); let json = json!({ "_type": "layout", "expires": "1970-01-01T00:00:00Z", "readme": "", "keys": { "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": { "keyid": "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf", "keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyid_hash_algorithms": [ "sha256", "sha512" ], "keyval": { "private": "", "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-----" } }, "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": { "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554", "keytype": "ed25519", "scheme": "ed25519", "keyval": { "private": "", "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6" } } }, "steps": [ { "_type": "step", "name": "write-code", "threshold": 1, "expected_materials": [ ], "expected_products": [ ["CREATE", "foo.py"] ], "pubkeys": [ "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554" ], "expected_command": ["vi"] }, { "_type": "step", "name": "package", "threshold": 1, "expected_materials": [ ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] ], "expected_products": [ ["CREATE", "foo.tar.gz"] ], "pubkeys": [ "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf" ], "expected_command": ["tar", "zcvf", "foo.tar.gz", "foo.py"] }], "inspect": [ { "_type": "inspection", "name": "inspect_tarball", "expected_materials": [ ["MATCH", "foo.tar.gz", "WITH", "PRODUCTS", "FROM", "package"] ], "expected_products": [ ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] ], "run": ["inspect_tarball.sh", "foo.tar.gz"] } ] }); let json_serialize = serde_json::to_value(&layout).unwrap(); assert_json_eq!(json, json_serialize); } #[test] fn deserialize_layout() { let json = r#"{ "_type": "layout", "expires": "1970-01-01T00:00:00Z", "readme": "", "keys": { "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": { "keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyid_hash_algorithms": [ "sha256", "sha512" ], "keyval": { "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-----" } }, "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": { "keytype": "ed25519", "scheme": "ed25519", "keyval": { "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6" } } }, "steps": [ { "_type": "step", "name": "write-code", "threshold": 1, "expected_materials": [], "expected_products": [ [ "CREATE", "foo.py" ] ], "pubkeys": [ "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554" ], "expected_command": [ "vi" ] }, { "_type": "step", "name": "package", "threshold": 1, "expected_materials": [ [ "MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code" ] ], "expected_products": [ [ "CREATE", "foo.tar.gz" ] ], "pubkeys": [ "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf" ], "expected_command": [ "tar", "zcvf", "foo.tar.gz", "foo.py" ] } ], "inspect": [ { "_type": "inspection", "name": "inspect_tarball", "expected_materials": [ [ "MATCH", "foo.tar.gz", "WITH", "PRODUCTS", "FROM", "package" ] ], "expected_products": [ [ "MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code" ] ], "run": [ "inspect_tarball.sh", "foo.tar.gz" ] } ] }"#; let layout = get_example_layout_metadata(); let layout_parse: Layout = serde_json::from_str(json).unwrap(); assert_eq!(layout, layout_parse); } } in-toto-0.4.0/src/models/layout/rule.rs000064400000000000000000000477541046102023000161430ustar 00000000000000//! # Artifact Rules //! //! Artifact rules are used to connect steps together through their //! materials or products. When connecting steps together, in-toto //! allows the project owner to enforce the existence of certain //! artifacts within a step (e.g., the README can only be created //! in the create-documentation step) and authorize operations on //! artifacts (e.g., the compile step can use the materials from //! the checkout-vcs). //! //! # Format of Artifact Rule //! //! Format of an Artifact Rule is the following: //! ```plaintext //! {MATCH [IN ] WITH (MATERIALS|PRODUCTS) [IN ] FROM || //! CREATE || //! DELETE || //! MODIFY || //! ALLOW || //! REQUIRE || //! DISALLOW } //! ``` //! //! Please refer to [`in-toto v0.9 spec`] for concrete functions //! for different rules (e.g., `MATCH`, `CREATE`, etc.) //! //! ## Instantialize //! //! As the format given in [`in-toto v0.9 spec`], rule types include //! `MATCH`, `CREATE`, `DELETE`, `MODIFY`, `ALLOW`, `REQUIRE` and //! `DISALLOW`. //! //! ### MATCH Rule //! //! A `MATCH` rule consists of a ``, //! an `IN ` (optional), a `WITH (MATERIALS|PRODUCTS)` //! (must), an `IN ` (optional) and a `FROM ` //! (must). //! //! We can build a MATCH rule like this //! ``` //! # use in_toto::{models::rule::{ArtifactRule, Artifact}, Result}; //! //! # fn main() -> Result<()> { //! let _match_rule = ArtifactRule::Match { //! pattern: "pattern/".into(), //! in_src: Some("src".into()), //! with: Artifact::Materials, //! in_dst: Some("dst".into()), //! from: "test_step".into(), //! }; //! //! Ok(()) //! # } //! //! ``` //! //! This rule equals to //! ```plaintext //! ["MATCH", "pattern/", "IN", "src", "WITH", "MATERIALS", "IN", "dst", "FROM", "test_step"] //! ``` //! //! ### Other Rules //! //! The other rules (s.t. `CREATE`, `DELETE`, `MODIFY`, `ALLOW`, //! `REQUIRE` and `DISALLOW`) only need one parameter ``. //! //! For example, we can build a CREATE rule like this //! //! ``` //! # use in_toto::{models::rule::ArtifactRule, Result}; //! //! # fn main() -> Result<()> { //! let _create_rule = ArtifactRule::Create("./artifact".into()); //! //! Ok(()) //! # } //! //! ``` //! //! This rule equals to //! ```plaintext //! ["CREATE", "./artifact"] //! ``` //! //! ## Deserialize and Serialize //! //! To make it easy to parse `.layout` files, format in [`in-toto v0.9 spec`] //! is supported when a `.layout` file is being deserialized. //! //! For example //! //! ``` //! # use serde_json::Error; //! # use in_toto::models::rule::ArtifactRule; //! //! # fn main() { //! let rule = ArtifactRule::Create("foo.py".into()); //! //! let rule_raw = r#"["CREATE", "foo.py"]"#; //! let rule_parsed: ArtifactRule = serde_json::from_str(rule_raw).unwrap(); //! assert_eq!(rule, rule_parsed); //! # } //! ``` //! //! Also, when being serialized, an Artifact Rule will be converted //! to the format as [`in-toto v0.9 spec`] gives //! //! ``` //! # use serde_json::{Error, json}; //! # use in_toto::models::rule::ArtifactRule; //! //! # fn main() { //! let rule = ArtifactRule::Create("foo.py".into()); //! //! let rule_value = json!(["CREATE", "foo.py"]); //! let rule_serialized = serde_json::to_value(&rule).unwrap(); //! assert_eq!(rule_serialized, rule_value); //! # } //! ``` //! //! [`in-toto v0.9 spec`]: https://github.com/in-toto/docs/blob/v0.9/in-toto-spec.md#433-artifact-rules use std::result::Result as StdResult; use serde::{ de::{self, SeqAccess, Unexpected, Visitor}, ser::{Serialize, SerializeSeq}, Deserialize, }; use crate::models::VirtualTargetPath; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Artifact { Materials, Products, } impl AsRef for Artifact { fn as_ref(&self) -> &str { match self { Artifact::Materials => "MATERIALS", Artifact::Products => "PRODUCTS", } } } /// Artifact rule enum #[derive(Clone, Debug, PartialEq, Eq)] pub enum ArtifactRule { /// indicates that products matched by the pattern must not appear /// as materials of this step. Create(VirtualTargetPath), /// indicates that materials matched by the pattern must not appear /// as products of this step. Delete(VirtualTargetPath), /// indicates that products matched by this pattern must appear as /// materials of this step, and their hashes must not be the same. Modify(VirtualTargetPath), /// indicates that artifacts matched by the pattern are allowed as /// materials or products of this step. Allow(VirtualTargetPath), /// indicates that a pattern must appear as a material or product /// of this step. Require(VirtualTargetPath), /// indicates that artifacts matched by the pattern are not allowed /// as materials or products of this step. Disallow(VirtualTargetPath), /// indicates that the artifacts filtered in using `"in_src/pattern"` /// must be matched to a `"MATERIAL"` or `"PRODUCT"` from a destination /// step with the "in_dst/pattern" filter. Match { pattern: VirtualTargetPath, in_src: Option, with: Artifact, in_dst: Option, from: String, }, } impl ArtifactRule { pub fn pattern(&self) -> &VirtualTargetPath { match self { ArtifactRule::Create(pattern) => pattern, ArtifactRule::Delete(pattern) => pattern, ArtifactRule::Modify(pattern) => pattern, ArtifactRule::Allow(pattern) => pattern, ArtifactRule::Require(pattern) => pattern, ArtifactRule::Disallow(pattern) => pattern, ArtifactRule::Match { pattern, .. } => pattern, } } } impl Serialize for ArtifactRule { fn serialize(&self, serializer: S) -> StdResult where S: serde::Serializer, { match self { ArtifactRule::Create(pattern) => { let mut seq = serializer.serialize_seq(Some(2))?; seq.serialize_element("CREATE")?; seq.serialize_element(pattern)?; seq.end() } ArtifactRule::Delete(pattern) => { let mut seq = serializer.serialize_seq(Some(2))?; seq.serialize_element("DELETE")?; seq.serialize_element(pattern)?; seq.end() } ArtifactRule::Modify(pattern) => { let mut seq = serializer.serialize_seq(Some(2))?; seq.serialize_element("MODIFY")?; seq.serialize_element(pattern)?; seq.end() } ArtifactRule::Allow(pattern) => { let mut seq = serializer.serialize_seq(Some(2))?; seq.serialize_element("ALLOW")?; seq.serialize_element(pattern)?; seq.end() } ArtifactRule::Require(pattern) => { let mut seq = serializer.serialize_seq(Some(2))?; seq.serialize_element("REQUIRE")?; seq.serialize_element(pattern)?; seq.end() } ArtifactRule::Disallow(pattern) => { let mut seq = serializer.serialize_seq(Some(2))?; seq.serialize_element("DISALLOW")?; seq.serialize_element(pattern)?; seq.end() } ArtifactRule::Match { pattern, in_src, with, in_dst, from, } => { let mut to_be_serialized = vec!["MATCH", pattern.as_ref()]; if let Some(src) = in_src { to_be_serialized.append(&mut vec!["IN", src]); } to_be_serialized.append(&mut vec!["WITH", with.as_ref()]); if let Some(dst) = in_dst { to_be_serialized.append(&mut vec!["IN", dst]); } to_be_serialized.append(&mut vec!["FROM", from]); let mut seq = serializer.serialize_seq(Some(to_be_serialized.len()))?; for e in to_be_serialized { seq.serialize_element(e)?; } seq.end() } } } } /// Visitor helps to deserialize `ArtifactRule` struct ArtifactRuleVisitor {} impl ArtifactRuleVisitor { pub fn new() -> Self { ArtifactRuleVisitor {} } } impl<'de> Visitor<'de> for ArtifactRuleVisitor { type Value = ArtifactRule; fn expecting( &self, formatter: &mut std::fmt::Formatter, ) -> std::fmt::Result { formatter.write_str("An Artifact Rule for in-toto") } /// Deserialize a sequence to an `ArtifactRule`. fn visit_seq(self, mut seq: V) -> StdResult where V: SeqAccess<'de>, { let mut len = 0; let typ: &str = seq .next_element()? .ok_or_else(|| de::Error::invalid_length(len, &self))?; len += 1; let pattern: VirtualTargetPath = seq .next_element()? .ok_or_else(|| de::Error::invalid_length(len, &self))?; len += 1; match typ { "CREATE" => Ok(ArtifactRule::Create(pattern)), "DELETE" => Ok(ArtifactRule::Delete(pattern)), "MODIFY" => Ok(ArtifactRule::Modify(pattern)), "ALLOW" => Ok(ArtifactRule::Allow(pattern)), "REQUIRE" => Ok(ArtifactRule::Require(pattern)), "DISALLOW" => Ok(ArtifactRule::Disallow(pattern)), "MATCH" => { let in_or_with: String = seq .next_element()? .ok_or_else(|| de::Error::invalid_length(len, &self))?; len += 1; let mut in_src = None; let mut with = Artifact::Materials; let mut in_dst = None; match &in_or_with[..] { "IN" => { let source_path_prefix: String = seq.next_element()?.ok_or_else(|| { de::Error::invalid_length(len, &self) })?; len += 1; in_src = Some(source_path_prefix); let in_: String = seq.next_element()?.ok_or_else(|| { de::Error::invalid_length(len, &self) })?; len += 1; if in_ != "WITH" { Err(de::Error::invalid_value( Unexpected::Str(&in_), &"IN", ))? } } "WITH" => {} _ => { return Err(de::Error::invalid_value( Unexpected::Str(&in_or_with), &"WITH or IN", )) } } let target: String = seq .next_element()? .ok_or_else(|| de::Error::invalid_length(len, &self))?; len += 1; match &target[..] { "MATERIALS" => {} "PRODUCTS" => with = Artifact::Products, _ => Err(de::Error::invalid_value( Unexpected::Str(&target), &"MATERIALS or PRODUCTS", ))?, }; let in_or_from: String = seq .next_element()? .ok_or_else(|| de::Error::invalid_length(len, &self))?; len += 1; match &in_or_from[..] { "IN" => { let destination_path_prefix: String = seq.next_element()?.ok_or_else(|| { de::Error::invalid_length(len, &self) })?; len += 1; in_dst = Some(destination_path_prefix); let from_: String = seq.next_element()?.ok_or_else(|| { de::Error::invalid_length(len, &self) })?; len += 1; if from_ != "FROM" { return Err(de::Error::invalid_value( Unexpected::Str(&from_), &"FROM", )); } } "FROM" => {} _ => { return Err(de::Error::invalid_value( Unexpected::Str(&in_or_from), &"IN or FROM", )); } }; let from: String = seq .next_element()? .ok_or_else(|| de::Error::invalid_length(len, &self))?; Ok(ArtifactRule::Match { pattern, in_src, with, in_dst, from, }) } others => { Err(de::Error::custom(format!("Unexpected token {}", others))) } } } } impl<'de> Deserialize<'de> for ArtifactRule { fn deserialize(deserializer: D) -> StdResult where D: serde::Deserializer<'de>, { deserializer.deserialize_struct( "ArtifactRule", &["inner"], ArtifactRuleVisitor::new(), ) } } #[cfg(test)] pub mod test { use rstest::rstest; use serde_json::json; use super::{Artifact, ArtifactRule}; /// generate a ARTIFACT_RULE as json: /// `[ /// "MATCH", /// "pattern/", /// "IN", /// "src", /// "WITH", /// "MATERIALS", /// "IN", /// "dst", /// "FROM", /// "test_step" /// ]` pub fn generate_materials_rule() -> ArtifactRule { ArtifactRule::Match { pattern: "pattern/".into(), in_src: Some("src".into()), with: Artifact::Materials, in_dst: Some("dst".into()), from: "test_step".into(), } } /// generate a ARTIFACT_RULE as json: /// `[ /// "MATCH", /// "pattern/", /// "IN", /// "src", /// "WITH", /// "PRODUCTS", /// "IN", /// "dst", /// "FROM", /// "test_step" /// ]` pub fn generate_products_rule() -> ArtifactRule { ArtifactRule::Match { pattern: "pattern/".into(), in_src: Some("src".into()), with: Artifact::Products, in_dst: Some("dst".into()), from: "test_step".into(), } } #[test] fn serialize_match_full() { let rule = generate_materials_rule(); let json = json!([ "MATCH", "pattern/", "IN", "src", "WITH", "MATERIALS", "IN", "dst", "FROM", "test_step" ]); let json_serialize = serde_json::to_value(&rule).unwrap(); assert_eq!( json, json_serialize, "{:#?} != {:#?}", json, json_serialize ); } #[test] fn serialize_match_without_source() { let rule = ArtifactRule::Match { pattern: "./".into(), in_src: None, with: Artifact::Materials, in_dst: Some("dst".into()), from: "build".into(), }; let json = json!([ "MATCH", "./", "WITH", "MATERIALS", "IN", "dst", "FROM", "build" ]); let json_serialize = serde_json::to_value(&rule).unwrap(); assert_eq!(json, json_serialize); } #[test] fn serialize_match_without_dest() { let rule = ArtifactRule::Match { pattern: "./".into(), in_src: Some("pre".into()), with: Artifact::Materials, in_dst: None, from: "build".into(), }; let json = json!([ "MATCH", "./", "IN", "pre", "WITH", "MATERIALS", "FROM", "build" ]); let json_serialize = serde_json::to_value(&rule).unwrap(); assert_eq!( json, json_serialize, "{:#?} != {:#?}", json, json_serialize ); } #[rstest] #[case(ArtifactRule::Create("./artifact".into()), json!(["CREATE", "./artifact"]))] #[case(ArtifactRule::Delete("./artifact".into()), json!(["DELETE", "./artifact"]))] #[case(ArtifactRule::Modify("./artifact".into()), json!(["MODIFY", "./artifact"]))] #[case(ArtifactRule::Allow("./artifact".into()), json!(["ALLOW", "./artifact"]))] #[case(ArtifactRule::Require("./artifact".into()), json!(["REQUIRE", "./artifact"]))] #[case(ArtifactRule::Disallow("./artifact".into()), json!(["DISALLOW", "./artifact"]))] fn serialize_tests( #[case] rule: ArtifactRule, #[case] json: serde_json::Value, ) { let json_serialize = serde_json::to_value(&rule).unwrap(); assert_eq!( json, json_serialize, "{:#?} != {:#?}", json, json_serialize ); } #[test] fn deserialize_full() { let json = r#"[ "MATCH", "pattern/", "IN", "src", "WITH", "MATERIALS", "IN", "dst", "FROM", "test_step" ]"#; let rule = generate_materials_rule(); let rule_parsed: ArtifactRule = serde_json::from_str(json).unwrap(); assert_eq!(rule, rule_parsed); } #[test] fn deserialize_match_without_source() { let json = r#"[ "MATCH", "foo.tar.gz", "WITH", "PRODUCTS", "IN", "./dst", "FROM", "package" ]"#; let rule = ArtifactRule::Match { pattern: "foo.tar.gz".into(), in_src: None, with: Artifact::Products, in_dst: Some("./dst".into()), from: "package".into(), }; let rule_parsed: ArtifactRule = serde_json::from_str(json).unwrap(); assert_eq!(rule, rule_parsed); } #[test] fn deserialize_without_dst() { let json = r#"[ "MATCH", "foo.tar.gz", "IN", "./src", "WITH", "PRODUCTS", "FROM", "package" ]"#; let rule = ArtifactRule::Match { pattern: "foo.tar.gz".into(), in_src: Some("./src".into()), with: Artifact::Products, in_dst: None, from: "package".into(), }; let rule_parsed: ArtifactRule = serde_json::from_str(json).unwrap(); assert_eq!(rule, rule_parsed); } #[rstest] #[case(ArtifactRule::Create("./artifact".into()), r#"["CREATE", "./artifact"]"#)] #[case(ArtifactRule::Delete("./artifact".into()), r#"["DELETE", "./artifact"]"#)] #[case(ArtifactRule::Modify("./artifact".into()), r#"["MODIFY", "./artifact"]"#)] #[case(ArtifactRule::Allow("./artifact".into()), r#"["ALLOW", "./artifact"]"#)] #[case(ArtifactRule::Require("./artifact".into()), r#"["REQUIRE", "./artifact"]"#)] #[case(ArtifactRule::Disallow("./artifact".into()), r#"["DISALLOW", "./artifact"]"#)] fn deserialize_tests(#[case] rule: ArtifactRule, #[case] json: &str) { let rule_parsed: ArtifactRule = serde_json::from_str(json).unwrap(); assert_eq!(rule, rule_parsed); } } in-toto-0.4.0/src/models/layout/step.rs000064400000000000000000000154551046102023000161400ustar 00000000000000//! in-toto layout's Step use std::str::FromStr; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use crate::crypto::KeyId; use crate::{supply_chain_item_derive, Error, Result}; use super::rule::ArtifactRule; use super::supply_chain_item::SupplyChainItem; /// Wrapper type for a command in step. #[derive( Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Deserialize, )] pub struct Command(Vec); impl Command { pub fn is_empty(&self) -> bool { self.0.is_empty() } } impl AsRef<[String]> for Command { fn as_ref(&self) -> &[String] { &self.0 } } impl From for Command { fn from(str: String) -> Self { let paras: Vec = str .split_whitespace() .collect::>() .iter() .map(|s| s.to_string()) .collect(); Command(paras) } } impl From> for Command { fn from(strs: Vec) -> Self { Command::from(&strs[..]) } } impl From<&[String]> for Command { fn from(strs: &[String]) -> Self { Command(strs.to_vec()) } } impl From<&str> for Command { fn from(str: &str) -> Self { let paras: Vec = str .split_whitespace() .collect::>() .iter() .map(|s| s.to_string()) .collect(); Command(paras) } } impl FromStr for Command { type Err = Error; /// Parse a Command from a string. fn from_str(string: &str) -> Result { let paras: Vec = string .split_whitespace() .collect::>() .iter() .map(|s| s.to_string()) .collect(); Ok(Command(paras)) } } impl Serialize for Command { fn serialize(&self, ser: S) -> ::std::result::Result where S: Serializer, { self.0.serialize(ser) } } /// Step represents an in-toto step of the supply chain performed by a functionary. /// During final product verification in-toto looks for corresponding Link /// metadata, which is used as signed evidence that the step was performed /// according to the supply chain definition. /// Materials and products used/produced by the step are constrained by the /// artifact rules in the step's supply_chain_item's expected_materials and /// expected_products fields. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Step { #[serde(rename = "_type")] pub typ: String, pub threshold: u32, pub name: String, pub expected_materials: Vec, pub expected_products: Vec, #[serde(rename = "pubkeys")] pub pub_keys: Vec, pub expected_command: Command, } impl Step { pub fn new(name: &str) -> Self { Step { pub_keys: Vec::new(), expected_command: Command::default(), threshold: 0, name: name.into(), expected_materials: Vec::new(), expected_products: Vec::new(), typ: "step".into(), } } /// Add a pub key for this Step pub fn add_key(mut self, key: KeyId) -> Self { self.pub_keys.push(key); self } /// Set expected command for this Step pub fn expected_command(mut self, command: Command) -> Self { self.expected_command = command; self } /// Set threshold for this Step pub fn threshold(mut self, threshold: u32) -> Self { self.threshold = threshold; self } // Derive operations on `materials`/`products` and `name` supply_chain_item_derive!(); } impl SupplyChainItem for Step { fn name(&self) -> &str { &self.name } fn expected_materials(&self) -> &Vec { &self.expected_materials } fn expected_products(&self) -> &Vec { &self.expected_products } } #[cfg(test)] mod test { use std::str::FromStr; use serde_json::json; use crate::{ crypto::KeyId, models::rule::{Artifact, ArtifactRule}, Result, }; use super::Step; #[test] fn serialize_step() -> Result<()> { let step = Step::new("package") .add_expected_material(ArtifactRule::Match { pattern: "foo.py".into(), in_src: None, with: Artifact::Products, in_dst: None, from: "write-code".into(), }) .add_expected_product(ArtifactRule::Create("foo.tar.gz".into())) .expected_command("tar zcvf foo.tar.gz foo.py".into()) .add_key(KeyId::from_str( "70ca5750c2eda80b18f41f4ec5f92146789b5d68dd09577be422a0159bd13680", )?) .threshold(1); let json_serialize = serde_json::to_value(&step)?; let json = json!( { "_type": "step", "name": "package", "expected_materials": [ ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] ], "expected_products": [ ["CREATE", "foo.tar.gz"] ], "expected_command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], "pubkeys": [ "70ca5750c2eda80b18f41f4ec5f92146789b5d68dd09577be422a0159bd13680" ], "threshold": 1 } ); assert_eq!( json, json_serialize, "{:#?} != {:#?}", json, json_serialize ); Ok(()) } #[test] fn deserialize_step() -> Result<()> { let json = r#" { "_type": "step", "name": "package", "expected_materials": [ ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] ], "expected_products": [ ["CREATE", "foo.tar.gz"] ], "expected_command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], "pubkeys": [ "70ca5750c2eda80b18f41f4ec5f92146789b5d68dd09577be422a0159bd13680" ], "threshold": 1 }"#; let step_parsed: Step = serde_json::from_str(json)?; let step = Step::new("package") .add_expected_material(ArtifactRule::Match { pattern: "foo.py".into(), in_src: None, with: Artifact::Products, in_dst: None, from: "write-code".into(), }) .add_expected_product(ArtifactRule::Create("foo.tar.gz".into())) .expected_command("tar zcvf foo.tar.gz foo.py".into()) .add_key(KeyId::from_str( "70ca5750c2eda80b18f41f4ec5f92146789b5d68dd09577be422a0159bd13680", )?) .threshold(1); assert_eq!(step_parsed, step); Ok(()) } } in-toto-0.4.0/src/models/layout/supply_chain_item.rs000064400000000000000000000030511046102023000206660ustar 00000000000000//! SupplyChainItem summarizes common fields of the two available supply chain //! item types in Inspection and Step. use super::rule::ArtifactRule; pub trait SupplyChainItem { /// Get the name of this item fn name(&self) -> &str; /// Get the expected material fn expected_materials(&self) -> &Vec; /// Get the expected products fn expected_products(&self) -> &Vec; } #[macro_export] macro_rules! supply_chain_item_derive { () => { /// Add an expected material artifact rule to this Step pub fn add_expected_material( mut self, expected_material: ArtifactRule, ) -> Self { self.expected_materials.push(expected_material); self } /// Set expected materials for this Step pub fn expected_materials( mut self, expected_materials: Vec, ) -> Self { self.expected_materials = expected_materials; self } /// Add an expected product artifact rule to this Step pub fn add_expected_product( mut self, expected_product: ArtifactRule, ) -> Self { self.expected_products.push(expected_product); self } /// Set expected products for this Step pub fn expected_products( mut self, expected_products: Vec, ) -> Self { self.expected_products = expected_products; self } }; } in-toto-0.4.0/src/models/link/byproducts.rs000064400000000000000000000136211046102023000167740ustar 00000000000000//! in-toto link's byproducts //! use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// byproducts of a link file /// # Example /// ``` /// use in_toto::models::byproducts::ByProducts; /// // let other_byproducts: BTreeMap = BTreeMap::new(); /// // ... /// // insert some other byproducts to other_byproducts /// let byproducts = ByProducts::new() /// .set_return_value(0) /// .set_stderr("".into()) /// .set_stdout("".into()); /// // .set_other_fields(other_byproducts); /// ``` /// /// Also, can directly set a whole BTree as other_fields /// /// ``` /// use std::collections::BTreeMap; /// use in_toto::models::byproducts::ByProducts; /// let mut other_byproducts: BTreeMap = BTreeMap::new(); /// other_byproducts.insert("key".into(), "value".into()); /// /// let byproducts = ByProducts::new() /// .set_return_value(0) /// .set_stderr("".into()) /// .set_stdout("".into()) /// .set_other_fields(other_byproducts); /// ``` #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct ByProducts { #[serde(rename = "return-value", skip_serializing_if = "Option::is_none")] return_value: Option, #[serde(skip_serializing_if = "Option::is_none")] stderr: Option, #[serde(skip_serializing_if = "Option::is_none")] stdout: Option, #[serde(flatten)] other_fields: BTreeMap, } impl ByProducts { pub fn new() -> Self { ByProducts { return_value: None, stderr: None, stdout: None, other_fields: BTreeMap::new(), } } /// Set return-value pub fn set_return_value(mut self, return_value: i32) -> Self { self.return_value = Some(return_value); self } /// Set stderr pub fn set_stderr(mut self, stderr: String) -> Self { self.stderr = Some(stderr); self } /// Set stdout pub fn set_stdout(mut self, stdout: String) -> Self { self.stdout = Some(stdout); self } /// Set other fields. /// Warning: This operation will overwrite all the present other-field /// set by `set_other_field` or `set_other_fields` before. pub fn set_other_fields( mut self, other_fields: BTreeMap, ) -> Self { self.other_fields = other_fields; self } /// Insert another field pub fn set_other_field(mut self, key: String, value: String) -> Self { self.other_fields.insert(key, value); self } /// Get return-value pub fn return_value(&self) -> Option { self.return_value } /// Get stderr pub fn stderr(&self) -> &Option { &self.stderr } /// Get stdout pub fn stdout(&self) -> &Option { &self.stdout } /// Get other fields pub fn other_fields(&self) -> &BTreeMap { &self.other_fields } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use serde_json::json; use super::ByProducts; #[test] fn serialize_byproducts_other_field() { let byproducts = ByProducts::new() .set_return_value(0) .set_stderr("a foo.py\n".into()) .set_stdout("".into()) .set_other_field("key1".into(), "value1".into()) .set_other_field("key2".into(), "value2".into()); let serialized_byproducts = serde_json::to_value(byproducts).unwrap(); let json = json!({ "return-value": 0, "stderr": "a foo.py\n", "stdout": "", "key1": "value1", "key2": "value2" }); assert_eq!(json, serialized_byproducts); } #[test] fn serialize_byproducts_other_fields() { let mut other_fields = BTreeMap::new(); other_fields.insert("key1".into(), "value1".into()); other_fields.insert("key2".into(), "value2".into()); let byproducts = ByProducts::new() .set_return_value(0) .set_stderr("a foo.py\n".into()) .set_stdout("".into()) .set_other_fields(other_fields); let serialized_byproducts = serde_json::to_value(byproducts).unwrap(); let json = json!({ "return-value": 0, "stderr": "a foo.py\n", "stdout": "", "key1": "value1", "key2": "value2" }); assert_eq!(json, serialized_byproducts); } #[test] fn deserialize_byproducts_other_field() { let json = r#"{ "return-value": 0, "stderr": "a foo.py\n", "stdout": "", "key1": "value1", "key2": "value2" }"#; let byproducts = ByProducts::new() .set_return_value(0) .set_stderr("a foo.py\n".into()) .set_stdout("".into()) .set_other_field("key1".into(), "value1".into()) .set_other_field("key2".into(), "value2".into()); let deserialized_byproducts: ByProducts = serde_json::from_str(json).unwrap(); assert_eq!(byproducts, deserialized_byproducts); } #[test] fn deserialize_byproducts_other_fields() { let json = r#"{ "return-value": 0, "stderr": "a foo.py\n", "stdout": "", "key1": "value1", "key2": "value2" }"#; let mut other_fields = BTreeMap::new(); other_fields.insert("key1".into(), "value1".into()); other_fields.insert("key2".into(), "value2".into()); let byproducts = ByProducts::new() .set_return_value(0) .set_stderr("a foo.py\n".into()) .set_stdout("".into()) .set_other_fields(other_fields); let deserialized_byproducts: ByProducts = serde_json::from_str(json).unwrap(); assert_eq!(byproducts, deserialized_byproducts); } } in-toto-0.4.0/src/models/link/metadata.rs000064400000000000000000000206271046102023000163620ustar 00000000000000//! in-toto link metadata. use serde::de::{Deserialize, Deserializer, Error as DeserializeError}; use serde::ser::{Error as SerializeError, Serialize, Serializer}; use std::collections::BTreeMap; use std::fmt::Debug; use std::fs::File; use std::io::BufReader; use crate::crypto::{self, PrivateKey}; use crate::interchange::{DataInterchange, Json}; use crate::Result; use crate::models::step::Command; use crate::models::{ Link, Metablock, Metadata, MetadataType, MetadataWrapper, TargetDescription, VirtualTargetPath, }; use super::byproducts::ByProducts; /// Helper to construct `LinkMetadata`. pub struct LinkMetadataBuilder { name: String, materials: BTreeMap, products: BTreeMap, env: Option>, byproducts: ByProducts, command: Command, } impl Default for LinkMetadataBuilder { fn default() -> Self { LinkMetadataBuilder::new() } } impl LinkMetadataBuilder { pub fn new() -> Self { LinkMetadataBuilder { name: String::new(), materials: BTreeMap::new(), products: BTreeMap::new(), env: None, byproducts: ByProducts::new(), command: Command::default(), } } /// Set the name number for this link pub fn name(mut self, name: String) -> Self { self.name = name; self } /// Set the materials for this metadata pub fn materials( mut self, materials: BTreeMap, ) -> Self { self.materials = materials; self } /// Set the products for this metadata pub fn products( mut self, products: BTreeMap, ) -> Self { self.products = products; self } pub fn add_material(mut self, material_path: VirtualTargetPath) -> Self { let file = File::open(material_path.to_string()).unwrap(); let mut reader = BufReader::new(file); let (_length, hashes) = crypto::calculate_hashes( &mut reader, &[crypto::HashAlgorithm::Sha256], ) .unwrap(); self.materials.insert(material_path, hashes); self } pub fn add_product(mut self, material_path: VirtualTargetPath) -> Self { let file = File::open(material_path.to_string()).unwrap(); let mut reader = BufReader::new(file); let (_length, hashes) = crypto::calculate_hashes( &mut reader, &[crypto::HashAlgorithm::Sha256], ) .unwrap(); self.products.insert(material_path, hashes); self } /// Set the products for this metadata pub fn env(mut self, env: Option>) -> Self { self.env = env; self } /// Set the products for this metadata pub fn byproducts(mut self, byproducts: ByProducts) -> Self { self.byproducts = byproducts; self } /// Set the command for this metadata pub fn command(mut self, command: Command) -> Self { self.command = command; self } pub fn build(self) -> Result { LinkMetadata::new( self.name, self.materials, self.products, self.env, self.byproducts, self.command, ) } /// Construct a new `Metablock`. pub fn signed(self, private_key: &PrivateKey) -> Result where D: DataInterchange, { Metablock::new(Box::new(self.build()?).into_enum(), &[private_key]) } /// Construct a new `Metablock`. pub fn unsigned(self) -> Result where D: DataInterchange, { Metablock::new(Box::new(self.build()?).into_enum(), &[]) } } /// link metadata #[derive(Debug, Clone, PartialEq, Eq)] pub struct LinkMetadata { pub name: String, pub materials: BTreeMap, pub products: BTreeMap, pub env: Option>, pub byproducts: ByProducts, pub command: Command, } impl LinkMetadata { /// Create new `LinkMetadata`. pub fn new( name: String, materials: BTreeMap, products: BTreeMap, env: Option>, byproducts: ByProducts, command: Command, ) -> Result { Ok(LinkMetadata { name, materials, products, env, byproducts, command, }) } } impl Metadata for LinkMetadata { fn typ(&self) -> MetadataType { MetadataType::Link } fn into_enum(self: Box) -> MetadataWrapper { MetadataWrapper::Link(*self) } fn to_bytes(&self) -> Result> { Json::canonicalize(&Json::serialize(self)?) } } impl Serialize for LinkMetadata { fn serialize(&self, ser: S) -> ::std::result::Result where S: Serializer, { Link::from(self) .map_err(|e| SerializeError::custom(format!("{:?}", e)))? .serialize(ser) } } impl<'de> Deserialize<'de> for LinkMetadata { fn deserialize>( de: D, ) -> ::std::result::Result { let intermediate: Link = Deserialize::deserialize(de)?; intermediate .try_into() .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } #[cfg(test)] mod test { use serde_json::json; use crate::models::{ byproducts::ByProducts, step::Command, LinkMetadata, LinkMetadataBuilder, VirtualTargetPath, }; #[test] fn serialize_linkmetadata() { let link_metadata = LinkMetadataBuilder::new() .name("".into()) .add_product( VirtualTargetPath::new("tests/test_link/foo.tar.gz".into()) .unwrap(), ) .byproducts( ByProducts::new() .set_return_value(0) .set_stderr("a foo.py\n".into()) .set_stdout("".into()), ) .command(Command::from("tar zcvf foo.tar.gz foo.py")) .build() .unwrap(); let serialized_linkmetadata = serde_json::to_value(link_metadata).unwrap(); let json = json!({ "_type": "link", "name": "", "materials": {}, "products": { "tests/test_link/foo.tar.gz": { "sha256": "52947cb78b91ad01fe81cd6aef42d1f6817e92b9e6936c1e5aabb7c98514f355" } }, "byproducts": { "return-value": 0, "stderr": "a foo.py\n", "stdout": "" }, "command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], "environment": null }); assert_eq!(json, serialized_linkmetadata); } #[test] fn deserialize_linkmetadata() { let json = r#"{ "_type": "link", "name": "", "materials": {}, "products": { "tests/test_link/foo.tar.gz": { "sha256": "52947cb78b91ad01fe81cd6aef42d1f6817e92b9e6936c1e5aabb7c98514f355" } }, "byproducts": { "return-value": 0, "stderr": "a foo.py\n", "stdout": "" }, "command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], "environment": null }"#; let link_metadata = LinkMetadataBuilder::new() .name("".into()) .add_product( VirtualTargetPath::new("tests/test_link/foo.tar.gz".into()) .unwrap(), ) .byproducts( ByProducts::new() .set_return_value(0) .set_stderr("a foo.py\n".into()) .set_stdout("".into()), ) .command(Command::from("tar zcvf foo.tar.gz foo.py")) .build() .unwrap(); let deserialized_link_metadata: LinkMetadata = serde_json::from_str(json).unwrap(); assert_eq!(link_metadata, deserialized_link_metadata); } } in-toto-0.4.0/src/models/link/mod.rs000064400000000000000000000026651046102023000153630ustar 00000000000000//! in-toto link use std::collections::BTreeMap; use std::fmt::Debug; use std::str; use crate::Result; use serde::{Deserialize, Serialize}; pub mod byproducts; mod metadata; pub use metadata::{LinkMetadata, LinkMetadataBuilder}; use crate::models::{TargetDescription, VirtualTargetPath}; use self::byproducts::ByProducts; use super::step::Command; // FIXME, we need to tag a spec //const SPEC_VERSION: &str = "0.9-dev"; #[derive(Debug, Serialize, Deserialize)] pub struct Link { #[serde(rename = "_type")] typ: String, name: String, materials: BTreeMap, products: BTreeMap, #[serde(rename = "environment")] env: Option>, byproducts: ByProducts, command: Command, } impl Link { pub fn from(meta: &LinkMetadata) -> Result { Ok(Link { typ: String::from("link"), name: meta.name.clone(), materials: meta.materials.clone(), products: meta.products.clone(), env: meta.env.clone(), byproducts: meta.byproducts.clone(), command: meta.command.clone(), }) } pub fn try_into(self) -> Result { LinkMetadata::new( self.name, self.materials, self.products, self.env, self.byproducts, self.command, ) } } in-toto-0.4.0/src/models/metadata.rs000064400000000000000000000530641046102023000154260ustar 00000000000000//! in-toto metadata. //! # Metadata & MetadataWrapper //! Metadata is the top level abstract for both layout metadata and link //! metadata. Metadata it is devided into two types //! //! * enum `MetadataWrapper` is used to do serialize, deserialize and //! other object unsafe operations. //! * trait `Metadata` is used to work for trait object. //! //! The reason please refer to issue //! //! # Metablock //! Metablock is the container for link metadata and layout metadata. //! Its serialized outcome can work as the content of a link file //! or a layout file. It provides `MetablockBuilder` for create //! an instance of Metablock, and methods to verify signatures, //! create signatures. use log::{debug, warn}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; use std::str; use strum::IntoEnumIterator; use strum_macros::EnumIter; use crate::crypto::{KeyId, PrivateKey, PublicKey, Signature}; use crate::error::Error; use crate::interchange::{DataInterchange, Json}; use crate::Result; use super::{LayoutMetadata, LinkMetadata}; pub const FILENAME_FORMAT: &str = "{step_name}.{keyid:.8}.link"; #[derive( Debug, Serialize, Deserialize, Hash, PartialEq, Eq, EnumIter, Clone, Copy, )] pub enum MetadataType { Layout, Link, } impl Display for MetadataType { fn fmt(&self, fmt: &mut Formatter) -> FmtResult { match self { MetadataType::Layout => fmt.write_str("layout")?, MetadataType::Link => fmt.write_str("link")?, } Ok(()) } } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(untagged)] pub enum MetadataWrapper { Layout(LayoutMetadata), Link(LinkMetadata), } impl MetadataWrapper { /// Convert from enum `MetadataWrapper` to trait `Metadata` pub fn into_trait(self) -> Box { match self { MetadataWrapper::Layout(layout_meta) => Box::new(layout_meta), MetadataWrapper::Link(link_meta) => Box::new(link_meta), } } /// Standard deserialize for MetadataWrapper by its metadata pub fn from_bytes( bytes: &[u8], metadata_type: MetadataType, ) -> Result { match metadata_type { MetadataType::Layout => serde_json::from_slice(bytes) .map(Self::Layout) .map_err(|e| e.into()), MetadataType::Link => serde_json::from_slice(bytes) .map(Self::Link) .map_err(|e| e.into()), } } /// Auto deserialize for MetadataWrapper by any possible metadata. pub fn try_from_bytes(bytes: &[u8]) -> Result { let mut metadata: Result = Err(Error::Programming("no available bytes parser".to_string())); for typ in MetadataType::iter() { metadata = MetadataWrapper::from_bytes(bytes, typ); if metadata.is_ok() { break; } } metadata } /// Standard serialize for MetadataWrapper by its metadata pub fn to_bytes(&self) -> Result> { Json::canonicalize(&Json::serialize(self)?) } } /// trait for Metadata pub trait Metadata { /// The version of Metadata fn typ(&self) -> MetadataType; /// Convert from trait `Metadata` to enum `MetadataWrapper` fn into_enum(self: Box) -> MetadataWrapper; /// Standard serialize for Metadata fn to_bytes(&self) -> Result>; } /// All signed files (link and layout files) have the format. /// * `signatures`: A pubkey => signature map. signatures are for the metadata. /// * `metadata`: dictionary. Also known as signed metadata. e.g., link /// or layout. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Metablock { pub signatures: Vec, #[serde(rename = "signed")] pub metadata: MetadataWrapper, } impl Metablock { /// Create a new Metablock, using data of metadata. And the signatures are /// generated by using private-keys to sign the metadata. pub fn new( metadata: MetadataWrapper, private_keys: &[&PrivateKey], ) -> Result { let raw = metadata.to_bytes()?; let metadata_string = String::from_utf8(raw) .map_err(|e| { Error::Encoding(format!( "Cannot convert metadata into a string: {}", e )) })? .replace("\\n", "\n"); // sign and collect signatures let mut signatures = Vec::new(); private_keys.iter().try_for_each(|key| -> Result<()> { let sig = key.sign(metadata_string.as_bytes())?; signatures.push(sig); Ok(()) })?; Ok(Self { signatures, metadata, }) } /// Verify this metadata. /// Each signature in the Metablock signed by an authorized key /// is a legal signature. Only legal the number signatures is /// not less than `threshold`, will return the wrapped Metadata. pub fn verify<'a, I>( &self, threshold: u32, authorized_keys: I, ) -> Result where I: IntoIterator, { if self.signatures.is_empty() { return Err(Error::VerificationFailure( "The metadata was not signed with any authorized keys.".into(), )); } if threshold < 1 { return Err(Error::VerificationFailure( "Threshold must be strictly greater than zero".into(), )); } let authorized_keys = authorized_keys .into_iter() .map(|k| (k.key_id(), k)) .collect::>(); let raw = self.metadata.to_bytes()?; let metadata = String::from_utf8(raw) .map_err(|e| { Error::Encoding(format!( "Cannot convert metadata into a string: {}", e )) })? .replace("\\n", "\n"); let mut signatures_needed = threshold; // Create a key_id->signature map to deduplicate the key_ids. let signatures = self .signatures .iter() .map(|sig| (sig.key_id(), sig)) .collect::>(); // check the signatures, if is signed by an authorized key, // signatures_needed - 1 for (key_id, sig) in signatures { match authorized_keys.get(key_id) { Some(pub_key) => match pub_key.verify(metadata.as_bytes(), sig) { Ok(()) => { debug!( "Good signature from key ID {:?}", pub_key.key_id() ); signatures_needed -= 1; } Err(e) => { warn!( "Bad signature from key ID {:?}: {:?}", pub_key.key_id(), e ); } }, None => { warn!( "Key ID {:?} was not found in the set of authorized keys.", sig.key_id() ); } } if signatures_needed == 0 { break; } } if signatures_needed > 0 { return Err(Error::VerificationFailure(format!( "Signature threshold not met: {}/{}", threshold - signatures_needed, threshold ))); } Ok(self.metadata.clone()) } } /// A helper to build Metablock pub struct MetablockBuilder { signatures: HashMap, metadata: MetadataWrapper, } impl MetablockBuilder { /// Create a new `MetablockBuilder` from a given `Metadata`. pub fn from_metadata(metadata: Box) -> Self { Self { signatures: HashMap::new(), metadata: metadata.into_enum(), } } /// Create a new `MetablockBuilder` from manually serialized metadata to be signed. /// Returns an error if `metadata` cannot be parsed into Metadata. pub fn from_raw_metadata(raw_metadata: &[u8]) -> Result { let metadata = MetadataWrapper::try_from_bytes(raw_metadata)?; Ok(Self { signatures: HashMap::new(), metadata, }) } /// Sign the metadata using the given `private_keys`, replacing any existing signatures with the /// same `KeyId`. pub fn sign(mut self, private_keys: &[&PrivateKey]) -> Result { let mut signatures = HashMap::new(); let raw = self.metadata.to_bytes()?; let metadata = String::from_utf8(raw) .map_err(|e| { Error::Encoding(format!( "Cannot convert metadata into a string: {}", e )) })? .replace("\\n", "\n"); private_keys.iter().try_for_each(|key| -> Result<()> { let sig = key.sign(metadata.as_bytes())?; signatures.insert(sig.key_id().clone(), sig); Ok(()) })?; self.signatures = signatures; Ok(self) } /// Construct a new `Metablock` using the included signatures, sorting the signatures by /// `KeyId`. pub fn build(self) -> Metablock { let mut signatures = self.signatures.into_values().collect::>(); signatures.sort_unstable_by(|a, b| a.key_id().cmp(b.key_id())); Metablock { signatures, metadata: self.metadata, } } } #[cfg(test)] mod tests { use std::{fs, str::FromStr}; use assert_json_diff::assert_json_eq; use chrono::DateTime; use serde_json::json; use crate::{ crypto::{PrivateKey, PublicKey}, models::{ byproducts::ByProducts, inspection::Inspection, rule::{Artifact, ArtifactRule}, step::{Command, Step}, LayoutMetadataBuilder, LinkMetadataBuilder, Metablock, VirtualTargetPath, }, }; use super::MetablockBuilder; const ALICE_PRIVATE_KEY: &'static [u8] = include_bytes!("../../tests/ed25519/ed25519-1"); const ALICE_PUB_KEY: &'static [u8] = include_bytes!("../../tests/ed25519/ed25519-1.pub"); const BOB_PUB_KEY: &'static [u8] = include_bytes!("../../tests/rsa/rsa-4096.spki.der"); const OWNER_PRIVATE_KEY: &'static [u8] = include_bytes!("../../tests/test_metadata/owner.der"); #[test] fn deserialize_layout_metablock() { let raw = fs::read("tests/test_metadata/demo.layout").unwrap(); assert!(serde_json::from_slice::(&raw).is_ok()); } #[test] fn deserialize_link_metablock() { let raw = fs::read("tests/test_metadata/demo.link").unwrap(); assert!(serde_json::from_slice::(&raw).is_ok()); } #[test] fn serialize_layout_metablock() { let alice_public_key = PublicKey::from_ed25519(ALICE_PUB_KEY).unwrap(); let bob_public_key = PublicKey::from_spki( BOB_PUB_KEY, crate::crypto::SignatureScheme::RsaSsaPssSha256, ) .unwrap(); let owner_private_key = PrivateKey::from_ed25519(OWNER_PRIVATE_KEY).unwrap(); let layout_metadata = Box::new( LayoutMetadataBuilder::new() .expires(DateTime::UNIX_EPOCH) .add_key(alice_public_key.clone()) .add_key(bob_public_key.clone()) .add_step( Step::new("write-code") .threshold(1) .add_expected_product(ArtifactRule::Create( "foo.py".into(), )) .expected_command(Command::from_str("vi").unwrap()) .add_key(alice_public_key.key_id().to_owned()), ) .add_step( Step::new("package") .threshold(1) .add_expected_material(ArtifactRule::Match { pattern: "foo.py".into(), in_src: None, with: Artifact::Products, in_dst: None, from: "write-code".into(), }) .add_expected_product(ArtifactRule::Create( "foo.tar.gz".into(), )) .expected_command( Command::from_str("tar zcvf foo.tar.gz foo.py") .unwrap(), ) .add_key(bob_public_key.key_id().to_owned()), ) .add_inspect( Inspection::new("inspect_tarball") .add_expected_material(ArtifactRule::Match { pattern: "foo.tar.gz".into(), in_src: None, with: Artifact::Products, in_dst: None, from: "package".into(), }) .add_expected_product(ArtifactRule::Match { pattern: "foo.py".into(), in_src: None, with: Artifact::Products, in_dst: None, from: "write-code".into(), }) .run( Command::from_str("inspect_tarball.sh foo.tar.gz") .unwrap(), ), ) .build() .unwrap(), ); let private_keys = vec![&owner_private_key]; let metablock = MetablockBuilder::from_metadata(layout_metadata) .sign(&private_keys) .unwrap() .build(); let serialized = serde_json::to_value(&metablock).unwrap(); let expected = json!({ "signed": { "_type": "layout", "expires": "1970-01-01T00:00:00Z", "readme": "", "keys": { "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": { "keyid": "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf", "keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyid_hash_algorithms": [ "sha256", "sha512" ], "keyval": { "private": "", "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-----" } }, "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": { "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554", "keytype": "ed25519", "scheme": "ed25519", "keyval": { "private": "", "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6" } } }, "steps": [ { "_type": "step", "name": "write-code", "threshold": 1, "expected_materials": [ ], "expected_products": [ ["CREATE", "foo.py"] ], "pubkeys": [ "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554" ], "expected_command": ["vi"] }, { "_type": "step", "name": "package", "threshold": 1, "expected_materials": [ ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] ], "expected_products": [ ["CREATE", "foo.tar.gz"] ], "pubkeys": [ "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf" ], "expected_command": ["tar", "zcvf", "foo.tar.gz", "foo.py"] }], "inspect": [ { "_type": "inspection", "name": "inspect_tarball", "expected_materials": [ ["MATCH", "foo.tar.gz", "WITH", "PRODUCTS", "FROM", "package"] ], "expected_products": [ ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"] ], "run": ["inspect_tarball.sh", "foo.tar.gz"] } ], "readme": "" }, "signatures": [{ "keyid" : "64786e5921b589af1ca1bf5767087bf201806a9b3ce2e6856c903682132bd1dd", "sig": "61b2551e3febfa1f110cd9f087243908d88d29fb639b83e7978f9e3bda109cb21452134534298c64825c85684700390fcd0a0f03ee468905405ec58f88becb06" }] }); assert_json_eq!(expected, serialized); } #[test] fn serialize_link_metablock() { let link_metadata = LinkMetadataBuilder::new() .name("".into()) .add_product( VirtualTargetPath::new("tests/test_link/foo.tar.gz".into()) .unwrap(), ) .byproducts( ByProducts::new() .set_return_value(0) .set_stderr("a foo.py\n".into()) .set_stdout("".into()), ) .command(Command::from("tar zcvf foo.tar.gz foo.py")) .build() .unwrap(); let alice_public_key = PrivateKey::from_ed25519(ALICE_PRIVATE_KEY).unwrap(); let private_keys = vec![&alice_public_key]; let metablock = MetablockBuilder::from_metadata(Box::new(link_metadata)) .sign(&private_keys) .unwrap() .build(); let serialized = serde_json::to_value(&metablock).unwrap(); let expected = json!({ "signed" : { "_type": "link", "name": "", "materials": {}, "products": { "tests/test_link/foo.tar.gz": { "sha256": "52947cb78b91ad01fe81cd6aef42d1f6817e92b9e6936c1e5aabb7c98514f355" } }, "byproducts": { "return-value": 0, "stderr": "a foo.py\n", "stdout": "" }, "command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], "environment": null }, "signatures" : [{ "keyid" : "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554", "sig": "62918f5f84fca149c15fcbc247a831e0360d33f0d9c8a89e6f623a011a8b807e2b0ef816a37356d966e9ad446ec234efb2b3bb4b04f338c0560d9cdfa1dcba0a" }] }); assert_eq!(expected, serialized); } #[test] fn verify_signatures_of_metablock() { let link_metadata = LinkMetadataBuilder::new() .name("".into()) .add_product( VirtualTargetPath::new("tests/test_link/foo.tar.gz".into()) .unwrap(), ) .byproducts( ByProducts::new() .set_return_value(0) .set_stderr("a foo.py\n".into()) .set_stdout("".into()), ) .command(Command::from("tar zcvf foo.tar.gz foo.py")) .build() .unwrap(); let alice_public_key = PrivateKey::from_ed25519(ALICE_PRIVATE_KEY).unwrap(); let private_keys = vec![&alice_public_key]; let metablock = MetablockBuilder::from_metadata(Box::new(link_metadata)) .sign(&private_keys) .unwrap() .build(); let public_key = PublicKey::from_ed25519(ALICE_PUB_KEY).unwrap(); let authorized_keys = vec![&public_key]; assert!(metablock.verify(1, authorized_keys).is_ok()); } } in-toto-0.4.0/src/models/mod.rs000064400000000000000000000011201046102023000144070ustar 00000000000000//! Models used in in-toto mod envelope; mod helpers; mod layout; mod link; mod metadata; mod predicate; mod statement; pub use helpers::*; pub use layout::*; pub use link::*; pub use metadata::*; pub use predicate::{PredicateLayout, PredicateVer, PredicateWrapper}; pub use statement::{StatementVer, StatementWrapper}; #[cfg(test)] mod test { use once_cell::sync::Lazy; use super::{LinkMetadata, LinkMetadataBuilder}; pub static BLANK_META: Lazy = Lazy::new(|| { let builder = LinkMetadataBuilder::default(); builder.build().unwrap() }); } in-toto-0.4.0/src/models/predicate/link_v02.rs000064400000000000000000000102231046102023000172200ustar 00000000000000use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use super::{PredicateLayout, PredicateVer, PredicateWrapper}; use crate::interchange::{DataInterchange, Json}; use crate::models::byproducts::ByProducts; use crate::models::step::Command; use crate::models::{TargetDescription, VirtualTargetPath}; use crate::Result; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] /// Predicate `LinkV02` means the predicate of original compatible format. /// /// [LinkV02](https://in-toto.io/Link/v0.2) /// can be used together with most states. pub struct LinkV02 { name: String, materials: BTreeMap, env: Option>, command: Command, byproducts: ByProducts, } impl PredicateLayout for LinkV02 { fn to_bytes(&self) -> Result> { Json::canonicalize(&Json::serialize(self)?) } fn into_enum(self: Box) -> PredicateWrapper { PredicateWrapper::LinkV0_2(*self) } fn version(&self) -> PredicateVer { PredicateVer::LinkV0_2 } } #[cfg(test)] pub mod test { use std::collections::BTreeMap; use std::str; use once_cell::sync::Lazy; use serde_json::{json, Value}; use strum::IntoEnumIterator; use super::LinkV02; use crate::{ interchange::{DataInterchange, Json}, models::{ byproducts::ByProducts, PredicateLayout, PredicateVer, PredicateWrapper, }, }; pub static STR_PREDICATE_LINK_V02: Lazy = Lazy::new(|| { let raw_data = json!( { "byproducts": { "return-value": 0, "stderr": "", "stdout": "" }, "command": [], "env": null, "materials": {}, "name": "" }); let value = serde_json::value::to_value(raw_data).unwrap(); let bytes = Json::canonicalize(&value).unwrap(); let data = str::from_utf8(&bytes).unwrap(); data.to_string() }); pub static PREDICATE_LINK_V02: Lazy = Lazy::new(|| LinkV02 { name: "".to_string(), materials: BTreeMap::new(), env: None, command: "".into(), byproducts: ByProducts::new() .set_return_value(0) .set_stderr("".into()) .set_stdout("".into()), }); #[test] fn into_trait_equal() { let predicate = PredicateWrapper::LinkV0_2(PREDICATE_LINK_V02.clone()); let real = Box::new(PREDICATE_LINK_V02.clone()).into_enum(); assert_eq!(predicate, real); } #[test] fn create_predicate_from_meta() { // TODO: convert from metadata is no supported recentely } #[test] fn serialize_predicate() { let predicate = Box::new(PREDICATE_LINK_V02.clone()).into_enum(); let buf = predicate.into_trait().to_bytes().unwrap(); let predicate_serialized = str::from_utf8(&buf).unwrap(); assert_eq!(predicate_serialized, *STR_PREDICATE_LINK_V02); } #[test] fn deserialize_predicate() { let value: Value = serde_json::from_str(&STR_PREDICATE_LINK_V02).unwrap(); let predicate = PredicateWrapper::from_value(value, PredicateVer::LinkV0_2) .unwrap(); let real = Box::new(PREDICATE_LINK_V02.clone()).into_enum(); assert_eq!(predicate, real); } #[test] fn deserialize_auto() { let value: Value = serde_json::from_str(&STR_PREDICATE_LINK_V02).unwrap(); let predicate = PredicateWrapper::try_from_value(value).unwrap(); let real = Box::new(PREDICATE_LINK_V02.clone()).into_enum(); assert_eq!(predicate, real); } #[test] fn deserialize_dismatch() { let value: Value = serde_json::from_str(&STR_PREDICATE_LINK_V02).unwrap(); for version in PredicateVer::iter() { if version == PredicateVer::LinkV0_2 { continue; } let predicate = PredicateWrapper::from_value(value.clone(), version); assert!(predicate.is_err()); } } } in-toto-0.4.0/src/models/predicate/mod.rs000064400000000000000000000114321046102023000163560ustar 00000000000000//! in-toto link pub mod link_v02; pub mod slsa_provenance_v01; pub mod slsa_provenance_v02; use std::convert::TryFrom; pub use link_v02::LinkV02; use serde_json::Value; pub use slsa_provenance_v01::SLSAProvenanceV01; pub use slsa_provenance_v02::SLSAProvenanceV02; use serde::de::{Deserializer, Error as DeserializeError}; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::EnumIter; use crate::{Error, Result}; #[derive(Debug, Hash, PartialEq, Eq, EnumIter, Clone, Copy)] pub enum PredicateVer { LinkV0_2, SLSAProvenanceV0_1, SLSAProvenanceV0_2, } impl TryFrom for PredicateVer { type Error = crate::Error; fn try_from(target: String) -> Result { match target.as_str() { "https://in-toto.io/Link/v0.2" => Ok(PredicateVer::LinkV0_2), "https://slsa.dev/provenance/v0.1" => { Ok(PredicateVer::SLSAProvenanceV0_1) } "https://slsa.dev/provenance/v0.2" => { Ok(PredicateVer::SLSAProvenanceV0_2) } _ => Err(Error::StringConvertFailed(target)), } } } impl From for String { fn from(value: PredicateVer) -> Self { match value { PredicateVer::LinkV0_2 => { "https://in-toto.io/Link/v0.2".to_string() } PredicateVer::SLSAProvenanceV0_1 => { "https://slsa.dev/provenance/v0.1".to_string() } PredicateVer::SLSAProvenanceV0_2 => { "https://slsa.dev/provenance/v0.2".to_string() } } } } impl Serialize for PredicateVer { fn serialize(&self, ser: S) -> ::std::result::Result where S: Serializer, { let target: String = (*self).into(); ser.serialize_str(&target) } } impl<'de> Deserialize<'de> for PredicateVer { fn deserialize>( de: D, ) -> ::std::result::Result { let target: String = Deserialize::deserialize(de)?; PredicateVer::try_from(target) .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } #[derive(Debug, Serialize, PartialEq, Eq, Clone)] #[serde(untagged)] pub enum PredicateWrapper { LinkV0_2(LinkV02), SLSAProvenanceV0_1(SLSAProvenanceV01), SLSAProvenanceV0_2(SLSAProvenanceV02), } impl<'de> Deserialize<'de> for PredicateWrapper { fn deserialize>( de: D, ) -> ::std::result::Result { let value = Value::deserialize(de)?; PredicateWrapper::try_from_value(value) .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } impl PredicateWrapper { /// Convert from enum `PredicateWrapper` to trait `PredicateLayout` pub fn into_trait(self) -> Box { match self { PredicateWrapper::LinkV0_2(link) => Box::new(link), PredicateWrapper::SLSAProvenanceV0_1(proven) => Box::new(proven), PredicateWrapper::SLSAProvenanceV0_2(proven) => Box::new(proven), } } /// Deserialize method for `PredicateWrapper` from `serde:Value` by its version fn from_value(value: Value, version: PredicateVer) -> Result { match version { PredicateVer::LinkV0_2 => serde_json::from_value(value) .map(Self::LinkV0_2) .map_err(|e| e.into()), PredicateVer::SLSAProvenanceV0_1 => serde_json::from_value(value) .map(Self::SLSAProvenanceV0_1) .map_err(|e| e.into()), PredicateVer::SLSAProvenanceV0_2 => serde_json::from_value(value) .map(Self::SLSAProvenanceV0_2) .map_err(|e| e.into()), } } /// Auto judge the `PredicateWrapper` version from `serde:Value` pub fn judge_from_value(value: &Value) -> Result { println!("{:?}", value); for version in PredicateVer::iter() { let wrapper = PredicateWrapper::from_value(value.clone(), version); if wrapper.is_ok() { return Ok(version); } } Err(Error::Programming("no available value parser".to_string())) } /// Auto deserialize for `PredicateWrapper` by any possible version. pub fn try_from_value(value: Value) -> Result { let version = Self::judge_from_value(&value)?; PredicateWrapper::from_value(value, version) } } pub trait PredicateLayout { /// The version of predicate fn version(&self) -> PredicateVer; /// Convert from trait `PredicateLayout` to enum `PredicateWrapper` fn into_enum(self: Box) -> PredicateWrapper; /// Standard serialize for PredicateLayout fn to_bytes(&self) -> Result>; } in-toto-0.4.0/src/models/predicate/slsa_provenance_v01.rs000064400000000000000000000242021046102023000214460ustar 00000000000000use std::collections::HashMap; use chrono::{DateTime, FixedOffset, SecondsFormat}; use serde::de::{Deserializer, Error as DeserializeError}; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use super::{PredicateLayout, PredicateVer, PredicateWrapper}; use crate::interchange::{DataInterchange, Json}; use crate::Result; #[derive(Debug, PartialEq, Eq, Clone)] pub struct TimeStamp(pub DateTime); impl Serialize for TimeStamp { fn serialize(&self, ser: S) -> ::std::result::Result where S: Serializer, { let form = self.0.to_rfc3339_opts(SecondsFormat::Secs, true); form.serialize(ser) } } impl<'de> Deserialize<'de> for TimeStamp { fn deserialize>( de: D, ) -> ::std::result::Result { let form: &str = Deserialize::deserialize(de)?; DateTime::parse_from_rfc3339(form) .map(TimeStamp) .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct TypeURI(pub String); #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct Builder { pub id: TypeURI, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct Completeness { #[serde(skip_serializing_if = "Option::is_none")] pub arguments: Option, #[serde(skip_serializing_if = "Option::is_none")] pub environment: Option, #[serde(skip_serializing_if = "Option::is_none")] pub materials: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct Recipe { #[serde(rename = "type")] pub typ: TypeURI, #[serde(rename = "definedInMaterial")] #[serde(skip_serializing_if = "Option::is_none")] pub defined_in_material: Option, #[serde(rename = "entryPoint")] #[serde(skip_serializing_if = "Option::is_none")] pub entry_point: Option, #[serde(skip_serializing_if = "Option::is_none")] pub arguments: Option, #[serde(skip_serializing_if = "Option::is_none")] pub environment: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct ProvenanceMetadata { #[serde(rename = "buildInvocationId")] #[serde(skip_serializing_if = "Option::is_none")] pub build_invocation_id: Option, #[serde(rename = "buildStartedOn")] #[serde(skip_serializing_if = "Option::is_none")] pub build_started_on: Option, #[serde(rename = "buildFinishedOn")] #[serde(skip_serializing_if = "Option::is_none")] pub build_finished_on: Option, #[serde(skip_serializing_if = "Option::is_none")] pub completeness: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reproducible: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct Material { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) uri: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) digest: Option>, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] /// Predicate `SLSAProvenanceV01` means the predicate of SLSA format. /// /// [SLSAProvenanceV01](https://slsa.dev/provenance/v0.1) /// can be used together with most states. pub struct SLSAProvenanceV01 { builder: Builder, #[serde(skip_serializing_if = "Option::is_none")] recipe: Option, #[serde(skip_serializing_if = "Option::is_none")] metadata: Option, #[serde(skip_serializing_if = "Option::is_none")] materials: Option>, } impl PredicateLayout for SLSAProvenanceV01 { fn to_bytes(&self) -> Result> { Json::canonicalize(&Json::serialize(self)?) } fn into_enum(self: Box) -> PredicateWrapper { PredicateWrapper::SLSAProvenanceV0_1(*self) } fn version(&self) -> PredicateVer { PredicateVer::SLSAProvenanceV0_1 } } #[cfg(test)] pub mod test { use std::{collections::HashMap, str}; use chrono::DateTime; use once_cell::sync::Lazy; use serde_json::{json, Value}; use strum::IntoEnumIterator; use super::{ Builder, Completeness, Material, ProvenanceMetadata, Recipe, SLSAProvenanceV01, TimeStamp, TypeURI, }; use crate::{ interchange::{DataInterchange, Json}, models::{PredicateLayout, PredicateVer, PredicateWrapper}, }; pub static STR_PREDICATE_PROVEN_V01: Lazy = Lazy::new(|| { let raw_data = json!({ "builder": { "id": "https://github.com/Attestations/GitHubHostedActions@v1" }, "recipe": { "type": "https://github.com/Attestations/GitHubActionsWorkflow@v1", "definedInMaterial": 0, "entryPoint": "build.yaml:maketgz" }, "metadata": { "buildInvocationId":"test_invocation_id", "completeness": { "environment": true } }, "materials": [{ "uri": "git+https://github.com/curl/curl-docker@master", "digest": { "sha1": "d6525c840a62b398424a78d792f457477135d0cf" } }, { "uri": "github_hosted_vm:ubuntu-18.04:20210123.1" }] }); let value = serde_json::value::to_value(raw_data).unwrap(); let bytes = Json::canonicalize(&value).unwrap(); let data = str::from_utf8(&bytes).unwrap(); data.to_string() }); pub static PREDICATE_PROVEN_V01: Lazy = Lazy::new(|| SLSAProvenanceV01 { builder: Builder { id: TypeURI( "https://github.com/Attestations/GitHubHostedActions@v1" .to_string(), ), }, recipe: Some(Recipe { typ: TypeURI( "https://github.com/Attestations/GitHubActionsWorkflow@v1" .to_string(), ), defined_in_material: Some(0), entry_point: Some("build.yaml:maketgz".to_string()), arguments: None, environment: None, }), metadata: Some(ProvenanceMetadata { build_invocation_id: Some("test_invocation_id".to_string()), build_started_on: None, build_finished_on: None, completeness: Some(Completeness { arguments: None, environment: Some(true), materials: None, }), reproducible: None, }), materials: Some(vec![ Material { uri: Some(TypeURI( "git+https://github.com/curl/curl-docker@master" .to_string(), )), digest: Some(HashMap::from([( "sha1".to_string(), "d6525c840a62b398424a78d792f457477135d0cf".to_string(), )])), }, Material { uri: Some(TypeURI( "github_hosted_vm:ubuntu-18.04:20210123.1".to_string(), )), digest: None, }, ]), }); #[test] fn serialize_deserialize_datetime() { let datetime = TimeStamp( DateTime::parse_from_rfc3339("2020-08-19T08:38:00Z").unwrap(), ); let datetime_raw = "\"2020-08-19T08:38:00Z\""; // serialize let buf = Json::canonicalize(&Json::serialize(&datetime).unwrap()).unwrap(); let datetime_serialized = str::from_utf8(&buf).unwrap(); assert_eq!(datetime_raw, datetime_serialized); // deserialize let datetime_deserialized: TimeStamp = serde_json::from_slice(datetime_raw.as_bytes()).unwrap(); assert_eq!(datetime_deserialized, datetime); } #[test] fn into_trait_equal() { let predicate = PredicateWrapper::SLSAProvenanceV0_1(PREDICATE_PROVEN_V01.clone()); let real = Box::new(PREDICATE_PROVEN_V01.clone()).into_enum(); assert_eq!(predicate, real); } #[test] fn create_predicate_from_meta() { // TODO: convert from metadata is no supported recentely } #[test] fn serialize_predicate() { let predicate = Box::new(PREDICATE_PROVEN_V01.clone()).into_enum(); let buf = predicate.into_trait().to_bytes().unwrap(); let predicate_serialized = str::from_utf8(&buf).unwrap(); assert_eq!(predicate_serialized, *STR_PREDICATE_PROVEN_V01); } #[test] fn deserialize_predicate() { let value: Value = serde_json::from_str(&STR_PREDICATE_PROVEN_V01).unwrap(); let predicate = PredicateWrapper::from_value( value, PredicateVer::SLSAProvenanceV0_1, ) .unwrap(); let real = Box::new(PREDICATE_PROVEN_V01.clone()).into_enum(); assert_eq!(predicate, real); } #[test] fn deserialize_auto() { let value: Value = serde_json::from_str(&STR_PREDICATE_PROVEN_V01).unwrap(); let predicate = PredicateWrapper::try_from_value(value).unwrap(); let real = Box::new(PREDICATE_PROVEN_V01.clone()).into_enum(); assert_eq!(predicate, real); } #[test] fn deserialize_dismatch() { let value: Value = serde_json::from_str(&STR_PREDICATE_PROVEN_V01).unwrap(); for version in PredicateVer::iter() { if version == PredicateVer::SLSAProvenanceV0_1 { continue; } let predicate = PredicateWrapper::from_value(value.clone(), version); assert!(predicate.is_err()); } } } in-toto-0.4.0/src/models/predicate/slsa_provenance_v02.rs000064400000000000000000000201551046102023000214520ustar 00000000000000use std::collections::HashMap; use serde::{Deserialize, Serialize}; use super::slsa_provenance_v01::{ Builder, Material, ProvenanceMetadata, TypeURI, }; use super::{PredicateLayout, PredicateVer, PredicateWrapper}; use crate::interchange::{DataInterchange, Json}; use crate::Result; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct ConfigSource { pub uri: Option, #[serde(skip_serializing_if = "Option::is_none")] pub digest: Option>, #[serde(rename = "entryPoint")] #[serde(skip_serializing_if = "Option::is_none")] pub entry_point: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct Invocation { #[serde(rename = "configSource")] #[serde(skip_serializing_if = "Option::is_none")] pub config_source: Option, #[serde(skip_serializing_if = "Option::is_none")] pub parameters: Option, #[serde(skip_serializing_if = "Option::is_none")] pub environment: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] /// Predicate `SLSAProvenanceV02` means the predicate of SLSA format. /// /// [SLSAProvenanceV02](https://slsa.dev/provenance/v0.2) /// can be used together with most states. pub struct SLSAProvenanceV02 { pub builder: Builder, #[serde(rename = "buildType")] pub build_type: TypeURI, #[serde(skip_serializing_if = "Option::is_none")] pub invocation: Option, #[serde(rename = "buildConfig")] #[serde(skip_serializing_if = "Option::is_none")] pub build_config: Option, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, #[serde(skip_serializing_if = "Option::is_none")] pub materials: Option>, } impl PredicateLayout for SLSAProvenanceV02 { fn to_bytes(&self) -> Result> { Json::canonicalize(&Json::serialize(self)?) } fn into_enum(self: Box) -> PredicateWrapper { PredicateWrapper::SLSAProvenanceV0_2(*self) } fn version(&self) -> PredicateVer { PredicateVer::SLSAProvenanceV0_2 } } #[cfg(test)] pub mod test { use std::collections::HashMap; use std::str; use once_cell::sync::Lazy; use serde_json::{json, Value}; use strum::IntoEnumIterator; use super::{ConfigSource, Invocation, SLSAProvenanceV02}; use crate::{ interchange::{DataInterchange, Json}, models::{ predicate::slsa_provenance_v01::{ Builder, Completeness, Material, ProvenanceMetadata, TypeURI, }, PredicateLayout, PredicateVer, PredicateWrapper, }, }; pub static STR_PREDICATE_PROVEN_V02: Lazy = Lazy::new(|| { let raw_data = json!({ "builder": { "id": "https://github.com/Attestations/GitHubHostedActions@v1" }, "buildType": "https://github.com/Attestations/GitHubActionsWorkflow@v1", "invocation": { "configSource": { "uri": "git+https://github.com/curl/curl-docker@master", "digest": { "sha1": "d6525c840a62b398424a78d792f457477135d0cf" }, "entryPoint": "build.yaml:maketgz" } }, "metadata": { "buildInvocationId":"test_invocation_id", "completeness": { "environment": true } }, "materials": [{ "uri": "git+https://github.com/curl/curl-docker@master", "digest": { "sha1": "d6525c840a62b398424a78d792f457477135d0cf" } }, { "uri": "github_hosted_vm:ubuntu-18.04:20210123.1" }] }); let value = serde_json::value::to_value(raw_data).unwrap(); let bytes = Json::canonicalize(&value).unwrap(); let data = str::from_utf8(&bytes).unwrap(); data.to_string() }); pub static PREDICATE_PROVEN_V02: Lazy = Lazy::new( || { let digest = HashMap::from([( "sha1".to_string(), "d6525c840a62b398424a78d792f457477135d0cf".to_string(), )]); SLSAProvenanceV02 { builder: Builder { id: TypeURI("https://github.com/Attestations/GitHubHostedActions@v1".to_string()), }, build_type: TypeURI( "https://github.com/Attestations/GitHubActionsWorkflow@v1".to_string(), ), invocation: Some(Invocation { config_source: Some(ConfigSource { uri: Some(TypeURI( "git+https://github.com/curl/curl-docker@master".to_string(), )), digest: Some(digest), entry_point: Some("build.yaml:maketgz".to_string()), }), parameters: None, environment: None, }), build_config: None, metadata: Some(ProvenanceMetadata { build_invocation_id: Some("test_invocation_id".to_string()), build_started_on: None, build_finished_on: None, completeness: Some(Completeness { arguments: None, environment: Some(true), materials: None, }), reproducible: None, }), materials: Some(vec![ Material { uri: Some(TypeURI( "git+https://github.com/curl/curl-docker@master".to_string(), )), digest: Some(HashMap::from([( "sha1".to_string(), "d6525c840a62b398424a78d792f457477135d0cf".to_string(), )])), }, Material { uri: Some(TypeURI( "github_hosted_vm:ubuntu-18.04:20210123.1".to_string(), )), digest: None, }, ]), } }, ); #[test] fn into_trait_equal() { let predicate = PredicateWrapper::SLSAProvenanceV0_2(PREDICATE_PROVEN_V02.clone()); let real = Box::new(PREDICATE_PROVEN_V02.clone()).into_enum(); assert_eq!(predicate, real); } #[test] fn create_predicate_from_meta() { // TODO: convert from metadata is no supported recentely } #[test] fn serialize_predicate() { let predicate = Box::new(PREDICATE_PROVEN_V02.clone()).into_enum(); let buf = predicate.into_trait().to_bytes().unwrap(); let predicate_serialized = str::from_utf8(&buf).unwrap(); assert_eq!(predicate_serialized, *STR_PREDICATE_PROVEN_V02); } #[test] fn deserialize_predicate() { let value: Value = serde_json::from_str(&STR_PREDICATE_PROVEN_V02).unwrap(); let predicate = PredicateWrapper::from_value( value, PredicateVer::SLSAProvenanceV0_2, ) .unwrap(); let real = Box::new(PREDICATE_PROVEN_V02.clone()).into_enum(); assert_eq!(predicate, real); } #[test] fn deserialize_auto() { let value: Value = serde_json::from_str(&STR_PREDICATE_PROVEN_V02).unwrap(); let predicate = PredicateWrapper::try_from_value(value).unwrap(); let real = Box::new(PREDICATE_PROVEN_V02.clone()).into_enum(); assert_eq!(predicate, real); } #[test] fn deserialize_dismatch() { let value: Value = serde_json::from_str(&STR_PREDICATE_PROVEN_V02).unwrap(); for version in PredicateVer::iter() { if version == PredicateVer::SLSAProvenanceV0_2 { continue; } let predicate = PredicateWrapper::from_value(value.clone(), version); assert!(predicate.is_err()); } } } in-toto-0.4.0/src/models/statement/mod.rs000064400000000000000000000110431046102023000164200ustar 00000000000000pub mod state_naive; pub mod state_v01; use serde_json::Value; pub use state_naive::StateNaive; pub use state_v01::StateV01; use std::convert::TryFrom; use std::fmt::{Display, Formatter, Result as FmtResult}; use serde::de::{Deserializer, Error as DeserializeError}; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::EnumIter; use super::{LinkMetadata, PredicateLayout}; use crate::Error; use crate::Result; #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, EnumIter)] pub enum StatementVer { Naive, V0_1, } impl TryFrom for StatementVer { type Error = crate::Error; fn try_from(target: String) -> Result { match target.as_str() { "link" => Ok(StatementVer::Naive), "https://in-toto.io/Statement/v0.1" => Ok(StatementVer::V0_1), _ => Err(Error::StringConvertFailed(target)), } } } impl From for String { fn from(value: StatementVer) -> Self { match value { StatementVer::Naive => "link".to_string(), StatementVer::V0_1 => { "https://in-toto.io/Statement/v0.1".to_string() } } } } impl Serialize for StatementVer { fn serialize(&self, ser: S) -> ::std::result::Result where S: Serializer, { let target: String = (*self).into(); ser.serialize_str(&target) } } impl<'de> Deserialize<'de> for StatementVer { fn deserialize>( de: D, ) -> ::std::result::Result { let target: String = Deserialize::deserialize(de)?; StatementVer::try_from(target) .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } impl Display for StatementVer { fn fmt(&self, fmt: &mut Formatter) -> FmtResult { match self { StatementVer::V0_1 => fmt.write_str("v0.1")?, StatementVer::Naive => fmt.write_str("naive")?, } Ok(()) } } #[derive(Debug, Serialize, PartialEq, Eq)] pub enum StatementWrapper { Naive(StateNaive), V0_1(StateV01), } impl<'de> Deserialize<'de> for StatementWrapper { fn deserialize>( de: D, ) -> ::std::result::Result { let value = Value::deserialize(de)?; StatementWrapper::try_from_value(value) .map_err(|e| DeserializeError::custom(format!("{:?}", e))) } } pub trait FromMerge: Sized { fn merge( meta: LinkMetadata, predicate: Option>, ) -> Result; } impl StatementWrapper { pub fn into_trait(self) -> Box { match self { StatementWrapper::Naive(link) => Box::new(link), StatementWrapper::V0_1(link) => Box::new(link), } } pub fn from_meta( meta: LinkMetadata, predicate: Option>, version: StatementVer, ) -> Self { match version { StatementVer::Naive => { Self::Naive(StateNaive::merge(meta, predicate).unwrap()) } StatementVer::V0_1 => { Self::V0_1(StateV01::merge(meta, predicate).unwrap()) } } } /// Deserialize method for `StatementWrapper` from `serde:Value` by its version fn from_value(value: Value, version: StatementVer) -> Result { match version { StatementVer::Naive => serde_json::from_value(value) .map(Self::Naive) .map_err(|e| e.into()), StatementVer::V0_1 => serde_json::from_value(value) .map(Self::V0_1) .map_err(|e| e.into()), } } /// Auto judge the `PredicateWrapper` version from `serde:Value` pub fn judge_from_value(value: &Value) -> Result { for version in StatementVer::iter() { let wrapper = StatementWrapper::from_value(value.clone(), version); if wrapper.is_ok() { return Ok(version); } } Err(Error::Programming("no available value parser".to_string())) } // Auto deserialize for `PredicateWrapper` by any possible version. pub fn try_from_value(value: Value) -> Result { let version = Self::judge_from_value(&value)?; StatementWrapper::from_value(value, version) } } pub trait StateLayout { fn version(&self) -> StatementVer; fn into_enum(self: Box) -> StatementWrapper; fn to_bytes(&self) -> Result>; } in-toto-0.4.0/src/models/statement/state_naive.rs000064400000000000000000000114651046102023000201530ustar 00000000000000use std::{collections::BTreeMap, fmt::Debug}; use serde::{Deserialize, Serialize}; use super::{FromMerge, StateLayout, StatementVer, StatementWrapper}; use crate::models::{LinkMetadata, TargetDescription, VirtualTargetPath}; use crate::{ interchange::{DataInterchange, Json}, models::{byproducts::ByProducts, step::Command, PredicateLayout}, Error, Result, }; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] /// Statement `Naive` means the predicate of original format. /// /// Can be used together with no predicate as `None`. pub struct StateNaive { #[serde(rename = "_type")] typ: String, name: String, materials: BTreeMap, products: BTreeMap, env: Option>, command: Command, byproducts: ByProducts, } impl StateLayout for StateNaive { fn version(&self) -> StatementVer { StatementVer::Naive } fn into_enum(self: Box) -> StatementWrapper { StatementWrapper::Naive(*self) } fn to_bytes(&self) -> Result> { Json::canonicalize(&Json::serialize(self)?) } } impl FromMerge for StateNaive { fn merge( meta: LinkMetadata, predicate: Option>, ) -> Result { if let Some(p) = predicate { return Err(Error::AttestationFormatDismatch( "None".to_string(), p.version().into(), )); }; let version = StatementVer::Naive.into(); Ok(StateNaive { typ: version, name: meta.name, materials: meta.materials, products: meta.products, env: meta.env, command: meta.command, byproducts: meta.byproducts, }) } } #[cfg(test)] pub mod test { use std::collections::BTreeMap; use std::str; use once_cell::sync::Lazy; use serde_json::{json, Value}; use strum::IntoEnumIterator; use super::StateNaive; use crate::interchange::{DataInterchange, Json}; use crate::models::byproducts::ByProducts; use crate::models::statement::{ StateLayout, StatementVer, StatementWrapper, }; use crate::models::test::BLANK_META; pub static STR_NAIVE: Lazy = Lazy::new(|| { let raw_data = json!({ "_type": "link", "byproducts": {}, "command": [], "env": null, "materials": {}, "name": "", "products": {} }); let value = serde_json::value::to_value(raw_data).unwrap(); let bytes = Json::canonicalize(&value).unwrap(); let data = str::from_utf8(&bytes).unwrap(); data.to_string() }); pub static STATE_NAIVE: Lazy = Lazy::new(|| StateNaive { typ: StatementVer::Naive.into(), name: "".to_string(), materials: BTreeMap::new(), products: BTreeMap::new(), env: None, command: "".into(), byproducts: ByProducts::new(), }); #[test] fn into_trait_equal() { let state = StatementWrapper::Naive(STATE_NAIVE.clone()); let real = Box::new(STATE_NAIVE.clone()).into_enum(); assert_eq!(state, real); } #[test] fn create_statement_from_meta() { let state = StatementWrapper::from_meta( BLANK_META.clone(), None, StatementVer::Naive, ); let real = Box::new(STATE_NAIVE.clone()).into_enum(); assert_eq!(state, real); } #[test] fn serialize_statement() { let state = Box::new(STATE_NAIVE.clone()).into_enum(); let buf = state.into_trait().to_bytes().unwrap(); let link_serialized = str::from_utf8(&buf).unwrap(); assert_eq!(link_serialized, *STR_NAIVE); } #[test] fn deserialize_statement() { let value: Value = serde_json::from_str(&STR_NAIVE).unwrap(); let link = StatementWrapper::from_value(value, StatementVer::Naive).unwrap(); let real = Box::new(STATE_NAIVE.clone()).into_enum(); assert_eq!(link, real); } #[test] fn deserialize_auto() { let value: Value = serde_json::from_str(&STR_NAIVE).unwrap(); let link = StatementWrapper::try_from_value(value).unwrap(); let real = Box::new(STATE_NAIVE.clone()).into_enum(); assert_eq!(link, real); } #[test] fn deserialize_dismatch() { let value: Value = serde_json::from_str(&STR_NAIVE).unwrap(); for version in StatementVer::iter() { if version == StatementVer::Naive { continue; } let state = StatementWrapper::from_value(value.clone(), version); assert!(state.is_err()); } } } in-toto-0.4.0/src/models/statement/state_v01.rs000064400000000000000000000121031046102023000174450ustar 00000000000000use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::{ interchange::{DataInterchange, Json}, models::{ LinkMetadata, PredicateLayout, PredicateVer, PredicateWrapper, TargetDescription, VirtualTargetPath, }, Error, }; use super::{FromMerge, StateLayout, StatementVer, StatementWrapper}; use crate::Result; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] /// Statement `V0_1` means the statement of contains a predicate for SLSA format. /// /// Can be used together with most predicate. pub struct StateV01 { #[serde(rename = "_type")] typ: String, subject: BTreeMap, #[serde(rename = "predicateType")] predicate_type: PredicateVer, predicate: PredicateWrapper, } impl StateLayout for StateV01 { fn version(&self) -> StatementVer { StatementVer::V0_1 } fn into_enum(self: Box) -> StatementWrapper { StatementWrapper::V0_1(*self) } fn to_bytes(&self) -> Result> { Json::canonicalize(&Json::serialize(self)?) } } impl FromMerge for StateV01 { fn merge( meta: LinkMetadata, predicate: Option>, ) -> Result { if predicate.is_none() { return Err(Error::AttestationFormatDismatch( StatementVer::V0_1.to_string(), "None".to_string(), )); } let p = predicate.ok_or_else(|| { Error::Programming("match rules failed for StateV01".to_string()) })?; let version = StatementVer::V0_1.into(); Ok(StateV01 { typ: version, subject: meta.products, predicate_type: p.version(), predicate: p.into_enum(), }) } } #[cfg(test)] pub mod test { use crate::{ interchange::{DataInterchange, Json}, models::{ predicate::link_v02::test::PREDICATE_LINK_V02, statement::{StateLayout, StatementVer, StatementWrapper}, test::BLANK_META, PredicateLayout, PredicateVer, }, }; use std::collections::BTreeMap; use std::str; use once_cell::sync::Lazy; use serde_json::{json, Value}; use strum::IntoEnumIterator; use super::StateV01; pub static STR_V01: Lazy = Lazy::new(|| { let raw_data = json!({ "_type": "https://in-toto.io/Statement/v0.1", "predicateType": "https://in-toto.io/Link/v0.2", "predicate": { "byproducts": { "return-value": 0, "stderr": "", "stdout": "" }, "command": [], "env": null, "materials": {}, "name": "" }, "subject": {} }); let value = serde_json::value::to_value(raw_data).unwrap(); let bytes = Json::canonicalize(&value).unwrap(); let data = str::from_utf8(&bytes).unwrap(); data.to_string() }); pub static STATE_V01: Lazy = Lazy::new(|| StateV01 { typ: StatementVer::V0_1.into(), subject: BTreeMap::new(), predicate_type: PredicateVer::LinkV0_2, predicate: Box::new(PREDICATE_LINK_V02.clone()).into_enum(), }); #[test] fn into_trait_equal() { let link = StatementWrapper::V0_1(STATE_V01.clone()); let real = Box::new(STATE_V01.clone()).into_enum(); assert_eq!(link, real); } #[test] fn create_statement_from_meta() { let link = StatementWrapper::from_meta( BLANK_META.clone(), Some(Box::new(PREDICATE_LINK_V02.clone())), StatementVer::V0_1, ); let real = Box::new(STATE_V01.clone()).into_enum(); assert_eq!(link, real); } #[test] fn serialize_statement() { let state = Box::new(STATE_V01.clone()).into_enum(); let buf = state.into_trait().to_bytes().unwrap(); let link_serialized = str::from_utf8(&buf).unwrap(); assert_eq!(link_serialized, *STR_V01); } #[test] fn deserialize_statement() { let value: Value = serde_json::from_str(&STR_V01).unwrap(); let link = StatementWrapper::from_value(value, StatementVer::V0_1).unwrap(); let real = Box::new(STATE_V01.clone()).into_enum(); assert_eq!(link, real); } #[test] fn deserialize_auto() { let value: Value = serde_json::from_str(&STR_V01).unwrap(); let link = StatementWrapper::try_from_value(value).unwrap(); let real = Box::new(STATE_V01.clone()).into_enum(); assert_eq!(link, real); } #[test] fn deserialize_dismatch() { let value: Value = serde_json::from_str(&STR_V01).unwrap(); for version in StatementVer::iter() { if version == StatementVer::V0_1 { continue; } let state = StatementWrapper::from_value(value.clone(), version); assert!(state.is_err()); } } } in-toto-0.4.0/src/rulelib.rs000064400000000000000000000345301046102023000140160ustar 00000000000000//! Helper for ArtifactRule to apply on LinkMetadata use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::PathBuf; use log::warn; use crate::models::rule::Artifact; use crate::models::supply_chain_item::SupplyChainItem; use crate::models::{rule::ArtifactRule, LinkMetadata}; use crate::models::{TargetDescription, VirtualTargetPath}; use crate::{Error, Result}; /// Canonicalize a given [`VirtualTargetPath`]. For example /// `/test/1/2/../3` -> `/test/1/3`. If any error /// occurs, just warn it and return None. fn canonicalize_path(path: &VirtualTargetPath) -> Option { let path = path_clean::clean(path.value()); VirtualTargetPath::new(path.into_os_string().into_string().unwrap()).ok() } /// Apply match rule. The parameters: /// * `rule`: MATCH rule to be applied (if not a MATCH rule, will be paniced) /// * `src_artifacts`: artifacts of a given link (either Products or Materials) /// * `src_artifact_queue`: artifact paths (canonicalized) of the same link (either Products or Materials) /// * `items_metadata`: a to hashmap /// /// This function will match the artifact paths of `src_artifact_queue` /// and the `dst_artifacts`. Here `dst_artifacts` can be calculated /// by indexing the step name from `items_metadata`. Return value is /// the matched artifact paths. fn verify_match_rule( rule: &ArtifactRule, src_artifacts: &BTreeMap, src_artifact_queue: &BTreeSet, items_metadata: &HashMap, ) -> BTreeSet { let mut consumed = BTreeSet::new(); match rule { ArtifactRule::Match { pattern, in_src, with, in_dst, from, } => { let dst_link = match items_metadata.get(from) { Some(lm) => lm, None => { warn!("no link metadata {} found.", from); return consumed; } }; let dst_artifact = match with { Artifact::Materials => &dst_link.materials, Artifact::Products => &dst_link.products, }; let src_artifacts: BTreeMap = src_artifacts .iter() .map(|(path, value)| { ( canonicalize_path(path) .unwrap_or_else(|| path.clone()), value.clone(), ) }) .collect(); let dst_artifacts: BTreeMap = dst_artifact .iter() .map(|(path, value)| { ( canonicalize_path(path) .unwrap_or_else(|| path.clone()), value.clone(), ) }) .collect(); let dst_prefix = { match in_dst { None => String::new(), Some(dst_dir) => { let mut res = PathBuf::new(); res.push(dst_dir); let mut res = res.to_string_lossy().to_string(); res.push('/'); res } } }; let src_prefix = { match in_src { None => String::new(), Some(src_dir) => { let mut res = PathBuf::new(); res.push(src_dir); let mut res = res.to_string_lossy().to_string(); res.push('/'); res } } }; for src_path in src_artifact_queue { let src_base_path = src_path .value() .strip_prefix(&src_prefix) .unwrap_or_else(|| src_path.value()); let src_base_path = VirtualTargetPath::new(src_base_path.to_string()) .expect("Unexpected VirtualTargetPath creation failed"); if let Err(e) = src_base_path.matches(pattern.value()) { warn!("match failed: {}", e.to_string()); continue; } let dst_path = { let mut res = PathBuf::new(); res.push(&dst_prefix); res.push(src_base_path.value()); VirtualTargetPath::new(res.to_string_lossy().to_string()) .expect("Unexpected VirtualTargetPath creation failed") }; if let Some(dst_artifact) = dst_artifacts.get(&dst_path) { if src_artifacts[src_path] == *dst_artifact { consumed.insert(src_path.clone()); } } } } _ => panic!("Unexpected rule type"), } consumed } /// Apply rules of the given [`SupplyChainItem`] onto the [`LinkMetadata`] pub(crate) fn apply_rules_on_link( item: &Box, reduced_link_files: &HashMap, ) -> Result<()> { // name of the given item let item_name = item.name(); // get the LinkMetadata for the given SupplyChainItem (`step` or `inspection`) let src_link = reduced_link_files.get(item_name).ok_or_else(|| { Error::VerificationFailure(format!( "can not find link metadata of step {}", item_name, )) })?; // materials of this link let material_paths: BTreeSet = src_link .materials .iter() .filter_map(|(path, _)| canonicalize_path(path)) .collect(); // products of this link let product_paths: BTreeSet = src_link .products .iter() .filter_map(|(path, _)| canonicalize_path(path)) .collect(); // prepare sets of artifacts for `create`, `delete` and `modify` rules. // these are calculated from the link's materials and products let created: BTreeSet<_> = product_paths.difference(&material_paths).cloned().collect(); let deleted: BTreeSet<_> = material_paths.difference(&product_paths).cloned().collect(); let modified: BTreeSet<_> = material_paths .intersection(&product_paths) .cloned() .filter_map(|name| { if src_link.materials[&name] != src_link.products[&name] { Some(name) } else { None } }) .collect(); #[derive(Debug)] struct VerificationDataList<'a> { src_type: Artifact, rules: &'a Vec, artifacts: &'a BTreeMap, artifact_paths: BTreeSet, } let list = [ // rule expected materials VerificationDataList { src_type: Artifact::Materials, rules: item.expected_materials(), artifacts: &src_link.materials, artifact_paths: material_paths, }, // rule expected products VerificationDataList { src_type: Artifact::Products, rules: item.expected_products(), artifacts: &src_link.products, artifact_paths: product_paths, }, ]; for verification_data in list { // rules to apply onto the link metadata let rules = verification_data.rules; // artifacts from the link metadata of this step, whose paths are all canonicalized let mut queue = verification_data.artifact_paths; // artifacts from the link metadata of this step and their digests let artifacts = verification_data.artifacts; // for every rule, we choose those items whose path matches the given pattern // rule in the queue as a set named `filtered`. and use the set to filter // items in `queue` using rule CREATE, DELETE, MODIFY, ALLOW, REQUIRE and DISALLOW. // besides, use MATCH rule to filter other items. for rule in rules { let filtered: BTreeSet<_> = queue .iter() .filter(|p| p.matches(rule.pattern().value()).unwrap_or(false)) .cloned() .collect(); let consumed = match rule { ArtifactRule::Create(_) => { filtered.intersection(&created).cloned().collect() } ArtifactRule::Delete(_) => { filtered.intersection(&deleted).cloned().collect() } ArtifactRule::Modify(_) => { filtered.intersection(&modified).cloned().collect() } ArtifactRule::Allow(_) => filtered, ArtifactRule::Require(_) => { if !queue.contains(rule.pattern()) { return Err(Error::ArtifactRuleError(format!( r#"artifact verification failed for {:?} in REQUIRE '{:?}', because {:?} is not in {:?}"#, verification_data.src_type, rule.pattern(), rule.pattern(), queue ))); } else { BTreeSet::new() } } ArtifactRule::Disallow(_) => { if !filtered.is_empty() { return Err(Error::ArtifactRuleError(format!( r#"artifact verification failed for {:?} in DISALLOW, because {:?} is disallowed by rule {:?} in {}"#, verification_data.src_type, filtered, rule, item_name, ))); } else { BTreeSet::new() } } ArtifactRule::Match { .. } => verify_match_rule( rule, artifacts, &queue, reduced_link_files, ), }; queue = queue.difference(&consumed).cloned().collect(); } } Ok(()) } #[cfg(test)] mod tests { use rstest::rstest; use crate::models::VirtualTargetPath; #[rstest] #[case("test/../1/1/2", "1/1/2")] #[case("test/../../1/2", "../1/2")] #[case("../././../1/2", "../../1/2")] fn canonicalize_path(#[case] given: &str, #[case] expected: &str) { let expected = Some( VirtualTargetPath::new(expected.to_string()) .expect("Unexpected creation failed"), ); let processed = VirtualTargetPath::new(given.to_string()) .expect("Unexpected creation failed"); let processed = super::canonicalize_path(&processed); assert_eq!(expected, processed); } #[rstest] #[ case( r#"["MATCH", "demo-project.tar.gz", "WITH", "PRODUCTS", "FROM", "package"]"#, r#"{"demo-project.tar.gz": {"sha256": "2989659e6836c941e9015bf38af3cb045365520dbf80460d8a44b2c5b6677fd9"}, "not-deleted.tar": {"sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}"#, r#"["demo-project.tar.gz", "not-deleted.tar"]"#, 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"}}}}"#, r#"["demo-project.tar.gz"]"#, )] #[ case( r#"["MATCH", "*", "WITH", "PRODUCTS", "IN", "test", "FROM", "package"]"#, r#"{"demo-project.tar.gz": {"sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}"#, r#"["demo-project.tar.gz"]"#, 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"}}}}"#, r#"["demo-project.tar.gz"]"#, ) ] #[ case( r#"["MATCH", "test1", "IN", "dir1", "WITH", "PRODUCTS", "IN", "test", "FROM", "package"]"#, r#"{"dir1/test1": {"sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}"#, r#"["dir1/test1"]"#, r#"{"package":{"_type":"link","byproducts":{},"command":[""],"environment":{},"materials":{},"name":"package","products":{"test/test1":{"sha256":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}}}"#, r#"["dir1/test1"]"#, ) ] fn verify_match_rule( #[case] rule: &str, #[case] src_artifacts: &str, #[case] src_artifact_queue: &str, #[case] items_metadata: &str, #[case] expected: &str, ) { let rule = serde_json::from_str(rule).expect("Parse artifact rule failed"); let src_artifacts = serde_json::from_str(src_artifacts) .expect("Parse Source Artifacts failed"); let src_artifact_queue = serde_json::from_str(src_artifact_queue) .expect("Parse Source Artifact Queue failed"); let items_metadata = serde_json::from_str(items_metadata) .expect("Parse Metadata HashMap failed"); let expected = serde_json::from_str(expected).expect("Parse failed"); let got = super::verify_match_rule( &rule, &src_artifacts, &src_artifact_queue, &items_metadata, ); assert_eq!(got, expected); } } in-toto-0.4.0/src/runlib.rs000064400000000000000000000513371046102023000136570ustar 00000000000000//! A tool that functionaries can use to create link metadata about a step. use path_clean::clean; use std::collections::{BTreeMap, HashSet}; use std::fs::{canonicalize as canonicalize_path, symlink_metadata, File}; use std::io::{self, BufReader, Write}; use std::process::Command; use walkdir::WalkDir; use crate::crypto::HashAlgorithm; use crate::interchange::Json; use crate::models::byproducts::ByProducts; use crate::models::{Metablock, TargetDescription}; use crate::{ crypto, crypto::PrivateKey, models::{LinkMetadataBuilder, VirtualTargetPath}, }; use crate::{Error, Result}; /// Reads and hashes an artifact given its path as a string literal, /// returning the `VirtualTargetPath` and `TargetDescription` of the file as a tuple, wrapped in `Result`. pub fn record_artifact( path: &str, hash_algorithms: &[HashAlgorithm], lstrip_paths: Option<&[&str]>, ) -> Result<(VirtualTargetPath, TargetDescription)> { let file = File::open(path)?; let mut reader = BufReader::new(file); let (_length, hashes) = crypto::calculate_hashes(&mut reader, hash_algorithms)?; let lstripped_path = apply_left_strip(path, lstrip_paths)?; Ok((VirtualTargetPath::new(lstripped_path)?, hashes)) } /// Given an artifact path in `&str` format, left strip path for given artifact based an optional array of `lstrip_paths` provided, /// returning the stripped file path in String format wrapped in `Result`. fn apply_left_strip( path: &str, lstrip_paths: Option<&[&str]>, ) -> Result { // If lstrip_paths is None, skip strip. // Else, check if path starts with any given lstrip paths and strip if lstrip_paths.is_none() { return Ok(String::from(path)); } let l_paths = lstrip_paths.unwrap(); let mut stripped_path = path; let mut find_prefix = ""; for l_path in l_paths.iter() { if !path.starts_with(l_path) { continue; } // if find possible prefix longer than if !find_prefix.is_empty() && find_prefix.len() >= l_path.len() { continue; } stripped_path = path.strip_prefix(l_path).ok_or_else(|| { Error::from(io::Error::new( std::io::ErrorKind::Other, format!( "Lstrip Error: error stripping {} from path {}", l_path, path ), )) })?; find_prefix = l_path; } Ok(String::from(stripped_path)) } /// Traverses through the passed array of paths, hashes the content of files /// encountered, and returns the path and hashed content in `BTreeMap` format, wrapped in `Result`. /// If a step in record_artifact fails, the error is returned. /// # Arguments /// /// * `paths` - An array of string slices (`&str`) that holds the paths to be traversed. If a symbolic link cycle is detected in the `paths` during traversal, it is skipped. /// * `hash_algorithms` - An array of string slice (`&str`) wrapped in an `Option` that holds the hash algorithms to be used. If `None` is provided, Sha256 is assumed as default. /// * `lstrip_paths` - An array of string slice (`&str`) wrapped in an `Option` that is left stripped from the path of every artifact that contains it. /// /// # Examples /// /// ``` /// // You can have rust code between fences inside the comments /// // If you pass --test to `rustdoc`, it will even test it for you! /// # use in_toto::runlib::{record_artifacts}; /// let materials = record_artifacts(&["tests/test_runlib"], None, None).unwrap(); /// ``` pub fn record_artifacts( paths: &[&str], hash_algorithms: Option<&[&str]>, lstrip_paths: Option<&[&str]>, ) -> Result> { // Verify hash_algorithms inputs are valid let available_algorithms = HashAlgorithm::return_all(); let hash_algorithms = match hash_algorithms { Some(hashes) => { let mut map = vec![]; for hash in hashes { if !available_algorithms.contains_key(*hash) { return Err(Error::UnknownHashAlgorithm( (*hash).to_string(), )); } let value = available_algorithms.get(*hash).unwrap(); map.push(value.clone()); } map } None => vec![HashAlgorithm::Sha256], }; let hash_algorithms = &hash_algorithms[..]; // Initialize artifacts let mut artifacts: BTreeMap = BTreeMap::new(); // For each path provided, walk the directory and add all files to artifacts for path in paths { // Normalize path let path = clean(path); let mut walker = WalkDir::new(path).follow_links(true).into_iter(); let mut visited_sym_links = HashSet::new(); while let Some(entry) = walker.next() { let path = dir_entry_to_path(entry)?; let file_type = std::fs::symlink_metadata(&path)?.file_type(); // If entry is a symlink, check it's unvisited. If so, continue. if file_type.is_symlink() { if visited_sym_links.contains(&path) { walker.skip_current_dir(); } else { visited_sym_links.insert(String::from(&path)); // s_path: the actual path the symbolic link is pointing to let s_path = match std::fs::read_link(&path)?.as_path().to_str() { Some(str) => String::from(str), None => break, }; if symlink_metadata(s_path)?.file_type().is_file() { let (virtual_target_path, hashes) = record_artifact( &path, hash_algorithms, lstrip_paths, )?; if artifacts.contains_key(&virtual_target_path) { return Err(Error::LinkGatheringError(format!( "non unique stripped path {virtual_target_path}" ))); } artifacts.insert(virtual_target_path, hashes); } } } // If entry is a file, open and hash the file if file_type.is_file() { let (virtual_target_path, hashes) = record_artifact(&path, hash_algorithms, lstrip_paths)?; if artifacts.contains_key(&virtual_target_path) { return Err(Error::LinkGatheringError(format!( "non unique stripped path {virtual_target_path}" ))); } artifacts.insert(virtual_target_path, hashes); } } } Ok(artifacts) } /// Given command arguments, executes commands on a software supply chain step /// and returns the `stdout`, `stderr`, and `return-value` as `byproducts` in `Result` format. /// If a commands in run_command fails to execute, `Error` is returned. /// # Arguments /// /// * `cmd_args` - An array of string slices (`&str`) that holds the command arguments to be executed. The first element of cmd_args is used as executable and the rest as command arguments. /// * `run_dir` - A string slice (`&str`) wrapped in an `Option` that holds the directory the commands are to be run in. If `None` is provided, the current directory is assumed as default. /// /// # Examples /// /// ``` /// // You can have rust code between fences inside the comments /// // If you pass --test to `rustdoc`, it will even test it for you! /// # use in_toto::runlib::{run_command}; /// let byproducts = run_command(&["sh", "-c", "printf hello"], Some("tests")).unwrap(); /// ``` pub fn run_command( cmd_args: &[&str], run_dir: Option<&str>, ) -> Result { // Format output into Byproduct if cmd_args.is_empty() { return Ok(ByProducts::new()); } let executable = cmd_args[0]; let args = (cmd_args[1..]) .iter() .map(|arg| { if VirtualTargetPath::new((*arg).into()).is_ok() { let absolute_path = canonicalize_path(*arg); match absolute_path { Ok(path_buf) => match path_buf.to_str() { Some(p) => p, None => *arg, }, Err(_) => *arg, }; } *arg }) .collect::>(); // TODO: Validate executable let mut cmd = Command::new(executable); let mut cmd = cmd.args(args); if let Some(dir) = run_dir { cmd = cmd.current_dir(dir) } let output = match cmd.output() { Ok(out) => out, Err(err) => { return Err(Error::IllegalArgument(format!( "Something went wrong with run_command inside in_toto_run. Error: {:?}", err ))) } }; // Emit stdout, stderror io::stdout().write_all(&output.stdout)?; io::stderr().write_all(&output.stderr)?; // Write to byproducts let stdout = match String::from_utf8(output.stdout) { Ok(output) => output, Err(error) => { return Err(Error::from(io::Error::new( std::io::ErrorKind::Other, format!("Utf8Error: {}", error), ))) } }; let stderr = match String::from_utf8(output.stderr) { Ok(output) => output, Err(error) => { return Err(Error::from(io::Error::new( std::io::ErrorKind::Other, format!("Utf8Error: {}", error), ))) } }; let status = output.status.code().ok_or_else(|| { Error::RunLibError("Process terminated by signal".to_string()) })?; let byproducts = ByProducts::new() .set_stdout(stdout) .set_stderr(stderr) .set_return_value(status); Ok(byproducts) } // TODO: implement default trait for in_toto_run's parameters /// Executes commands on a software supply chain step, then generates and returns its corresponding `LinkMetadata` /// as a `Metablock` component, wrapped in `Result`. /// If a symbolic link cycle is detected in the material or product paths, paths causing the cycle are skipped. /// # Arguments /// /// * `name` - The unique string used to associate link metadata with a step or inspection. /// * `run_dir` - A string slice (`&str`) wrapped in an `Option` that holds the directory the commands are to be run in. If `None` is provided, the current directory is assumed as default. /// * `material_paths` - A string slice (`&str`) of artifact paths to be recorded before command execution. Directories are traversed recursively. /// * `product_paths` - A string slice (`&str`) of artifact paths to be recorded after command execution. Directories are traversed recursively. /// * `cmd_args` - A string slice (`&str`) where the first element is a command and the remaining elements are arguments passed to that command. /// * `key` - A key used to sign the resulting link metadata. /// * `hash_algorithms` - An array of string slice (`&str`) wrapped in an `Option` that holds the hash algorithms to be used. If `None` is provided, Sha256 is assumed as default. /// * `lstrip_paths` - An array of string slice (`&str`) wrapped in an `Option` that is left stripped from the path of every artifact that contains it. /// /// # Examples /// /// ``` /// // You can have rust code between fences inside the comments /// // If you pass --test to `rustdoc`, it will even test it for you! /// # use in_toto::runlib::{in_toto_run}; /// # use in_toto::crypto::PrivateKey; /// const ED25519_1_PRIVATE_KEY: &'static [u8] = include_bytes!("../tests/ed25519/ed25519-1"); /// let key = PrivateKey::from_ed25519(ED25519_1_PRIVATE_KEY).unwrap(); /// let link = in_toto_run("example", Some("tests"), &["tests/test_runlib"], &["tests/test_runlib"], &["sh", "-c", "echo 'in_toto says hi' >> hello_intoto"], Some(&key), Some(&["sha512", "sha256"]), Some(&["tests/test_runlib/"])).unwrap(); /// let json = serde_json::to_value(&link).unwrap(); /// println!("Generated link: {}", json); /// ``` pub fn in_toto_run( name: &str, run_dir: Option<&str>, material_paths: &[&str], product_paths: &[&str], cmd_args: &[&str], key: Option<&PrivateKey>, hash_algorithms: Option<&[&str]>, lstrip_paths: Option<&[&str]>, // env: Option> ) -> Result { // Record Materials: Given the material_paths, recursively traverse and record files in given path(s) let materials = record_artifacts(material_paths, hash_algorithms, lstrip_paths)?; // Execute commands provided in cmd_args let byproducts = run_command(cmd_args, run_dir)?; // Record Products: Given the product_paths, recursively traverse and record files in given path(s) let products = record_artifacts(product_paths, hash_algorithms, lstrip_paths)?; // Create link based on values collected above let link_metadata_builder = LinkMetadataBuilder::new() .name(name.to_string()) .materials(materials) .byproducts(byproducts) .products(products); // Sign the link with key param supplied. If no key is found, return Metablock with // no signatures (for inspection purposes) match key { Some(k) => link_metadata_builder.signed::(k), None => link_metadata_builder.unsigned::(), } } /// A private helper function that, given a `DirEntry`, return the entry's path as a `String` /// wrapped in `Result`. If the entry's path is invalid, `Error` is returned. fn dir_entry_to_path( entry: std::result::Result, ) -> Result { let path = match entry { Ok(dir_entry) => match dir_entry.path().to_str() { Some(str) => String::from(str), None => { return Err(Error::IllegalArgument(format!( "Invalid Path {}; non-UTF-8 string", dir_entry.path().display() ))) } }, // If WalkDir errored, check if it's due to a symbolic link loop sighted, // if so, override the error and continue using the symbolic link path. // If this doesn't work, something hacky to consider would be reinvoking WalkDir // using the error_path as root. // Current behavior: when symbolic link is a directory and directly loops to parent, // it skips the symbolic link recording. // If this is not the desired behavior and we want to record the symbolic link's content // , we can probably do it in a hacky way by recursively calling record_artifacts and // extending the results to artifacts variable. Err(error) => { if error.loop_ancestor().is_some() { match error.path() { None => { return Err(Error::from(io::Error::new( std::io::ErrorKind::Other, format!("Walkdir Error: {}", error), ))) } Some(error_path) => { let sym_path = match error_path.to_str() { Some(str) => String::from(str), None => { return Err(Error::IllegalArgument(format!( "Invalid Path {}; non-UTF-8 string", error_path.display() ))) } }; // TODO: Emit a warning that a symlink cycle is detected and it will be skipped // Add it to the link itself sym_path } } } else { return Err(Error::from(io::Error::new( std::io::ErrorKind::Other, format!("Walkdir Error: {}", error), ))); } } }; Ok(clean(path).into_os_string().into_string().unwrap()) } #[cfg(test)] mod test { use data_encoding::HEXLOWER; use std::collections::HashMap; use super::*; fn create_target_description( hash_algorithm: crypto::HashAlgorithm, hash_value: &[u8], ) -> TargetDescription { let mut hash = HashMap::new(); hash.insert( hash_algorithm, crypto::HashValue::new(HEXLOWER.decode(hash_value).unwrap()), ); hash } #[test] fn test_record_artifacts() { let mut expected: BTreeMap = BTreeMap::new(); expected.insert( VirtualTargetPath::new("tests/test_runlib/.hidden/foo".to_string()).unwrap(), create_target_description( crypto::HashAlgorithm::Sha256, b"7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", ), ); expected.insert( VirtualTargetPath::new("tests/test_runlib/.hidden/.bar".to_string()).unwrap(), create_target_description( crypto::HashAlgorithm::Sha256, b"b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c", ), ); expected.insert( VirtualTargetPath::new("tests/test_runlib/hello./world".to_string()).unwrap(), create_target_description( crypto::HashAlgorithm::Sha256, b"25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", ), ); assert_eq!( record_artifacts(&["tests/test_runlib"], None, None).unwrap(), expected ); assert_eq!(record_artifacts(&["tests"], None, None).is_ok(), true); assert_eq!( record_artifacts(&["file-does-not-exist"], None, None).is_err(), true ); } #[test] fn test_prefix_record_artifacts() { let mut expected: BTreeMap = BTreeMap::new(); expected.insert( VirtualTargetPath::new("world".to_string()).unwrap(), create_target_description( crypto::HashAlgorithm::Sha256, b"25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d", ), ); assert_eq!( record_artifacts( &["tests/test_prefix/left"], None, Some(&["tests/test_prefix/left/"]) ) .unwrap(), expected ); // conflict of file "left/world" and "right/world" assert_eq!( record_artifacts( &["tests/test_prefix"], None, Some(&["tests/test_prefix/left/", "tests/test_prefix/right/"]) ) .is_err(), true ); } #[test] fn test_left_strip() { let mut stripped_path: String; stripped_path = apply_left_strip( "tests/test_runlib/.hidden/foo", Some(&["tests/test_runlib"]), ) .unwrap(); assert_eq!(stripped_path, "/.hidden/foo"); stripped_path = apply_left_strip( "tests/test_runlib/.hidden/foo", Some(&["tests/test_runlib/"]), ) .unwrap(); assert_eq!(stripped_path, ".hidden/foo"); stripped_path = apply_left_strip( "tests/test_runlib/.hidden/foo", Some(&["tests/test_runlib/.hidden/"]), ) .unwrap(); assert_eq!(stripped_path, "foo"); stripped_path = apply_left_strip( "tests/test_runlib/.hidden/foo", Some(&["path-does-not-exist"]), ) .unwrap(); assert_eq!(stripped_path, "tests/test_runlib/.hidden/foo"); stripped_path = apply_left_strip( "tests/test_runlib/.hidden/foo", Some(&["path-does-not-exist", "tests/"]), ) .unwrap(); assert_eq!(stripped_path, "test_runlib/.hidden/foo"); stripped_path = apply_left_strip( "tests/test_runlib/.hidden/foo", Some(&["tests/", "tests/test_runlib/.hidden/"]), ) .unwrap(); assert_eq!(stripped_path, "foo"); } #[test] fn test_run_command() { let byproducts = run_command(&["sh", "-c", "printf hello"], Some("tests")).unwrap(); let expected = ByProducts::new() .set_stderr("".to_string()) .set_stdout("hello".to_string()) .set_return_value(0); assert_eq!(byproducts, expected); assert_eq!( run_command(&["command-does-not-exist", "true"], None).is_err(), true ); } } in-toto-0.4.0/src/verifylib.rs000064400000000000000000000525761046102023000143650ustar 00000000000000//! A tool to be used by the client to perform verification on the final product. use std::{ collections::HashMap, fs, path::{Path, PathBuf}, }; use glob::glob; use log::{debug, info, warn}; use crate::{ crypto::{KeyId, PublicKey}, models::{ step::Step, supply_chain_item::SupplyChainItem, LayoutMetadata, LinkMetadata, LinkMetadataBuilder, Metablock, MetadataWrapper, }, rulelib::apply_rules_on_link, runlib::in_toto_run, }; use crate::{Error, Result}; /// verify_layout_signatures can verify the layout wrapped in a Metablock with given /// set of public keys. If verification fails, an error occurs. fn verify_layout_signatures( layout: &Metablock, layout_keys: &HashMap, ) -> Result { layout.verify(layout_keys.len() as u32, layout_keys.values()) } /// verify_layout_expiration will verify whether the layout has expired fn verify_layout_expiration(layout: &LayoutMetadata) -> Result<()> { let time = layout.expires; let now = chrono::Utc::now(); if time < now { return Err(Error::VerificationFailure("layout expired".to_string())); } Ok(()) } /// load content from path to a Metablock fn load_linkfile(path: &PathBuf) -> Result { let content = fs::read_to_string(path)?; let meta = serde_json::from_str(&content)?; Ok(meta) } /// Match signer's key id and metablock's signatures, if one of the /// signatures is signed with the signer's key, insert it into the /// given links_per_step map. fn match_signatures( link_metablock: Metablock, signer_short_key_id: &str, links_per_step: &mut HashMap, ) { for sig in &link_metablock.signatures { if sig.key_id().prefix() == signer_short_key_id { links_per_step.insert(sig.key_id().clone(), link_metablock); break; } } } /// load_links_for_layout will load Metablock from disk, /// return a map containing the Metablocks. /// The returned value is a nested HashMap /// * step-name => (key-id => Metablock) fn load_links_for_layout( layout: &LayoutMetadata, link_dir: &str, ) -> Result>> { let mut steps_links_metadata = HashMap::new(); for step in &layout.steps { let mut links_per_step = HashMap::new(); let pattern = format!("{}.????????.link", step.name); let mut path_pattern = PathBuf::from(link_dir); path_pattern.push(pattern); let path_pattern = path_pattern.to_str().ok_or_else(|| { Error::VerificationFailure(format!( "Pathbuf convert to str failed: {:?}", path_pattern )) })?; let matched_files = glob(path_pattern).map_err(|e| { Error::VerificationFailure(format!("Path glob error: {}", e)) })?; for link_path in matched_files.flatten() { // load link from the disk, canbe either a linkfile or a layout file let link_metablock = load_linkfile(&link_path)?; // Get the key-id that signed this link file let signer_short_key_id = link_path .file_name() .ok_or_else(|| { Error::VerificationFailure( "link_file name get failed.".into(), ) })? .to_str() .ok_or_else(|| { Error::VerificationFailure( "link_file name get failed.".into(), ) })? .to_string(); // by trim filename's start ." and end ".link" let signer_short_key_id = signer_short_key_id .trim_end_matches(".link") .trim_start_matches(&step.name) .trim_start_matches('.'); match_signatures( link_metablock, signer_short_key_id, &mut links_per_step, ); } let lins_per_step_len = links_per_step.len(); if lins_per_step_len < step.threshold as usize { return Err(Error::VerificationFailure(format!( "Step {} requires {} link metadata file(s), found {}", step.name, step.threshold, lins_per_step_len ))); } else { steps_links_metadata.insert(step.name.clone(), links_per_step); } } Ok(steps_links_metadata) } /// Verify given step's links' signature, and checkout whether /// at least "threshold" signatures are validated. Returns /// validated metadata. fn verify_link_signature_thresholds_step( step: &Step, links: &HashMap, pubkeys: &HashMap, ) -> Result> { let mut metablocks = HashMap::new(); // Get all links for the given step, verify them, and record the good // links in the HashMap. for (signer_key_id, link_metablock) in links { // For each link corresponding to a step, check that the signer key was // authorized by checking whether it's included in the layout. // Only good links are stored, to verify thresholds. // The sign key of the link is not authorized in the layout if let Some(authorized_key) = pubkeys.get(signer_key_id) { let authorized_key = vec![authorized_key]; if link_metablock.verify(1, authorized_key).is_ok() { metablocks .insert(signer_key_id.clone(), link_metablock.clone()); } } // in-toto v0.9's signature doesn't have a cert field, // thus no cert relative operations will be performed. } if metablocks.len() < step.threshold as usize { return Err(Error::VerificationFailure( format!( "step '{}' requires {} link metadata file(s). {} out of {} available link(s) have a valid signature from an authorized signer", step.name, step.threshold, metablocks.len(), links.len(), ) )); } Ok(metablocks) } /// verify_link_signature_thresholds will verify links' signature /// and check whether link file number meets each step's threshold. /// Returns only validated link files. fn verify_link_signature_thresholds( layout: &LayoutMetadata, steps_links_metadata: HashMap>, ) -> Result>> { let mut metadata_verified = HashMap::new(); for step in &layout.steps { // Verify this single step, return verified links. let metadata_per_step_verified = verify_link_signature_thresholds_step( step, steps_links_metadata .get(&step.name) .unwrap_or(&HashMap::new()), &layout.keys, )?; metadata_verified.insert(step.name.clone(), metadata_per_step_verified); } Ok(metadata_verified) } /// verify_sublayouts will check if any step has been /// delegated by the functionary, recurses into the delegation and /// replaces the layout object in the chain_link_dict by an /// equivalent link object. fn verify_sublayouts( layout: &LayoutMetadata, chain_link_dict: HashMap>, link_dir: &str, ) -> Result>> { let mut steps_link_metadata = HashMap::new(); for (step_name, key_link_dict) in chain_link_dict { let mut link_per_step = HashMap::new(); for (keyid, link) in &key_link_dict { let link_metadata = match &link.metadata { MetadataWrapper::Layout(_) => { // If it's a layout, go ahead. debug!("Verifying sublayout {}...", step_name); let mut layout_key_dict = HashMap::new(); let pubkey = layout.keys.get(keyid).ok_or_else(|| { Error::VerificationFailure(format!( "Can not find public key {:?}", keyid )) })?; layout_key_dict.insert(keyid.to_owned(), pubkey.clone()); let sub_link_dir = format!("{step_name}.{}", keyid.prefix()); let sublayout_link_dir_path = Path::new(link_dir).join(&sub_link_dir); let sublayout_link_dir_path = sublayout_link_dir_path.to_str().ok_or_else(|| { Error::VerificationFailure(format!( "failed to convert dir {} in {}", &sub_link_dir, link_dir )) })?; let summary_link = in_toto_verify( link, layout_key_dict, sublayout_link_dir_path, Some(&step_name), )?; match summary_link.metadata { MetadataWrapper::Layout(_) => { panic!("unexpected layout") } MetadataWrapper::Link(inner) => inner, } } MetadataWrapper::Link(inner) => inner.clone(), }; link_per_step.insert(keyid.clone(), link_metadata); } steps_link_metadata.insert(step_name.clone(), link_per_step); } Ok(steps_link_metadata) } /// verify_all_steps_command_alignment will iteratively check if all /// expected commands as defined in the Steps of a Layout align with /// the actual commands as recorded in the Link metadata. fn verify_all_steps_command_alignment( layout: &LayoutMetadata, link_files: &HashMap>, ) -> Result<()> { for step in &layout.steps { let expected_command = &step.expected_command; let key_link_dict = link_files.get(&step.name).ok_or_else(|| { Error::VerificationFailure(format!( "can not find LinkMetadata of step {}", step.name )) })?; for link in key_link_dict.values() { let command = &link.command; if *command != *expected_command { warn!( "Run command {:?} different from expected command {:?}", command, expected_command ); } } } Ok(()) } /// verify_threshold_constraints will verify that all links /// corresponding to a given step report the same materials /// and products. fn verify_threshold_constraints( layout: &LayoutMetadata, link_files: &HashMap>, ) -> Result<()> { for step in &layout.steps { if step.threshold <= 1 { info!( "Skipping threshold verification for step '{}' with threshold {}.", step.name, step.threshold ); continue; } let key_link_per_step = link_files.get(&step.name).ok_or_else(|| { Error::VerificationFailure(format!( "step {} does not have validated links.", step.name )) })?; if key_link_per_step.len() < step.threshold as usize { return Err(Error::VerificationFailure(format!( "step {} does not be performed by enough functionaries.", step.name ))); } let reference_keyid = key_link_per_step.keys().next().ok_or_else(|| { Error::VerificationFailure(format!( "step {} does not have enough key ids.", step.name )) })?; let reference_link = &key_link_per_step[reference_keyid]; for link in key_link_per_step.values() { if link.materials != reference_link.materials || link.products != reference_link.products { return Err(Error::VerificationFailure(format!( "Links {} have different artifacts.", link.name ))); } } } Ok(()) } /// reduce_chain_links will iterates through the passed /// chain_link_dict and builds a dict with step-name as /// keys and link objects as values. We already check if /// the links of different functionaries are identical. fn reduce_chain_links( link_files: HashMap>, ) -> Result> { let mut res = HashMap::new(); link_files.iter().try_for_each(|(k, v)| -> Result<()> { res.insert( k.clone(), v.values() .last() .ok_or_else(|| { Error::VerificationFailure(format!( "step {} does not have enough LinkMetadata.", k, )) })? .clone(), ); Ok(()) })?; Ok(res) } /// verify_all_item_rules will iteratively verify artifact rules /// of passed steps. fn verify_all_item_rules( steps: &Vec>, reduced_link_files: &HashMap, ) -> Result<()> { for step in steps { apply_rules_on_link(step, reduced_link_files)?; } Ok(()) } /// run_all_inspections will extracts all inspections from a passed /// Layout's inspect field and iteratively run each command defined /// in the Inspection's `run` field using `runlib::in_toto_run`, which /// returns a Metablock object containing a Link object. fn run_all_inspections( layout: &LayoutMetadata, ) -> Result> { let material_paths = ["."]; let product_paths = ["."]; let mut inspection_links = HashMap::new(); for inspect in &layout.inspect { let cmd_args: Vec<&str> = inspect.run.as_ref().iter().map(|arg| &arg[..]).collect(); let metablock = in_toto_run( inspect.name(), Some("."), &material_paths, &product_paths, &cmd_args, None, None, None, )?; // dump the metadata let filename = format!("{}.link", inspect.name()); std::fs::write(filename, serde_json::to_string_pretty(&metablock)?)?; // record in the hashmap let link_metadata = match metablock.metadata { MetadataWrapper::Layout(_) => panic!("Unexpected layout."), MetadataWrapper::Link(inner) => inner, }; inspection_links.insert(inspect.name().to_string(), link_metadata); } Ok(inspection_links) } fn get_summary_link( layout: &LayoutMetadata, reduced_link_files: &HashMap, name: &str, ) -> Result { let builder = LinkMetadataBuilder::new(); let link_metadata = if layout.steps.is_empty() { builder.build()? } else { builder .materials( reduced_link_files[layout.steps[0].name()].materials.clone(), ) .products( reduced_link_files[layout.steps[layout.steps.len() - 1].name()] .products .clone(), ) .byproducts( reduced_link_files[layout.steps[layout.steps.len() - 1].name()] .byproducts .clone(), ) .command( reduced_link_files[layout.steps[layout.steps.len() - 1].name()] .command .clone(), ) .name(name.to_string()) .build()? }; Metablock::new(MetadataWrapper::Link(link_metadata), &[]) } /// in_toto_verify can be used to verify an entire software supply chain according to /// the in-toto specification v0.9. It requires the metadata of the root layout, a map /// that contains public keys to verify the root layout signatures, a path to a /// directory from where it can load link metadata files, which are treated as /// signed evidence for the steps defined in the layout, a step name, and a /// parameter dictionary used for parameter substitution. The step name only /// matters for sublayouts, where it's important to associate the summary of that /// step with a unique name. The verification routine is as follows: /// /// 1. Verify layout signature(s) using passed key(s) /// 2. Verify layout expiration date /// 3. Load link metadata files for steps of layout /// 4. Verify signatures and signature thresholds for steps of layout /// 5. Verify sublayouts recursively /// 6. Verify command alignment for steps of layout (only warns) /// 7. Verify artifact rules for steps of layout /// 8. Execute inspection commands (generates link metadata for each inspection) /// 9. Verify artifact rules for inspections of layout /// /// in_toto_verify returns a summary link wrapped in a Metablock object or an error. /// If any of the verification routines fail, verification is aborted and error is /// returned. /// /// # Parameters /// * `layout`: The LayoutMetadata wrapped in a Metablock. /// * `layout_keys`: A `key_id` to `Pubkey` map defined in layout. /// * `link_dir`: The directory where link files are stored. /// * `step_name`(Optional): A name assigned to the returned link. This is mostly /// useful during recursive sublayout verification. /// /// # Side-Effects /// * I/O: Read link files from the disk. /// * Process: Run commands using subprocess. /// /// # Return Value /// * A LinkMetadata which summarizes the materials /// and products of the whole software supply chain. pub fn in_toto_verify( layout: &Metablock, layout_keys: HashMap, link_dir: &str, step_name: Option<&str>, ) -> Result { // Verify layout signature(s) using passed key(s) and // judge whether the Metablock has layout inside let layout = match verify_layout_signatures(layout, &layout_keys)? { MetadataWrapper::Layout(inner) => inner, _ => { return Err(Error::IllegalArgument( "The input Metablock is not a layout.".to_string(), )) } }; // Verify layout expiration date verify_layout_expiration(&layout)?; // Load metadata files for steps of layout let steps_links_metadata = load_links_for_layout(&layout, link_dir)?; // Verify signatures and signature thresholds for steps of layout let link_files = verify_link_signature_thresholds(&layout, steps_links_metadata)?; // Verify sublayouts recursively let link_files = verify_sublayouts(&layout, link_files, link_dir)?; // Verify command alignment for steps of layout (only warns) verify_all_steps_command_alignment(&layout, &link_files)?; // Verify threshold verify_threshold_constraints(&layout, &link_files)?; // Reduce link files let mut reduced_link_files = reduce_chain_links(link_files)?; let steps = layout .steps .iter() .map(|step| Box::new(step.clone()) as Box) .collect(); // Verify artifact rules for steps of layout verify_all_item_rules(&steps, &reduced_link_files)?; // Execute inspection commands (generates link metadata for each inspection) let inspection_link_files = run_all_inspections(&layout)?; reduced_link_files.extend(inspection_link_files); let inspects = layout .inspect .iter() .map(|step| Box::new(step.clone()) as Box) .collect(); // Verify artifact rules for inspections of layout verify_all_item_rules(&inspects, &reduced_link_files)?; get_summary_link(&layout, &reduced_link_files, step_name.unwrap_or("")) } #[cfg(test)] mod tests { use std::{collections::HashMap, fs, str::FromStr}; use crate::{ crypto::{KeyId, PublicKey, SignatureScheme}, error::Error::VerificationFailure, models::Metablock, }; use std::path::Path; use super::in_toto_verify; #[test] fn verify_demo() { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); let working_dir = Path::new(&manifest_dir) .join("tests") .join("test_verifylib") .join("workdir"); let root_layout_file = working_dir.join("root.layout"); let raw = fs::read(root_layout_file).expect("read layout failed"); let layout = serde_json::from_slice::(&raw) .expect("deserialize metablock failed"); let public_key_file = working_dir.join("alice.pub"); let public_key_string = fs::read_to_string(public_key_file) .expect("read public key failed"); let pem = pem::parse(public_key_string).expect("parse pem failed"); let pub_key = PublicKey::from_spki( pem.contents(), SignatureScheme::RsaSsaPssSha256, ) .expect("create public key failed"); let key_id = KeyId::from_str( "556caebdc0877eed53d419b60eddb1e57fa773e4e31d70698b588f3e9cc48b35", ) .expect("key id parse failed"); let layout_keys = HashMap::from([(key_id, pub_key)]); let result = in_toto_verify(&layout, layout_keys, "../links", None); match result { Ok(_) => {} Err(VerificationFailure(msg)) => assert!(msg == "layout expired"), Err(error) => panic!("{}", error), } } } in-toto-0.4.0/tests/ecdsa/ec000064400000000000000000000001711046102023000137500ustar 000000000000000w  MJ'S2n;fO5`c *H=DBe;84RdT?`PlЃS(Bqw" D jWSEhݖ-J 8HIk\';"b;XB%ӌFein-toto-0.4.0/tests/ed25519/ed25519-1.pk8.der000064400000000000000000000001251046102023000157050ustar 000000000000000S0+ep" <@MlLN>hݖ-J 8HI#!k\';"b;XB%ӌFein-toto-0.4.0/tests/ed25519/ed25519-1.pub000064400000000000000000000000401046102023000152140ustar 00000000000000k\';"b;XB%ӌFein-toto-0.4.0/tests/ed25519/ed25519-1.spki.der000064400000000000000000000000561046102023000161540ustar 000000000000000,0+ep!k\';"b;XB%ӌFein-toto-0.4.0/tests/ed25519/ed25519-2.pk8.der000064400000000000000000000001251046102023000157060ustar 000000000000000S0+ep" p[gN2;¤<=Oۓ9!u:\0|,S@~38d'It){2+(Aց X -EFYX0djk W`)shhc jh<%/|آ[E{g̺s~ךEd..Y!9–*hc<{m}aw3,ճ/``i: k Æ[jCҢȍ2*lc~.6 ÏWJ]7Ck'C]KWAF{C℩wѨLAղzm8^?BK!S&r^7mq؂P_NMo^hJ,麼whvCAn!JԱ-5W$?A;I̢sDT($̅_]ow\Icz'ww9J;}qֽ]s{zn_$ςRM#Ytu5;8sQ/MT(-^D"LpﻅO0PqHͯr y44L91|5U+sw!/Yrn!D 2AIs5Ƞ=ԍJ!?.&7J I1n){8x{,..,v|/]>yrEnX]:BrgPC&"ɱH'{4cR2ɛ3 ܀;?1 w!)ԉ9yk$ZUQ^*b;&:/n8ۯ0]k"PmqXJ0&>;2S +e=|eK̀/Bmf?5c!Sຬ%}lH,1!>l$3XJ^^xvJ~m#H{Cƺ+)5\9A͚&yU5L:@hnZxsF (͟y:XV"g]`_6*#$ 3;S\nF qO> iJ:Gӈ5Ӱ%K1WMz#NIN(4 9ё_R6Mxin-toto-0.4.0/tests/rsa/rsa-2048.der000064400000000000000000000022521046102023000150220ustar 000000000000000ύ9!5XE) 8[Mwϧ2w(@C #u6 \xDcI2:dה/ 6o,LcxAz0U#\s9ɚIFeol5NVuH>A;|oK3}qZ[ATL##B*1B` cϗm ҙ<1\c35חcc#+p(8dz)*B^ΖcfPx6q)SUhC =p%Pm:Ij8?ZxݙOMd\O"aĔb?Fa|zmdžv\%WOW@TER1~wW"1d1ҿ0oU0Z\ Iϲ6^ϡMETh~>5[237Va>ﺳn,p+Pl1nMPΉ`ŭ rW]U}S! ]`7]?rR#P 9vpukB͐_ JBG{mKm:RM\\ў1 bsR^;dh6 5i`6 kv0)hHy w"cqxЫ[ٳ |zVN IBO;N.b} WFLTS $1L"X9>N 5r xUE4N UwPhtr_5O0qHFbuS V>_BwBKoߜx=I,@F&6\S+lPG#Cv%^mםB-QE|T,x6 Z;ήms<;f ^J!ە^~x :viܑŋin-toto-0.4.0/tests/rsa/rsa-2048.pk8.der000064400000000000000000000023041046102023000155210ustar 0000000000000000  *H 0ύ9!5XE) 8[Mwϧ2w(@C #u6 \xDcI2:dה/ 6o,LcxAz0U#\s9ɚIFeol5NVuH>A;|oK3}qZ[ATL##B*1B` cϗm ҙ<1\c35חcc#+p(8dz)*B^ΖcfPx6q)SUhC =p%Pm:Ij8?ZxݙOMd\O"aĔb?Fa|zmdžv\%WOW@TER1~wW"1d1ҿ0oU0Z\ Iϲ6^ϡMETh~>5[237Va>ﺳn,p+Pl1nMPΉ`ŭ rW]U}S! ]`7]?rR#P 9vpukB͐_ JBG{mKm:RM\\ў1 bsR^;dh6 5i`6 kv0)hHy w"cqxЫ[ٳ |zVN IBO;N.b} WFLTS $1L"X9>N 5r xUE4N UwPhtr_5O0qHFbuS V>_BwBKoߜx=I,@F&6\S+lPG#Cv%^mםB-QE|T,x6 Z;ήms<;f ^J!ە^~x :viܑŋin-toto-0.4.0/tests/rsa/rsa-2048.pkcs1.der000064400000000000000000000004161046102023000160420ustar 000000000000000 ύ9!5XE) 8[Mwϧ2w(@C #u6 \xDcI2:dה/ 6o,LcxAz0U#\s9ɚIFeol5NVuH>A;|oin-toto-0.4.0/tests/rsa/rsa-2048.spki.der000064400000000000000000000004461046102023000157720ustar 000000000000000"0  *H 0 ύ9!5XE) 8[Mwϧ2w(@C #u6 \xDcI2:dה/ 6o,LcxAz0U#\s9ɚIFeol5NVuH>A;|oin-toto-0.4.0/tests/rsa/rsa-4096.der000064400000000000000000000044541046102023000150350ustar 000000000000000 (_ζ84>J9wy=Ym%F^ȨwMNme,F&=],dwKcɏ4 pl?mIXw枈D/#y҂X4`A?7-8$M6|tl Ǜ 'hszdk2EQwrوEK$)ZT& ZDg4a=2(Ձ_~NMpiw!cGM.NBBR-,NI]N%b#w_wize7๜Tl9RK-{Kw`]A*8od,x `G?6.l2CŇy.ZHOSCLfu79/k0 #iԫ!ۻ&W'T`ɸxmvZn¿ yӹsFh*go™ E1~e3T[ s%\\B[ذ'Yl l#'lY&ZV?Sd$kP ;!8&2`?-A 튆Hi%$=+y__V$}=~sUqnf<1We/Z>c2+NzwM˕a5 6?.^WLjpE5k+c:Y%P,}*ƞE1ЂDțIDot jJo֭2>k&x׷FQ.s ط0-3y+y\Ռh"{ `b>'{ [WD`q;.F-miL^|An_&A^@V,)(D;[EBT ƚˤOT BcAX&%mBDjHEكg$bkC= ^n㧦V s_5e<+r)>FްJF{Q;mdvW+lu_v7?g5'W´G4qE L$$M+y&#ζh 0=KR$GlDSxDlFsKCk2-lv @=^ ?Koνi%H1Q kqOS|F]5d[|f N[f%l h᪗q [64hrF7 '8⮀?b5 V&4lJI}rvT"oJJjJ,z/* @K VT2=e3̣hkxié fo$- Wf^nv {5V2a+N_!86u`ri*CVfSZѦ5D;.+zu[b~N8}Az˘tӳòodKQq}~{2$FwS bЭat(_W9τ aYap8OS_eB=Axh-c.KPi)ڔ"ZƟS$e0 hݸ6WG1 ,SzIcO^/EW/B w[.:r4rL"+?%CYd6>&>V, *;e>k?Iz`8xnDo9!tjXJS >iiAmSJQV|uϯ''g%U *NA5<R3}ccͱ2e~ήj{[JQcb+yxA*e72?{J9wy=Ym%F^ȨwMNme,F&=],dwKcɏ4 pl?mIXw枈D/#y҂X4`A?7-8$M6|tl Ǜ 'hszdk2EQwrوEK$)ZT& ZDg4a=2(Ձ_~NMpiw!cGM.NBBR-,NI]N%b#w_wize7๜Tl9RK-{Kw`]A*8od,x `G?6.l2CŇy.ZHOSCLfu79/k0 #iԫ!ۻ&W'T`ɸxmvZn¿ yӹsFh*go™ E1~e3T[ s%\\B[ذ'Yl l#'lY&ZV?Sd$kP ;!8&2`?-A 튆Hi%$=+y__V$}=~sUqnf<1We/Z>c2+NzwM˕a5 6?.^WLjpE5k+c:Y%P,}*ƞE1ЂDțIDot jJo֭2>k&x׷FQ.s ط0-3y+y\Ռh"{ `b>'{ [WD`q;.F-miL^|An_&A^@V,)(D;[EBT ƚˤOT BcAX&%mBDjHEكg$bkC= ^n㧦V s_5e<+r)>FްJF{Q;mdvW+lu_v7?g5'W´G4qE L$$M+y&#ζh 0=KR$GlDSxDlFsKCk2-lv @=^ ?Koνi%H1Q kqOS|F]5d[|f N[f%l h᪗q [64hrF7 '8⮀?b5 V&4lJI}rvT"oJJjJ,z/* @K VT2=e3̣hkxié fo$- Wf^nv {5V2a+N_!86u`ri*CVfSZѦ5D;.+zu[b~N8}Az˘tӳòodKQq}~{2$FwS bЭat(_W9τ aYap8OS_eB=Axh-c.KPi)ڔ"ZƟS$e0 hݸ6WG1 ,SzIcO^/EW/B w[.:r4rL"+?%CYd6>&>V, *;e>k?Iz`8xnDo9!tjXJS >iiAmSJQV|uϯ''g%U *NA5<R3}ccͱ2e~ήj{[JQcb+yxA*e72?{J9wy=Ym%F^ȨwMNme,F&=],dwKcɏ4 pl?mIXw枈D/#y҂X4`A?7-8$M6|tl Ǜ 'hszdk2EQwrوEK$)ZT& ZDg4a=2(Ձ_~NMpiw!cGM.NBBR-,NI]N%b#w_wize7๜Tl9RK-{Kw`]A*8od,x `G?6.l2CŇy.ZHOSCLfu79/k0 #iԫ!ۻ&W'T`ɸxmvZn¿ yӹsFh*go™ E1~e3T[ s%\\B[ذ'Yl l#'lY&ZV?Sd$in-toto-0.4.0/tests/rsa/rsa-4096.spki.der000064400000000000000000000010461046102023000157740ustar 000000000000000"0  *H 0 _ζ84>J9wy=Ym%F^ȨwMNme,F&=],dwKcɏ4 pl?mIXw枈D/#y҂X4`A?7-8$M6|tl Ǜ 'hszdk2EQwrوEK$)ZT& ZDg4a=2(Ձ_~NMpiw!cGM.NBBR-,NI]N%b#w_wize7๜Tl9RK-{Kw`]A*8od,x `G?6.l2CŇy.ZHOSCLfu79/k0 #iԫ!ۻ&W'T`ɸxmvZn¿ yӹsFh*go™ E1~e3T[ s%\\B[ذ'Yl l#'lY&ZV?Sd$in-toto-0.4.0/tests/runlib.rs000064400000000000000000000147531046102023000142330ustar 00000000000000use in_toto::{ crypto::{KeyType, PrivateKey, SignatureScheme}, interchange::Json, models::{byproducts::ByProducts, LinkMetadataBuilder, VirtualTargetPath}, runlib::in_toto_run, }; use std::fs::{canonicalize, write}; use std::os::unix::fs; use tempfile::tempdir; #[macro_use] extern crate lazy_static; lazy_static! { pub static ref TEST_KEY: Vec = PrivateKey::new(KeyType::Ed25519).unwrap(); pub static ref TEST_PRIVATE_KEY: PrivateKey = PrivateKey::from_pkcs8( &PrivateKey::new(KeyType::Ed25519).unwrap(), SignatureScheme::Ed25519 ) .unwrap(); } /* TODO ERRORS - Signature Values don't match up - "IllegalArgument("Cannot start with \'/\'")', tests/runlib.rs:38:5" error -> workaround added using tempdir */ // Default link generated Metablock like step_name and default key /* Test Cases - in_toto_run_record_file - in_toto_run_record_new_file - in_toto_run_record_modified_file (TODO) - in_toto_run_record_symlink_file (TODO) - in_toto_run_record_symlink_cycle (TODO) - in_toto_run_handle_nonexistent_materials (TODO) - in_toto_run_test_key_signature (TODO) - One test where things *fail* */ #[test] fn in_toto_run_record_file() { // Initialization let dir = tempdir().unwrap(); let dir_canonical = canonicalize(dir.path()).unwrap(); let dir_path = dir_canonical.to_str().unwrap(); // Create file write(format!("{}/foo.txt", dir_path), "lorem ipsum").unwrap(); print!("Path: {}\n", dir_path); // Expected value let byproducts = ByProducts::new() .set_return_value(0) .set_stderr(String::from("")) .set_stdout(String::from("in_toto says hi\n")); let expected = LinkMetadataBuilder::new() .name(String::from("test")) .byproducts(byproducts) .add_material( VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), ) .add_product( VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), ) .signed::(&TEST_PRIVATE_KEY) .unwrap(); // Result value let result = in_toto_run( "test", None, &vec![dir_path], &vec![dir_path], &["sh", "-c", "echo 'in_toto says hi'"], Some(&TEST_PRIVATE_KEY), None, None, ) .unwrap(); assert_eq!(expected, result); // Clean-up dir.close().unwrap(); } #[test] fn in_toto_run_record_new_file() { // Initialization let dir = tempdir().unwrap(); let dir_canonical = canonicalize(dir.path()).unwrap(); let dir_path = dir_canonical.to_str().unwrap(); // Create file write(format!("{}/foo.txt", dir_path), "lorem ipsum").unwrap(); print!("Path: {}\n", dir_path); // Result Value let result = in_toto_run( "test", None, &vec![dir_path], &vec![dir_path], &[ "sh", "-c", &format!("echo 'in_toto says hi' >> {}/bar.txt", dir_path), ], Some(&TEST_PRIVATE_KEY), None, None, ) .unwrap(); let byproducts = ByProducts::new() .set_return_value(0) .set_stderr(String::from("")) .set_stdout(String::from("")); // Expected value let expected = LinkMetadataBuilder::new() .name(String::from("test")) .add_material( VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), ) .add_product( VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), ) .add_product( VirtualTargetPath::new(format!("{}/bar.txt", dir_path)).unwrap(), ) .byproducts(byproducts) .signed::(&TEST_PRIVATE_KEY) .unwrap(); assert_eq!(expected, result); // Clean-up work dir.close().unwrap(); } #[test] fn in_toto_run_new_line_in_stdout() { // Initialization let dir = tempdir().unwrap(); let dir_canonical = canonicalize(dir.path()).unwrap(); let dir_path = dir_canonical.to_str().unwrap(); // Create file write(format!("{}/foo.txt", dir_path), "lorem ipsum").unwrap(); let byproducts = ByProducts::new() .set_return_value(0) .set_stderr(String::from("")) .set_stdout(String::from("Cloning into 'some-project'...\n")); let link = LinkMetadataBuilder::new() .name(String::from("test")) .add_material( VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), ) .add_product( VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), ) .byproducts(byproducts) .signed::(&TEST_PRIVATE_KEY) .unwrap(); assert!(link.verify(1, [TEST_PRIVATE_KEY.public()]).is_ok()); // Clean-up work dir.close().unwrap(); } #[test] fn in_toto_run_record_modified_file() { // TODO } #[test] fn in_toto_run_record_symlink_file() { // Initialization let dir = tempdir().unwrap(); let dir_canonical = canonicalize(dir.path()).unwrap(); let dir_path = dir_canonical.to_str().unwrap(); // Create symlink file write(format!("{}/foo.txt", dir_path), "lorem ipsum").unwrap(); fs::symlink( format!("{}/foo.txt", dir_path), format!("{}/symfile.txt", dir_path), ) .unwrap(); print!("Path: {}\n", dir_path); let byproducts = ByProducts::new() .set_return_value(0) .set_stderr(String::from("")) .set_stdout(String::from("in_toto says hi\n")); // Expected value let expected = LinkMetadataBuilder::new() .name(String::from("test")) .add_material( VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), ) .add_material( VirtualTargetPath::new(format!("{}/symfile.txt", dir_path)) .unwrap(), ) .add_product( VirtualTargetPath::new(format!("{}/foo.txt", dir_path)).unwrap(), ) .add_product( VirtualTargetPath::new(format!("{}/symfile.txt", dir_path)) .unwrap(), ) .byproducts(byproducts) .signed::(&TEST_PRIVATE_KEY) .unwrap(); // Result Value let result = in_toto_run( "test", None, &vec![dir_path], &vec![dir_path], &["sh", "-c", "echo 'in_toto says hi'"], Some(&TEST_PRIVATE_KEY), None, None, ) .unwrap(); assert_eq!(expected, result); // Clean-up work dir.close().unwrap(); } #[test] fn in_toto_run_record_symlink_cycle() { // TODO } in-toto-0.4.0/tests/test_link/foo.tar.gz000064400000000000000000000002241046102023000162640ustar 00000000000000tX= @ԞbZ ^" ː&>0S|s>NnSw67V8uk/5 趵QZ%i鮞Ÿo7^NcJY.C%~ G(in-toto-0.4.0/tests/test_metadata/demo.layout000064400000000000000000000103331046102023000173620ustar 00000000000000{ "signatures": [ { "keyid": "64786e5921b589af1ca1bf5767087bf201806a9b3ce2e6856c903682132bd1dd", "sig": "0c2c5bb8fb58ccbb644e17bfbda0b754cc13f71ddb5ae4be1fff7ad7ec5c94543bec3818b0c45c4a9dd17545382b4ec6d9fcc71366be08c131505981ca415d04" } ], "signed": { "_type": "layout", "expires": "1970-01-01T00:00:00Z", "readme": "", "keys": { "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": { "keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyid_hash_algorithms": [ "sha256", "sha512" ], "keyval": { "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-----" } }, "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": { "keytype": "ed25519", "scheme": "ed25519", "keyval": { "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6" } } }, "steps": [ { "_type": "step", "name": "write-code", "threshold": 1, "expected_materials": [], "expected_products": [ [ "CREATE", "foo.py" ] ], "pubkeys": [ "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554" ], "expected_command": [ "vi" ] }, { "_type": "step", "name": "package", "threshold": 1, "expected_materials": [ [ "MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code" ] ], "expected_products": [ [ "CREATE", "foo.tar.gz" ] ], "pubkeys": [ "3e26343b3a7907b5652dec86222e8fd60e456ebbb6fe4875a1f4281ffd5bd9ae" ], "expected_command": [ "tar", "zcvf", "foo.tar.gz", "foo.py" ] } ], "inspect": [ { "_type": "inspection", "name": "inspect_tarball", "expected_materials": [ [ "MATCH", "foo.tar.gz", "WITH", "PRODUCTS", "FROM", "package" ] ], "expected_products": [ [ "MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code" ] ], "run": [ "inspect_tarball.sh", "foo.tar.gz" ] } ] } }in-toto-0.4.0/tests/test_metadata/demo.link000064400000000000000000000011051046102023000167770ustar 00000000000000{ "signed": { "_type": "link", "name": "", "materials": {}, "products": { "tests/test_link/foo.tar.gz": { "sha256": "52947cb78b91ad01fe81cd6aef42d1f6817e92b9e6936c1e5aabb7c98514f355" } }, "byproducts": { "return-value": 0, "stderr": "a foo.py\n", "stdout": "" }, "command": ["tar", "zcvf", "foo.tar.gz", "foo.py"], "environment": null }, "signatures": [] }in-toto-0.4.0/tests/test_metadata/owner.der000064400000000000000000000001001046102023000170140ustar 00000000000000P9 *IJG9\:BhUm̲6DȩQhzin-toto-0.4.0/tests/test_prefix/left/world000064400000000000000000000000141046102023000167130ustar 00000000000000lorem ipsum in-toto-0.4.0/tests/test_prefix/right/world000064400000000000000000000000141046102023000170760ustar 00000000000000lorem ipsum in-toto-0.4.0/tests/test_runlib/.hidden/.bar000064400000000000000000000000041046102023000167620ustar 00000000000000foo in-toto-0.4.0/tests/test_runlib/.hidden/foo000064400000000000000000000000041046102023000167230ustar 00000000000000bar in-toto-0.4.0/tests/test_runlib/hello./world000064400000000000000000000000141046102023000171400ustar 00000000000000lorem ipsum in-toto-0.4.0/tests/test_verifylib/links/clone.776a00e2.link000064400000000000000000000024121046102023000215620ustar 00000000000000{ "signatures": [ { "keyid": "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5", "sig": "3b77aba17d665ae408a9df69ef15ee5279d47637dee60b9fea69dc09c08f858fe12ad4c416c1558be3eb09397c3f9ece2a6d0743322d3861cf8bd0827494a0163d4f26590e9ad9a7684ec211daade7f71a64a896f95088bd05401aad50f0b1d8991b8bd25a55117a1493569f12878078ce3dc1242960dacb430b815ca42656e21e0b6df3e525028376e6c452b716221af857a28a6062643f08f37f1225e620182b9a3a8493121643ca7f21a71138cb7d8a5ae3be267680aef7e5de594a950d426bbbfdc3069762a7baec9f475773251b9dd6835f4173ed62340f8690c3e0db3e6435f29b730d2544cf96f1a3e47466ddad94bd745af6a19a04e9edb227866a8f6975abd76099adf5cb2ed54cdbad243e404671f3510be2ce9eed49cba8edbd03ba1b335e5180b78eac596d23021e542615db8607558839ad358001fab85ad0913381db70814388932e56bde067f92c94fc3ff8ff7eb3eb79b95a74eed8b28c716f83764020618214e2131367a7708260c13c3ede27853a9e2e185f1dd181ad0f" } ], "signed": { "_type": "link", "byproducts": { "return-value": 0, "stderr": "", "stdout": "" }, "command": [ "git", "clone", "https://github.com/in-toto/demo-project.git" ], "environment": {}, "materials": {}, "name": "clone", "products": { "demo-project/foo.py": { "sha256": "ebebf8778035e0e842a4f1aeb92a601be8ea8e621195f3b972316c60c9e12235" } } } }in-toto-0.4.0/tests/test_verifylib/links/package.2f89b927.link000064400000000000000000000026731046102023000221070ustar 00000000000000{ "signatures": [ { "keyid": "2f89b9272acfc8f4a0a0f094d789fdb0ba798b0fe41f2f5f417c12f0085ff498", "sig": "b61da0bacfe8970bfbb45dcb0809024a840cc426c71e3e72dd35dcf3b9703355ec386a70967fca2891cdbba49dcf8c78619f9ee3e26709b37b0fa657a48352608d70b38c5192ced88a7af5b15e3ea010d6516d7baa7f3cc0f39f4a69f47160d1983ecf916b891a340c0e084fbd73269a91e58397615de56990d046d2ad145583d6dc1c579aa63afc7285be3cbee227b399cb75477be9f29d1ce59886e9a1ee25a73cb40f253e044ad1d2ba57b552f9eb64087cdc0b5f603d787c9264bbe613c6121c5166f707d616ac8bb3a8f88425a6707d58df46c0b6a0e3138117dba20104750090aabb4e26e4ee1c5717e16fc811f1c2669e949287b1022cb2fdae89d6d60a3ac06bd298cede01e848b7d1aa55d2d0737a81ff0a5812fc099b62d26cf02af403938d897c170169e5240c297ff1ee006caefef7c658e44ae6f9f4babb9dc27e01c931af64200d4ddb8cc073b6738fb4dcf4aecb3b6da498645b4ada15e92a9db85424411505cb0643250118f1787b22904047662200eded168fc0d085bf94" } ], "signed": { "_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" } } } }in-toto-0.4.0/tests/test_verifylib/links/update-version.776a00e2.link000064400000000000000000000024041046102023000234300ustar 00000000000000{ "signatures": [ { "keyid": "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5", "sig": "60b8f0b42afbcbffda3ea9bfeb4e5bef67d2a6584607aa000dbc95dd7d607047487b619627d56ecb800d1fa6d9749e7f65415bd7117679786a0ab603b3746be3909442ab5607d5bb1cda8bebd951deccfd3cc1b2c11fe4fcfcbf4a2a7a6937339e22dbc61742c6200927a01a0abe798ba39ee2e3bdd39eb814d68072b852869cb557726686aa303b1dd6e4e4d60d46c468ae235120762144c22a6ffc30047dd66fb4e24feb28a1df79f9e0fbcc68228330cbb3f4e45e865aad30ffd49c9084ea81777e81ff5506ad34b7bf12b516ce7e133ea50f27d6848e9594bc35279fefcd80b180c18f3aaf498f599225a0cb54705affa86cdcb4bf25b36655a5d2aa77a5d932a576ced3f2d4fb052fe5a3db1020b6fb6e76e6a000c1757ac8fb2a2de7d42e3288e7afe03d2f780c7bcfbf4a21243dd963e0f3f7c6d17337fb112780de04d0275ef373ff1350d254e1528569cb9245ce8c2da7790a84327fcecad71d18f0561377213dae2ab2d694a8137f159435119c7f15dd9072b9ea13598bc01e6984" } ], "signed": { "_type": "link", "byproducts": {}, "command": [], "environment": {}, "materials": { "demo-project/foo.py": { "sha256": "ebebf8778035e0e842a4f1aeb92a601be8ea8e621195f3b972316c60c9e12235" } }, "name": "update-version", "products": { "demo-project/foo.py": { "sha256": "c2c0ea54fa94fac3a4e1575d6ed3bbd1b01a6d0b8deb39196bdc31c457ef731b" } } } }in-toto-0.4.0/tests/test_verifylib/workdir/alice.pub000064400000000000000000000011601046102023000206560ustar 00000000000000-----BEGIN PUBLIC KEY----- MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxPX3kFs/z645x4UOC3KF Y3V80YQtKrp6YS3qU+Jlvx/XzK53lb4sCDRU9jqBBx3We45TmFUibroMd8tQXCUS e8gYCBUBqBmmz0dEHJYbW0tYF7IoapMIxhRYn76YqNdl1JoRTcmzIaOJ7QrHxQrS GpivvTm6kQ9WLeApG1GLYJ3C3Wl4bnsI1bKSv55Zi45/JawHzTzYUAIXX9qCd3Io HzDucz9IAj9Ookw0va/q9FjoPGrRB80IReVxLVnbo6pYJfu/O37jvEobHFa8ckHd YxUIg8wvkIOy1O3M74lBDm6CVI0ZO25xPlDB/4nHAE1PbA3aF3lw8JGuxLDsetxm fzgAleVt4vXLQiCrZaLf+0cM97JcT7wdHcbIvRLsij9LNP+2tWZgeZ/hIAOEdaDq cYANPDIAxfTvbe9I0sXrCtrLer1SS7GqUmdFCdkdun8erXdNF0ls9Rp4cbYhjdf3 yMxdI/24LUOOQ71cHW3ITIDImm6I8KmrXFM2NewTARKfAgMBAAE= -----END PUBLIC KEY-----in-toto-0.4.0/tests/test_verifylib/workdir/demo-project.tar.gz000064400000000000000000000003141046102023000226100ustar 00000000000000ѻ @y%Dwce)h`/!WwJK 8t+aQ&]ڶ3:ycFZkSCyj{(JZU_h+\Sۓh2V}c~Qf;iv+A@[| (in-toto-0.4.0/tests/test_verifylib/workdir/root.layout000064400000000000000000000204171046102023000213210ustar 00000000000000{ "signatures": [ { "keyid": "556caebdc0877eed53d419b60eddb1e57fa773e4e31d70698b588f3e9cc48b35", "sig": "a2e420a830389fbe32761b203f87fb2521068a16c60c5780baac13e600b37ffc451b2f7b76546bde211e343ba670e4e87680e0cd2dce46cd8566c2bd23c2cb7c6f78bdd91b5f24480f260d9bdc9c0bb217ca1ba8869873f790d7d2663af42d821a2952951f411d760a38bc9e1367e7048a6c4cc7de245a5b14dfd98e3ed227460b5c305541d88c7f9300d71091211a80d1363f68ba090cd8cee7d552e49a0396b9aff863e562225886d0f33195141d21fb3b0f67ab6b800c40ccac073a354a79e6d797d0aa8b9436799da783e217a3d7527a891f0f2ebf04cd6c6430430e725c8a7b68b2d40978bfe20340374f5b0cbe001dd3c8e3589e9b988fac1c47917391884d4b6bbc83b7b9265eeb6f96764a82fca5a9ea4770d8096e563b43adac50f414b117f497c663d68b17faeac3d4535eeaa973b6cfe6e81b309f7e9c8ef05fceb9b748f712d0691f2ef7def3b0a858c8436b4752687861e69dc94ed3ae8b5023161c6201bdffcaa1e8286b83d3818bf0d3ba4383fefa4c699f493635813e51b0" } ], "signed": { "_type": "layout", "expires": "2022-12-02T18:41:07Z", "inspect": [ { "_type": "inspection", "expected_materials": [ [ "MATCH", "demo-project.tar.gz", "WITH", "PRODUCTS", "FROM", "package" ], [ "ALLOW", ".keep" ], [ "ALLOW", "alice.pub" ], [ "ALLOW", "root.layout" ], [ "DISALLOW", "*" ] ], "expected_products": [ [ "MATCH", "demo-project/foo.py", "WITH", "PRODUCTS", "FROM", "update-version" ], [ "ALLOW", "demo-project/.git/*" ], [ "ALLOW", "demo-project.tar.gz" ], [ "ALLOW", ".keep" ], [ "ALLOW", "alice.pub" ], [ "ALLOW", "root.layout" ], [ "DISALLOW", "*" ] ], "name": "untar", "run": [ "tar", "xzf", "demo-project.tar.gz" ] } ], "keys": { "2f89b9272acfc8f4a0a0f094d789fdb0ba798b0fe41f2f5f417c12f0085ff498": { "keyid": "2f89b9272acfc8f4a0a0f094d789fdb0ba798b0fe41f2f5f417c12f0085ff498", "keyid_hash_algorithms": [ "sha256", "sha512" ], "keytype": "rsa", "keyval": { "private": "", "public": "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAzgLBsMFSgwBiWTBmVsyW\n5KbJwLFSodAzdUhU2Bq6SdRz/W6UOBGdojZXibxupjRtAaEQW/eXDe+1CbKg6ENZ\nGt2D9HGFCQZgQS8ONgNDQGiNxgApMA0T21AaUhru0vEofzdN1DfEF4CAGv5AkcgK\nsalhTyONervFIjFEdXGelFZ7dVMV3Pp5WkZPG0jFQWjnmDZhUrtSxEtqbVghc3kK\nAUj9Ll/3jyi2wS92Z1j5ueN8X62hWX2xBqQ6nViOMzdujkoiYCRSwuMLRqzW2CbT\nL8hF1+S5KWKFzxl5sCVfpPe7V5HkgEHjwCILXTbCn2fCMKlaSbJ/MG2lW7qSY2Ro\nwVXWkp1wDrsJ6Ii9f2dErv9vJeOVZeO9DsooQ5EuzLCfQLEU5mn7ul7bU7rFsb8J\nxYOeudkNBatnNCgVMAkmDPiNA7E33bmL5ARRwU0iZicsqLQR32pmwdap8PjofxqQ\nk7Gtvz/iYzaLrZv33cFWWTsEOqK1gKqigSqgW9T26wO9AgMBAAE=\n-----END PUBLIC KEY-----" }, "scheme": "rsassa-pss-sha256" }, "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5": { "keyid": "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5", "keyid_hash_algorithms": [ "sha256", "sha512" ], "keytype": "rsa", "keyval": { "private": "", "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-----" }, "scheme": "rsassa-pss-sha256" } }, "readme": "", "steps": [ { "_type": "step", "expected_command": [ "git", "clone", "https://github.com/in-toto/demo-project.git" ], "expected_materials": [], "expected_products": [ [ "CREATE", "demo-project/foo.py" ], [ "DISALLOW", "*" ] ], "name": "clone", "pubkeys": [ "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5" ], "threshold": 1 }, { "_type": "step", "expected_command": [], "expected_materials": [ [ "MATCH", "demo-project/*", "WITH", "PRODUCTS", "FROM", "clone" ], [ "DISALLOW", "*" ] ], "expected_products": [ [ "MODIFY", "demo-project/foo.py" ], [ "DISALLOW", "*" ] ], "name": "update-version", "pubkeys": [ "776a00e29f3559e0141b3b096f696abc6cfb0c657ab40f441132b345b08453f5" ], "threshold": 1 }, { "_type": "step", "expected_command": [ "tar", "--exclude", ".git", "-zcvf", "demo-project.tar.gz", "demo-project" ], "expected_materials": [ [ "MATCH", "demo-project/*", "WITH", "PRODUCTS", "FROM", "update-version" ], [ "DISALLOW", "*" ] ], "expected_products": [ [ "CREATE", "demo-project.tar.gz" ], [ "DISALLOW", "*" ] ], "name": "package", "pubkeys": [ "2f89b9272acfc8f4a0a0f094d789fdb0ba798b0fe41f2f5f417c12f0085ff498" ], "threshold": 1 } ] } }