From 50153fa09b45a78e6d709436125cfebb3fbf16e0 Mon Sep 17 00:00:00 2001 From: "BISWAS, SREYAN" Date: Mon, 6 Jul 2026 12:29:36 +0530 Subject: [PATCH 1/2] feat: add simulated CCA token builder with COSE signing and P-384 keys - Replace the old FakeTsmBuilder with SimulatedTokenBuilder that constructs real CBOR tag 399 tokens from JSON claims and JWK keys - Add encode_cca_token() for building CCA tokens with two COSE_Sign1 signatures (platform + realm) using ES384 on P-384 - Add CcaSimulatedAttester implementing the Attester trait - Add CcaRatsdAttester that posts to RATSD and parses the CMW envelope - Add crypto helpers: load_p384_jwk, build_rak_cose_key, sign_cose_sign1, is_supported_hash_alg, compute_hash - Add serde-enabled CCA token types with CBOR conversions - Add decode_cca_token() and pretty_print_token() utilities - Add CBOR key constants per the Arm CCA attestation token spec - Switch cmw dependency from git to crates.io v0.1.1 Signed-off-by: BISWAS, SREYAN fix: clarify comment on line 50 in encode.rs Explain which From impls are used for the CBOR conversion, where they are defined, and what they do -- addressing review comment from PR #4. Signed-off-by: BISWAS, SREYAN --- Cargo.lock | 386 +++++++++++++++++++++++--- Cargo.toml | 13 +- LICENSE | 2 +- README.md | 32 ++- deny.toml | 6 +- examples/attester.rs | 23 +- src/attesters/cca/mod.rs | 56 ++-- src/attesters/cca/ratsd.rs | 4 +- src/attesters/cca/simulated.rs | 216 +++++++++++---- src/attesters/cca/utils/constants.rs | 94 +++++++ src/attesters/cca/utils/crypto.rs | 254 +++++++++++++++++ src/attesters/cca/utils/decode.rs | 111 +++----- src/attesters/cca/utils/encode.rs | 396 +++++++++++++++++++++++++++ src/attesters/cca/utils/mod.rs | 13 +- src/attesters/cca/utils/print.rs | 31 --- src/attesters/cca/utils/types.rs | 301 ++++++++++++++++---- src/attesters/ratsd.rs | 6 +- src/tsm/mod.rs | 2 +- test-data/cca-claims.json | 107 ++++++++ test-data/iak.jwk | 7 + test-data/rak.jwk | 7 + 21 files changed, 1778 insertions(+), 289 deletions(-) create mode 100644 src/attesters/cca/utils/constants.rs create mode 100644 src/attesters/cca/utils/crypto.rs create mode 100644 src/attesters/cca/utils/encode.rs delete mode 100644 src/attesters/cca/utils/print.rs create mode 100644 test-data/cca-claims.json create mode 100644 test-data/iak.jwk create mode 100644 test-data/rak.jwk diff --git a/Cargo.lock b/Cargo.lock index 4b99ac8..ad1e728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,12 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -171,6 +177,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c6d128af408d8ebd08331f0331cf2cf20d19e6c44a7aec58791641ecc8c0b5" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "1.3.2" @@ -192,6 +204,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bs58" version = "0.5.1" @@ -368,8 +389,9 @@ dependencies = [ [[package]] name = "cmw" -version = "0.1.0" -source = "git+https://github.com/veraison/rust-cmw#9c64abd29195a05ae59b57ae21eef3da89b8e3ee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "663d68ee8ffca6259c4cf9db740a703964674b93684bc5ba55a643b4fbf22f3a" dependencies = [ "base64 0.22.1", "base64-serde", @@ -405,6 +427,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation" version = "0.9.4" @@ -442,6 +476,16 @@ dependencies = [ "rand 0.8.6", ] +[[package]] +name = "coset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eb98d5e9155e2cf7cd942c8b3033097d4563b6fb0a00b9caecb74669555c058" +dependencies = [ + "ciborium", + "ciborium-io", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -451,6 +495,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -463,16 +516,37 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "darling" version = "0.23.0" @@ -507,6 +581,17 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -522,8 +607,21 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", ] [[package]] @@ -568,6 +666,44 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "base64ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serde_json", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -579,9 +715,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -589,9 +725,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", @@ -643,6 +779,16 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -760,12 +906,13 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -808,6 +955,17 @@ dependencies = [ "wasip3", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.14" @@ -904,6 +1062,24 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "http" version = "1.4.2" @@ -983,6 +1159,15 @@ dependencies = [ "url", ] +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -1288,9 +1473,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.100" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ "cfg-if", "futures-util", @@ -1347,9 +1532,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -1490,9 +1675,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ "bitflags 2.13.0", "cfg-if", @@ -1530,9 +1715,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -1541,6 +1726,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parking" version = "2.2.1" @@ -1566,6 +1763,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1621,6 +1827,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -1676,6 +1892,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1878,12 +2103,17 @@ dependencies = [ "ciborium", "clap", "cmw", + "coset", + "ecdsa", "env_logger", "httpmock", "log", + "p384", + "rand_core 0.6.4", "reqwest", "serde", "serde_json", + "sha2 0.11.0", "tempfile", "thiserror 2.0.18", "url", @@ -1935,6 +2165,16 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2048,6 +2288,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2174,6 +2429,16 @@ dependencies = [ "syn", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2181,8 +2446,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -2207,6 +2494,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest 0.10.7", "rand_core 0.6.4", ] @@ -2256,6 +2544,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2396,9 +2694,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.48" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1aa89044e7786ffb2ec017acb22cb7de5b0be46d0f21aea2b224b8561e5db2" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", "num-conv", @@ -2416,9 +2714,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.28" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d3bfe86347f0cc659f586f01e26303ccd32418f26f30c7b0309b3ca3a07d695" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" dependencies = [ "num-conv", "time-core", @@ -2681,9 +2979,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen 0.57.1", ] @@ -2699,9 +2997,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -2712,9 +3010,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.73" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ "js-sys", "wasm-bindgen", @@ -2722,9 +3020,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2732,9 +3030,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", @@ -2745,9 +3043,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ "unicode-ident", ] @@ -2788,9 +3086,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.100" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3213,18 +3511,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3c33da1..b40c128 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "regl" version = "0.1.0" edition = "2024" -description = "Rust Evidence Generation Library — collects attestation evidence from TEE platforms" +description = "RATS Evidence Generation Library - collects attestation evidence from TEE platforms" license = "Apache-2.0" repository = "https://github.com/veraison/rust-regl" keywords = ["cca", "attestation", "arm", "evidence", "tee"] @@ -12,16 +12,21 @@ categories = ["hardware-support", "cryptography"] thiserror = "2.0.18" tempfile = "3" ccatoken = { git = "https://github.com/veraison/rust-ccatoken.git" } -cmw = { git = "https://github.com/veraison/rust-cmw" } +cmw = "0.1.1" reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" base64 = { version = "0.22", features = ["alloc"] } ciborium = "0.2" +coset = "0.4" +p384 = { version = "0.13", features = ["ecdsa", "jwk"] } +ecdsa = { version = "0.16", features = ["signing"] } +sha2 = "0.11" +rand_core = { version = "0.6", features = ["getrandom"] } url = "2" [dev-dependencies] -log = "0.4.29" -env_logger = "0.11.10" +log = "0.4.33" +env_logger = "0.11.11" clap = { version = "4.6.1", features = ["derive"] } httpmock = "0.8" diff --git a/LICENSE b/LICENSE index 261eeb9..1d66459 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2026 Contributors to the Veraison project. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index c64f89c..166f965 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # rust-regl -Rust Evidence Generation Library (REGL) — collects attestation evidence from TEE platforms. +RATS Evidence Generation Library (REGL) - collects attestation evidence from TEE platforms. ## Attesters | Attester | Struct | Backend | Description | |---|---|---|---| -| `cca-tsm` | `CcaTsmAttester` | Linux TSM (`/sys/kernel/config/tsm`) | Talks directly to the kernel TSM interface on Arm CCA hardware. Requires root or write access to configfs-tsm. | -| `cca-ratsd` | `CcaRatsdAttester` | RATSD daemon | Posts a challenge to a [RATSD](https://github.com/veraison/ratsd) daemon, then parses the CMW envelope to extract the Arm CCA attestation token. "CCA-specific" means it knows how to find and decode CCA evidence inside the CMW — it looks for items whose content type contains `configfs-tsm` and whose provider is `arm_cca_guest`. | -| `cca-sim` | `CcaSimulatedAttester` | Embedded blob | Returns a pre-built CCA token embedded at compile time. Useful for testing and development without hardware or a running RATSD. | -| `ratsd` | `RatsdAttester` | RATSD daemon (generic) | Posts a challenge to a RATSD daemon and returns the raw JSON response as-is. No TEE-specific parsing — use this if you want the CMW envelope or other RATSD-level data directly. | +| `cca-tsm` | `CcaTsmAttester` | Linux TSM (`/sys/kernel/config/tsm`) | Talks directly to the kernel TSM interface on Arm CCA hardware. Requires root. | +| `cca-ratsd` | `CcaRatsdAttester` | RATSD daemon | Posts a challenge to a [RATSD](https://github.com/veraison/ratsd) daemon and extracts the CCA attestation token from the CMW envelope. | +| `cca-sim` | `CcaSimulatedAttester` | Pure Rust | Builds a CCA token from JSON claims and JWK keys with ES384 COSE_Sign1 signatures. No hardware needed. | +| `ratsd` | `RatsdAttester` | RATSD daemon | Posts a challenge to a RATSD daemon and returns the raw JSON response. No TEE-specific parsing. | ## Usage @@ -17,12 +17,12 @@ Rust Evidence Generation Library (REGL) — collects attestation evidence from T use regl::attesters::{cca, ratsd, Attester}; use url::Url; -// Generic RATSD — explicit URL required +// Generic RATSD - returns the raw JSON response, no TEE-specific parsing let url = Url::parse("http://localhost:8895").unwrap(); let attester = ratsd::RatsdAttester::with_url(url); let response: Vec = attester.get_evidence(&challenge).unwrap(); -// CCA-specific RATSD — parses CMW envelope, returns CCA token bytes +// CCA-specific RATSD - parses CMW envelope, returns CCA token bytes let url = Url::parse("http://localhost:8895").unwrap(); let attester = cca::CcaRatsdAttester::with_url(url); let evidence = attester.get_evidence(&challenge).unwrap(); @@ -30,11 +30,18 @@ let evidence = attester.get_evidence(&challenge).unwrap(); // TSM-backed attester (requires Linux CCA TSM hardware and root/sudo) let attester = cca::CcaTsmAttester::default(); let evidence = attester.get_evidence(&challenge).unwrap(); + +// Simulated attester - builds a token from JSON claims and JWK keys (no hardware needed) +let claims_json = std::fs::read_to_string("test-data/cca-claims.json").unwrap(); +let iak_jwk = std::fs::read_to_string("test-data/iak.jwk").unwrap(); +let rak_jwk = std::fs::read_to_string("test-data/rak.jwk").unwrap(); +let attester = cca::CcaSimulatedAttester::new(&claims_json, &iak_jwk, Some(&rak_jwk)).unwrap(); +let evidence = attester.get_evidence(&challenge).unwrap(); ``` > **Note:** The library itself does not read environment variables. The > `RATSD_URL` env var is resolved only in the example binaries -> (`examples/attester.rs`) for convenience — they fall back to +> (`examples/attester.rs`) for convenience - they fall back to > `http://localhost:8895` if the variable is not set. Production code > should pass an explicit `Url` via `with_url()`. @@ -95,11 +102,12 @@ Set `RUST_LOG=info` to see progress logs from the attester. ## Utilities -`regl::attesters::cca::utils` provides CCA evidence decoding and pretty-printing: +`regl::attesters::cca::utils` provides CCA evidence encoding, decoding, and pretty-printing: -- `decode::decode_cca_token()` — decode raw CBOR evidence to typed Rust structs -- `print::pretty_print_token()` — decode and format the evidence as JSON -- `types` — serde-enabled CCA evidence structs with human-readable field names +- `regl::attesters::cca::utils::encode_cca_token()` - build a CCA token (CBOR tag 399) from typed claims and signing keys +- `regl::attesters::cca::utils::decode_cca_token()` - decode raw CBOR evidence to typed Rust structs (`CcaToken`, `PlatformClaims`, `RealmClaims`, `SwComponent`) +- `regl::attesters::cca::utils::pretty_print_token()` - decode and serialize a CCA token as indented JSON +- `CcaToken`, `PlatformClaims`, `RealmClaims`, `SwComponent` - serde-enabled CCA evidence structs with human-readable field names ## License diff --git a/deny.toml b/deny.toml index 7e684b8..1c55165 100644 --- a/deny.toml +++ b/deny.toml @@ -233,12 +233,14 @@ skip-tree = [ unknown-registry = "warn" # Lint level for what to happen when a crate from a git repository that is not # in the allow list is encountered -unknown-git = "warn" +unknown-git = "deny" # List of URLs for allowed crate registries. Defaults to the crates.io index # if not specified. If it is specified but empty, no registries are allowed. allow-registry = ["https://github.com/rust-lang/crates.io-index"] # List of URLs for allowed Git repositories -allow-git = [] +allow-git = [ + "https://github.com/veraison/rust-ccatoken", +] [sources.allow-org] # 1 or more github.com organizations to allow git sources for diff --git a/examples/attester.rs b/examples/attester.rs index 374e42a..d7d813e 100644 --- a/examples/attester.rs +++ b/examples/attester.rs @@ -1,6 +1,6 @@ use clap::{Parser, ValueEnum}; use log::{error, info}; -use regl::attesters::cca::utils::print::pretty_print_token; +use regl::attesters::cca::utils::pretty_print_token; use regl::attesters::{Attester, cca, cca::CcaError}; use std::fs; use url::Url; @@ -28,10 +28,29 @@ enum AttesterType { CcaRatsd, } +fn create_sim_attester() -> Box> { + let claims_path = + std::env::var("CCA_CLAIMS_FILE").unwrap_or_else(|_| "test-data/cca-claims.json".into()); + let iak_path = std::env::var("CCA_IAK_FILE").unwrap_or_else(|_| "test-data/iak.jwk".into()); + let rak_path = std::env::var("CCA_RAK_FILE").ok(); + + let claims = + fs::read_to_string(&claims_path).unwrap_or_else(|e| panic!("reading {claims_path}: {e}")); + let iak = fs::read_to_string(&iak_path).unwrap_or_else(|e| panic!("reading {iak_path}: {e}")); + let rak = rak_path + .as_ref() + .map(|p| fs::read_to_string(p).unwrap_or_else(|e| panic!("reading {p}: {e}"))); + + Box::new( + cca::CcaSimulatedAttester::new(&claims, &iak, rak.as_deref()) + .expect("failed to create simulated attester"), + ) +} + fn create_attester(kind: &AttesterType) -> Box> { match kind { AttesterType::CcaTsm => Box::new(cca::CcaTsmAttester::default()), - AttesterType::CcaSim => Box::new(cca::CcaSimulatedAttester::default()), + AttesterType::CcaSim => create_sim_attester(), AttesterType::CcaRatsd => { let raw = std::env::var("RATSD_URL").unwrap_or_else(|_| "http://localhost:8895".into()); let url = Url::parse(&raw).expect("RATSD_URL must be a valid URL"); diff --git a/src/attesters/cca/mod.rs b/src/attesters/cca/mod.rs index 4b13ba8..d1892d9 100644 --- a/src/attesters/cca/mod.rs +++ b/src/attesters/cca/mod.rs @@ -11,7 +11,7 @@ mod ratsd; mod simulated; pub use ratsd::CcaRatsdAttester; -use simulated::FakeTsmBuilder; +pub use simulated::CcaSimulatedAttester; /// Arm CCA nonce size in bytes, as required by the CCA specification /// (https://datatracker.ietf.org/doc/draft-ffm-rats-cca-token/). @@ -20,9 +20,6 @@ const NONCE_SIZE: usize = 64; #[derive(Debug, Default)] pub struct CcaTsmAttester {} -#[derive(Debug, Default)] -pub struct CcaSimulatedAttester {} - impl Attester for CcaTsmAttester { type AttesterError = CcaError; @@ -39,22 +36,6 @@ impl Attester for CcaTsmAttester { } } -impl Attester for CcaSimulatedAttester { - type AttesterError = CcaError; - - fn get_evidence(&self, challenge: &[u8]) -> Result> { - if challenge.len() != NONCE_SIZE { - return Err(CcaError::InvalidNonce(format!( - "expected {NONCE_SIZE} bytes, got {}", - challenge.len() - ))); - } - let builder = FakeTsmBuilder::default(); - let challenge = challenge.to_vec(); - Ok(get_tsm_report(builder, challenge)?.outblob) - } -} - fn get_tsm_report(generator: B, inblob: Vec) -> Result where B: TsmReportBuilder, @@ -133,4 +114,39 @@ mod tests { let msg = format!("{err}"); assert!(msg.contains("something went wrong")); } + + // --- Error display --- + + #[test] + fn invalid_nonce_displays_message() { + // Test that the Display impl formats the error with the expected and actual lengths. + // Using a sample bad length of 5 bytes for a visual check; the real value is + // generated at runtime based on the actual nonce passed by the caller. + let err = CcaError::InvalidNonce(format!( + "expected {} bytes, got {}", + NONCE_SIZE, + b"short".len() + )); + let msg = format!("{err}"); + assert!(msg.contains("invalid nonce")); + assert!(msg.contains("64")); + assert!(msg.contains("5")); + } + + #[test] + fn tsm_error_displays_message() { + let err = CcaError::Tsm(TsmError::Unsupported); + let msg = format!("{err}"); + assert!(msg.contains("TSM error")); + } + + #[test] + fn ratsd_error_displays_message() { + let err = CcaError::Ratsd(crate::attesters::ratsd::RatsdError::Custom( + "bad response".into(), + )); + let msg = format!("{err}"); + assert!(msg.contains("RATSD error")); + assert!(msg.contains("bad response")); + } } diff --git a/src/attesters/cca/ratsd.rs b/src/attesters/cca/ratsd.rs index 842c7f3..4e350fc 100644 --- a/src/attesters/cca/ratsd.rs +++ b/src/attesters/cca/ratsd.rs @@ -149,7 +149,7 @@ mod tests { } // ----------------------------------------------------------------------- - // extract_cca_token — success case + // extract_cca_token - success case // ----------------------------------------------------------------------- #[test] @@ -181,7 +181,7 @@ mod tests { } // ----------------------------------------------------------------------- - // extract_cca_token — error cases + // extract_cca_token - error cases // ----------------------------------------------------------------------- #[test] diff --git a/src/attesters/cca/simulated.rs b/src/attesters/cca/simulated.rs index a6a3aaa..ae2e374 100644 --- a/src/attesters/cca/simulated.rs +++ b/src/attesters/cca/simulated.rs @@ -1,77 +1,193 @@ -use crate::tsm::{TsmError, TsmReport, TsmReportBuilder}; +//! Simulated CCA attester - Rust equivalent of `evcli cca create`. +//! +//! Builds a structurally valid CCA token (CBOR tag 399) with two +//! COSE_Sign1 tokens (platform + realm) signed with ES384. +//! +//! The application is responsible for reading files and passing their contents +//! to the builder - no environment variables are read by the library itself. -#[derive(Debug, Default)] -pub struct FakeTsmBuilder { - provider: String, - inblob: Vec, +use super::Attester; +use super::CcaError; +use super::utils::SimulatedTokenBuilder; +use p384::ecdsa::SigningKey; + +/// Simulated CCA attester that builds tokens from JSON claims and JWK key texts. +#[derive(Debug)] +pub struct CcaSimulatedAttester { + builder: SimulatedTokenBuilder, } -impl TsmReportBuilder for FakeTsmBuilder { - fn provider(&self) -> String { - self.provider.clone() +impl CcaSimulatedAttester { + /// Create a new simulated attester from JSON claims and JWK key texts. + /// + /// See [`SimulatedTokenBuilder::new`] for parameter details. + pub fn new(claims_json: &str, iak_jwk: &str, rak_jwk: Option<&str>) -> Result { + Ok(Self { + builder: SimulatedTokenBuilder::new(claims_json, iak_jwk, rak_jwk)?, + }) } - fn privlevel_floor(&self) -> u32 { - 0 + /// Create a new simulated attester from JSON claims and P-384 [`SigningKey`]s. + /// + /// This constructor accepts COSE keys directly, skipping the JWK + /// parsing step. + pub fn with_keys( + claims_json: &str, + iak: SigningKey, + rak: SigningKey, + ) -> Result { + Ok(Self { + builder: SimulatedTokenBuilder::with_keys(claims_json, iak, rak)?, + }) } +} + +impl Attester for CcaSimulatedAttester { + type AttesterError = CcaError; - fn inblob(mut self, blob: Vec) -> Self { - self.inblob = blob; - self + fn get_evidence(&self, challenge: &[u8]) -> Result, CcaError> { + if challenge.len() != super::NONCE_SIZE { + return Err(CcaError::InvalidNonce(format!( + "expected {} bytes, got {}", + super::NONCE_SIZE, + challenge.len() + ))); + } + self.builder.build_token(challenge) } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::attesters::Attester; - fn privlevel(self, _: u32) -> Self { - self + fn load_test_claims() -> String { + std::fs::read_to_string("test-data/cca-claims.json").unwrap() } - fn service_provider(self, _: String) -> Self { - self + fn load_test_iak() -> String { + std::fs::read_to_string("test-data/iak.jwk").unwrap() } - fn service_guid(self, _: [u8; 16]) -> Self { - self + fn load_test_rak() -> String { + std::fs::read_to_string("test-data/rak.jwk").unwrap() } - fn get_report(self) -> Result { - generate_simulated_evidence(self.inblob) + // ----------------------------------------------------------------------- + // CcaSimulatedAttester::new + // ----------------------------------------------------------------------- + + #[test] + fn new_with_test_data_succeeds() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let attester = CcaSimulatedAttester::new(&claims, &iak, Some(&rak)); + assert!(attester.is_ok(), "expected Ok, got {:?}", attester.err()); } -} -fn generate_simulated_evidence(_challenge: Vec) -> Result { - Ok(TsmReport { - outblob: Vec::from(include_bytes!("data/evidence.cbor")), - auxblob: Vec::new(), - manifestblob: Vec::new(), - }) -} + #[test] + fn new_without_rak_generates_key() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let attester = CcaSimulatedAttester::new(&claims, &iak, None); + assert!(attester.is_ok(), "expected Ok, got {:?}", attester.err()); + } -#[cfg(test)] -mod tests { - use super::*; - use crate::attesters::Attester; - use crate::attesters::cca::CcaSimulatedAttester; + #[test] + fn new_with_invalid_claims_returns_error() { + let iak = load_test_iak(); + let rak = load_test_rak(); + let err = CcaSimulatedAttester::new("not-valid-json", &iak, Some(&rak)).unwrap_err(); + assert!(format!("{err}").contains("parsing claims JSON")); + } + + #[test] + fn new_with_invalid_iak_returns_error() { + let claims = load_test_claims(); + let rak = load_test_rak(); + let err = CcaSimulatedAttester::new(&claims, "not-a-jwk", Some(&rak)).unwrap_err(); + assert!(format!("{err}").contains("IAK")); + } - const VALID_NONCE: &[u8; 64] = &[0u8; 64]; + // ----------------------------------------------------------------------- + // CcaSimulatedAttester::with_keys + // ----------------------------------------------------------------------- #[test] - fn fake_tsm_builder_chain_produces_report() { - let report = FakeTsmBuilder::default() - .inblob(vec![0u8; 64]) - .privlevel(2) - .service_provider("test-provider".into()) - .service_guid([0xAA; 16]) - .get_report() - .unwrap(); - assert!(!report.outblob.is_empty()); - assert!(report.auxblob.is_empty()); - assert!(report.manifestblob.is_empty()); + fn with_keys_produces_valid_attester_and_token() { + use p384::ecdsa::SigningKey; + use rand_core::OsRng; + let claims = load_test_claims(); + let iak = SigningKey::random(&mut OsRng); + let rak = SigningKey::random(&mut OsRng); + let attester = CcaSimulatedAttester::with_keys(&claims, iak, rak).unwrap(); + let evidence = attester.get_evidence(&[0u8; 64]).unwrap(); + assert!(!evidence.is_empty()); } + // ----------------------------------------------------------------------- + // get_evidence + // ----------------------------------------------------------------------- + #[test] - fn simulated_attester_returns_non_empty_evidence_for_valid_nonce() { - let evidence = CcaSimulatedAttester::default() - .get_evidence(VALID_NONCE) - .unwrap(); + fn get_evidence_returns_valid_token() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let attester = CcaSimulatedAttester::new(&claims, &iak, Some(&rak)).unwrap(); + let evidence = attester.get_evidence(&[0u8; 64]).unwrap(); assert!(!evidence.is_empty()); } + + #[test] + fn get_evidence_different_nonce_produces_different_token() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let attester = CcaSimulatedAttester::new(&claims, &iak, Some(&rak)).unwrap(); + let token_a = attester.get_evidence(&[0u8; 64]).unwrap(); + let token_b = attester.get_evidence(&[1u8; 64]).unwrap(); + assert_ne!(token_a, token_b); + } + + #[test] + fn get_evidence_same_nonce_is_deterministic() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let attester = CcaSimulatedAttester::new(&claims, &iak, Some(&rak)).unwrap(); + let token1 = attester.get_evidence(&[0u8; 64]).unwrap(); + let token2 = attester.get_evidence(&[0u8; 64]).unwrap(); + assert_eq!(token1, token2); + } + + // ----------------------------------------------------------------------- + // Nonce validation + // ----------------------------------------------------------------------- + + #[test] + fn get_evidence_rejects_short_nonce() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let attester = CcaSimulatedAttester::new(&claims, &iak, Some(&rak)).unwrap(); + let err = attester.get_evidence(b"short").unwrap_err(); + assert!(matches!(err, CcaError::InvalidNonce(_))); + } + + #[test] + fn get_evidence_rejects_long_nonce() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let attester = CcaSimulatedAttester::new(&claims, &iak, Some(&rak)).unwrap(); + let err = attester.get_evidence(&[0u8; 65]).unwrap_err(); + assert!(matches!(err, CcaError::InvalidNonce(_))); + } } diff --git a/src/attesters/cca/utils/constants.rs b/src/attesters/cca/utils/constants.rs new file mode 100644 index 0000000..3514a67 --- /dev/null +++ b/src/attesters/cca/utils/constants.rs @@ -0,0 +1,94 @@ +// CBOR key constants for CCA token claims. +// +// These values are defined by the Arm CCA attestation token specification. + +// CCA token top-level +pub const CCA_TOKEN_TAG: u64 = 399; +pub const CCA_PLATFORM_TOKEN_KEY: u64 = 44234; // 0xACCA +pub const CCA_REALM_TOKEN_KEY: u64 = 44241; // 0xACD1 + +// Platform claim integer keys +pub const PLAT_CHALLENGE: u64 = 10; +pub const PLAT_INSTANCE_ID: u64 = 256; +pub const PLAT_PROFILE: u64 = 265; +pub const PLAT_LIFECYCLE: u64 = 2395; +pub const PLAT_IMPLEMENTATION_ID: u64 = 2396; +pub const PLAT_SW_COMPONENTS: u64 = 2399; +pub const PLAT_SERVICE_INDICATOR: u64 = 2400; +pub const PLAT_CONFIG: u64 = 2401; +pub const PLAT_HASH_ALGO: u64 = 2402; + +// Platform SW component integer keys +pub const SW_MEASUREMENT_TYPE: u64 = 1; +pub const SW_MEASUREMENT_VALUE: u64 = 2; +pub const SW_SIGNER_ID: u64 = 5; +pub const SW_MEASUREMENT_DESC: u64 = 6; + +// Realm claim integer keys +pub const REALM_CHALLENGE: u64 = 10; +pub const REALM_PROFILE: u64 = 265; +pub const REALM_PERSONALIZATION: u64 = 44235; +pub const REALM_HASH_ALGO: u64 = 44236; +pub const REALM_PUBLIC_KEY: u64 = 44237; +pub const REALM_INITIAL_MEASUREMENT: u64 = 44238; +pub const REALM_EXTENSIBLE_MEASUREMENTS: u64 = 44239; +pub const REALM_PUBLIC_KEY_HASH_ALGO: u64 = 44240; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cca_token_tag_is_399() { + assert_eq!(CCA_TOKEN_TAG, 399); + } + + #[test] + fn cca_platform_token_key_is_44234() { + assert_eq!(CCA_PLATFORM_TOKEN_KEY, 44234); + assert_eq!(CCA_PLATFORM_TOKEN_KEY, 0xACCA); + } + + #[test] + fn cca_realm_token_key_is_44241() { + assert_eq!(CCA_REALM_TOKEN_KEY, 44241); + assert_eq!(CCA_REALM_TOKEN_KEY, 0xACD1); + } + + #[test] + fn platform_claim_keys_have_expected_values() { + assert_eq!(PLAT_CHALLENGE, 10); + assert_eq!(PLAT_INSTANCE_ID, 256); + assert_eq!(PLAT_PROFILE, 265); + assert_eq!(PLAT_LIFECYCLE, 2395); + assert_eq!(PLAT_IMPLEMENTATION_ID, 2396); + assert_eq!(PLAT_SW_COMPONENTS, 2399); + assert_eq!(PLAT_SERVICE_INDICATOR, 2400); + assert_eq!(PLAT_CONFIG, 2401); + assert_eq!(PLAT_HASH_ALGO, 2402); + } + + #[test] + fn sw_component_keys_have_expected_values() { + assert_eq!(SW_MEASUREMENT_TYPE, 1); + assert_eq!(SW_MEASUREMENT_VALUE, 2); + assert_eq!(SW_SIGNER_ID, 5); + assert_eq!(SW_MEASUREMENT_DESC, 6); + } + + #[test] + fn realm_claim_keys_have_expected_values() { + assert_eq!(REALM_CHALLENGE, 10); + assert_eq!(REALM_PROFILE, 265); + assert_eq!(REALM_PERSONALIZATION, 44235); + assert_eq!(REALM_HASH_ALGO, 44236); + assert_eq!(REALM_PUBLIC_KEY, 44237); + assert_eq!(REALM_INITIAL_MEASUREMENT, 44238); + assert_eq!(REALM_EXTENSIBLE_MEASUREMENTS, 44239); + assert_eq!(REALM_PUBLIC_KEY_HASH_ALGO, 44240); + } +} diff --git a/src/attesters/cca/utils/crypto.rs b/src/attesters/cca/utils/crypto.rs new file mode 100644 index 0000000..c77ad18 --- /dev/null +++ b/src/attesters/cca/utils/crypto.rs @@ -0,0 +1,254 @@ +//! Cryptographic operations for CCA token signing and key management. +//! +//! Handles P-384 key loading, COSE key building, ES384 signing, +//! and SHA-2 hashing. + +use coset::{AsCborValue, CoseKeyBuilder, CoseSign1Builder, HeaderBuilder}; +use ecdsa::signature::Signer; +use p384::SecretKey; +use p384::ecdsa::{Signature, SigningKey}; +use p384::elliptic_curve::JwkEcKey; +use sha2::{Digest, Sha256, Sha384, Sha512}; + +use crate::attesters::cca::CcaError; + +/// Load a P-384 JWK private key from a JSON string. +pub fn load_p384_jwk(jwk_json: &str, label: &str) -> Result { + let jwk: JwkEcKey = jwk_json + .parse() + .map_err(|e| CcaError::custom(format!("parsing {label} JWK: {e}")))?; + let secret = SecretKey::from_jwk(&jwk) + .map_err(|e| CcaError::custom(format!("{label} JWK to secret key: {e}")))?; + Ok(SigningKey::from(secret)) +} + +/// Build a COSE_Key (CBOR-encoded) from a P-384 signing key's public part. +/// +/// Returns the raw CBOR bytes of the COSE_Key map. +pub fn build_rak_cose_key(rak: &SigningKey) -> Result, CcaError> { + let verifying_key = rak.verifying_key(); + let point = verifying_key.to_encoded_point(false); + let x = point + .x() + .ok_or_else(|| CcaError::custom("RAK public key x coordinate".to_string()))?; + let y = point + .y() + .ok_or_else(|| CcaError::custom("RAK public key y coordinate".to_string()))?; + + let cose_key = + CoseKeyBuilder::new_ec2_pub_key(coset::iana::EllipticCurve::P_384, x.to_vec(), y.to_vec()) + .algorithm(coset::iana::Algorithm::ES384) + .build(); + + let value = cose_key + .to_cbor_value() + .map_err(|e| CcaError::custom(format!("COSE_Key to CBOR value: {e}")))?; + + let mut buf = Vec::new(); + ciborium::into_writer(&value, &mut buf) + .map_err(|e| CcaError::custom(format!("COSE_Key CBOR: {e}")))?; + Ok(buf) +} + +/// Create a COSE_Sign1 (CBOR tag 18) over `payload` signed with `key`. +/// +/// Uses ES384 (ECDSA with SHA-384 on P-384) as the signing algorithm, +/// deterministic nonce generation per RFC 6979 (built into the p384 crate). +pub fn sign_cose_sign1(payload: &[u8], key: &SigningKey) -> Result, CcaError> { + let protected = HeaderBuilder::new() + .algorithm(coset::iana::Algorithm::ES384) + .build(); + + let cose = CoseSign1Builder::new() + .protected(protected) + .payload(payload.to_vec()) + .create_signature(b"", |to_sign| { + let sig: Signature = key.sign(to_sign); + sig.to_bytes().to_vec() + }) + .build(); + + use coset::TaggedCborSerializable; + let tagged = cose + .to_tagged_vec() + .map_err(|e| CcaError::custom(format!("COSE_Sign1 serialize: {e}")))?; + Ok(tagged) +} + +/// Check whether a hash algorithm string is supported. +/// +/// Accepts `"sha-256"`, `"sha-384"`, and `"sha-512"` per the IANA Named +/// Information Hash Algorithm Registry and the RMM specification. +/// This is intentionally broader than `ccatoken::token::is_valid_hash` +/// which currently omits SHA-384. +pub fn is_supported_hash_alg(s: &str) -> bool { + matches!(s, "sha-256" | "sha-384" | "sha-512") +} + +/// Hash `data` using the algorithm named in `algo`. +/// +/// Accepts `"sha-256"`, `"sha-384"`, and `"sha-512"` per the IANA Named +/// Information Hash Algorithm Registry referenced by the CCA token spec. +pub fn compute_hash(data: &[u8], algo: &str) -> Result, CcaError> { + match algo { + "sha-256" => Ok(Sha256::digest(data).to_vec()), + "sha-384" => Ok(Sha384::digest(data).to_vec()), + "sha-512" => Ok(Sha512::digest(data).to_vec()), + other => Err(CcaError::custom(format!( + "unsupported hash algorithm '{other}' for cca-realm-public-key-hash-algo-id; \ + expected sha-256, sha-384, or sha-512" + ))), + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use rand_core::OsRng; + + // ----------------------------------------------------------------------- + // is_supported_hash_alg + // ----------------------------------------------------------------------- + + #[test] + fn supported_hash_alg_accepts_sha256_sha384_sha512() { + assert!(is_supported_hash_alg("sha-256")); + assert!(is_supported_hash_alg("sha-384")); + assert!(is_supported_hash_alg("sha-512")); + } + + #[test] + fn supported_hash_alg_rejects_unknown() { + assert!(!is_supported_hash_alg("md5")); + assert!(!is_supported_hash_alg("sha-1")); + assert!(!is_supported_hash_alg("SHA-256")); + assert!(!is_supported_hash_alg("")); + assert!(!is_supported_hash_alg("sha384")); + } + + // ----------------------------------------------------------------------- + // compute_hash + // ----------------------------------------------------------------------- + + #[test] + fn compute_hash_sha256_produces_32_bytes() { + let hash = compute_hash(b"hello", "sha-256").unwrap(); + assert_eq!(hash.len(), 32); + } + + #[test] + fn compute_hash_sha384_produces_48_bytes() { + let hash = compute_hash(b"hello", "sha-384").unwrap(); + assert_eq!(hash.len(), 48); + } + + #[test] + fn compute_hash_sha512_produces_64_bytes() { + let hash = compute_hash(b"hello", "sha-512").unwrap(); + assert_eq!(hash.len(), 64); + } + + #[test] + fn compute_hash_is_deterministic() { + let a = compute_hash(b"same input", "sha-256").unwrap(); + let b = compute_hash(b"same input", "sha-256").unwrap(); + assert_eq!(a, b); + } + + #[test] + fn compute_hash_different_inputs_differ() { + let a = compute_hash(b"input A", "sha-256").unwrap(); + let b = compute_hash(b"input B", "sha-256").unwrap(); + assert_ne!(a, b); + } + + #[test] + fn compute_hash_unknown_algorithm_returns_error() { + let err = compute_hash(b"data", "md5").unwrap_err(); + assert!(format!("{err}").contains("unsupported hash algorithm")); + } + + // ----------------------------------------------------------------------- + // sign_cose_sign1 + // ----------------------------------------------------------------------- + + #[test] + fn sign_cose_sign1_produces_non_empty_output() { + let key = SigningKey::random(&mut OsRng); + let payload = b"test payload for signing"; + let signed = sign_cose_sign1(payload, &key).unwrap(); + assert!(!signed.is_empty()); + } + + #[test] + fn sign_cose_sign1_is_deterministic() { + let key = SigningKey::random(&mut OsRng); + let payload = b"deterministic test"; + let sig1 = sign_cose_sign1(payload, &key).unwrap(); + let sig2 = sign_cose_sign1(payload, &key).unwrap(); + assert_eq!(sig1, sig2); + } + + #[test] + fn sign_cose_sign1_different_payloads_differ() { + let key = SigningKey::random(&mut OsRng); + let sig_a = sign_cose_sign1(b"payload A", &key).unwrap(); + let sig_b = sign_cose_sign1(b"payload B", &key).unwrap(); + assert_ne!(sig_a, sig_b); + } + + // ----------------------------------------------------------------------- + // build_rak_cose_key + // ----------------------------------------------------------------------- + + #[test] + fn build_rak_cose_key_produces_cbor() { + let key = SigningKey::random(&mut OsRng); + let cose_key_bytes = build_rak_cose_key(&key).unwrap(); + assert!(!cose_key_bytes.is_empty()); + } + + #[test] + fn build_rak_cose_key_is_deterministic() { + let key = SigningKey::random(&mut OsRng); + let k1 = build_rak_cose_key(&key).unwrap(); + let k2 = build_rak_cose_key(&key).unwrap(); + assert_eq!(k1, k2); + } + + // ----------------------------------------------------------------------- + // load_p384_jwk + // ----------------------------------------------------------------------- + + fn test_iak_jwk() -> &'static str { + r#"{ + "crv": "P-384", + "d": "isCQyZWGn2GsE1jwKwIaJqtus4YgOsc1186YVVOLkfdMRLDVgCQ--3maKT3LqgiZ", + "kty": "EC", + "x": "IShnxS4rlQiwpCCpBWDzlNLfqiG911FP8akBr-fh94uxHU5m-Kijivp2r2oxxN6M", + "y": "hM4tr8mWQli1P61xh3T0ViDREbF26DGOEYfbAjWjGNN7pZf-6A4OTHYqEryz6m7U" + }"# + } + + #[test] + fn load_p384_jwk_valid_key_succeeds() { + let key = load_p384_jwk(test_iak_jwk(), "IAK"); + assert!(key.is_ok(), "expected Ok, got {:?}", key.err()); + } + + #[test] + fn load_p384_jwk_invalid_json_returns_error() { + let err = load_p384_jwk("not-json", "IAK").unwrap_err(); + assert!(format!("{err}").contains("parsing IAK JWK")); + } + + #[test] + fn load_p384_jwk_label_appears_in_error() { + let err = load_p384_jwk("not-json", "RAK").unwrap_err(); + assert!(format!("{err}").contains("RAK")); + } +} diff --git a/src/attesters/cca/utils/decode.rs b/src/attesters/cca/utils/decode.rs index abd4fbb..c06dc41 100644 --- a/src/attesters/cca/utils/decode.rs +++ b/src/attesters/cca/utils/decode.rs @@ -1,8 +1,8 @@ //! CCA token decoding and type conversion. //! //! The `ccatoken` crate decodes CBOR into its own internal types (`Platform`, -//! `Realm`, `SwComponent`). This module provides bridge functions — -//! `convert_platform`, `convert_sw_component`, `convert_realm` — that map +//! `Realm`, `SwComponent`). This module provides bridge functions - +//! `convert_platform`, `convert_sw_component`, `convert_realm` - that map //! those internal types into our public serde-enabled types (`PlatformClaims`, //! `RealmClaims`, `SwComponent`) for use by callers that need JSON output //! (e.g. `pretty_print_token`). @@ -10,8 +10,7 @@ //! This indirection keeps our public API stable even if the ccatoken library //! changes its field names. -use ccatoken::token::SwComponent as CcaSwComponent; -use ccatoken::token::{Evidence, Platform, Realm}; +use ccatoken::token::Evidence; use super::super::CcaError; use super::types::*; @@ -26,72 +25,11 @@ pub fn decode_cca_token(data: &[u8]) -> Result { Evidence::decode(data).map_err(|e| CcaError::custom(format!("ccatoken decode: {e}")))?; Ok(CcaToken { - platform: convert_platform(&evidence.platform_claims), - realm: convert_realm(&evidence.realm_claims), + platform: evidence.platform_claims.into(), + realm: evidence.realm_claims.into(), }) } -/// Convert the upstream `ccatoken::Platform` struct into our -/// serde-enabled [`PlatformClaims`]. -/// -/// This is a simple field-by-field copy with minor transformations: -/// - `lifecycle` is narrowed from `i128` to `u64` -/// - `verification_service` is mapped to `service_indicator` -/// - `hash_alg` is mapped to `hash_algo_id` (stored as `Option` -/// so that empty strings from the library become `None`) -/// - each software component is converted via [`convert_sw_component`] -fn convert_platform(p: &Platform) -> PlatformClaims { - PlatformClaims { - profile: p.profile.clone(), - challenge: p.challenge.to_vec(), - implementation_id: p.impl_id.to_vec(), - instance_id: p.inst_id.to_vec(), - config: p.config.clone(), - lifecycle: p.lifecycle as u64, - sw_components: p.sw_components.iter().map(convert_sw_component).collect(), - service_indicator: p.verification_service.clone(), - hash_algo_id: Some(p.hash_alg.clone()).filter(|s| !s.is_empty()), - } -} - -/// Convert the upstream `ccatoken::SwComponent` into our -/// serde-enabled [`SwComponent`]. -/// -/// Field name mapping from the ccatoken library: -/// - `mtyp` → `measurement_type` -/// - `mval` → `measurement_value` -/// - `hash_alg` → `measurement_description` -fn convert_sw_component(c: &CcaSwComponent) -> SwComponent { - SwComponent { - measurement_type: c.mtyp.clone(), - measurement_value: c.mval.clone(), - version: c.version.clone(), - signer_id: c.signer_id.clone(), - measurement_description: c.hash_alg.clone(), - } -} - -/// Convert the upstream `ccatoken::Realm` struct into our -/// serde-enabled [`RealmClaims`]. -/// -/// Uses `get_realm_key()` to select the correct public key encoding -/// (COSE_Key for realm-profile tokens, raw bytes otherwise) — matching -/// the logic in the ccatoken library itself. -fn convert_realm(r: &Realm) -> RealmClaims { - let public_key = r.get_realm_key().unwrap_or_default(); - - RealmClaims { - profile: Some(r.profile.clone()).filter(|s| !s.is_empty()), - challenge: r.challenge.to_vec(), - personalization_value: r.perso.to_vec(), - initial_measurement: r.rim.clone(), - extensible_measurements: r.rem.to_vec(), - hash_algo_id: r.hash_alg.clone(), - public_key, - public_key_hash_algo_id: r.rak_hash_alg.clone(), - } -} - #[cfg(test)] mod tests { use super::*; @@ -118,3 +56,42 @@ mod tests { assert!(decode_cca_token(b"").is_err()); } } + +// --------------------------------------------------------------------------- +// pretty_print_token - convenience wrapper +// --------------------------------------------------------------------------- + +/// Decode a raw CCA token and serialize it as an indented JSON string. +/// +/// This is a convenience wrapper that calls [`decode_cca_token`] followed +/// by `serde_json::to_string_pretty`. +pub fn pretty_print_token(data: &[u8]) -> Result { + let token = decode_cca_token(data)?; + serde_json::to_string_pretty(&token) + .map_err(|e| CcaError::custom(format!("JSON serialize: {e}"))) +} + +#[cfg(test)] +mod print_tests { + use super::*; + use crate::attesters::Attester; + use crate::attesters::cca::CcaSimulatedAttester; + use std::fs; + + #[test] + fn pretty_print_token_produces_valid_json_with_expected_keys() { + let claims = fs::read_to_string("test-data/cca-claims.json").unwrap(); + let iak = fs::read_to_string("test-data/iak.jwk").unwrap(); + let rak = fs::read_to_string("test-data/rak.jwk").unwrap(); + + let evidence = CcaSimulatedAttester::new(&claims, &iak, Some(&rak)) + .unwrap() + .get_evidence(&[0u8; 64]) + .unwrap(); + let json = pretty_print_token(&evidence).unwrap(); + assert!(json.contains("cca-platform-profile")); + assert!(json.contains("cca-realm-delegated-token")); + assert!(json.contains("cca-platform-challenge")); + assert!(json.contains("cca-realm-challenge")); + } +} diff --git a/src/attesters/cca/utils/encode.rs b/src/attesters/cca/utils/encode.rs new file mode 100644 index 0000000..ffb8c39 --- /dev/null +++ b/src/attesters/cca/utils/encode.rs @@ -0,0 +1,396 @@ +//! CCA token construction from typed claims. +//! +//! Converts [`PlatformClaims`] and [`RealmClaims`] into a CBOR tag 399 +//! token with two COSE_Sign1 signatures (platform + realm). +//! +//! The typed structs are converted to [`ciborium::Value`] via their +//! [`From`] implementations, then signed and wrapped. + +use ccatoken::token::{is_valid_lifecycle, is_valid_measurement}; +use ciborium::Value as CborValue; +use p384::ecdsa::SigningKey; +use rand_core::OsRng; + +use super::constants::*; +use super::crypto::{ + build_rak_cose_key, compute_hash, is_supported_hash_alg, load_p384_jwk, sign_cose_sign1, +}; +use super::types::{CcaToken, PlatformClaims, RealmClaims}; +use crate::attesters::cca::CcaError; + +/// Build a full CCA token (CBOR tag 399) from typed claims and signing keys. +/// +/// The caller's nonce is injected as the realm challenge. The platform challenge +/// is derived from the RAK public key (per the CCA spec) and injected into the +/// platform claims before conversion. +pub fn encode_cca_token( + mut platform_claims: PlatformClaims, + mut realm_claims: RealmClaims, + iak: &SigningKey, + rak: &SigningKey, + nonce: &[u8], +) -> Result, CcaError> { + // Build the RAK COSE_Key and derive the platform challenge from it. + let rak_cose_key_bytes = build_rak_cose_key(rak)?; + let rak_hash_algo = &realm_claims.public_key_hash_algo_id; + let platform_challenge = compute_hash(&rak_cose_key_bytes, rak_hash_algo)?; + + // Validate typed fields before encoding. + validate_platform(&platform_claims)?; + validate_realm(&realm_claims)?; + + // Inject runtime fields. + platform_claims.challenge = platform_challenge; + realm_claims.challenge = nonce.to_vec(); + realm_claims.public_key = rak_cose_key_bytes; + + // Convert typed claims into CBOR map values. + // The From for CborValue and From for CborValue + // implementations (in types.rs) map each claim field to its spec-defined + // CBOR integer key and value type. + let platform_payload_raw: CborValue = platform_claims.into(); + let realm_payload_raw: CborValue = realm_claims.into(); + + // Serialize to CBOR bytes. + let mut platform_payload = Vec::new(); + ciborium::into_writer(&platform_payload_raw, &mut platform_payload) + .map_err(|e| CcaError::custom(format!("platform payload CBOR: {e}")))?; + let mut realm_payload = Vec::new(); + ciborium::into_writer(&realm_payload_raw, &mut realm_payload) + .map_err(|e| CcaError::custom(format!("realm payload CBOR: {e}")))?; + + // Sign both payloads. + let platform_cose = sign_cose_sign1(&platform_payload, iak)?; + let realm_cose = sign_cose_sign1(&realm_payload, rak)?; + + // Wrap in CBOR tag 399. + let outer = CborValue::Tag( + CCA_TOKEN_TAG, + Box::new(CborValue::Map(vec![ + ( + CborValue::Integer(CCA_PLATFORM_TOKEN_KEY.into()), + CborValue::Bytes(platform_cose), + ), + ( + CborValue::Integer(CCA_REALM_TOKEN_KEY.into()), + CborValue::Bytes(realm_cose), + ), + ])), + ); + + let mut buf = Vec::new(); + ciborium::into_writer(&outer, &mut buf) + .map_err(|e| CcaError::custom(format!("CBOR encode: {e}")))?; + Ok(buf) +} + +// --------------------------------------------------------------------------- +// Validation helpers +// --------------------------------------------------------------------------- + +fn validate_platform(p: &PlatformClaims) -> Result<(), CcaError> { + if !is_valid_lifecycle(p.lifecycle as i128) { + return Err(CcaError::custom(format!( + "invalid lifecycle value 0x{:04x}", + p.lifecycle + ))); + } + if let Some(ref algo) = p.hash_algo_id + && !is_supported_hash_alg(algo) + { + return Err(CcaError::custom(format!( + "platform: unsupported hash algorithm '{algo}'" + ))); + } + for (i, c) in p.sw_components.iter().enumerate() { + if !is_valid_measurement(&c.measurement_value) { + return Err(CcaError::custom(format!( + "sw component {i}: invalid measurement length {}", + c.measurement_value.len() + ))); + } + if !is_valid_measurement(&c.signer_id) { + return Err(CcaError::custom(format!( + "sw component {i}: invalid signer ID length {}", + c.signer_id.len() + ))); + } + } + Ok(()) +} + +fn validate_realm(r: &RealmClaims) -> Result<(), CcaError> { + if !is_valid_measurement(&r.initial_measurement) { + return Err(CcaError::custom(format!( + "invalid initial measurement length {}", + r.initial_measurement.len() + ))); + } + for (i, m) in r.extensible_measurements.iter().enumerate() { + if !is_valid_measurement(m) { + return Err(CcaError::custom(format!( + "extensible measurement {i}: invalid length {}", + m.len() + ))); + } + } + if !is_supported_hash_alg(&r.hash_algo_id) { + return Err(CcaError::custom(format!( + "realm: unsupported hash algorithm '{}'", + r.hash_algo_id + ))); + } + if !is_supported_hash_alg(&r.public_key_hash_algo_id) { + return Err(CcaError::custom(format!( + "realm: unsupported public key hash algo '{}'", + r.public_key_hash_algo_id + ))); + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// SimulatedTokenBuilder - convenience wrapper +// --------------------------------------------------------------------------- + +/// Token builder that holds parsed claims and signing keys. +/// +/// Two construction paths are available: +/// +/// | Constructor | IAK | RAK | +/// |---|---|---| +/// | [`new`](SimulatedTokenBuilder::new) | JWK JSON string | JWK JSON string (optional) | +/// | [`with_keys`](SimulatedTokenBuilder::with_keys) | [`SigningKey`] | [`SigningKey`] | +/// +/// Both delegate to [`encode_cca_token`] for the actual CBOR construction +/// and signing. +#[derive(Debug)] +pub(crate) struct SimulatedTokenBuilder { + platform_claims: PlatformClaims, + realm_claims: RealmClaims, + iak: SigningKey, + rak: SigningKey, +} + +impl SimulatedTokenBuilder { + /// Create a builder from JSON claims and JWK key text. + /// + /// If `rak_jwk` is `None`, a random RAK is generated. + pub fn new(claims_json: &str, iak_jwk: &str, rak_jwk: Option<&str>) -> Result { + let claims: CcaToken = serde_json::from_str(claims_json) + .map_err(|e| CcaError::custom(format!("parsing claims JSON: {e}")))?; + + let iak = load_p384_jwk(iak_jwk, "IAK")?; + let rak = match rak_jwk { + Some(jwk) => load_p384_jwk(jwk, "RAK")?, + None => SigningKey::random(&mut OsRng), + }; + + Ok(Self { + platform_claims: claims.platform, + realm_claims: claims.realm, + iak, + rak, + }) + } + + /// Create a builder from JSON claims and P-384 [`SigningKey`]s. + /// + /// This constructor accepts COSE keys directly, skipping the JWK + /// parsing step. The `rak` key is used for both signing the realm + /// token and deriving the platform challenge (per the CCA spec). + pub fn with_keys( + claims_json: &str, + iak: SigningKey, + rak: SigningKey, + ) -> Result { + let claims: CcaToken = serde_json::from_str(claims_json) + .map_err(|e| CcaError::custom(format!("parsing claims JSON: {e}")))?; + + Ok(Self { + platform_claims: claims.platform, + realm_claims: claims.realm, + iak, + rak, + }) + } + + pub fn build_token(&self, nonce: &[u8]) -> Result, CcaError> { + encode_cca_token( + self.platform_claims.clone(), + self.realm_claims.clone(), + &self.iak, + &self.rak, + nonce, + ) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + fn load_test_claims() -> String { + std::fs::read_to_string("test-data/cca-claims.json").unwrap() + } + + fn load_test_iak() -> String { + std::fs::read_to_string("test-data/iak.jwk").unwrap() + } + + fn load_test_rak() -> String { + std::fs::read_to_string("test-data/rak.jwk").unwrap() + } + + // ----------------------------------------------------------------------- + // SimulatedTokenBuilder::new + // ----------------------------------------------------------------------- + + #[test] + fn builder_new_with_test_data_succeeds() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let builder = SimulatedTokenBuilder::new(&claims, &iak, Some(&rak)); + assert!(builder.is_ok(), "expected Ok, got {:?}", builder.err()); + } + + #[test] + fn builder_new_without_rak_generates_key() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let builder = SimulatedTokenBuilder::new(&claims, &iak, None).unwrap(); + let token = builder.build_token(&[0u8; 64]).unwrap(); + assert!(!token.is_empty()); + } + + #[test] + fn builder_new_with_invalid_claims_returns_error() { + let iak = load_test_iak(); + let rak = load_test_rak(); + let err = SimulatedTokenBuilder::new("not-json", &iak, Some(&rak)).unwrap_err(); + assert!(format!("{err}").contains("parsing claims JSON")); + } + + #[test] + fn builder_new_with_invalid_iak_returns_error() { + let claims = load_test_claims(); + let rak = load_test_rak(); + let err = SimulatedTokenBuilder::new(&claims, "not-a-jwk", Some(&rak)).unwrap_err(); + assert!(format!("{err}").contains("IAK")); + } + + #[test] + fn builder_new_with_invalid_rak_returns_error() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let err = SimulatedTokenBuilder::new(&claims, &iak, Some("not-a-jwk")).unwrap_err(); + assert!(format!("{err}").contains("RAK")); + } + + // ----------------------------------------------------------------------- + // SimulatedTokenBuilder::with_keys + // ----------------------------------------------------------------------- + + #[test] + fn builder_with_keys_succeeds() { + let claims = load_test_claims(); + let iak = SigningKey::random(&mut OsRng); + let rak = SigningKey::random(&mut OsRng); + let builder = SimulatedTokenBuilder::with_keys(&claims, iak, rak); + assert!(builder.is_ok()); + } + + // ----------------------------------------------------------------------- + // build_token + // ----------------------------------------------------------------------- + + #[test] + fn build_token_produces_valid_cbor_tag_399() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let builder = SimulatedTokenBuilder::new(&claims, &iak, Some(&rak)).unwrap(); + let token = builder.build_token(&[0u8; 64]).unwrap(); + assert!(!token.is_empty()); + + // Verify outer structure: must be a CBOR tag 399 containing a map. + let value: CborValue = + ciborium::from_reader(token.as_slice()).expect("token should be valid CBOR"); + match value { + CborValue::Tag(tag, inner) => { + assert_eq!(tag, CCA_TOKEN_TAG); + match *inner { + CborValue::Map(ref entries) => { + assert_eq!(entries.len(), 2); + let has_platform = entries.iter().any(|(k, _)| { + if let CborValue::Integer(i) = k { + i128::from(*i) == CCA_PLATFORM_TOKEN_KEY as i128 + } else { + false + } + }); + let has_realm = entries.iter().any(|(k, _)| { + if let CborValue::Integer(i) = k { + i128::from(*i) == CCA_REALM_TOKEN_KEY as i128 + } else { + false + } + }); + assert!(has_platform, "missing platform token key"); + assert!(has_realm, "missing realm token key"); + } + other => panic!("expected Map under tag, got {other:?}"), + } + } + other => panic!("expected Tag, got {other:?}"), + } + } + + #[test] + fn build_token_is_deterministic() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let builder = SimulatedTokenBuilder::new(&claims, &iak, Some(&rak)).unwrap(); + let token1 = builder.build_token(&[0u8; 64]).unwrap(); + let token2 = builder.build_token(&[0u8; 64]).unwrap(); + assert_eq!(token1, token2); + } + + #[test] + fn build_token_different_nonce_produces_different_token() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let builder = SimulatedTokenBuilder::new(&claims, &iak, Some(&rak)).unwrap(); + let token_a = builder.build_token(&[0u8; 64]).unwrap(); + let token_b = builder.build_token(&[1u8; 64]).unwrap(); + assert_ne!(token_a, token_b); + } + + // ----------------------------------------------------------------------- + // encode_cca_token + // ----------------------------------------------------------------------- + + #[test] + fn encode_cca_token_produces_non_empty_output() { + let claims = load_test_claims(); + let iak = load_test_iak(); + let rak = load_test_rak(); + let builder = SimulatedTokenBuilder::new(&claims, &iak, Some(&rak)).unwrap(); + let token = encode_cca_token( + builder.platform_claims, + builder.realm_claims, + &builder.iak, + &builder.rak, + &[0u8; 64], + ) + .unwrap(); + assert!(!token.is_empty()); + } +} diff --git a/src/attesters/cca/utils/mod.rs b/src/attesters/cca/utils/mod.rs index d91a33d..da5c0d5 100644 --- a/src/attesters/cca/utils/mod.rs +++ b/src/attesters/cca/utils/mod.rs @@ -1,3 +1,10 @@ -pub mod decode; -pub mod print; -pub mod types; +mod constants; +mod crypto; +mod decode; +mod encode; +mod types; + +pub use decode::{decode_cca_token, pretty_print_token}; +pub(super) use encode::SimulatedTokenBuilder; +pub use encode::encode_cca_token; +pub use types::{CcaToken, PlatformClaims, RealmClaims, SwComponent}; diff --git a/src/attesters/cca/utils/print.rs b/src/attesters/cca/utils/print.rs deleted file mode 100644 index 42e3574..0000000 --- a/src/attesters/cca/utils/print.rs +++ /dev/null @@ -1,31 +0,0 @@ -use super::super::CcaError; -use super::decode::decode_cca_token; - -/// Decode a raw CCA token and serialize it as an indented JSON string. -/// -/// This is a convenience wrapper that calls [`decode_cca_token`] followed -/// by `serde_json::to_string_pretty`. -pub fn pretty_print_token(data: &[u8]) -> Result { - let token = decode_cca_token(data)?; - serde_json::to_string_pretty(&token) - .map_err(|e| CcaError::custom(format!("JSON serialize: {e}"))) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::attesters::Attester; - use crate::attesters::cca::CcaSimulatedAttester; - - #[test] - fn pretty_print_token_produces_valid_json_with_expected_keys() { - let evidence = CcaSimulatedAttester::default() - .get_evidence(&[0u8; 64]) - .unwrap(); - let json = pretty_print_token(&evidence).unwrap(); - assert!(json.contains("cca-platform-profile")); - assert!(json.contains("cca-realm-delegated-token")); - assert!(json.contains("cca-platform-challenge")); - assert!(json.contains("cca-realm-challenge")); - } -} diff --git a/src/attesters/cca/utils/types.rs b/src/attesters/cca/utils/types.rs index c260844..8533382 100644 --- a/src/attesters/cca/utils/types.rs +++ b/src/attesters/cca/utils/types.rs @@ -1,52 +1,5 @@ use serde::{Deserialize, Serialize}; -/// Base64 (standard encoding) serde helpers for `Vec` fields. -/// -/// Use as `#[serde(with = "b64")]` on any `Vec` field. -pub mod b64 { - use base64::{Engine, engine::general_purpose}; - use serde::{Deserialize, Deserializer, Serializer, de::Error}; - - pub fn serialize(v: &[u8], s: S) -> Result { - s.serialize_str(&general_purpose::STANDARD.encode(v)) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { - let encoded = String::deserialize(d)?; - general_purpose::STANDARD - .decode(&encoded) - .map_err(D::Error::custom) - } -} - -/// Base64 (standard encoding) serde helpers for `Vec>` fields. -/// -/// Use as `#[serde(with = "b64_vec")]` on any `Vec>` field. -pub mod b64_vec { - use base64::{Engine, engine::general_purpose}; - use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; - - pub fn serialize(v: &[Vec], s: S) -> Result { - let encoded: Vec = v - .iter() - .map(|b| general_purpose::STANDARD.encode(b)) - .collect(); - encoded.serialize(s) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { - let encoded = Vec::::deserialize(d)?; - encoded - .iter() - .map(|s| { - general_purpose::STANDARD - .decode(s) - .map_err(D::Error::custom) - }) - .collect() - } -} - /// Top-level CCA attestation token. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CcaToken { @@ -144,6 +97,260 @@ pub struct RealmClaims { pub public_key_hash_algo_id: String, } +// --------------------------------------------------------------------------- +// Ciborium conversions +// +// The types above are serde-annotated for JSON serialization/deserialization. +// These From impls convert owned PlatformClaims / RealmClaims into +// ciborium::Value (CBOR maps), enabling the encode pipeline: +// [json claims] -> [PlatformClaims, RealmClaims] -> [CborValue] -> [cbor token] +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +use super::constants::*; +use ciborium::Value as CborValue; + +impl From for CborValue { + #[allow(clippy::vec_init_then_push)] + fn from(p: PlatformClaims) -> Self { + let mut m: Vec<(CborValue, CborValue)> = Vec::new(); + + m.push(( + CborValue::Integer(PLAT_CHALLENGE.into()), + CborValue::Bytes(p.challenge), + )); + m.push(( + CborValue::Integer(PLAT_INSTANCE_ID.into()), + CborValue::Bytes(p.instance_id), + )); + m.push(( + CborValue::Integer(PLAT_PROFILE.into()), + CborValue::Text(p.profile), + )); + m.push(( + CborValue::Integer(PLAT_LIFECYCLE.into()), + CborValue::Integer(p.lifecycle.into()), + )); + m.push(( + CborValue::Integer(PLAT_IMPLEMENTATION_ID.into()), + CborValue::Bytes(p.implementation_id), + )); + + // Software components. + let components: Vec = p + .sw_components + .into_iter() + .map(|c| { + let mut cm: Vec<(CborValue, CborValue)> = Vec::new(); + if let Some(t) = c.measurement_type { + cm.push(( + CborValue::Integer(SW_MEASUREMENT_TYPE.into()), + CborValue::Text(t), + )); + } + cm.push(( + CborValue::Integer(SW_MEASUREMENT_VALUE.into()), + CborValue::Bytes(c.measurement_value), + )); + cm.push(( + CborValue::Integer(SW_SIGNER_ID.into()), + CborValue::Bytes(c.signer_id), + )); + if let Some(d) = c.measurement_description { + cm.push(( + CborValue::Integer(SW_MEASUREMENT_DESC.into()), + CborValue::Text(d), + )); + } + CborValue::Map(cm) + }) + .collect(); + m.push(( + CborValue::Integer(PLAT_SW_COMPONENTS.into()), + CborValue::Array(components), + )); + + if let Some(v) = p.service_indicator { + m.push(( + CborValue::Integer(PLAT_SERVICE_INDICATOR.into()), + CborValue::Text(v), + )); + } + m.push(( + CborValue::Integer(PLAT_CONFIG.into()), + CborValue::Bytes(p.config), + )); + if let Some(algo) = p.hash_algo_id { + m.push(( + CborValue::Integer(PLAT_HASH_ALGO.into()), + CborValue::Text(algo), + )); + } + + CborValue::Map(m) + } +} + +impl From for CborValue { + fn from(r: RealmClaims) -> Self { + let mut m: Vec<(CborValue, CborValue)> = Vec::new(); + + m.push(( + CborValue::Integer(REALM_CHALLENGE.into()), + CborValue::Bytes(r.challenge), + )); + if let Some(profile) = r.profile { + m.push(( + CborValue::Integer(REALM_PROFILE.into()), + CborValue::Text(profile), + )); + } + m.push(( + CborValue::Integer(REALM_PERSONALIZATION.into()), + CborValue::Bytes(r.personalization_value), + )); + m.push(( + CborValue::Integer(REALM_HASH_ALGO.into()), + CborValue::Text(r.hash_algo_id), + )); + m.push(( + CborValue::Integer(REALM_PUBLIC_KEY.into()), + CborValue::Bytes(r.public_key), + )); + m.push(( + CborValue::Integer(REALM_INITIAL_MEASUREMENT.into()), + CborValue::Bytes(r.initial_measurement), + )); + + let ext: Vec = r + .extensible_measurements + .into_iter() + .map(CborValue::Bytes) + .collect(); + m.push(( + CborValue::Integer(REALM_EXTENSIBLE_MEASUREMENTS.into()), + CborValue::Array(ext), + )); + + m.push(( + CborValue::Integer(REALM_PUBLIC_KEY_HASH_ALGO.into()), + CborValue::Text(r.public_key_hash_algo_id), + )); + + CborValue::Map(m) + } +} + +// --------------------------------------------------------------------------- +// ccatoken conversions +// +// Convert from the upstream `ccatoken` library's internal types into our +// serde-enriched types. This mirrors the CBOR conversion direction - +// typed structs stay typed until the last possible moment. +// --------------------------------------------------------------------------- + +use ccatoken::token::SwComponent as CcaSwComponent; +use ccatoken::token::{Platform, Realm}; + +impl From for PlatformClaims { + fn from(p: Platform) -> Self { + PlatformClaims { + profile: p.profile, + challenge: p.challenge, + implementation_id: p.impl_id.to_vec(), + instance_id: p.inst_id.to_vec(), + config: p.config, + lifecycle: p.lifecycle as u64, + sw_components: p.sw_components.into_iter().map(|c| c.into()).collect(), + service_indicator: p.verification_service, + hash_algo_id: Some(p.hash_alg).filter(|s| !s.is_empty()), + } + } +} + +impl From for SwComponent { + fn from(c: CcaSwComponent) -> Self { + SwComponent { + measurement_type: c.mtyp, + measurement_value: c.mval, + version: c.version, + signer_id: c.signer_id, + measurement_description: c.hash_alg, + } + } +} + +impl From for RealmClaims { + fn from(r: Realm) -> Self { + let public_key = r.get_realm_key().unwrap_or_default(); + RealmClaims { + profile: Some(r.profile).filter(|s| !s.is_empty()), + challenge: r.challenge.to_vec(), + personalization_value: r.perso.to_vec(), + initial_measurement: r.rim, + extensible_measurements: r.rem.into_iter().collect(), + hash_algo_id: r.hash_alg, + public_key, + public_key_hash_algo_id: r.rak_hash_alg, + } + } +} + +// --------------------------------------------------------------------------- +// Base64 serde helpers +// +// These serde helper modules are kept at the end of the file because they +// are implementation details used only by the `#[serde(with = "...")]` +// annotations on the types above. +// --------------------------------------------------------------------------- + +/// Base64 (standard encoding) serde helpers for `Vec` fields. +/// +/// Use as `#[serde(with = "b64")]` on any `Vec` field. +mod b64 { + use base64::{Engine, engine::general_purpose}; + use serde::{Deserialize, Deserializer, Serializer, de::Error}; + + pub fn serialize(v: &[u8], s: S) -> Result { + s.serialize_str(&general_purpose::STANDARD.encode(v)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let encoded = String::deserialize(d)?; + general_purpose::STANDARD + .decode(&encoded) + .map_err(D::Error::custom) + } +} + +/// Base64 (standard encoding) serde helpers for `Vec>` fields. +/// +/// Use as `#[serde(with = "b64_vec")]` on any `Vec>` field. +mod b64_vec { + use base64::{Engine, engine::general_purpose}; + use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; + + pub fn serialize(v: &[Vec], s: S) -> Result { + let encoded: Vec = v + .iter() + .map(|b| general_purpose::STANDARD.encode(b)) + .collect(); + encoded.serialize(s) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { + let encoded = Vec::::deserialize(d)?; + encoded + .iter() + .map(|s| { + general_purpose::STANDARD + .decode(s) + .map_err(D::Error::custom) + }) + .collect() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/attesters/ratsd.rs b/src/attesters/ratsd.rs index a08dae6..c5c1692 100644 --- a/src/attesters/ratsd.rs +++ b/src/attesters/ratsd.rs @@ -1,4 +1,4 @@ -//! Generic RATSD attester — posts a challenge to a RATSD daemon and +//! Generic RATSD attester - posts a challenge to a RATSD daemon and //! returns the raw JSON response. //! //! This module handles only the HTTP transport layer. Attester-specific @@ -105,7 +105,7 @@ mod tests { use httpmock::prelude::*; // ----------------------------------------------------------------------- - // Mock server — success path + // Mock server - success path // ----------------------------------------------------------------------- #[test] @@ -133,7 +133,7 @@ mod tests { } // ----------------------------------------------------------------------- - // Mock server — error path + // Mock server - error path // ----------------------------------------------------------------------- #[test] diff --git a/src/tsm/mod.rs b/src/tsm/mod.rs index 915bddf..554c0a8 100644 --- a/src/tsm/mod.rs +++ b/src/tsm/mod.rs @@ -50,7 +50,7 @@ mod tests { #[test] fn tsm_report_default_has_empty_blobs() { - // TsmReport::default() must initialise all three blobs to empty — a + // TsmReport::default() must initialise all three blobs to empty - a // non-empty default would silently carry stale data into callers. let report = TsmReport::default(); assert!(report.outblob.is_empty()); diff --git a/test-data/cca-claims.json b/test-data/cca-claims.json new file mode 100644 index 0000000..5d2931a --- /dev/null +++ b/test-data/cca-claims.json @@ -0,0 +1,107 @@ +{ + "cca-platform-token": { + "cca-platform-profile": "tag:arm.com,2023:cca_platform#1.0.0", + "cca-platform-challenge": "PU8hmS4J+6OFzL6HbpqlMIsZtINOw0gBwN0UVSTedUo=", + "cca-platform-implementation-id": "f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAUFgAAAAAAAA=", + "cca-platform-instance-id": "AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY", + "cca-platform-config": "z8/Pzw==", + "cca-platform-lifecycle": 12291, + "cca-platform-sw-components": [ + { + "measurement-type": "RSE_BL1_2", + "measurement-value": "micfKpFrC27mzsskJvCzIG7wdFeL5V2byU9vP+Orhqo=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "RSE_BL2", + "measurement-value": "U8I05ehHK2rFHBrhyrP+BvrQU7646/2Jd7AQZVv908M=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "RSE_S", + "measurement-value": "ESHPzNWRPwpj/sQKb/1E6mT53BNcZmNLoAHRC89DAqI=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "AP_BL1", + "measurement-value": "FXG17Hi9aFEr94MLtqKkSyBHx99XvOeeuKHA5b6gpQE=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "AP_BL2", + "measurement-value": "EBWbryYrQ6ktldtZ2uH3LGRRJzAWYeCjzk44spWpfFg=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "SCP_BL1", + "measurement-value": "EBIuhWs/zUnwY2NjF0dhSctzChqhz6rYGFUrcvVtb2g=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "SCP_BL2", + "measurement-value": "qmehabC7oheqCqiKZTRpIMhMQkR8NrpffqZfQiwf5dg=", + "signer-id": "8UtJh5BLy1gU5EWaBX7U0g9YpjMVIoinYSFNzSh4C1Y=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "AP_BL31", + "measurement-value": "Lm0xpZg6kSUb+uWu+hwKGdi6PPYB0OinBrTPqWYaa4o=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "RMM", + "measurement-value": "oftQ5shvrhZ57zNRKW/WcTQRoIz43ReQpP0F+uhogWQ=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "HW_CONFIG", + "measurement-value": "GiUkApcvYFf6U8wXK1K5/8ppjhgxH6zQ87Buyq73nhc=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "FW_CONFIG", + "measurement-value": "mpKtvAzuOO9ljHHOGxv4xlZo8Wa/shNkTIlcyxrQeiU=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "TB_FW_CONFIG", + "measurement-value": "I4kDGAzBBOwsXYs/IMW8YbOJ7AqWffjMIIzcfNRUF08=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + }, + { + "measurement-type": "SOC_FW_CONFIG", + "measurement-value": "5sIejSYP5xiC3r2zOdJAKiynZIUpvCMD9IZJvOA4ABc=", + "signer-id": "U3h5YwdTXfPsjYsVouLcVkFBnD0wYM/jIjjA+pc/eqM=", + "measurement-description": "sha-256" + } + ], + "cca-platform-service-indicator": "https://veraison.example/.well-known/veraison/verification", + "cca-platform-hash-algo-id": "sha-256" + }, + "cca-realm-delegated-token": { + "cca-realm-profile": "tag:arm.com,2023:realm#1.0.0", + "cca-realm-challenge": "bobW2XzHE7xt1D285JGmtAMRwCeov4WjnaY+nORMEyqKEZ0pb65qaZnpvz5EcbDOASRdiJQkwx6JeTs7HWsVBA==", + "cca-realm-personalization-value": "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIDEzIGxhenkgZG9ncy5UaGUgcXVpY2sgYnJvd24gZm94IA==", + "cca-realm-initial-measurement": "MRMUq3NiA1DPdYg0rlxl2ejC3H/r5ufZZUu+hk4wDUk=", + "cca-realm-extensible-measurements": [ + "JNWwopbMBcvYBoxQZ8W9Rzt3Ddpq4IL+O6MKvj+aarE=", + "eI/AkL/GuO2QMVK6hBTnPa9bjHux55rVAqsGmbZZ7RY=", + "2sRqWEFdw6ANenQYUgCOnK5k9S0DufdtdvSzZE/vxBY=", + "MsavxiflVYXAMVU1nzMaDiJfaEDblH3Zbvq4G+JnGTk=" + ], + "cca-realm-hash-algo-id": "sha-256", + "cca-realm-public-key": "pQECAzgiIAIhWDCKli/Fdn5kFevspJCbDHIJhqMwdkEmMniYzIn/xg8F6qYG1gtQPzAcYfMxE9yhEuEiWDC8u1EwP8oTFHsE9AW1Zig7dKYQzJr4aveSh6atCsCmCcY5UmGhykjSzh87ZzY+l5k=", + "cca-realm-public-key-hash-algo-id": "sha-256" + } +} diff --git a/test-data/iak.jwk b/test-data/iak.jwk new file mode 100644 index 0000000..2df2021 --- /dev/null +++ b/test-data/iak.jwk @@ -0,0 +1,7 @@ +{ + "crv": "P-384", + "d": "isCQyZWGn2GsE1jwKwIaJqtus4YgOsc1186YVVOLkfdMRLDVgCQ--3maKT3LqgiZ", + "kty": "EC", + "x": "IShnxS4rlQiwpCCpBWDzlNLfqiG911FP8akBr-fh94uxHU5m-Kijivp2r2oxxN6M", + "y": "hM4tr8mWQli1P61xh3T0ViDREbF26DGOEYfbAjWjGNN7pZf-6A4OTHYqEryz6m7U" +} diff --git a/test-data/rak.jwk b/test-data/rak.jwk new file mode 100644 index 0000000..ec44064 --- /dev/null +++ b/test-data/rak.jwk @@ -0,0 +1,7 @@ +{ + "kty": "EC", + "crv": "P-384", + "d": "RxB-TitOn6pJ-iRsK0YxJ0WE6YEMR7_wpqCf_XH5zxz1AYKtbI6RhbwTwXJXvfZt", + "x": "ipYvxXZ-ZBXr7KSQmwxyCYajMHZBJjJ4mMyJ_8YPBeqmBtYLUD8wHGHzMRPcoRLh", + "y": "vLtRMD_KExR7BPQFtWYoO3SmEMya-Gr3koemrQrApgnGOVJhocpI0s4fO2c2PpeZ" +} From b4ff6bad2dabcdee8431a6044295d99ea1b9c8ce Mon Sep 17 00:00:00 2001 From: Dhanus M Lal Date: Wed, 8 Jul 2026 12:11:11 +0530 Subject: [PATCH 2/2] Add license header Signed-off-by: Dhanus M Lal --- examples/attester.rs | 3 +++ examples/tsm.rs | 3 +++ src/attesters/cca/mod.rs | 3 +++ src/attesters/cca/ratsd.rs | 3 +++ src/attesters/cca/simulated.rs | 3 +++ src/attesters/cca/utils/constants.rs | 3 +++ src/attesters/cca/utils/crypto.rs | 3 +++ src/attesters/cca/utils/decode.rs | 3 +++ src/attesters/cca/utils/encode.rs | 3 +++ src/attesters/cca/utils/mod.rs | 3 +++ src/attesters/cca/utils/types.rs | 3 +++ src/attesters/mod.rs | 3 +++ src/attesters/ratsd.rs | 3 +++ src/lib.rs | 3 +++ src/tsm/error.rs | 3 +++ src/tsm/linuxtsm.rs | 3 +++ src/tsm/mod.rs | 3 +++ 17 files changed, 51 insertions(+) diff --git a/examples/attester.rs b/examples/attester.rs index d7d813e..4b32972 100644 --- a/examples/attester.rs +++ b/examples/attester.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + use clap::{Parser, ValueEnum}; use log::{error, info}; use regl::attesters::cca::utils::pretty_print_token; diff --git a/examples/tsm.rs b/examples/tsm.rs index 7b5d1eb..84d183a 100644 --- a/examples/tsm.rs +++ b/examples/tsm.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + use clap::Parser; use log::{error, info}; use regl::tsm::{TsmReportBuilder, linuxtsm::LinuxTsmReportBuilder}; diff --git a/src/attesters/cca/mod.rs b/src/attesters/cca/mod.rs index d1892d9..90bc29f 100644 --- a/src/attesters/cca/mod.rs +++ b/src/attesters/cca/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + use super::Attester; use crate::tsm::linuxtsm::LinuxTsmReportBuilder; use crate::tsm::{TsmError, TsmReport, TsmReportBuilder}; diff --git a/src/attesters/cca/ratsd.rs b/src/attesters/cca/ratsd.rs index 4e350fc..a462e2b 100644 --- a/src/attesters/cca/ratsd.rs +++ b/src/attesters/cca/ratsd.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + //! CCA-specific RATSD attester. //! //! Uses the generic [`RatsdAttester`](crate::attesters::ratsd::RatsdAttester) diff --git a/src/attesters/cca/simulated.rs b/src/attesters/cca/simulated.rs index ae2e374..3bbfaf1 100644 --- a/src/attesters/cca/simulated.rs +++ b/src/attesters/cca/simulated.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + //! Simulated CCA attester - Rust equivalent of `evcli cca create`. //! //! Builds a structurally valid CCA token (CBOR tag 399) with two diff --git a/src/attesters/cca/utils/constants.rs b/src/attesters/cca/utils/constants.rs index 3514a67..970b617 100644 --- a/src/attesters/cca/utils/constants.rs +++ b/src/attesters/cca/utils/constants.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + // CBOR key constants for CCA token claims. // // These values are defined by the Arm CCA attestation token specification. diff --git a/src/attesters/cca/utils/crypto.rs b/src/attesters/cca/utils/crypto.rs index c77ad18..8a5daa1 100644 --- a/src/attesters/cca/utils/crypto.rs +++ b/src/attesters/cca/utils/crypto.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + //! Cryptographic operations for CCA token signing and key management. //! //! Handles P-384 key loading, COSE key building, ES384 signing, diff --git a/src/attesters/cca/utils/decode.rs b/src/attesters/cca/utils/decode.rs index c06dc41..c6f5aa6 100644 --- a/src/attesters/cca/utils/decode.rs +++ b/src/attesters/cca/utils/decode.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + //! CCA token decoding and type conversion. //! //! The `ccatoken` crate decodes CBOR into its own internal types (`Platform`, diff --git a/src/attesters/cca/utils/encode.rs b/src/attesters/cca/utils/encode.rs index ffb8c39..604a881 100644 --- a/src/attesters/cca/utils/encode.rs +++ b/src/attesters/cca/utils/encode.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + //! CCA token construction from typed claims. //! //! Converts [`PlatformClaims`] and [`RealmClaims`] into a CBOR tag 399 diff --git a/src/attesters/cca/utils/mod.rs b/src/attesters/cca/utils/mod.rs index da5c0d5..ddd20a5 100644 --- a/src/attesters/cca/utils/mod.rs +++ b/src/attesters/cca/utils/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + mod constants; mod crypto; mod decode; diff --git a/src/attesters/cca/utils/types.rs b/src/attesters/cca/utils/types.rs index 8533382..33ff0c8 100644 --- a/src/attesters/cca/utils/types.rs +++ b/src/attesters/cca/utils/types.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + use serde::{Deserialize, Serialize}; /// Top-level CCA attestation token. diff --git a/src/attesters/mod.rs b/src/attesters/mod.rs index 580da68..b43df08 100644 --- a/src/attesters/mod.rs +++ b/src/attesters/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + pub mod cca; pub mod ratsd; diff --git a/src/attesters/ratsd.rs b/src/attesters/ratsd.rs index c5c1692..3c07391 100644 --- a/src/attesters/ratsd.rs +++ b/src/attesters/ratsd.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + //! Generic RATSD attester - posts a challenge to a RATSD daemon and //! returns the raw JSON response. //! diff --git a/src/lib.rs b/src/lib.rs index 4dd40dd..3dcd118 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,5 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + pub mod attesters; pub mod tsm; diff --git a/src/tsm/error.rs b/src/tsm/error.rs index cda1689..ce9d330 100644 --- a/src/tsm/error.rs +++ b/src/tsm/error.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + use thiserror::Error; #[derive(Debug, Error)] diff --git a/src/tsm/linuxtsm.rs b/src/tsm/linuxtsm.rs index 4caf464..af31451 100644 --- a/src/tsm/linuxtsm.rs +++ b/src/tsm/linuxtsm.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + use super::{TsmError, TsmReport, TsmReportBuilder}; use std::fs; diff --git a/src/tsm/mod.rs b/src/tsm/mod.rs index 554c0a8..fc55259 100644 --- a/src/tsm/mod.rs +++ b/src/tsm/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2026 Contributors to the Veraison project +// SPDX-License-Identifier: Apache-2.0 + mod error; pub mod linuxtsm; pub use error::TsmError;