diff --git a/Cargo.lock b/Cargo.lock index c8e8c24b747..99f22d641ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -728,8 +728,24 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", + "asn1-rs-derive 0.4.0", + "asn1-rs-impl 0.1.0", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ad1373757efa0f70ec53939aabc7152e1591cb485208052993070ac8d2429d" +dependencies = [ + "asn1-rs-derive 0.5.0", + "asn1-rs-impl 0.2.0", "displaydoc", "nom", "num-traits", @@ -747,7 +763,19 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", - "synstructure", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "synstructure 0.13.1", ] [[package]] @@ -761,6 +789,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -2277,7 +2316,21 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.1", "displaydoc", "nom", "num-bigint", @@ -6750,7 +6803,7 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", - "synstructure", + "synstructure 0.12.6", ] [[package]] @@ -6966,6 +7019,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-format" version = "0.4.4" @@ -7077,7 +7136,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", +] + +[[package]] +name = "oid-registry" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c958dd45046245b9c3c2547369bb634eb461670b2e7e0de552905801a648d1d" +dependencies = [ + "asn1-rs 0.6.1", ] [[package]] @@ -10608,6 +10676,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sp-certificate-registry" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "sc-executor", + "scale-info", + "sp-api", + "sp-blockchain", + "sp-core", + "sp-externalities", + "sp-runtime", + "sp-runtime-interface", + "subspace-runtime-primitives", + "time", + "x509-parser 0.16.0", +] + [[package]] name = "sp-consensus" version = "0.10.0-dev" @@ -12377,6 +12463,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -12520,12 +12617,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -12540,10 +12638,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -13893,13 +13992,13 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", "base64 0.13.1", "data-encoding", - "der-parser", + "der-parser 8.2.0", "lazy_static", "nom", - "oid-registry", + "oid-registry 0.6.1", "rusticata-macros", "thiserror", "time", @@ -13911,12 +14010,30 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", + "data-encoding", + "der-parser 8.2.0", + "lazy_static", + "nom", + "oid-registry 0.6.1", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.1", "data-encoding", - "der-parser", + "der-parser 9.0.0", "lazy_static", "nom", - "oid-registry", + "oid-registry 0.7.0", + "ring 0.17.7", "rusticata-macros", "thiserror", "time", diff --git a/domains/primitives/certificate-registry/Cargo.toml b/domains/primitives/certificate-registry/Cargo.toml new file mode 100644 index 00000000000..ec07c5f555d --- /dev/null +++ b/domains/primitives/certificate-registry/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "sp-certificate-registry" +version = "0.1.0" +authors = ["Vedhavyas Singareddi "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://subspace.network" +repository = "https://github.com/subspace/subspace" +description = "Primitives for X509 Cerificate verification" +include = [ + "/src", + "/Cargo.toml", + "/README.md", +] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.5", default-features = false, features = ["derive"] } +scale-info = { version = "2.7.0", default-features = false, features = ["derive"] } +sc-executor = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "d6b500960579d73c43fc4ef550b703acfa61c4c8", default-features = false, optional = true } +sp-api = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "d6b500960579d73c43fc4ef550b703acfa61c4c8" } +sp-blockchain = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "d6b500960579d73c43fc4ef550b703acfa61c4c8", optional = true } +sp-core = { version = "21.0.0", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "d6b500960579d73c43fc4ef550b703acfa61c4c8" } +sp-externalities = { version = "0.19.0", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "d6b500960579d73c43fc4ef550b703acfa61c4c8" } +sp-runtime = { version = "24.0.0", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "d6b500960579d73c43fc4ef550b703acfa61c4c8" } +sp-runtime-interface = { version = "17.0.0", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "d6b500960579d73c43fc4ef550b703acfa61c4c8" } +subspace-runtime-primitives = { version = "0.1.0", default-features = false, path = "../../../crates/subspace-runtime-primitives" } +time = {version = "0.3.34", optional = true } +x509-parser = { version = "0.16.0", features = ["verify"], optional = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "sc-executor", + "sp-api/std", + "sp-blockchain", + "sp-core/std", + "sp-externalities/std", + "sp-runtime/std", + "sp-runtime-interface/std", + "subspace-runtime-primitives/std", + "time", + "x509-parser" +] + +runtime-benchmarks = [] diff --git a/domains/primitives/certificate-registry/src/host_functions.rs b/domains/primitives/certificate-registry/src/host_functions.rs new file mode 100644 index 00000000000..379bc3586b5 --- /dev/null +++ b/domains/primitives/certificate-registry/src/host_functions.rs @@ -0,0 +1,64 @@ +use crate::{Validity, X509CertificateVerificationRequest, X509CertificateVerificationResponse}; +use sp_core::U256; +use std::sync::Arc; +use std::time::{Duration, UNIX_EPOCH}; +use time::OffsetDateTime; +use x509_parser::certificate::X509Certificate; +use x509_parser::prelude::{FromDer, SubjectPublicKeyInfo}; + +/// Host function trait for Certificate registration +pub trait HostFunctions: Send + Sync { + fn verify_x509_certificate( + &self, + req: X509CertificateVerificationRequest, + ) -> Option; +} + +sp_externalities::decl_extension! { + pub struct HostFunctionExtension(Arc); +} + +impl HostFunctionExtension { + /// Create a new instance of [`HostFunctionExtension`]. + #[allow(dead_code)] + pub fn new(inner: Arc) -> Self { + Self(inner) + } +} + +/// Implementation of host functions for Certificate registry. +#[derive(Default)] +pub struct HostFunctionsImpl; + +impl HostFunctions for HostFunctionsImpl { + fn verify_x509_certificate( + &self, + req: X509CertificateVerificationRequest, + ) -> Option { + let (cert_bytes, maybe_issuer_pki, block_time) = req.destruct(); + let (_, cert) = X509Certificate::from_der(cert_bytes.as_ref()).ok()?; + if let Some(encoded_issuer_pki) = maybe_issuer_pki { + let (_, pki) = SubjectPublicKeyInfo::from_der(encoded_issuer_pki.as_ref()).ok()?; + // verify the certificate signature using issuer pki + cert.verify_signature(Some(&pki)).ok()? + } else { + // verify signature using subject pki as an issuer + cert.verify_signature(None).ok()?; + }; + + // block time is in milliseconds since unix epoch + let block_time = OffsetDateTime::from(UNIX_EPOCH + Duration::from_millis(block_time)); + + // check certificate validity + cert.validity() + .is_valid_at(block_time.into()) + .then_some(())?; + + Some(X509CertificateVerificationResponse { + issuer_serial: U256::from_big_endian(&cert.serial.to_bytes_be()), + subject: cert.subject.as_raw().to_vec().into(), + subject_pki: cert.subject_pki.raw.to_vec().into(), + validity: Validity::from(cert.validity().clone()), + }) + } +} diff --git a/domains/primitives/certificate-registry/src/lib.rs b/domains/primitives/certificate-registry/src/lib.rs new file mode 100644 index 00000000000..6a5ecab1bbb --- /dev/null +++ b/domains/primitives/certificate-registry/src/lib.rs @@ -0,0 +1,128 @@ +// Copyright (C) 2021 Subspace Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Primitives for X509 certificate verification + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] +mod host_functions; +mod runtime_interface; + +#[cfg(not(feature = "std"))] +extern crate alloc; + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_core::U256; +use sp_runtime_interface::pass_by; +use sp_runtime_interface::pass_by::PassBy; +use subspace_runtime_primitives::Moment; + +/// X509 Certificate verification request. +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub enum X509CertificateVerificationRequest { + V3(X509V3CertificateVerificationData), +} + +impl PassBy for X509CertificateVerificationRequest { + type PassBy = pass_by::Codec; +} + +/// DER encoded bytes +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct DerVec(pub Vec); + +impl AsRef<[u8]> for DerVec { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From> for DerVec { + fn from(value: Vec) -> Self { + Self(value) + } +} + +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub enum X509V3CertificateVerificationData { + /// A issuer certificate + Issuer { + /// Der encoded X509 certificate. + certificate: DerVec, + /// Block time form the runtime. + block_time: Moment, + }, + Leaf { + /// Der encoded X509 certificate. + certificate: DerVec, + /// Der encoded PKI of the issuer. + issuer_pki: DerVec, + /// Block time form the runtime. + block_time: Moment, + }, +} + +impl X509CertificateVerificationRequest { + fn destruct(self) -> (DerVec, Option, Moment) { + match self { + X509CertificateVerificationRequest::V3(data) => match data { + X509V3CertificateVerificationData::Issuer { + certificate, + block_time, + } => (certificate, None, block_time), + X509V3CertificateVerificationData::Leaf { + certificate, + issuer_pki, + block_time, + } => (certificate, Some(issuer_pki), block_time), + }, + } + } +} + +/// Validity of a given certificate. +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct Validity { + /// Not valid before the time since UNIX_EPOCH + pub not_before: Moment, + /// Not valid after the time since UNIX_EPOCH + pub not_after: Moment, +} + +impl From for Validity { + fn from(value: x509_parser::prelude::Validity) -> Self { + Validity { + not_before: value.not_before.timestamp() as u64, + not_after: value.not_after.timestamp() as u64, + } + } +} + +/// X509 certificate verification response. +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct X509CertificateVerificationResponse { + /// Big endian encoded issuer serial number for this certificate + pub issuer_serial: U256, + /// Der encoded certificate's subject. + pub subject: DerVec, + /// Der encoded certificate's subject's public key info + pub subject_pki: DerVec, + /// Validity of the certificate + pub validity: Validity, +} diff --git a/domains/primitives/certificate-registry/src/runtime_interface.rs b/domains/primitives/certificate-registry/src/runtime_interface.rs new file mode 100644 index 00000000000..36223758daf --- /dev/null +++ b/domains/primitives/certificate-registry/src/runtime_interface.rs @@ -0,0 +1,22 @@ +#[cfg(feature = "std")] +use crate::host_functions::HostFunctionExtension; +use crate::{X509CertificateVerificationRequest, X509CertificateVerificationResponse}; +#[cfg(feature = "std")] +use sp_externalities::ExternalitiesExt; +use sp_runtime_interface::runtime_interface; + +/// Certificate registry runtime interfaces. +#[runtime_interface] +pub trait RuntimeInterface { + #[allow(dead_code)] + fn verify_x509_certificate( + &mut self, + req: X509CertificateVerificationRequest, + ) -> Option { + self.extension::() + .expect( + "No `CertificateRegistryHostFunctionExtension` associated for the current context!", + ) + .verify_x509_certificate(req) + } +}