diff --git a/CHANGELOG.md b/CHANGELOG.md index 72ef94fb8..be9f22026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,14 @@ At the moment this project **does not** adhere to structure, and the `NodeInfoChanged` event were removed from the Staking Extension pallet. The `AttestationHandler` config type was added to the Staking Extension pallet. The `KeyProvider` and `AttestationQueue` config types were removed from the Attestation pallet. +- In [#1068](https://github.com/entropyxyz/entropy-core/pull/1068) an extra type `PckCertChainVerifier` + was added to the staking extension pallet's `Config` trait. - In [#1134](https://github.com/entropyxyz/entropy-core/pull/1134/) the ```no-sync``` option was removed + ### Changed - Use correct key rotation endpoint in OCW ([#1104](https://github.com/entropyxyz/entropy-core/pull/1104)) - Change attestation flow to be pull based ([#1109](https://github.com/entropyxyz/entropy-core/pull/1109/)) +- Handle PCK certificates ([#1068](https://github.com/entropyxyz/entropy-core/pull/1068)) - Remove declare synced ([#1134](https://github.com/entropyxyz/entropy-core/pull/1134/)) ## [0.3.0-rc.1](https://github.com/entropyxyz/entropy-core/compare/release/v0.2.0...release/v0.3.0-rc.1) - 2024-10-04 diff --git a/Cargo.lock b/Cargo.lock index 23fa0e60a..2ad521315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2055,6 +2055,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "der_derive", + "flagset", "zeroize", ] @@ -2072,6 +2074,17 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "deranged" version = "0.3.11" @@ -3170,6 +3183,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + [[package]] name = "flate2" version = "1.0.28" @@ -6423,6 +6442,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -6694,8 +6730,10 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" dependencies = [ + "ecdsa", "elliptic-curve", "primeorder", + "sha2 0.10.8", ] [[package]] @@ -7430,6 +7468,7 @@ dependencies = [ "frame-support 29.0.2", "frame-system", "log", + "p256", "pallet-bags-list", "pallet-balances", "pallet-parameters", @@ -7438,6 +7477,7 @@ dependencies = [ "pallet-staking-reward-curve", "pallet-timestamp", "parity-scale-codec", + "rand", "rand_chacha 0.3.1", "rand_core 0.6.4", "scale-info", @@ -7449,7 +7489,9 @@ dependencies = [ "sp-runtime 32.0.0", "sp-staking 27.0.0", "sp-std 14.0.0", + "spki", "tdx-quote", + "x509-verify", ] [[package]] @@ -7957,6 +7999,17 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -8971,6 +9024,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle 2.5.0", + "zeroize", +] + [[package]] name = "rtnetlink" version = "0.10.1" @@ -16532,6 +16605,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", +] + +[[package]] +name = "x509-ocsp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e54e695a31f0fecb826cf59ae2093c941d7ef932a1f8508185dd23b29ce2e2e" +dependencies = [ + "const-oid", + "der", + "spki", + "x509-cert", +] + [[package]] name = "x509-parser" version = "0.14.0" @@ -16550,6 +16646,27 @@ dependencies = [ "time", ] +[[package]] +name = "x509-verify" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605feeee7186660fcb5ddaa1263b6d3b9ba16f128a6b48cb5e24ef7a241e43ab" +dependencies = [ + "const-oid", + "der", + "ecdsa", + "ed25519-dalek", + "k256", + "p256", + "p384", + "rsa", + "sha2 0.10.8", + "signature", + "spki", + "x509-cert", + "x509-ocsp", +] + [[package]] name = "yamux" version = "0.10.2" diff --git a/crates/client/entropy_metadata.scale b/crates/client/entropy_metadata.scale index c8e04d463..fcfbd8800 100644 Binary files a/crates/client/entropy_metadata.scale and b/crates/client/entropy_metadata.scale differ diff --git a/pallets/attestation/src/mock.rs b/pallets/attestation/src/mock.rs index f75f82b58..c546530e4 100644 --- a/pallets/attestation/src/mock.rs +++ b/pallets/attestation/src/mock.rs @@ -315,6 +315,7 @@ impl pallet_staking_extension::Config for Test { type AttestationHandler = (); type Currency = Balances; type MaxEndpointLength = MaxEndpointLength; + type PckCertChainVerifier = pallet_staking_extension::pck::MockPckCertChainVerifier; type Randomness = TestPastRandomness; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); diff --git a/pallets/propagation/src/mock.rs b/pallets/propagation/src/mock.rs index 31e4e1600..424010c8b 100644 --- a/pallets/propagation/src/mock.rs +++ b/pallets/propagation/src/mock.rs @@ -309,6 +309,7 @@ impl pallet_staking_extension::Config for Test { type AttestationHandler = (); type Currency = Balances; type MaxEndpointLength = MaxEndpointLength; + type PckCertChainVerifier = pallet_staking_extension::pck::MockPckCertChainVerifier; type Randomness = TestPastRandomness; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); diff --git a/pallets/registry/src/mock.rs b/pallets/registry/src/mock.rs index 4f20fe263..ad5b26c3b 100644 --- a/pallets/registry/src/mock.rs +++ b/pallets/registry/src/mock.rs @@ -306,6 +306,7 @@ impl pallet_staking_extension::Config for Test { type AttestationHandler = (); type Currency = Balances; type MaxEndpointLength = MaxEndpointLength; + type PckCertChainVerifier = pallet_staking_extension::pck::MockPckCertChainVerifier; type Randomness = TestPastRandomness; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); diff --git a/pallets/staking/Cargo.toml b/pallets/staking/Cargo.toml index 5729b1850..185002c38 100644 --- a/pallets/staking/Cargo.toml +++ b/pallets/staking/Cargo.toml @@ -28,6 +28,10 @@ sp-runtime ={ version="32.0.0", default-features=false } sp-staking ={ version="27.0.0", default-features=false } sp-std ={ version="14.0.0", default-features=false } sp-consensus-babe ={ version="0.33.0", default-features=false } +x509-verify ={ version="0.4.6", features=["x509"] } +spki ="0.7.3" +p256 ={ version="0.13.2", default-features=false, features=["ecdsa"] } +rand ={ version="0.8.5", default-features=false, features=["alloc"] } pallet-parameters={ version="0.3.0-rc.1", path="../parameters", default-features=false } entropy-shared={ version="0.3.0-rc.1", path="../../crates/shared", features=[ diff --git a/pallets/staking/src/benchmarking.rs b/pallets/staking/src/benchmarking.rs index 126cd6fed..7969acbd3 100644 --- a/pallets/staking/src/benchmarking.rs +++ b/pallets/staking/src/benchmarking.rs @@ -16,6 +16,7 @@ //! Benchmarking setup for pallet-propgation #![allow(unused_imports)] use super::*; +use crate::pck::{signing_key_from_seed, MOCK_PCK_DERIVED_FROM_NULL_ARRAY}; #[allow(unused_imports)] use crate::Pallet as Staking; use entropy_shared::{AttestationHandler, MAX_SIGNERS}; @@ -71,6 +72,46 @@ pub fn create_validators( validators } +/// Sets up a mock quote and requests an attestation in preparation for calling the `validate` +/// extrinsic +fn prepare_attestation_for_validate( + threshold: T::AccountId, + x25519_public_key: [u8; 32], + endpoint: Vec, + block_number: u32, +) -> (Vec, JoiningServerInfo) { + let nonce = NULL_ARR; + let quote = { + let pck = signing_key_from_seed(NULL_ARR); + /// This is a randomly generated secret p256 ECDSA key - for mocking attestation + const ATTESTATION_KEY: [u8; 32] = [ + 167, 184, 203, 130, 240, 249, 191, 129, 206, 9, 200, 29, 99, 197, 64, 81, 135, 166, 59, + 73, 31, 27, 206, 207, 69, 248, 56, 195, 64, 92, 109, 46, + ]; + + let attestation_key = tdx_quote::SigningKey::from_bytes(&ATTESTATION_KEY.into()).unwrap(); + + let input_data = + entropy_shared::QuoteInputData::new(&threshold, x25519_public_key, nonce, block_number); + + tdx_quote::Quote::mock(attestation_key.clone(), pck, input_data.0).as_bytes().to_vec() + }; + + let joining_server_info = JoiningServerInfo { + tss_account: threshold.clone(), + x25519_public_key, + endpoint, + // Since we are using the mock PckCertChainVerifier, this needs to be the same seed for + // generating the PCK as we used to sign the quote above + pck_certificate_chain: vec![NULL_ARR.to_vec()], + }; + + // We need to tell the attestation handler that we want a quote. This will let the system to + // know to expect one back when we call `validate()`. + T::AttestationHandler::request_quote(&threshold, nonce); + (quote, joining_server_info) +} + fn prep_bond_and_validate( validate_also: bool, caller: T::AccountId, @@ -91,22 +132,19 @@ fn prep_bond_and_validate( )); if validate_also { - let server_info = ServerInfo { - tss_account: threshold, + let block_number = 0; + let endpoint = vec![20, 20]; + let (quote, joining_server_info) = prepare_attestation_for_validate::( + threshold, x25519_public_key, - endpoint: vec![20, 20], - provisioning_certification_key: BoundedVec::with_max_capacity(), - }; - - // Note: This isn't a valid quote, but for testing benches this will pass. - // - // For actually running benches a valid quote will be required in the future. - let quote = [0; 32].to_vec(); + endpoint, + block_number, + ); assert_ok!(>::validate( RawOrigin::Signed(bonder.clone()).into(), ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), quote, )); @@ -114,7 +152,17 @@ fn prep_bond_and_validate( .or(Err(Error::::InvalidValidatorId)) .unwrap(); - ThresholdToStash::::insert(&server_info.tss_account, &validator_id); + ThresholdToStash::::insert(&joining_server_info.tss_account, &validator_id); + + let server_info = ServerInfo { + tss_account: joining_server_info.tss_account, + x25519_public_key: joining_server_info.x25519_public_key, + endpoint: joining_server_info.endpoint, + provisioning_certification_key: MOCK_PCK_DERIVED_FROM_NULL_ARRAY + .to_vec() + .try_into() + .unwrap(), + }; ThresholdServers::::insert(&validator_id, server_info); } } @@ -152,7 +200,7 @@ benchmarks! { endpoint: vec![20, 20], tss_account: _bonder.clone(), x25519_public_key: NULL_ARR, - provisioning_certification_key: BoundedVec::with_max_capacity(), + provisioning_certification_key: MOCK_PCK_DERIVED_FROM_NULL_ARRAY.to_vec().try_into().unwrap(), }; assert_last_event::(Event::::ThresholdAccountChanged(bonder, server_info).into()); } @@ -269,7 +317,6 @@ benchmarks! { .or(Err(Error::::InvalidValidatorId)) .unwrap(); - let block_number = 1; let nonce = NULL_ARR; let x25519_public_key = NULL_ARR; let endpoint = vec![]; @@ -283,48 +330,10 @@ benchmarks! { x25519_public_key.clone() ); - /// This is a randomly generated secret p256 ECDSA key - for mocking the provisioning certification - /// key - const PCK: [u8; 32] = [ - 117, 153, 212, 7, 220, 16, 181, 32, 110, 138, 4, 68, 208, 37, 104, 54, 1, 110, 232, 207, 100, - 168, 16, 99, 66, 83, 21, 178, 81, 155, 132, 37, - ]; - - let pck = tdx_quote::SigningKey::from_bytes(&PCK.into()).unwrap(); - let pck_encoded = tdx_quote::encode_verifying_key(pck.verifying_key()).unwrap(); - let provisioning_certification_key = BoundedVec::try_from(pck_encoded.to_vec()).unwrap(); - - let quote = { - /// This is a randomly generated secret p256 ECDSA key - for mocking attestation - const ATTESTATION_KEY: [u8; 32] = [ - 167, 184, 203, 130, 240, 249, 191, 129, 206, 9, 200, 29, 99, 197, 64, 81, 135, 166, 59, 73, 31, - 27, 206, 207, 69, 248, 56, 195, 64, 92, 109, 46, - ]; - - let attestation_key = tdx_quote::SigningKey::from_bytes(&ATTESTATION_KEY.into()).unwrap(); - - let input_data = entropy_shared::QuoteInputData::new( - &threshold_account, - x25519_public_key, - nonce, - block_number, - ); - - tdx_quote::Quote::mock(attestation_key.clone(), pck, input_data.0).as_bytes().to_vec() - }; - - let server_info = ServerInfo { - tss_account: threshold_account.clone(), - x25519_public_key, - endpoint: endpoint.clone(), - provisioning_certification_key, - }; - - // We need to tell the attestation handler that we want a quote. This will let the system to - // know to expect one back when we call `validate()`. - T::AttestationHandler::request_quote(&threshold_account, nonce); - - }: _(RawOrigin::Signed(bonder.clone()), ValidatorPrefs::default(), server_info, quote) + let block_number = 1; + let (quote, joining_server_info) = + prepare_attestation_for_validate::(threshold_account.clone(), x25519_public_key, endpoint.clone(), block_number); + }: _(RawOrigin::Signed(bonder.clone()), ValidatorPrefs::default(), joining_server_info, quote) verify { assert_last_event::( Event::::ValidatorCandidateAccepted( diff --git a/pallets/staking/src/lib.rs b/pallets/staking/src/lib.rs index cd583982d..fcd37e88a 100644 --- a/pallets/staking/src/lib.rs +++ b/pallets/staking/src/lib.rs @@ -41,6 +41,8 @@ use serde::{Deserialize, Serialize}; pub use crate::weights::WeightInfo; +pub mod pck; + #[cfg(test)] mod mock; @@ -68,6 +70,7 @@ pub mod pallet { DefaultNoBound, }; use frame_system::pallet_prelude::*; + use pck::PckCertChainVerifier; use rand_chacha::{ rand_core::{RngCore, SeedableRng}, ChaCha20Rng, ChaChaRng, @@ -94,6 +97,9 @@ pub mod pallet { /// The weight information of this pallet. type WeightInfo: WeightInfo; + /// A type that verifies a provisioning certification key (PCK) certificate chain. + type PckCertChainVerifier: PckCertChainVerifier; + /// Something that provides randomness in the runtime. type Randomness: Randomness>; @@ -124,6 +130,18 @@ pub mod pallet { pub endpoint: TssServerURL, pub provisioning_certification_key: VerifyingKey, } + + /// Information about a threshold server in the process of joining + /// This becomes a [ServerInfo] when the Pck certificate chain has been validated + #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)] + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + pub struct JoiningServerInfo { + pub tss_account: AccountId, + pub x25519_public_key: X25519PublicKey, + pub endpoint: TssServerURL, + pub pck_certificate_chain: Vec>, + } + /// Info that is requiered to do a proactive refresh #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, Default)] pub struct RefreshInfo { @@ -311,9 +329,24 @@ pub mod pallet { NoUnnominatingWhenSigner, NoUnnominatingWhenNextSigner, NoChangingThresholdAccountWhenSigner, + PckCertificateParse, + PckCertificateVerify, + PckCertificateBadPublicKey, + PckCertificateNoCertificate, FailedAttestationCheck, } + impl From for Error { + fn from(error: pck::PckParseVerifyError) -> Self { + match error { + pck::PckParseVerifyError::Parse => Error::::PckCertificateParse, + pck::PckParseVerifyError::Verify => Error::::PckCertificateVerify, + pck::PckParseVerifyError::BadPublicKey => Error::::PckCertificateBadPublicKey, + pck::PckParseVerifyError::NoCertificate => Error::::PckCertificateNoCertificate, + } + } + } + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -504,11 +537,26 @@ pub mod pallet { pub fn validate( origin: OriginFor, prefs: ValidatorPrefs, - server_info: ServerInfo, + joining_server_info: JoiningServerInfo, quote: Vec, ) -> DispatchResult { let who = ensure_signed(origin.clone())?; + let provisioning_certification_key = + T::PckCertChainVerifier::verify_pck_certificate_chain( + joining_server_info.pck_certificate_chain, + ) + .map_err(|error| { + let e: Error = error.into(); + e + })?; + + let server_info = ServerInfo:: { + tss_account: joining_server_info.tss_account, + x25519_public_key: joining_server_info.x25519_public_key, + endpoint: joining_server_info.endpoint, + provisioning_certification_key, + }; ensure!( server_info.endpoint.len() as u32 <= T::MaxEndpointLength::get(), Error::::EndpointTooLong diff --git a/pallets/staking/src/mock.rs b/pallets/staking/src/mock.rs index 58898d137..6dea3171c 100644 --- a/pallets/staking/src/mock.rs +++ b/pallets/staking/src/mock.rs @@ -37,6 +37,7 @@ use sp_staking::{EraIndex, SessionIndex}; use sp_std::vec; use crate as pallet_staking_extension; +use pallet_staking_extension::pck::MockPckCertChainVerifier; type Block = frame_system::mocking::MockBlock; type BlockNumber = u64; @@ -419,6 +420,7 @@ impl pallet_staking_extension::Config for Test { type AttestationHandler = MockAttestationHandler; type Currency = Balances; type MaxEndpointLength = MaxEndpointLength; + type PckCertChainVerifier = MockPckCertChainVerifier; type Randomness = TestPastRandomness; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); diff --git a/pallets/staking/src/pck/Intel_SGX_Provisioning_Certification_RootCA.cer b/pallets/staking/src/pck/Intel_SGX_Provisioning_Certification_RootCA.cer new file mode 100644 index 000000000..768806c67 Binary files /dev/null and b/pallets/staking/src/pck/Intel_SGX_Provisioning_Certification_RootCA.cer differ diff --git a/pallets/staking/src/pck/mock.rs b/pallets/staking/src/pck/mock.rs new file mode 100644 index 000000000..5623cdd0d --- /dev/null +++ b/pallets/staking/src/pck/mock.rs @@ -0,0 +1,56 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use super::{CompressedVerifyingKey, PckCertChainVerifier, PckParseVerifyError}; +use p256::ecdsa::{SigningKey, VerifyingKey}; +use rand::{rngs::StdRng, SeedableRng}; +use sp_std::vec::Vec; + +/// This is used in the benchmarking tests to check that ServerInfo is as expected +pub const MOCK_PCK_DERIVED_FROM_NULL_ARRAY: [u8; 33] = [ + 3, 237, 193, 27, 177, 204, 234, 67, 54, 141, 157, 13, 62, 87, 113, 224, 4, 121, 206, 251, 190, + 151, 134, 87, 68, 46, 37, 163, 127, 97, 252, 174, 108, +]; + +/// A PCK certificate chain verifier for testing. +/// Rather than actually use test certificates, we give here the TSS account ID instead of the first +/// certificate, and derive a keypair from it. The same keypair will be derived when creating a mock +/// quote in entropy-tss +pub struct MockPckCertChainVerifier {} + +impl PckCertChainVerifier for MockPckCertChainVerifier { + fn verify_pck_certificate_chain( + pck_certificate_chain: Vec>, + ) -> Result { + let first_certificate = + pck_certificate_chain.first().ok_or(PckParseVerifyError::NoCertificate)?; + + // Read the certificate bytes as a TSS account id + let tss_account_id: [u8; 32] = + first_certificate.clone().try_into().map_err(|_| PckParseVerifyError::Parse)?; + + // Derive a keypair + let pck_secret = signing_key_from_seed(tss_account_id); + + // Convert/compress the public key + let pck_public = VerifyingKey::from(&pck_secret); + let pck_public = pck_public.to_encoded_point(true).as_bytes().to_vec(); + pck_public.try_into().map_err(|_| PckParseVerifyError::Parse) + } +} + +pub fn signing_key_from_seed(input: [u8; 32]) -> SigningKey { + let mut pck_seeder = StdRng::from_seed(input); + SigningKey::random(&mut pck_seeder) +} diff --git a/pallets/staking/src/pck/mod.rs b/pallets/staking/src/pck/mod.rs new file mode 100644 index 000000000..3694d988a --- /dev/null +++ b/pallets/staking/src/pck/mod.rs @@ -0,0 +1,58 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +mod mock; +pub use mock::{signing_key_from_seed, MockPckCertChainVerifier, MOCK_PCK_DERIVED_FROM_NULL_ARRAY}; +mod production; +use super::VerifyingKey as CompressedVerifyingKey; +use core::array::TryFromSliceError; +use sp_std::vec::Vec; + +/// Provides a way of verifying a chain of certificates to give a chain of trust between the +/// provisioning certification key used to sign a TDX quote to the Intel route certificate authority +pub trait PckCertChainVerifier { + /// Verify an arbitrary chain of DER-encoded x509 certificates against Intel's root CA. + /// Typically this is two certificates, the PCK certificate and an intermediary provider + /// certificate + fn verify_pck_certificate_chain( + pck_certificate_chain: Vec>, + ) -> Result; +} + +/// An error when parsing or verifying a PCK or provider certificate +#[derive(Debug)] +pub enum PckParseVerifyError { + Parse, + Verify, + BadPublicKey, + NoCertificate, +} + +impl From for PckParseVerifyError { + fn from(_: spki::der::Error) -> PckParseVerifyError { + PckParseVerifyError::Parse + } +} + +impl From for PckParseVerifyError { + fn from(_: x509_verify::Error) -> PckParseVerifyError { + PckParseVerifyError::Verify + } +} + +impl From for PckParseVerifyError { + fn from(_: TryFromSliceError) -> PckParseVerifyError { + PckParseVerifyError::BadPublicKey + } +} diff --git a/pallets/staking/src/pck/production.rs b/pallets/staking/src/pck/production.rs new file mode 100644 index 000000000..7a0404123 --- /dev/null +++ b/pallets/staking/src/pck/production.rs @@ -0,0 +1,110 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use sp_std::vec::Vec; +use x509_verify::{ + der::{Decode, Encode}, + x509_cert::Certificate, + Signature, VerifyInfo, VerifyingKey, +}; + +use super::{CompressedVerifyingKey, PckCertChainVerifier, PckParseVerifyError}; + +/// Intels root CA certificate in DER format available from here: +/// https://certificates.trustedservices.intel.com/Intel_SGX_Provisioning_Certification_RootCA.cer +/// Valid until December 31 2049 +const INTEL_ROOT_CA_DER: &[u8; 659] = + include_bytes!("Intel_SGX_Provisioning_Certification_RootCA.cer"); + +/// A PCK certificate chain verifier for use in production where entropy-tss is running on TDX +/// hardware and we have a PCK certificate chain +pub struct ProductionPckCertChainVerifier {} + +impl PckCertChainVerifier for ProductionPckCertChainVerifier { + fn verify_pck_certificate_chain( + pck_certificate_chain: Vec>, + ) -> Result { + let pck_uncompressed = verify_pck_cert_chain(pck_certificate_chain)?; + + // Compress / convert public key + let point = p256::EncodedPoint::from_bytes(pck_uncompressed) + .map_err(|_| PckParseVerifyError::BadPublicKey)?; + let pck_verifying_key = p256::ecdsa::VerifyingKey::from_encoded_point(&point) + .map_err(|_| PckParseVerifyError::BadPublicKey)?; + let pck_compressed = pck_verifying_key.to_encoded_point(true).as_bytes().to_vec(); + pck_compressed.try_into().map_err(|_| PckParseVerifyError::BadPublicKey) + } +} + +/// Validate PCK and provider certificates and if valid return the PCK +/// These certificates will be provided by a joining validator +fn verify_pck_cert_chain(certificates_der: Vec>) -> Result<[u8; 65], PckParseVerifyError> { + if certificates_der.is_empty() { + return Err(PckParseVerifyError::NoCertificate); + } + + // Parse the certificates + let mut certificates = Vec::new(); + for certificate in certificates_der { + certificates.push(Certificate::from_der(&certificate)?); + } + // Add the root certificate to the end of the chain. Since the root cert is self-signed, this + // will work regardless of whether the user has included this certicate in the chain or not + certificates.push(Certificate::from_der(INTEL_ROOT_CA_DER)?); + + // Verify the certificate chain + for i in 0..certificates.len() { + let verifying_key: &VerifyingKey = if i + 1 == certificates.len() { + &certificates[i].tbs_certificate.subject_public_key_info.clone().try_into()? + } else { + &certificates[i + 1].tbs_certificate.subject_public_key_info.clone().try_into()? + }; + verify_cert(&certificates[i], verifying_key)?; + } + + // Get the first certificate + let pck_key = &certificates + .first() + .ok_or(PckParseVerifyError::NoCertificate)? + .tbs_certificate + .subject_public_key_info + .subject_public_key; + + Ok(pck_key.as_bytes().ok_or(PckParseVerifyError::BadPublicKey)?.try_into()?) +} + +/// Given a cerificate and a public key, verify the certificate +fn verify_cert(subject: &Certificate, issuer_pk: &VerifyingKey) -> Result<(), PckParseVerifyError> { + let verify_info = VerifyInfo::new( + subject.tbs_certificate.to_der()?.into(), + Signature::new( + &subject.signature_algorithm, + subject.signature.as_bytes().ok_or(PckParseVerifyError::Parse)?, + ), + ); + Ok(issuer_pk.verify(&verify_info)?) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_verify_pck_cert_chain() { + let pck = include_bytes!("../../test_pck_certs/pck_cert.der").to_vec(); + let platform = include_bytes!("../../test_pck_certs/platform_pcs_cert.der").to_vec(); + assert!(ProductionPckCertChainVerifier::verify_pck_certificate_chain(vec![pck, platform]) + .is_ok()); + } +} diff --git a/pallets/staking/src/tests.rs b/pallets/staking/src/tests.rs index 98faa0d24..0340d4cf9 100644 --- a/pallets/staking/src/tests.rs +++ b/pallets/staking/src/tests.rs @@ -14,7 +14,8 @@ // along with this program. If not, see . use crate::{ - mock::*, tests::RuntimeEvent, Error, NextSignerInfo, NextSigners, ServerInfo, Signers, + mock::*, pck::MOCK_PCK_DERIVED_FROM_NULL_ARRAY, tests::RuntimeEvent, Error, JoiningServerInfo, + NextSignerInfo, NextSigners, ServerInfo, Signers, }; use codec::Encode; use frame_support::{assert_noop, assert_ok}; @@ -60,16 +61,16 @@ fn it_takes_in_an_endpoint() { pallet_staking::RewardDestination::Account(1), )); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 3, x25519_public_key: NULL_ARR, endpoint: vec![20], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_ok!(Staking::validate( RuntimeOrigin::signed(1), pallet_staking::ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), VALID_QUOTE.to_vec(), )); @@ -78,33 +79,33 @@ fn it_takes_in_an_endpoint() { assert_eq!(tss_account, 3); assert_eq!(Staking::threshold_to_stash(3).unwrap(), 1); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 3, x25519_public_key: NULL_ARR, endpoint: vec![20; 26], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_noop!( Staking::validate( RuntimeOrigin::signed(4), pallet_staking::ValidatorPrefs::default(), - server_info, + joining_server_info, VALID_QUOTE.to_vec(), ), Error::::EndpointTooLong ); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 5, x25519_public_key: NULL_ARR, endpoint: vec![20, 20], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_noop!( Staking::validate( RuntimeOrigin::signed(4), pallet_staking::ValidatorPrefs::default(), - server_info, + joining_server_info, VALID_QUOTE.to_vec(), ), pallet_staking::Error::::NotController @@ -121,16 +122,16 @@ fn it_will_not_allow_validator_to_use_existing_tss_account() { pallet_staking::RewardDestination::Account(1), )); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 3, x25519_public_key: NULL_ARR, endpoint: vec![20], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_ok!(Staking::validate( RuntimeOrigin::signed(1), pallet_staking::ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), VALID_QUOTE.to_vec(), )); @@ -144,7 +145,7 @@ fn it_will_not_allow_validator_to_use_existing_tss_account() { Staking::validate( RuntimeOrigin::signed(2), pallet_staking::ValidatorPrefs::default(), - server_info, + joining_server_info, VALID_QUOTE.to_vec(), ), Error::::TssAccountAlreadyExists @@ -161,16 +162,16 @@ fn it_changes_endpoint() { pallet_staking::RewardDestination::Account(1), )); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 3, x25519_public_key: NULL_ARR, endpoint: vec![20], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_ok!(Staking::validate( RuntimeOrigin::signed(1), pallet_staking::ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), VALID_QUOTE.to_vec(), )); @@ -193,16 +194,16 @@ fn it_changes_threshold_account() { pallet_staking::RewardDestination::Account(1), )); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 3, x25519_public_key: NULL_ARR, endpoint: vec![20], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_ok!(Staking::validate( RuntimeOrigin::signed(1), pallet_staking::ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), VALID_QUOTE.to_vec(), )); @@ -222,16 +223,16 @@ fn it_changes_threshold_account() { pallet_staking::RewardDestination::Account(2), )); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 5, x25519_public_key: NULL_ARR, endpoint: vec![20], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_ok!(Staking::validate( RuntimeOrigin::signed(2), pallet_staking::ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), VALID_QUOTE.to_vec(), )); @@ -257,16 +258,16 @@ fn it_will_not_allow_existing_tss_account_when_changing_threshold_account() { pallet_staking::RewardDestination::Account(1), )); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 3, x25519_public_key: NULL_ARR, endpoint: vec![20], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_ok!(Staking::validate( RuntimeOrigin::signed(1), pallet_staking::ValidatorPrefs::default(), - server_info, + joining_server_info, VALID_QUOTE.to_vec(), )); @@ -277,16 +278,16 @@ fn it_will_not_allow_existing_tss_account_when_changing_threshold_account() { pallet_staking::RewardDestination::Account(2), )); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 5, x25519_public_key: NULL_ARR, endpoint: vec![20], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_ok!(Staking::validate( RuntimeOrigin::signed(2), pallet_staking::ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), VALID_QUOTE.to_vec(), )); @@ -309,16 +310,16 @@ fn it_deletes_when_no_bond_left() { pallet_staking::RewardDestination::Account(1), )); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: 3, x25519_public_key: NULL_ARR, endpoint: vec![20], - provisioning_certification_key: BoundedVec::with_max_capacity(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; assert_ok!(Staking::validate( RuntimeOrigin::signed(2), pallet_staking::ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), VALID_QUOTE.to_vec(), )); @@ -588,11 +589,11 @@ fn it_requires_attestation_before_validate_is_succesful() { pallet_staking::RewardDestination::Account(alice), )); - let server_info = ServerInfo { + let joining_server_info = JoiningServerInfo { tss_account: bob, x25519_public_key: NULL_ARR, endpoint: vec![20], - provisioning_certification_key: BoundedVec::try_from([0; 32].to_vec()).unwrap(), + pck_certificate_chain: vec![[0u8; 32].to_vec()], }; // First we test that an invalid attestation doesn't allow us to submit our candidacy. @@ -600,23 +601,32 @@ fn it_requires_attestation_before_validate_is_succesful() { Staking::validate( RuntimeOrigin::signed(alice), pallet_staking::ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), INVALID_QUOTE.to_vec(), ), Error::::FailedAttestationCheck ); assert_eq!(Staking::threshold_server(bob), None); - assert_eq!(Staking::threshold_to_stash(server_info.tss_account), None); + assert_eq!(Staking::threshold_to_stash(joining_server_info.tss_account), None); // Next we test that a valid attestation gets us into a candidate state. assert_ok!(Staking::validate( RuntimeOrigin::signed(alice), pallet_staking::ValidatorPrefs::default(), - server_info.clone(), + joining_server_info.clone(), VALID_QUOTE.to_vec(), )); + let server_info = ServerInfo:: { + tss_account: joining_server_info.tss_account, + x25519_public_key: joining_server_info.x25519_public_key, + endpoint: joining_server_info.endpoint, + provisioning_certification_key: MOCK_PCK_DERIVED_FROM_NULL_ARRAY + .to_vec() + .try_into() + .unwrap(), + }; assert_eq!(Staking::threshold_to_stash(bob), Some(alice)); assert_eq!(Staking::threshold_server(alice), Some(server_info)); }) diff --git a/pallets/staking/test_pck_certs/pck_cert.der b/pallets/staking/test_pck_certs/pck_cert.der new file mode 100644 index 000000000..69279aeca Binary files /dev/null and b/pallets/staking/test_pck_certs/pck_cert.der differ diff --git a/pallets/staking/test_pck_certs/platform_pcs_cert.der b/pallets/staking/test_pck_certs/platform_pcs_cert.der new file mode 100644 index 000000000..9efb3ba72 Binary files /dev/null and b/pallets/staking/test_pck_certs/platform_pcs_cert.der differ diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index b50d3dcaa..f801c6a94 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -718,6 +718,7 @@ impl pallet_staking_extension::Config for Runtime { type AttestationHandler = Attestation; type Currency = Balances; type MaxEndpointLength = MaxEndpointLength; + type PckCertChainVerifier = pallet_staking_extension::pck::MockPckCertChainVerifier; type Randomness = pallet_babe::RandomnessFromOneEpochAgo; type RuntimeEvent = RuntimeEvent; type WeightInfo = weights::pallet_staking_extension::WeightInfo;