From 539efad6a67aabfddb76f1a320d5c16125549388 Mon Sep 17 00:00:00 2001 From: Shantanu Singh Date: Tue, 4 Jun 2019 13:13:43 -0700 Subject: [PATCH] Add a new external provisioning mode that uses a hosting environment endpoint for retrieving device specific information. (#1144) * Add a new external provisioning mode that uses a hosting environment endpoint for retrieving device specific information. * Use a sentinel while activating primary key in external mode. * Minor changes * Remove temporary change * Reverting changes made to other files with older version of rustfmt. * Fix spelling * Clippy fix * Set the hosting endpoint environment var before the crypto lib is initialized. * Pass hsm_lock param. * Allow cyclomatic complexity warning in method. * Incorporate review comments. * Rename to external provisioning from hosting. * Changing spec * Incorporating review comments. * Update external prov interface * Prov changes * New changes * With generics * draft * draft * Compiling * Working changes * Clippy fixes * Update tests * Add more tests * Fix other tests * Clippy fix * Incorporate review comments. * Incorporating review comments again. --- .gitignore | 3 + edgelet/Cargo.lock | 40 ++ edgelet/Cargo.toml | 2 + ...xternalProvisioningVersion_2019_04_10.yaml | 100 +++++ edgelet/contrib/config/linux/config.yaml | 12 +- .../contrib/config/linux/debian/config.yaml | 12 +- edgelet/contrib/config/windows/config.yaml | 12 +- edgelet/edgelet-config/src/lib.rs | 36 ++ .../test/linux/sample_settings.external.yaml | 32 ++ .../windows/sample_settings.external.yaml | 32 ++ .../Cargo.toml | 23 ++ .../src/client/external_provisioning.rs | 236 +++++++++++ .../src/client/mod.rs | 4 + .../src/error.rs | 75 ++++ .../src/lib.rs | 13 + .../.swagger-codegen-ignore | 23 ++ .../.swagger-codegen/VERSION | 1 + edgelet/external-provisioning/Cargo.toml | 24 ++ edgelet/external-provisioning/README.md | 41 ++ .../external-provisioning/docs/Credentials.md | 14 + .../docs/DeviceProvisioningInfo.md | 12 + .../docs/ErrorResponse.md | 10 + .../docs/ExternalProvisioningApi.md | 36 ++ .../external-provisioning/src/apis/client.rs | 27 ++ .../src/apis/configuration.rs | 37 ++ .../src/apis/external_provisioning_api.rs | 113 ++++++ edgelet/external-provisioning/src/apis/mod.rs | 55 +++ edgelet/external-provisioning/src/lib.rs | 8 + .../src/models/credentials.rs | 121 ++++++ .../src/models/device_provisioning_info.rs | 81 ++++ .../src/models/error_response.rs | 38 ++ .../external-provisioning/src/models/mod.rs | 9 + edgelet/iotedged/Cargo.toml | 1 + edgelet/iotedged/src/error.rs | 5 + edgelet/iotedged/src/lib.rs | 174 +++++--- edgelet/provisioning/Cargo.toml | 2 + edgelet/provisioning/src/error.rs | 3 + edgelet/provisioning/src/lib.rs | 5 +- edgelet/provisioning/src/provisioning.rs | 376 +++++++++++++++++- .../windows/setup/IotEdgeSecurityDaemon.ps1 | 46 ++- 40 files changed, 1831 insertions(+), 63 deletions(-) create mode 100644 edgelet/api/externalProvisioningVersion_2019_04_10.yaml create mode 100644 edgelet/edgelet-config/test/linux/sample_settings.external.yaml create mode 100644 edgelet/edgelet-config/test/windows/sample_settings.external.yaml create mode 100644 edgelet/edgelet-http-external-provisioning/Cargo.toml create mode 100644 edgelet/edgelet-http-external-provisioning/src/client/external_provisioning.rs create mode 100644 edgelet/edgelet-http-external-provisioning/src/client/mod.rs create mode 100644 edgelet/edgelet-http-external-provisioning/src/error.rs create mode 100644 edgelet/edgelet-http-external-provisioning/src/lib.rs create mode 100644 edgelet/external-provisioning/.swagger-codegen-ignore create mode 100644 edgelet/external-provisioning/.swagger-codegen/VERSION create mode 100644 edgelet/external-provisioning/Cargo.toml create mode 100644 edgelet/external-provisioning/README.md create mode 100644 edgelet/external-provisioning/docs/Credentials.md create mode 100644 edgelet/external-provisioning/docs/DeviceProvisioningInfo.md create mode 100644 edgelet/external-provisioning/docs/ErrorResponse.md create mode 100644 edgelet/external-provisioning/docs/ExternalProvisioningApi.md create mode 100644 edgelet/external-provisioning/src/apis/client.rs create mode 100644 edgelet/external-provisioning/src/apis/configuration.rs create mode 100644 edgelet/external-provisioning/src/apis/external_provisioning_api.rs create mode 100644 edgelet/external-provisioning/src/apis/mod.rs create mode 100644 edgelet/external-provisioning/src/lib.rs create mode 100644 edgelet/external-provisioning/src/models/credentials.rs create mode 100644 edgelet/external-provisioning/src/models/device_provisioning_info.rs create mode 100644 edgelet/external-provisioning/src/models/error_response.rs create mode 100644 edgelet/external-provisioning/src/models/mod.rs diff --git a/.gitignore b/.gitignore index d44c490589e..a0bc9a3ad22 100644 --- a/.gitignore +++ b/.gitignore @@ -313,6 +313,9 @@ stylecop.json # JetBrains Rider files .idea/ +# IntelliJ idea files +*.iml + # linux dev files *.swp *.swo diff --git a/edgelet/Cargo.lock b/edgelet/Cargo.lock index 5c36cf40998..c72f27dc8d6 100755 --- a/edgelet/Cargo.lock +++ b/edgelet/Cargo.lock @@ -456,6 +456,26 @@ dependencies = [ "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "edgelet-http-external-provisioning" +version = "0.1.0" +dependencies = [ + "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "edgelet-core 0.1.0", + "edgelet-http 0.1.0", + "external-provisioning 0.1.0", + "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.12.17 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "edgelet-http-mgmt" version = "0.1.0" @@ -602,6 +622,23 @@ dependencies = [ "backtrace 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "external-provisioning" +version = "0.1.0" +dependencies = [ + "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "edgelet-core 0.1.0", + "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.12.17 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", + "typed-headers 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "failure" version = "0.1.2" @@ -929,6 +966,7 @@ dependencies = [ "edgelet-docker 0.1.0", "edgelet-hsm 0.1.0", "edgelet-http 0.1.0", + "edgelet-http-external-provisioning 0.1.0", "edgelet-http-mgmt 0.1.0", "edgelet-http-workload 0.1.0", "edgelet-iothub 0.1.0", @@ -1373,7 +1411,9 @@ dependencies = [ "edgelet-core 0.1.0", "edgelet-hsm 0.1.0", "edgelet-http 0.1.0", + "edgelet-http-external-provisioning 0.1.0", "edgelet-utils 0.1.0", + "external-provisioning 0.1.0", "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", "hsm 0.1.0", diff --git a/edgelet/Cargo.toml b/edgelet/Cargo.toml index b878b62c9a0..f3350863503 100644 --- a/edgelet/Cargo.toml +++ b/edgelet/Cargo.toml @@ -7,12 +7,14 @@ members = [ "edgelet-docker", "edgelet-hsm", "edgelet-http", + "edgelet-http-external-provisioning", "edgelet-http-mgmt", "edgelet-http-workload", "edgelet-iothub", "edgelet-kube", "edgelet-test-utils", "edgelet-utils", + "external-provisioning", "kube-client", "hsm-rs", "hsm-sys", diff --git a/edgelet/api/externalProvisioningVersion_2019_04_10.yaml b/edgelet/api/externalProvisioningVersion_2019_04_10.yaml new file mode 100644 index 00000000000..cee1e57ad30 --- /dev/null +++ b/edgelet/api/externalProvisioningVersion_2019_04_10.yaml @@ -0,0 +1,100 @@ +swagger: '2.0' +schemes: + - http +info: + title: IoT Edge External Provisioning Environment API + version: '2019-04-10' +tags: + - name: ExternalProvisioning + x-displayName: ExternalProvisioning + description: | + +paths: + '/device/provisioninginformation': + get: + tags: + - ExternalProvisioning + summary: Gets the IoT hub provisioning information of the device. + produces: + - application/json + description: | + This returns the IoT hub provisioning information of the device. + operationId: GetDeviceProvisioningInformation + parameters: + - $ref: '#/parameters/api-version' + responses: + '200': + description: Ok + schema: + $ref: '#/definitions/DeviceProvisioningInfo' + default: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + +definitions: + DeviceProvisioningInfo: + type: object + properties: + hubName: + type: string + description: The host name of the IoT hub. + example: mytesthub.azure-devices.net + deviceId: + type: string + description: The ID of the device in IoT hub. + example: device01 + credentials: + $ref: '#/definitions/Credentials' + required: + - hubName + - deviceId + - credentials + Credentials: + type: object + properties: + authType: + type: string + description: Indicates the type of authentication credential used. + enum: + - symmetric-key + - x509 + source: + type: string + description: Indicates the source of the authentication credential. + enum: + - payload + - hsm + key: + type: string + format: byte + description: The symmetric key used for authentication. Specified only if the 'authType' is 'symmetric-key' and the 'source' is 'payload'. + identityCert: + type: string + format: byte + description: The identity certificate. Should be a PEM formatted byte array if the 'authType' is 'x509' and the 'source' is 'payload' or should be a reference to the certificate if the 'authType' is 'x509' and the 'source' is 'hsm'. + identityPrivateKey: + type: string + format: byte + description: The identity private key. Should be a PEM formatted byte array if the 'authType' is 'x509' and the 'source' is 'payload' or should be a reference to the private key if the 'authType' is 'x509' and the 'source' is 'hsm'. + + required: + - authType + - source + + ErrorResponse: + type: object + properties: + message: + type: string + required: + - message + +parameters: + api-version: + name: api-version + in: query + description: The version of the API. + required: true + type: string + default: '2019-04-10' diff --git a/edgelet/contrib/config/linux/config.yaml b/edgelet/contrib/config/linux/config.yaml index 25ee7d498b4..723484079ed 100644 --- a/edgelet/contrib/config/linux/config.yaml +++ b/edgelet/contrib/config/linux/config.yaml @@ -16,8 +16,9 @@ # Configures the identity provisioning mode of the daemon. # # Supported modes: -# manual - using an iothub connection string -# dps - using dps for provisioning +# manual - using an iothub connection string +# dps - using dps for provisioning +# external - the device has been provisioned externally. Uses an external provisioning endpoint to get device specific information. # # DPS Settings # scope_id - Required. Value of a specific DPS instance's ID scope @@ -25,6 +26,8 @@ # symmetric_key - Optional. This entry should only be specified when # provisioning devices configured for symmetric key # attestation +# External Settings +# endpoint - Required. Value of the endpoint used to retrieve device specific information such as its IoT hub connection information. ############################################################################### # Manual provisioning configuration @@ -62,6 +65,11 @@ provisioning: # identity_cert: "" # identity_pk: "" +# External provisioning configuration +# provisioning: +# source: "external" +# endpoint: "http://localhost:9999" + ############################################################################### # Certificate settings ############################################################################### diff --git a/edgelet/contrib/config/linux/debian/config.yaml b/edgelet/contrib/config/linux/debian/config.yaml index 3a539b3dd4e..525fbd1a1e8 100644 --- a/edgelet/contrib/config/linux/debian/config.yaml +++ b/edgelet/contrib/config/linux/debian/config.yaml @@ -16,8 +16,9 @@ # Configures the identity provisioning mode of the daemon. # # Supported modes: -# manual - using an iothub connection string -# dps - using dps for provisioning +# manual - using an iothub connection string +# dps - using dps for provisioning +# external - the device has been provisioned externally. Uses an external provisioning endpoint to get device specific information. # # DPS Settings # scope_id - Required. Value of a specific DPS instance's ID scope @@ -25,6 +26,8 @@ # symmetric_key - Optional. This entry should only be specified when # provisioning devices configured for symmetric key # attestation +# External Settings +# endpoint - Required. Value of the endpoint used to retrieve device specific information such as its IoT hub connection information. ############################################################################### # Manual provisioning configuration @@ -62,6 +65,11 @@ provisioning: # identity_cert: "" # identity_pk: "" +# External provisioning configuration +# provisioning: +# source: "external" +# endpoint: "http://localhost:9999" + ############################################################################### # Certificate settings ############################################################################### diff --git a/edgelet/contrib/config/windows/config.yaml b/edgelet/contrib/config/windows/config.yaml index a571b490ee0..f601645b24a 100644 --- a/edgelet/contrib/config/windows/config.yaml +++ b/edgelet/contrib/config/windows/config.yaml @@ -16,8 +16,9 @@ # Configures the identity provisioning mode of the daemon. # # Supported modes: -# manual - using an iothub connection string -# dps - using dps for provisioning +# manual - using an iothub connection string +# dps - using dps for provisioning +# external - the device has been provisioned externally. Uses an external provisioning endpoint to get device specific information. # # DPS Settings # scope_id - Required. Value of a specific DPS instance's ID scope @@ -25,6 +26,8 @@ # symmetric_key - Optional. This entry should only be specified when # provisioning devices configured for symmetric key # attestation +# External Settings +# endpoint - Required. Value of the endpoint used to retrieve device specific information such as its IoT hub connection information. ############################################################################### # Manual provisioning configuration @@ -62,6 +65,11 @@ provisioning: # identity_cert: "" # identity_pk: "" +# External provisioning configuration +# provisioning: +# source: "external" +# endpoint: "http://localhost:9999" + ############################################################################### # Certificate settings ############################################################################### diff --git a/edgelet/edgelet-config/src/lib.rs b/edgelet/edgelet-config/src/lib.rs index b255852c200..9a77f2b29fc 100644 --- a/edgelet/edgelet-config/src/lib.rs +++ b/edgelet/edgelet-config/src/lib.rs @@ -258,12 +258,30 @@ impl Dps { } } +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub struct External { + #[serde(with = "url_serde")] + endpoint: Url, +} + +impl External { + pub fn new(endpoint: Url) -> Self { + External { endpoint } + } + + pub fn endpoint(&self) -> &Url { + &self.endpoint + } +} + #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "source")] #[serde(rename_all = "lowercase")] pub enum Provisioning { Manual(Manual), Dps(Dps), + External(External), } #[derive(Debug, Deserialize, Serialize)] @@ -553,6 +571,8 @@ mod tests { static BAD_SETTINGS_DPS_X5091: &str = "test/linux/bad_settings.dps.x509.1.yaml"; #[cfg(unix)] static BAD_SETTINGS_DPS_X5092: &str = "test/linux/bad_settings.dps.x509.2.yaml"; + #[cfg(unix)] + static GOOD_SETTINGS_EXTERNAL: &str = "test/linux/sample_settings.external.yaml"; #[cfg(windows)] static GOOD_SETTINGS: &str = "test/windows/sample_settings.yaml"; @@ -588,6 +608,8 @@ mod tests { static BAD_SETTINGS_DPS_X5091: &str = "test/windows/bad_settings.dps.x509.1.yaml"; #[cfg(windows)] static BAD_SETTINGS_DPS_X5092: &str = "test/windows/bad_settings.dps.x509.2.yaml"; + #[cfg(windows)] + static GOOD_SETTINGS_EXTERNAL: &str = "test/windows/sample_settings.external.yaml"; fn unwrap_manual_provisioning(p: &Provisioning) -> String { match p { @@ -786,6 +808,20 @@ mod tests { }; } + #[test] + fn external_prov_get_settings() { + let settings = Settings::::new(Some(Path::new(GOOD_SETTINGS_EXTERNAL))); + println!("{:?}", settings); + assert!(settings.is_ok()); + let s = settings.unwrap(); + match s.provisioning() { + Provisioning::External(ref external) => { + assert_eq!(external.endpoint().as_str(), "http://localhost:9999/"); + } + _ => unreachable!(), + }; + } + #[test] fn diff_with_same_cached_returns_false() { let tmp_dir = TempDir::new("blah").unwrap(); diff --git a/edgelet/edgelet-config/test/linux/sample_settings.external.yaml b/edgelet/edgelet-config/test/linux/sample_settings.external.yaml new file mode 100644 index 00000000000..c60f151e72b --- /dev/null +++ b/edgelet/edgelet-config/test/linux/sample_settings.external.yaml @@ -0,0 +1,32 @@ + +# Configures the provisioning mode +provisioning: + source: 'external' + endpoint: 'http://localhost:9999' + +agent: + name: "edgeAgent" + type: "docker" + env: + abc: "value1" + acd: "value2" + config: + image: "microsoft/azureiotedge-agent:1.0" + auth: {} +hostname: "localhost" + +# Sets the connection uris for clients +connect: + workload_uri: "http://localhost:8081" + management_uri: "http://localhost:8080" + +# Sets the uris to listen on +# These can be different than the connect uris. +# For instance, when using the fd:// scheme for systemd +listen: + workload_uri: "http://0.0.0.0:8081" + management_uri: "http://0.0.0.0:8080" +homedir: "/tmp" +moby_runtime: + uri: "http://localhost:2375" + network: "azure-iot-edge" diff --git a/edgelet/edgelet-config/test/windows/sample_settings.external.yaml b/edgelet/edgelet-config/test/windows/sample_settings.external.yaml new file mode 100644 index 00000000000..c50ecac2808 --- /dev/null +++ b/edgelet/edgelet-config/test/windows/sample_settings.external.yaml @@ -0,0 +1,32 @@ + +# Configures the provisioning mode +provisioning: + source: 'external' + endpoint: 'http://localhost:9999' + +agent: + name: "edgeAgent" + type: "docker" + env: + abc: "value1" + acd: "value2" + config: + image: "microsoft/azureiotedge-agent:1.0" + auth: {} +hostname: "localhost" + +# Sets the connection uris for clients +connect: + workload_uri: "http://localhost:8081" + management_uri: "http://localhost:8080" + +# Sets the uris to listen on +# These can be different than the connect uris. +# For instance, when using the fd:// scheme for systemd +listen: + workload_uri: "http://0.0.0.0:8081" + management_uri: "http://0.0.0.0:8080" +homedir: "C:\\Temp" +moby_runtime: + uri: "npipe://./pipe/iotedge_moby_engine" + network: "azure-iot-edge" diff --git a/edgelet/edgelet-http-external-provisioning/Cargo.toml b/edgelet/edgelet-http-external-provisioning/Cargo.toml new file mode 100644 index 00000000000..1567f400cac --- /dev/null +++ b/edgelet/edgelet-http-external-provisioning/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "edgelet-http-external-provisioning" +version = "0.1.0" +authors = ["Azure IoT Edge Devs"] +publish = false +edition = "2018" + +[dependencies] +base64 = "0.9" +bytes = "0.4" +chrono = { version = "0.4", features = ["serde"] } +failure = "0.1.2" +futures = "0.1" +hyper = "0.12" +log = "0.4" +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" +url = "1.7" + +edgelet-core = { path = "../edgelet-core" } +edgelet-http = { path = "../edgelet-http" } +external-provisioning = { path = "../external-provisioning" } diff --git a/edgelet/edgelet-http-external-provisioning/src/client/external_provisioning.rs b/edgelet/edgelet-http-external-provisioning/src/client/external_provisioning.rs new file mode 100644 index 00000000000..36c3bc01652 --- /dev/null +++ b/edgelet/edgelet-http-external-provisioning/src/client/external_provisioning.rs @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft. All rights reserved. + +use std::sync::Arc; + +use external_provisioning::apis::client::APIClient; +use external_provisioning::apis::configuration::Configuration; +use external_provisioning::apis::ExternalProvisioningApi; +use external_provisioning::models::*; +use failure::{Fail, ResultExt}; +use futures::prelude::*; +use hyper::Client; +use url::Url; + +use edgelet_core::UrlExt; +use edgelet_http::UrlConnector; + +use crate::error::{Error, ErrorKind}; + +pub trait ExternalProvisioningInterface { + type Error: Fail; + + type DeviceProvisioningInformationFuture: Future< + Item = DeviceProvisioningInfo, + Error = Self::Error, + > + Send; + + fn get_device_provisioning_information(&self) -> Self::DeviceProvisioningInformationFuture; +} + +pub trait GetApi { + fn get_api(&self) -> &dyn ExternalProvisioningApi; +} + +pub struct ExternalProvisioningClient { + client: Arc, +} + +impl GetApi for APIClient { + fn get_api(&self) -> &dyn ExternalProvisioningApi { + self.external_provisioning_api() + } +} + +impl ExternalProvisioningClient { + pub fn new(url: &Url) -> Result { + let client = Client::builder().build( + UrlConnector::new(url).context(ErrorKind::InitializeExternalProvisioningClient)?, + ); + + let base_path = url + .to_base_path() + .context(ErrorKind::InitializeExternalProvisioningClient)?; + let mut configuration = Configuration::new(client); + configuration.base_path = base_path + .to_str() + .ok_or(ErrorKind::InitializeExternalProvisioningClient)? + .to_string(); + + let scheme = url.scheme().to_string(); + configuration.uri_composer = Box::new(move |base_path, path| { + Ok(UrlConnector::build_hyper_uri(&scheme, base_path, path)?) + }); + + let external_provisioning_client = ExternalProvisioningClient { + client: Arc::new(APIClient::new(configuration)), + }; + + Ok(external_provisioning_client) + } +} + +impl ExternalProvisioningInterface for ExternalProvisioningClient { + type Error = Error; + + type DeviceProvisioningInformationFuture = + Box + Send>; + + fn get_device_provisioning_information(&self) -> Self::DeviceProvisioningInformationFuture { + let connection_info = self + .client + .get_api() + .get_device_provisioning_information(crate::EXTERNAL_PROVISIONING_API_VERSION) + .map_err(|err| { + Error::from_external_provisioning_error( + err, + ErrorKind::GetDeviceProvisioningInformation, + ) + }); + Box::new(connection_info) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use external_provisioning::apis::ApiError as ExternalProvisioningApiError; + use external_provisioning::apis::Error as ExternalProvisioningError; + use futures::Future; + + #[test] + fn invalid_external_provisioning_url() { + let client = ExternalProvisioningClient::new(&(Url::parse("fd://").unwrap())); + match client { + Ok(_t) => panic!("Unexpected to succeed with invalid Url."), + Err(ref err) => { + if let ErrorKind::InitializeExternalProvisioningClient = err.kind() { + } else { + panic!( + "Expected `InitializeExternalProvisioningClient` but got {:?}", + err + ); + } + } + }; + } + + #[test] + fn valid_external_provisioning_url() { + let client = + ExternalProvisioningClient::new(&(Url::parse("http://localhost:99/").unwrap())); + assert!(client.is_ok()); + } + + struct TestExternalProvisioningApiError(ExternalProvisioningApiError); + + impl Clone for TestExternalProvisioningApiError { + fn clone(&self) -> Self { + Self(ExternalProvisioningApiError { + code: self.0.code, + content: self.0.content.clone(), + }) + } + } + + struct TestExternalProvisioningApi { + pub error: Option, + } + + impl GetApi for TestExternalProvisioningApi { + fn get_api(&self) -> &dyn ExternalProvisioningApi { + self + } + } + + impl ExternalProvisioningApi for TestExternalProvisioningApi { + fn get_device_provisioning_information( + &self, + _api_version: &str, + ) -> Box< + dyn Future< + Item = external_provisioning::models::DeviceProvisioningInfo, + Error = ExternalProvisioningError, + > + Send, + > { + match self.error.as_ref() { + None => { + let mut credentials = + Credentials::new("symmetric-key".to_string(), "payload".to_string()); + credentials.set_key("test-key".to_string()); + let provisioning_info = DeviceProvisioningInfo::new( + "TestHub".to_string(), + "TestDevice".to_string(), + credentials, + ); + + Box::new(Ok(provisioning_info).into_future()) + } + Some(s) => Box::new(Err(ExternalProvisioningError::Api(s.clone().0)).into_future()), + } + } + } + + #[test] + fn get_device_provisioning_info_error() { + let external_provisioning_error = + TestExternalProvisioningApiError(ExternalProvisioningApiError { + code: hyper::StatusCode::from_u16(400).unwrap(), + content: None, + }); + let external_provisioning_api = TestExternalProvisioningApi { + error: Some(external_provisioning_error), + }; + let client = ExternalProvisioningClient { + client: Arc::new(external_provisioning_api), + }; + + let res = client + .get_device_provisioning_information() + .then(|result| match result { + Ok(_) => panic!("Expected a failure."), + Err(err) => match err.kind() { + ErrorKind::GetDeviceProvisioningInformation => Ok::<_, Error>(()), + _ => panic!( + "Expected `GetDeviceProvisioningInformation` but got {:?}", + err + ), + }, + }) + .wait() + .is_ok(); + + assert!(res); + } + + #[test] + fn get_device_provisioning_info_success() { + let external_provisioning_api = TestExternalProvisioningApi { error: None }; + let client = ExternalProvisioningClient { + client: Arc::new(external_provisioning_api), + }; + + let res = client + .get_device_provisioning_information() + .then(|result| match result { + Ok(item) => { + assert_eq!("TestHub", item.hub_name()); + assert_eq!("TestDevice", item.device_id()); + assert_eq!("symmetric-key", item.credentials().auth_type()); + assert_eq!("payload", item.credentials().source()); + + if let Some(key) = item.credentials().key() { + assert_eq!(key, "test-key"); + } else { + panic!("A key was expected in the response.") + } + + Ok::<_, Error>(()) + } + Err(_err) => panic!("Did not expect a failure."), + }) + .wait() + .is_ok(); + + assert!(res); + } +} diff --git a/edgelet/edgelet-http-external-provisioning/src/client/mod.rs b/edgelet/edgelet-http-external-provisioning/src/client/mod.rs new file mode 100644 index 00000000000..0c76a508b12 --- /dev/null +++ b/edgelet/edgelet-http-external-provisioning/src/client/mod.rs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft. All rights reserved. + +mod external_provisioning; +pub use self::external_provisioning::{ExternalProvisioningClient, ExternalProvisioningInterface}; diff --git a/edgelet/edgelet-http-external-provisioning/src/error.rs b/edgelet/edgelet-http-external-provisioning/src/error.rs new file mode 100644 index 00000000000..53b904c1468 --- /dev/null +++ b/edgelet/edgelet-http-external-provisioning/src/error.rs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +use external_provisioning::apis::Error as ExternalProvisioningError; +use failure::{Backtrace, Context, Fail}; +use serde_json; +use std::fmt::{self, Display}; + +pub type Result = ::std::result::Result; + +#[derive(Debug)] +pub struct Error { + inner: Context, +} + +#[derive(Debug, Fail)] +pub enum ErrorKind { + // Note: This errorkind is always wrapped in another errorkind context + #[fail(display = "Client error")] + Client(ExternalProvisioningError), + + #[fail(display = "Could not get device provisioning info")] + GetDeviceProvisioningInformation, + + #[fail(display = "External provisioning client initialization")] + InitializeExternalProvisioningClient, +} + +impl Fail for Error { + fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.inner, f) + } +} + +impl Error { + pub fn kind(&self) -> &ErrorKind { + self.inner.get_context() + } + + pub fn from_external_provisioning_error( + error: ExternalProvisioningError, + context: ErrorKind, + ) -> Self { + match error { + ExternalProvisioningError::Hyper(h) => Error::from(h.context(context)), + ExternalProvisioningError::Serde(s) => Error::from(s.context(context)), + ExternalProvisioningError::Api(_) => { + Error::from(ErrorKind::Client(error).context(context)) + } + } + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Error { + inner: Context::new(kind), + } + } +} + +impl From> for Error { + fn from(inner: Context) -> Self { + Error { inner } + } +} diff --git a/edgelet/edgelet-http-external-provisioning/src/lib.rs b/edgelet/edgelet-http-external-provisioning/src/lib.rs new file mode 100644 index 00000000000..33d55b3fd2e --- /dev/null +++ b/edgelet/edgelet-http-external-provisioning/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +#![deny(rust_2018_idioms, warnings)] +#![deny(clippy::all, clippy::pedantic)] +#![allow(clippy::module_name_repetitions, clippy::use_self)] + +pub mod client; +pub mod error; + +pub use client::{ExternalProvisioningClient, ExternalProvisioningInterface}; +pub use error::{Error, ErrorKind}; + +pub const EXTERNAL_PROVISIONING_API_VERSION: &str = "2019-04-10"; diff --git a/edgelet/external-provisioning/.swagger-codegen-ignore b/edgelet/external-provisioning/.swagger-codegen-ignore new file mode 100644 index 00000000000..c5fa491b4c5 --- /dev/null +++ b/edgelet/external-provisioning/.swagger-codegen-ignore @@ -0,0 +1,23 @@ +# Swagger Codegen Ignore +# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/edgelet/external-provisioning/.swagger-codegen/VERSION b/edgelet/external-provisioning/.swagger-codegen/VERSION new file mode 100644 index 00000000000..855ff9501eb --- /dev/null +++ b/edgelet/external-provisioning/.swagger-codegen/VERSION @@ -0,0 +1 @@ +2.4.0-SNAPSHOT \ No newline at end of file diff --git a/edgelet/external-provisioning/Cargo.toml b/edgelet/external-provisioning/Cargo.toml new file mode 100644 index 00000000000..441fdf33fd1 --- /dev/null +++ b/edgelet/external-provisioning/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "external-provisioning" +version = "0.1.0" +authors = ["Azure IoT Edge Devs"] +publish = false +description = """ +Includes the code-generated implementation of the external provisioning +api. +""" +edition = "2018" + +[dependencies] +base64 = "0.9" +failure = "0.1" +futures = "0.1" +hyper = "0.12" +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" +serde_yaml = "0.7" +typed-headers = "0.1" +url = "1.5" + +edgelet-core = { path = "../edgelet-core" } diff --git a/edgelet/external-provisioning/README.md b/edgelet/external-provisioning/README.md new file mode 100644 index 00000000000..270d649cf85 --- /dev/null +++ b/edgelet/external-provisioning/README.md @@ -0,0 +1,41 @@ +# Rust API client for swagger + +No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen) + +## Overview +This API client was generated by the [swagger-codegen](https://github.com/swagger-api/swagger-codegen) project. By using the [swagger-spec](https://github.com/swagger-api/swagger-spec) from a remote server, you can easily generate an API client. + +- API version: 2019-04-10 +- Package version: 1.0.0 +- Build package: io.swagger.codegen.languages.RustClientCodegen + +## Installation +Put the package under your project folder and add the following in import: +``` + "./swagger" +``` + +## Documentation for API Endpoints + +All URIs are relative to *http://localhost* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +*ExternalProvisioningApi* | [**get_device_provisioning_information**](docs/ExternalProvisioningApi.md#get_device_provisioning_information) | **Get** /edge/device/provisioninginformation | Gets the IoT hub provisioning information of the device. + + +## Documentation For Models + + - [Credentials](docs/Credentials.md) + - [DeviceProvisioningInfo](docs/DeviceProvisioningInfo.md) + - [ErrorResponse](docs/ErrorResponse.md) + + +## Documentation For Authorization + Endpoints do not require authorization. + + +## Author + + + diff --git a/edgelet/external-provisioning/docs/Credentials.md b/edgelet/external-provisioning/docs/Credentials.md new file mode 100644 index 00000000000..4ceab8a65f6 --- /dev/null +++ b/edgelet/external-provisioning/docs/Credentials.md @@ -0,0 +1,14 @@ +# Credentials + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**auth_type** | **String** | Indicates the type of authentication credential used. | [default to null] +**source** | **String** | Indicates the source of the authentication credential. | [default to null] +**key** | **String** | The symmetric key used for authentication. Specified only if the 'authType' is 'symmetric-key' and the 'source' is 'payload'. | [optional] [default to null] +**identity_cert** | **String** | The identity certificate. Should be a PEM formatted byte array if the 'authType' is 'x509' and the 'source' is 'payload' or should be a reference to the certificate if the 'authType' is 'x509' and the 'source' is 'hsm'. | [optional] [default to null] +**identity_private_key** | **String** | The identity private key. Should be a PEM formatted byte array if the 'authType' is 'x509' and the 'source' is 'payload' or should be a reference to the private key if the 'authType' is 'x509' and the 'source' is 'hsm'. | [optional] [default to null] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/edgelet/external-provisioning/docs/DeviceProvisioningInfo.md b/edgelet/external-provisioning/docs/DeviceProvisioningInfo.md new file mode 100644 index 00000000000..e25a6d12ce9 --- /dev/null +++ b/edgelet/external-provisioning/docs/DeviceProvisioningInfo.md @@ -0,0 +1,12 @@ +# DeviceProvisioningInfo + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**hub_name** | **String** | The host name of the IoT hub. | [default to null] +**device_id** | **String** | The ID of the device in IoT hub. | [default to null] +**credentials** | [***::models::Credentials**](Credentials.md) | | [default to null] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/edgelet/external-provisioning/docs/ErrorResponse.md b/edgelet/external-provisioning/docs/ErrorResponse.md new file mode 100644 index 00000000000..edc87529b73 --- /dev/null +++ b/edgelet/external-provisioning/docs/ErrorResponse.md @@ -0,0 +1,10 @@ +# ErrorResponse + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**message** | **String** | | [default to null] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/edgelet/external-provisioning/docs/ExternalProvisioningApi.md b/edgelet/external-provisioning/docs/ExternalProvisioningApi.md new file mode 100644 index 00000000000..d3b1b33defb --- /dev/null +++ b/edgelet/external-provisioning/docs/ExternalProvisioningApi.md @@ -0,0 +1,36 @@ +# \ExternalProvisioningApi + +All URIs are relative to *http://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**get_device_provisioning_information**](ExternalProvisioningApi.md#get_device_provisioning_information) | **Get** /edge/device/provisioninginformation | Gets the IoT hub provisioning information of the device. + + +# **get_device_provisioning_information** +> ::models::DeviceProvisioningInfo get_device_provisioning_information(api_version) +Gets the IoT hub provisioning information of the device. + +This returns the IoT hub provisioning information of the device. + +### Required Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **api_version** | **String**| The version of the API. | [default to 2019-04-10] + +### Return type + +[**::models::DeviceProvisioningInfo**](DeviceProvisioningInfo.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/edgelet/external-provisioning/src/apis/client.rs b/edgelet/external-provisioning/src/apis/client.rs new file mode 100644 index 00000000000..1a3876218e2 --- /dev/null +++ b/edgelet/external-provisioning/src/apis/client.rs @@ -0,0 +1,27 @@ +use std::sync::Arc; + +use super::configuration::Configuration; +use hyper; + +pub struct APIClient { + external_provisioning_api: Box, +} + +impl APIClient { + pub fn new(configuration: Configuration) -> Self + where + C: hyper::client::connect::Connect + 'static, + { + let configuration = Arc::new(configuration); + + APIClient { + external_provisioning_api: Box::new(crate::apis::ExternalProvisioningApiClient::new( + configuration.clone(), + )), + } + } + + pub fn external_provisioning_api(&self) -> &dyn crate::apis::ExternalProvisioningApi { + self.external_provisioning_api.as_ref() + } +} diff --git a/edgelet/external-provisioning/src/apis/configuration.rs b/edgelet/external-provisioning/src/apis/configuration.rs new file mode 100644 index 00000000000..0f0025153eb --- /dev/null +++ b/edgelet/external-provisioning/src/apis/configuration.rs @@ -0,0 +1,37 @@ +/* + * IoT Edge External Provisioning Environment API + * + * No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen) + * + * OpenAPI spec version: 2019-04-10 + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +use failure::{format_err, Error}; +use hyper::client::connect::Connect; +use hyper::{Client, Uri}; + +pub struct Configuration { + pub base_path: String, + pub user_agent: Option, + pub client: Client, + pub uri_composer: Box Result + Send + Sync>, + pub sas_token: Option, +} + +impl Configuration { + pub fn new(client: Client) -> Configuration { + Configuration { + base_path: "http://localhost".to_owned(), + user_agent: Some(edgelet_core::version_with_source_version().to_string()), + client, + uri_composer: Box::new(|base_path, path| { + format!("{}{}", base_path, path) + .parse() + .map_err(|_| format_err!("Url parse error")) + }), + sas_token: None, + } + } +} diff --git a/edgelet/external-provisioning/src/apis/external_provisioning_api.rs b/edgelet/external-provisioning/src/apis/external_provisioning_api.rs new file mode 100644 index 00000000000..e18263dedb4 --- /dev/null +++ b/edgelet/external-provisioning/src/apis/external_provisioning_api.rs @@ -0,0 +1,113 @@ +/* + * IoT Edge External Provisioning Environment API + * + * No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen) + * + * OpenAPI spec version: 2019-04-10 + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +use std::borrow::Borrow; +use std::sync::Arc; + +use futures; +use futures::{Future, Stream}; +use hyper; +use serde_json; + +use typed_headers::{self, http}; + +use super::{configuration, Error}; + +pub struct ExternalProvisioningApiClient { + configuration: Arc>, +} + +impl ExternalProvisioningApiClient { + pub fn new( + configuration: Arc>, + ) -> ExternalProvisioningApiClient { + ExternalProvisioningApiClient { configuration } + } +} + +pub trait ExternalProvisioningApi: Send + Sync { + fn get_device_provisioning_information( + &self, + api_version: &str, + ) -> Box< + dyn Future> + + Send, + >; +} + +impl ExternalProvisioningApi + for ExternalProvisioningApiClient +where + C: hyper::client::connect::Connect + 'static, + ::Transport: 'static, + ::Future: 'static, +{ + fn get_device_provisioning_information( + &self, + api_version: &str, + ) -> Box< + dyn Future> + + Send, + > { + let configuration: &configuration::Configuration = self.configuration.borrow(); + + let method = hyper::Method::GET; + + let query = ::url::form_urlencoded::Serializer::new(String::new()) + .append_pair("api-version", &api_version.to_string()) + .finish(); + let uri_str = format!("/device/provisioninginformation?{}", query); + + let uri = (configuration.uri_composer)(&configuration.base_path, &uri_str); + // TODO(farcaller): handle error + // if let Err(e) = uri { + // return Box::new(futures::future::err(e)); + // } + let mut req = hyper::Request::builder(); + req.method(method).uri(uri.unwrap()); + if let Some(ref user_agent) = configuration.user_agent { + req.header(http::header::USER_AGENT, &**user_agent); + } + + let req = req + .body(hyper::Body::empty()) + .expect("could not build hyper::Request"); + + // if let Some(ref sas_token) = configuration.sas_token { + // req.headers_mut().set(Authorization(sas_token.clone())); + // } + + // send request + Box::new( + configuration + .client + .request(req) + .map_err(Error::from) + .and_then(|resp| { + let (http::response::Parts { status, .. }, body) = resp.into_parts(); + body.concat2() + .and_then(move |body| Ok((status, body))) + .map_err(Error::from) + }) + .and_then(|(status, body)| { + if status.is_success() { + Ok(body) + } else { + Err(Error::from((status, &*body))) + } + }) + .and_then(|body| { + let parsed: Result = + serde_json::from_slice(&body); + parsed.map_err(Error::from) + }), + ) + } +} diff --git a/edgelet/external-provisioning/src/apis/mod.rs b/edgelet/external-provisioning/src/apis/mod.rs new file mode 100644 index 00000000000..41022b50ec6 --- /dev/null +++ b/edgelet/external-provisioning/src/apis/mod.rs @@ -0,0 +1,55 @@ +use hyper; +use serde; +use serde_json; + +#[derive(Debug)] +pub enum Error { + Hyper(hyper::Error), + Serde(serde_json::Error), + Api(ApiError), +} + +#[derive(Debug)] +pub struct ApiError { + pub code: hyper::StatusCode, + pub content: Option, +} + +impl<'de, T> From<(hyper::StatusCode, &'de [u8])> for Error +where + T: serde::Deserialize<'de>, +{ + fn from(e: (hyper::StatusCode, &'de [u8])) -> Self { + if e.1.is_empty() { + return Error::Api(ApiError { + code: e.0, + content: None, + }); + } + match serde_json::from_slice::(e.1) { + Ok(t) => Error::Api(ApiError { + code: e.0, + content: Some(t), + }), + Err(e) => Error::from(e), + } + } +} + +impl From for Error { + fn from(e: hyper::Error) -> Self { + Error::Hyper(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Serde(e) + } +} + +mod external_provisioning_api; +pub use self::external_provisioning_api::{ExternalProvisioningApi, ExternalProvisioningApiClient}; + +pub mod client; +pub mod configuration; diff --git a/edgelet/external-provisioning/src/lib.rs b/edgelet/external-provisioning/src/lib.rs new file mode 100644 index 00000000000..fb676831946 --- /dev/null +++ b/edgelet/external-provisioning/src/lib.rs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. All rights reserved. + +#![deny(rust_2018_idioms, warnings)] +#![deny(clippy::all, clippy::pedantic)] +#![allow(clippy::module_name_repetitions, clippy::use_self)] + +pub mod apis; +pub mod models; diff --git a/edgelet/external-provisioning/src/models/credentials.rs b/edgelet/external-provisioning/src/models/credentials.rs new file mode 100644 index 00000000000..1a1bb1dbc58 --- /dev/null +++ b/edgelet/external-provisioning/src/models/credentials.rs @@ -0,0 +1,121 @@ +/* + * IoT Edge External Provisioning Environment API + * + * No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen) + * + * OpenAPI spec version: 2019-04-10 + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +use serde_derive::{Deserialize, Serialize}; +#[allow(unused_imports)] +use serde_json::Value; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Credentials { + /// Indicates the type of authentication credential used. + #[serde(rename = "authType")] + auth_type: String, + /// Indicates the source of the authentication credential. + #[serde(rename = "source")] + source: String, + /// The symmetric key used for authentication. Specified only if the 'authType' is 'symmetric-key' and the 'source' is 'payload'. + #[serde(rename = "key", skip_serializing_if = "Option::is_none")] + key: Option, + /// The identity certificate. Should be a PEM formatted byte array if the 'authType' is 'x509' and the 'source' is 'payload' or should be a reference to the certificate if the 'authType' is 'x509' and the 'source' is 'hsm'. + #[serde(rename = "identityCert", skip_serializing_if = "Option::is_none")] + identity_cert: Option, + /// The identity private key. Should be a PEM formatted byte array if the 'authType' is 'x509' and the 'source' is 'payload' or should be a reference to the private key if the 'authType' is 'x509' and the 'source' is 'hsm'. + #[serde(rename = "identityPrivateKey", skip_serializing_if = "Option::is_none")] + identity_private_key: Option, +} + +impl Credentials { + pub fn new(auth_type: String, source: String) -> Credentials { + Credentials { + auth_type, + source, + key: None, + identity_cert: None, + identity_private_key: None, + } + } + + pub fn set_auth_type(&mut self, auth_type: String) { + self.auth_type = auth_type; + } + + pub fn with_auth_type(mut self, auth_type: String) -> Credentials { + self.auth_type = auth_type; + self + } + + pub fn auth_type(&self) -> &str { + &self.auth_type + } + + pub fn set_source(&mut self, source: String) { + self.source = source; + } + + pub fn with_source(mut self, source: String) -> Credentials { + self.source = source; + self + } + + pub fn source(&self) -> &str { + &self.source + } + + pub fn set_key(&mut self, key: String) { + self.key = Some(key); + } + + pub fn with_key(mut self, key: String) -> Credentials { + self.key = Some(key); + self + } + + pub fn key(&self) -> Option<&str> { + self.key.as_ref().map(AsRef::as_ref) + } + + pub fn reset_key(&mut self) { + self.key = None; + } + + pub fn set_identity_cert(&mut self, identity_cert: String) { + self.identity_cert = Some(identity_cert); + } + + pub fn with_identity_cert(mut self, identity_cert: String) -> Credentials { + self.identity_cert = Some(identity_cert); + self + } + + pub fn identity_cert(&self) -> Option<&str> { + self.identity_cert.as_ref().map(AsRef::as_ref) + } + + pub fn reset_identity_cert(&mut self) { + self.identity_cert = None; + } + + pub fn set_identity_private_key(&mut self, identity_private_key: String) { + self.identity_private_key = Some(identity_private_key); + } + + pub fn with_identity_private_key(mut self, identity_private_key: String) -> Credentials { + self.identity_private_key = Some(identity_private_key); + self + } + + pub fn identity_private_key(&self) -> Option<&str> { + self.identity_private_key.as_ref().map(AsRef::as_ref) + } + + pub fn reset_identity_private_key(&mut self) { + self.identity_private_key = None; + } +} diff --git a/edgelet/external-provisioning/src/models/device_provisioning_info.rs b/edgelet/external-provisioning/src/models/device_provisioning_info.rs new file mode 100644 index 00000000000..41f8c343856 --- /dev/null +++ b/edgelet/external-provisioning/src/models/device_provisioning_info.rs @@ -0,0 +1,81 @@ +/* + * IoT Edge External Provisioning Environment API + * + * No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen) + * + * OpenAPI spec version: 2019-04-10 + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +use serde_derive::{Deserialize, Serialize}; +#[allow(unused_imports)] +use serde_json::Value; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DeviceProvisioningInfo { + /// The host name of the IoT hub. + #[serde(rename = "hubName")] + hub_name: String, + /// The ID of the device in IoT hub. + #[serde(rename = "deviceId")] + device_id: String, + #[serde(rename = "credentials")] + credentials: crate::models::Credentials, +} + +impl DeviceProvisioningInfo { + pub fn new( + hub_name: String, + device_id: String, + credentials: crate::models::Credentials, + ) -> DeviceProvisioningInfo { + DeviceProvisioningInfo { + hub_name, + device_id, + credentials, + } + } + + pub fn set_hub_name(&mut self, hub_name: String) { + self.hub_name = hub_name; + } + + pub fn with_hub_name(mut self, hub_name: String) -> DeviceProvisioningInfo { + self.hub_name = hub_name; + self + } + + pub fn hub_name(&self) -> &str { + &self.hub_name + } + + pub fn set_device_id(&mut self, device_id: String) { + self.device_id = device_id; + } + + pub fn with_device_id(mut self, device_id: String) -> DeviceProvisioningInfo { + self.device_id = device_id; + self + } + + pub fn device_id(&self) -> &str { + &self.device_id + } + + pub fn set_credentials(&mut self, credentials: crate::models::Credentials) { + self.credentials = credentials; + } + + pub fn with_credentials( + mut self, + credentials: crate::models::Credentials, + ) -> DeviceProvisioningInfo { + self.credentials = credentials; + self + } + + pub fn credentials(&self) -> &crate::models::Credentials { + &self.credentials + } +} diff --git a/edgelet/external-provisioning/src/models/error_response.rs b/edgelet/external-provisioning/src/models/error_response.rs new file mode 100644 index 00000000000..ab89b5a32b5 --- /dev/null +++ b/edgelet/external-provisioning/src/models/error_response.rs @@ -0,0 +1,38 @@ +/* + * IoT Edge External Provisioning Environment API + * + * No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen) + * + * OpenAPI spec version: 2019-04-10 + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +use serde_derive::{Deserialize, Serialize}; +#[allow(unused_imports)] +use serde_json::Value; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ErrorResponse { + #[serde(rename = "message")] + message: String, +} + +impl ErrorResponse { + pub fn new(message: String) -> ErrorResponse { + ErrorResponse { message } + } + + pub fn set_message(&mut self, message: String) { + self.message = message; + } + + pub fn with_message(mut self, message: String) -> ErrorResponse { + self.message = message; + self + } + + pub fn message(&self) -> &str { + &self.message + } +} diff --git a/edgelet/external-provisioning/src/models/mod.rs b/edgelet/external-provisioning/src/models/mod.rs new file mode 100644 index 00000000000..a252e964cb1 --- /dev/null +++ b/edgelet/external-provisioning/src/models/mod.rs @@ -0,0 +1,9 @@ +mod credentials; +pub use self::credentials::Credentials; +mod device_provisioning_info; +pub use self::device_provisioning_info::DeviceProvisioningInfo; +mod error_response; +pub use self::error_response::ErrorResponse; + +// TODO(farcaller): sort out files +pub struct File; diff --git a/edgelet/iotedged/Cargo.toml b/edgelet/iotedged/Cargo.toml index 77fcc386195..6afd1dba24b 100644 --- a/edgelet/iotedged/Cargo.toml +++ b/edgelet/iotedged/Cargo.toml @@ -31,6 +31,7 @@ edgelet-core = { path = "../edgelet-core" } edgelet-docker = { path = "../edgelet-docker" } edgelet-hsm = { path = "../edgelet-hsm" } edgelet-http = { path = "../edgelet-http" } +edgelet-http-external-provisioning = { path = "../edgelet-http-external-provisioning" } edgelet-http-mgmt = { path = "../edgelet-http-mgmt" } edgelet-http-workload = { path = "../edgelet-http-workload" } edgelet-iothub = { path = "../edgelet-iothub" } diff --git a/edgelet/iotedged/src/error.rs b/edgelet/iotedged/src/error.rs index a8cdf656c60..0e58cd7ba45 100644 --- a/edgelet/iotedged/src/error.rs +++ b/edgelet/iotedged/src/error.rs @@ -160,6 +160,7 @@ pub enum InitializeErrorReason { DeviceClient, DpsProvisioningClient, EdgeRuntime, + ExternalProvisioningClient, Hsm, HttpClient, InvalidDeviceConfig, @@ -214,6 +215,10 @@ impl fmt::Display for InitializeErrorReason { InitializeErrorReason::EdgeRuntime => write!(f, "Could not initialize edge runtime"), + InitializeErrorReason::ExternalProvisioningClient => { + write!(f, "Could not initialize external provisioning client") + } + InitializeErrorReason::Hsm => write!(f, "Could not initialize HSM"), InitializeErrorReason::HttpClient => write!(f, "Could not initialize HTTP client"), diff --git a/edgelet/iotedged/src/lib.rs b/edgelet/iotedged/src/lib.rs index dd1ab2e18d8..53936c834d7 100644 --- a/edgelet/iotedged/src/lib.rs +++ b/edgelet/iotedged/src/lib.rs @@ -63,6 +63,7 @@ use edgelet_http::certificate_manager::CertificateManager; use edgelet_http::client::{Client as HttpClient, ClientImpl}; use edgelet_http::logging::LoggingService; use edgelet_http::{HyperExt, MaybeProxyClient, API_VERSION}; +use edgelet_http_external_provisioning::ExternalProvisioningClient; use edgelet_http_mgmt::ManagementService; use edgelet_http_workload::WorkloadService; use edgelet_iothub::{HubIdentityManager, SasTokenSource}; @@ -71,8 +72,8 @@ use hsm::tpm::Tpm; use hsm::ManageTpmKeys; use iothubservice::DeviceClient; use provisioning::provisioning::{ - BackupProvisioning, DpsSymmetricKeyProvisioning, DpsTpmProvisioning, ManualProvisioning, - Provision, ProvisioningResult, ReprovisioningStatus, + AuthType, BackupProvisioning, DpsSymmetricKeyProvisioning, DpsTpmProvisioning, + ExternalProvisioning, ManualProvisioning, Provision, ProvisioningResult, ReprovisioningStatus, }; use crate::workload::WorkloadData; @@ -129,6 +130,10 @@ const DEVICE_CA_CERT_KEY: &str = "IOTEDGE_DEVICE_CA_CERT"; const DEVICE_CA_PK_KEY: &str = "IOTEDGE_DEVICE_CA_PK"; const TRUSTED_CA_CERTS_KEY: &str = "IOTEDGE_TRUSTED_CA_CERTS"; +/// The HSM lib expects this variable to be set to the endpoint of the external provisioning environment in the 'external' +/// provisioning mode. +const EXTERNAL_PROVISIONING_ENDPOINT_KEY: &str = "IOTEDGE_EXTERNAL_PROVISIONING_ENDPOINT"; + /// This is the key for the docker network Id. const EDGE_NETWORKID_KEY: &str = "NetworkId"; @@ -147,11 +152,14 @@ const EDGE_SETTINGS_STATE_FILENAME: &str = "settings_state"; const EDGE_SETTINGS_SUBDIR: &str = "cache"; /// These are the properties of the workload CA certificate -const IOTEDGED_VALIDITY: u64 = 7_776_000; // 90 days +const IOTEDGED_VALIDITY: u64 = 7_776_000; +// 90 days const IOTEDGED_COMMONNAME: &str = "iotedged workload ca"; const IOTEDGED_TLS_COMMONNAME: &str = "iotedged"; -const IOTEDGED_MIN_EXPIRATION_DURATION: i64 = 300; // 5 mins -const IOTEDGE_ID_CERT_MAX_DURATION_SECS: i64 = 7200; // 2 hours +const IOTEDGED_MIN_EXPIRATION_DURATION: i64 = 300; +// 5 mins +const IOTEDGE_ID_CERT_MAX_DURATION_SECS: i64 = 7200; +// 2 hours const IOTEDGE_SERVER_CERT_MAX_DURATION_SECS: i64 = 7_776_000; // 90 days #[derive(PartialEq)] @@ -169,6 +177,8 @@ impl Main { Main { settings } } + // Allowing cognitive complexity errors for now. TODO: Refactor method later. + #[allow(clippy::cognitive_complexity)] pub fn run_until(self, make_shutdown_signal: G) -> Result<(), Error> where F: Future + Send + 'static, @@ -189,6 +199,14 @@ impl Main { } } + if let Provisioning::External(ref external) = settings.provisioning() { + // Set the external provisioning endpoint environment variable for use by the custom HSM library. + env::set_var( + EXTERNAL_PROVISIONING_ENDPOINT_KEY, + external.endpoint().as_str(), + ); + } + let hyper_client = MaybeProxyClient::new(get_proxy_uri(None)?) .context(ErrorKind::Initialize(InitializeErrorReason::HttpClient))?; @@ -246,67 +264,96 @@ impl Main { &mut tokio_runtime, )?; - info!("Provisioning edge device..."); - match settings.provisioning() { - Provisioning::Manual(manual) => { - let (key_store, provisioning_result, root_key) = - manual_provision(&manual, &mut tokio_runtime)?; + macro_rules! start_edgelet { + ($key_store:ident, $provisioning_result:ident, $root_key:ident, $runtime:ident) => {{ info!("Finished provisioning edge device."); + let cfg = WorkloadData::new( - provisioning_result.hub_name().to_string(), - provisioning_result.device_id().to_string(), + $provisioning_result.hub_name().to_string(), + $provisioning_result.device_id().to_string(), IOTEDGE_ID_CERT_MAX_DURATION_SECS, IOTEDGE_SERVER_CERT_MAX_DURATION_SECS, ); // This "do-while" loop runs until a StartApiReturnStatus::Shutdown // is received. If the TLS cert needs a restart, we will loop again. - while { + loop { let code = start_api( &settings, hyper_client.clone(), - &runtime, - &key_store, + &$runtime, + &$key_store, cfg.clone(), - root_key.clone(), + $root_key.clone(), make_shutdown_signal(), &crypto, &mut tokio_runtime, )?; - code == StartApiReturnStatus::Restart - } {} + + if code != StartApiReturnStatus::Restart { + break; + } + } + }}; + } + + info!("Provisioning edge device..."); + match settings.provisioning() { + Provisioning::Manual(manual) => { + info!("Starting provisioning edge device via manual mode..."); + let (key_store, provisioning_result, root_key) = + manual_provision(&manual, &mut tokio_runtime)?; + start_edgelet!(key_store, provisioning_result, root_key, runtime); + } + Provisioning::External(external) => { + info!("Starting provisioning edge device via external provisioning mode..."); + let external_provisioning_client = + ExternalProvisioningClient::new(external.endpoint()).context( + ErrorKind::Initialize(InitializeErrorReason::ExternalProvisioningClient), + )?; + let external_provisioning = ExternalProvisioning::new(external_provisioning_client); + + let provision_fut = external_provisioning + .provision(MemoryKeyStore::new()) + .map_err(|err| { + Error::from(err.context(ErrorKind::Initialize( + InitializeErrorReason::ExternalProvisioningClient, + ))) + }); + + let prov_result = tokio_runtime.block_on(provision_fut)?; + + let credentials = if let Some(credentials) = prov_result.credentials() { + credentials + } else { + info!("Credentials are expected to be populated for external provisioning."); + + return Err(Error::from(ErrorKind::Initialize( + InitializeErrorReason::ExternalProvisioningClient, + ))); + }; + + match credentials.auth_type() { + AuthType::SymmetricKey(symmetric_key) => { + if let Some(key) = symmetric_key.key() { + let (derived_key_store, memory_key) = external_provision_payload(key); + start_edgelet!(derived_key_store, prov_result, memory_key, runtime); + } else { + let (derived_key_store, tpm_key) = + external_provision_tpm(hsm_lock.clone())?; + start_edgelet!(derived_key_store, prov_result, tpm_key, runtime); + } + } + AuthType::X509(_) => { + info!("Unexpected auth type. Only symmetric keys are expected"); + return Err(Error::from(ErrorKind::Initialize( + InitializeErrorReason::ExternalProvisioningClient, + ))); + } + }; } Provisioning::Dps(dps) => { let dps_path = cache_subdir_path.join(EDGE_PROVISIONING_BACKUP_FILENAME); - macro_rules! start_edgelet { - ($key_store:ident, $provisioning_result:ident, $root_key:ident, $runtime:ident) => {{ - info!("Finished provisioning edge device."); - - let cfg = WorkloadData::new( - $provisioning_result.hub_name().to_string(), - $provisioning_result.device_id().to_string(), - IOTEDGE_ID_CERT_MAX_DURATION_SECS, - IOTEDGE_SERVER_CERT_MAX_DURATION_SECS, - ); - // This "do-while" loop runs until a StartApiReturnStatus::Shutdown - // is received. If the TLS cert needs a restart, we will loop again. - while { - let code = start_api( - &settings, - hyper_client.clone(), - &$runtime, - &$key_store, - cfg.clone(), - $root_key.clone(), - make_shutdown_signal(), - &crypto, - &mut tokio_runtime, - )?; - code == StartApiReturnStatus::Restart - } {} - }}; - } - match dps.attestation() { AttestationMethod::Tpm(ref tpm) => { info!("Starting provisioning edge device via TPM..."); @@ -712,6 +759,39 @@ fn manual_provision( tokio_runtime.block_on(provision) } +fn external_provision_payload(key: &str) -> (DerivedKeyStore, MemoryKey) { + let memory_key = MemoryKey::new(key); + let mut memory_hsm = MemoryKeyStore::new(); + memory_hsm.insert(&KeyIdentity::Device, "primary", memory_key.clone()); + + let derived_key_store = DerivedKeyStore::new(memory_key.clone()); + (derived_key_store, memory_key) +} + +fn external_provision_tpm( + hsm_lock: Arc, +) -> Result<(DerivedKeyStore, TpmKey), Error> { + let tpm = Tpm::new().context(ErrorKind::Initialize( + InitializeErrorReason::ExternalProvisioningClient, + ))?; + + let tpm_hsm = TpmKeyStore::from_hsm(tpm, hsm_lock).context(ErrorKind::Initialize( + InitializeErrorReason::ExternalProvisioningClient, + ))?; + + tpm_hsm + .get(&KeyIdentity::Device, "primary") + .map_err(|err| { + Error::from(err.context(ErrorKind::Initialize( + InitializeErrorReason::ExternalProvisioningClient, + ))) + }) + .and_then(|k| { + let derived_key_store = DerivedKeyStore::new(k.clone()); + Ok((derived_key_store, k)) + }) +} + fn dps_symmetric_key_provision( provisioning: &Dps, hyper_client: HC, diff --git a/edgelet/provisioning/Cargo.toml b/edgelet/provisioning/Cargo.toml index 9167f05e50f..78a18e32a2f 100644 --- a/edgelet/provisioning/Cargo.toml +++ b/edgelet/provisioning/Cargo.toml @@ -21,7 +21,9 @@ dps = { path = "../dps" } edgelet-core = { path = "../edgelet-core" } edgelet-hsm = { path = "../edgelet-hsm" } edgelet-http = { path = "../edgelet-http" } +edgelet-http-external-provisioning = { path = "../edgelet-http-external-provisioning" } edgelet-utils = { path = "../edgelet-utils" } +external-provisioning = { path = "../external-provisioning" } [dev_dependencies] tempdir = "0.3.7" diff --git a/edgelet/provisioning/src/error.rs b/edgelet/provisioning/src/error.rs index 458a73f8bc9..5f4ec5e7a95 100644 --- a/edgelet/provisioning/src/error.rs +++ b/edgelet/provisioning/src/error.rs @@ -21,6 +21,9 @@ pub enum ErrorKind { #[fail(display = "Could not initialize DPS provisioning client")] DpsInitialization, + #[fail(display = "Could not initialize External provisioning client")] + ExternalProvisioningInitialization, + #[fail(display = "Could not provision device")] Provision, } diff --git a/edgelet/provisioning/src/lib.rs b/edgelet/provisioning/src/lib.rs index 24ca5a58c69..bdaa961164e 100644 --- a/edgelet/provisioning/src/lib.rs +++ b/edgelet/provisioning/src/lib.rs @@ -9,6 +9,7 @@ pub mod provisioning; pub use crate::error::Error; pub use crate::provisioning::{ - BackupProvisioning, DpsSymmetricKeyProvisioning, DpsTpmProvisioning, DpsX509Provisioning, - Provision, ProvisioningResult, ReprovisioningStatus, + AuthType, BackupProvisioning, Credentials, DpsSymmetricKeyProvisioning, DpsTpmProvisioning, + DpsX509Provisioning, Provision, ProvisioningResult, ReprovisioningStatus, + SymmetricKeyCredential, }; diff --git a/edgelet/provisioning/src/provisioning.rs b/edgelet/provisioning/src/provisioning.rs index 567019f8ce3..811b47b569e 100644 --- a/edgelet/provisioning/src/provisioning.rs +++ b/edgelet/provisioning/src/provisioning.rs @@ -2,6 +2,7 @@ use std::fs::File; use std::io::{Read, Write}; +use std::marker::PhantomData; use std::path::PathBuf; use bytes::Bytes; @@ -17,6 +18,7 @@ use dps::registration::{DpsAuthKind, DpsClient, DpsTokenSource}; use edgelet_core::crypto::{Activate, KeyIdentity, KeyStore, MemoryKey, MemoryKeyStore}; use edgelet_hsm::tpm::{TpmKey, TpmKeyStore}; use edgelet_http::client::{Client as HttpClient, ClientImpl}; +use edgelet_http_external_provisioning::ExternalProvisioningInterface; use edgelet_utils::log_failure; use hsm::TpmKey as HsmTpmKey; use log::{debug, Level}; @@ -33,6 +35,62 @@ pub enum ReprovisioningStatus { DeviceDataReset, } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SymmetricKeyCredential { + #[serde(skip_serializing_if = "Option::is_none")] + key: Option, +} + +impl SymmetricKeyCredential { + pub fn key(&self) -> Option<&str> { + self.key.as_ref().map(AsRef::as_ref) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct X509Credential { + identity_cert: String, + identity_private_key: String, +} + +impl X509Credential { + pub fn identity_cert(&self) -> &str { + self.identity_cert.as_str() + } + + pub fn identity_private_key(&self) -> &str { + self.identity_private_key.as_str() + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum AuthType { + SymmetricKey(SymmetricKeyCredential), + X509(X509Credential), +} + +#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)] +pub enum CredentialSource { + Payload, + Hsm, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Credentials { + auth_type: AuthType, + source: CredentialSource, +} + +impl Credentials { + pub fn auth_type(&self) -> &AuthType { + &self.auth_type + } + + pub fn source(&self) -> &CredentialSource { + &self.source + } +} + impl From<&str> for ReprovisioningStatus { fn from(s: &str) -> ReprovisioningStatus { // TODO: check with DPS substatus value for DeviceDataUpdated when it is implemented on service side @@ -62,6 +120,8 @@ pub struct ProvisioningResult { sha256_thumbprint: Option, #[serde(skip)] reconfigure: ReprovisioningStatus, + #[serde(skip_serializing_if = "Option::is_none")] + credentials: Option, } impl ProvisioningResult { @@ -76,6 +136,10 @@ impl ProvisioningResult { pub fn reconfigure(&self) -> ReprovisioningStatus { self.reconfigure } + + pub fn credentials(&self) -> Option<&Credentials> { + self.credentials.as_ref() + } } pub trait Provision { @@ -128,12 +192,108 @@ impl Provision for ManualProvisioning { hub_name: hub, reconfigure: ReprovisioningStatus::DeviceDataNotUpdated, sha256_thumbprint: None, + credentials: None, }) .map_err(|err| Error::from(err.context(ErrorKind::Provision))); Box::new(result.into_future()) } } +pub struct ExternalProvisioning { + client: T, + + // ExternalProvisioning is not restricted to a single HSM implementation, so it uses + // PhantomData to be generic on them. + phantom: PhantomData, +} + +impl ExternalProvisioning { + pub fn new(client: T) -> Self { + ExternalProvisioning { + client, + phantom: PhantomData, + } + } +} + +impl Provision for ExternalProvisioning +where + T: 'static + ExternalProvisioningInterface, + U: 'static + Activate + KeyStore + Send, +{ + type Hsm = U; + + fn provision( + self, + mut _key_activator: Self::Hsm, + ) -> Box + Send> { + let result = self + .client + .get_device_provisioning_information() + .map_err(|err| Error::from(err.context(ErrorKind::Provision))) + .and_then(move |device_provisioning_info| { + info!( + "External device registration information: Device \"{}\" in hub \"{}\"", + device_provisioning_info.device_id(), + device_provisioning_info.hub_name() + ); + + let credentials_info = device_provisioning_info.credentials(); + let credentials = if let "symmetric-key" = credentials_info.auth_type() { + match credentials_info.source() { + "payload" => { + credentials_info.key().map_or_else( + || { + info!( + "A key is expected in the response with the 'symmetric-key' authentication type and the 'source' set to 'payload'."); + Err(Error::from(ErrorKind::Provision)) + }, + |key| { + Ok(Credentials { + auth_type: AuthType::SymmetricKey( + SymmetricKeyCredential { + key: Some(key.to_string()), + }), + source: CredentialSource::Payload, + }) + }) + + }, + "hsm" => Ok(Credentials { + auth_type: AuthType::SymmetricKey( + SymmetricKeyCredential { + key: None, + }), + source: CredentialSource::Hsm, + }), + _ => { + info!( + "Unexpected value of credential source \"{}\" received from external environment.", + credentials_info.source() + ); + Err(Error::from(ErrorKind::Provision)) + } + } + } + else { + info!("External Provisioning is currently only supported for the 'symmetric-key' authentication type."); + Err(Error::from(ErrorKind::Provision)) + // TODO: implement + }?; + + Ok(ProvisioningResult { + device_id: device_provisioning_info.device_id().to_string(), + hub_name: device_provisioning_info.hub_name().to_string(), + reconfigure: ReprovisioningStatus::DeviceDataNotUpdated, + sha256_thumbprint: None, + credentials: Some(credentials) + }) + }); + + Box::new(result) + } +} + pub struct DpsTpmProvisioning where C: ClientImpl, @@ -214,6 +374,7 @@ where // keys when the deployment is executed by EdgeAgent. reconfigure: ReprovisioningStatus::InitialAssignment, sha256_thumbprint: None, + credentials: None, } }) .map_err(|err| Error::from(err.context(ErrorKind::Provision))), @@ -296,6 +457,7 @@ where hub_name, reconfigure, sha256_thumbprint: None, + credentials: None, } }) .map_err(|err| Error::from(err.context(ErrorKind::Provision))), @@ -379,6 +541,7 @@ where hub_name, reconfigure, sha256_thumbprint: None, + credentials: None, } }) .map_err(|err| Error::from(err.context(ErrorKind::Provision))), @@ -517,11 +680,13 @@ where mod tests { use super::*; + use edgelet_config::{Manual, ParseManualDeviceConnectionStringError}; + use external_provisioning::models::{Credentials, DeviceProvisioningInfo}; + use failure::Fail; + use std::fmt::{self, Display}; use tempdir::TempDir; use tokio; - use edgelet_config::{Manual, ParseManualDeviceConnectionStringError}; - use crate::error::ErrorKind; struct TestProvisioning {} @@ -538,6 +703,7 @@ mod tests { hub_name: "TestHub".to_string(), reconfigure: ReprovisioningStatus::DeviceDataUpdated, sha256_thumbprint: None, + credentials: None, })) } } @@ -556,6 +722,7 @@ mod tests { hub_name: "TestHubUpdated".to_string(), reconfigure: ReprovisioningStatus::DeviceDataUpdated, sha256_thumbprint: None, + credentials: None, })) } } @@ -815,6 +982,7 @@ mod tests { hub_name: "something".to_string(), reconfigure: ReprovisioningStatus::DeviceDataNotUpdated, sha256_thumbprint: None, + credentials: None, }) .unwrap(); assert_eq!( @@ -824,4 +992,208 @@ mod tests { let result: ProvisioningResult = serde_json::from_str(&json).unwrap(); assert_eq!(result.reconfigure, ReprovisioningStatus::InitialAssignment) } + + struct TestExternalProvisioningInterface { + pub error: Option, + pub provisioning_info: DeviceProvisioningInfo, + } + + #[derive(Debug, Fail)] + struct TestError {} + + impl Display for TestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "test error") + } + } + + impl ExternalProvisioningInterface for TestExternalProvisioningInterface { + type Error = TestError; + + type DeviceProvisioningInformationFuture = + Box + Send>; + + fn get_device_provisioning_information(&self) -> Self::DeviceProvisioningInformationFuture { + match self.error.as_ref() { + None => Box::new(Ok(self.provisioning_info.clone()).into_future()), + Some(_s) => Box::new(Err(TestError {}).into_future()), + } + } + } + + #[test] + fn external_get_provisioning_info_symmetric_key_payload_success() { + let mut credentials = Credentials::new("symmetric-key".to_string(), "payload".to_string()); + credentials.set_key("test-key".to_string()); + let provisioning_info = DeviceProvisioningInfo::new( + "TestHub".to_string(), + "TestDevice".to_string(), + credentials, + ); + + let provisioning = ExternalProvisioning::new(TestExternalProvisioningInterface { + error: None, + provisioning_info, + }); + let memory_hsm = MemoryKeyStore::new(); + let task = provisioning + .provision(memory_hsm.clone()) + .then(|result| match result { + Ok(result) => { + assert_eq!(result.hub_name, "TestHub".to_string()); + assert_eq!(result.device_id, "TestDevice".to_string()); + + if let Some(credentials) = result.credentials() { + if let AuthType::SymmetricKey(symmetric_key) = credentials.auth_type() { + if let Some(key) = &symmetric_key.key { + assert_eq!(key, "test-key"); + } else { + panic!("A key was expected in the response.") + } + } else { + panic!("Unexpected authentication type.") + } + } else { + panic!("No credentials found. This is unexpected") + } + + Ok::<_, Error>(()) + } + Err(err) => panic!("Unexpected {:?}", err), + }); + tokio::runtime::current_thread::Runtime::new() + .unwrap() + .block_on(task) + .unwrap(); + } + + #[test] + fn external_get_provisioning_info_symmetric_key_hsm_success() { + let credentials = Credentials::new("symmetric-key".to_string(), "hsm".to_string()); + let provisioning_info = DeviceProvisioningInfo::new( + "TestHub".to_string(), + "TestDevice".to_string(), + credentials, + ); + + let provisioning = ExternalProvisioning::new(TestExternalProvisioningInterface { + error: None, + provisioning_info, + }); + let memory_hsm = MemoryKeyStore::new(); + let task = provisioning + .provision(memory_hsm.clone()) + .then(|result| match result { + Ok(result) => { + assert_eq!(result.hub_name, "TestHub".to_string()); + assert_eq!(result.device_id, "TestDevice".to_string()); + + if let Some(credentials) = result.credentials() { + if let AuthType::SymmetricKey(symmetric_key) = credentials.auth_type() { + if symmetric_key.key.is_some() { + panic!("No key was expected in the response.") + } + } else { + panic!("Unexpected authentication type.") + } + } else { + panic!("No credentials found. This is unexpected") + } + + Ok::<_, Error>(()) + } + Err(err) => panic!("Unexpected {:?}", err), + }); + tokio::runtime::current_thread::Runtime::new() + .unwrap() + .block_on(task) + .unwrap(); + } + + #[test] + fn external_get_provisioning_info_invalid_credentials_source() { + let credentials = Credentials::new("symmetric-key".to_string(), "xyz".to_string()); + let provisioning_info = DeviceProvisioningInfo::new( + "TestHub".to_string(), + "TestDevice".to_string(), + credentials, + ); + + let provisioning = ExternalProvisioning::new(TestExternalProvisioningInterface { + error: None, + provisioning_info, + }); + let memory_hsm = MemoryKeyStore::new(); + let task = provisioning + .provision(memory_hsm.clone()) + .then(|result| match result { + Ok(_) => panic!("Expected a failure."), + Err(err) => match err.kind() { + ErrorKind::Provision => Ok::<_, Error>(()), + _ => panic!("Expected `Provision` but got {:?}", err), + }, + }); + tokio::runtime::current_thread::Runtime::new() + .unwrap() + .block_on(task) + .unwrap(); + } + + #[test] + fn external_get_provisioning_info_x509_unsupported() { + let credentials = Credentials::new("x509".to_string(), "payload".to_string()); + let provisioning_info = DeviceProvisioningInfo::new( + "TestHub".to_string(), + "TestDevice".to_string(), + credentials, + ); + + let provisioning = ExternalProvisioning::new(TestExternalProvisioningInterface { + error: None, + provisioning_info, + }); + let memory_hsm = MemoryKeyStore::new(); + let task = provisioning + .provision(memory_hsm.clone()) + .then(|result| match result { + Ok(_) => panic!("Expected a failure."), + Err(err) => match err.kind() { + ErrorKind::Provision => Ok::<_, Error>(()), + _ => panic!("Expected `Provision` but got {:?}", err), + }, + }); + tokio::runtime::current_thread::Runtime::new() + .unwrap() + .block_on(task) + .unwrap(); + } + + #[test] + fn external_get_provisioning_info_failure() { + let credentials = Credentials::new("symmetric-key".to_string(), "payload".to_string()); + let provisioning_info = DeviceProvisioningInfo::new( + "TestHub".to_string(), + "TestDevice".to_string(), + credentials, + ); + + let provisioning = ExternalProvisioning::new(TestExternalProvisioningInterface { + error: Some(TestError {}), + provisioning_info, + }); + let memory_hsm = MemoryKeyStore::new(); + let task = provisioning + .provision(memory_hsm.clone()) + .then(|result| match result { + Ok(_) => panic!("Expected a failure."), + Err(err) => match err.kind() { + ErrorKind::Provision => Ok::<_, Error>(()), + _ => panic!("Expected `Provision` but got {:?}", err), + }, + }); + tokio::runtime::current_thread::Runtime::new() + .unwrap() + .block_on(task) + .unwrap(); + } } diff --git a/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 b/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 index 41cff7e6a52..245930ec7b1 100644 --- a/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 +++ b/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 @@ -109,6 +109,11 @@ PS> Initialize-IoTEdge -Dps -ScopeId $scopeId -ContainerOs Windows -X509Identity .EXAMPLE PS> Initialize-IoTEdge -Dps -ScopeId $scopeId -RegistrationId $registrationId -ContainerOs Windows -AutoGenX509IdentityCertificate $true -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle + + +.EXAMPLE + +PS> Initialize-IoTEdge -External -ExternalProvisioningEndpoint $externalProvisioningEndpoint -ContainerOs Windows -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle #> function Initialize-IoTEdge { [CmdletBinding(DefaultParameterSetName = 'Manual')] @@ -121,6 +126,10 @@ function Initialize-IoTEdge { [Parameter(ParameterSetName = 'DPS')] [Switch] $Dps, + # Specified the daemon will be configured using an external provisioning endpoint. + [Parameter(ParameterSetName = 'External')] + [Switch] $External, + # The device connection string. [Parameter(Mandatory = $true, ParameterSetName = 'Manual')] [String] $DeviceConnectionString, @@ -165,6 +174,11 @@ function Initialize-IoTEdge { [ValidateNotNullOrEmpty()] [String] $DeviceTrustbundle, + # The external provisioning environment endpoint for the External provisioning mode. + [Parameter(Mandatory = $true, ParameterSetName = 'External')] + [ValidateNotNullOrEmpty()] + [String] $ExternalProvisioningEndpoint, + # The base OS of all the containers that will be run on this device via the security daemon. # # If set to Linux, a separate installation of Docker for Windows is expected. @@ -448,6 +462,11 @@ PS> Install-IoTEdge -Dps -ScopeId $scopeId -ContainerOs Windows -X509IdentityCer .EXAMPLE PS> Install-IoTEdge -Dps -ScopeId $scopeId -RegistrationId $registrationId -ContainerOs Windows -AutoGenX509IdentityCertificate $true -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle + + +.EXAMPLE + +PS> Install-IoTEdge -External -ExternalProvisioningEndpoint $externalProvisioningEndpoint -ContainerOs Windows -DeviceCACertificate $deviceCACertificate -DeviceCAPrivateKey $deviceCAPrivateKey -DeviceTrustbundle $deviceTrustbundle #> function Install-IoTEdge { [CmdletBinding(DefaultParameterSetName = 'Manual')] @@ -460,6 +479,10 @@ function Install-IoTEdge { [Parameter(ParameterSetName = 'DPS')] [Switch] $Dps, + # Specified the daemon will be configured using an external provisioning endpoint. + [Parameter(ParameterSetName = 'External')] + [Switch] $External, + # The device connection string. [Parameter(Mandatory = $true, ParameterSetName = 'Manual')] [String] $DeviceConnectionString, @@ -504,6 +527,11 @@ function Install-IoTEdge { [ValidateNotNullOrEmpty()] [String] $DeviceTrustbundle, + # The external provisioning environment endpoint for the External provisioning mode. + [Parameter(Mandatory = $true, ParameterSetName = 'External')] + [ValidateNotNullOrEmpty()] + [String] $ExternalProvisioningEndpoint, + # The base OS of all the containers that will be run on this device via the security daemon. # # If set to Linux, a separate installation of Docker for Windows is expected. @@ -520,18 +548,12 @@ function Install-IoTEdge { [String] $OfflineInstallationPath, # IoT Edge Agent image to pull for the initial configuration. - [Parameter(ParameterSetName = 'Manual')] - [Parameter(ParameterSetName = 'DPS')] [String] $AgentImage, # Username used to access the container registry and pull the IoT Edge Agent image. - [Parameter(ParameterSetName = 'Manual')] - [Parameter(ParameterSetName = 'DPS')] [String] $Username, # Password used to access the container registry and pull the IoT Edge Agent image. - [Parameter(ParameterSetName = 'Manual')] - [Parameter(ParameterSetName = 'DPS')] [SecureString] $Password, # Splatted into every Invoke-WebRequest invocation. Can be used to set extra options. @@ -578,6 +600,7 @@ function Install-IoTEdge { if ($Manual) { $Params["-Manual"] = $true } if ($Dps) { $Params["-Dps"] = $true } + if ($External) { $Params["-External"] = $true } if ($DeviceConnectionString) { $Params["-DeviceConnectionString"] = $DeviceConnectionString } if ($ScopeId) { $Params["-ScopeId"] = $ScopeId } if ($RegistrationId) { $Params["-RegistrationId"] = $RegistrationId } @@ -588,6 +611,7 @@ function Install-IoTEdge { if ($DeviceCACertificate) { $Params["-DeviceCACertificate"] = $DeviceCACertificate } if ($DeviceCAPrivateKey) { $Params["-DeviceCAPrivateKey"] = $DeviceCAPrivateKey } if ($DeviceTrustbundle) { $Params["-DeviceTrustbundle"] = $DeviceTrustbundle } + if ($ExternalProvisioningEndpoint) { $Params["-ExternalProvisioningEndpoint"] = $ExternalProvisioningEndpoint } if ($AgentImage) { $Params["-AgentImage"] = $AgentImage } if ($Username) { $Params["-Username"] = $Username } if ($Password) { $Params["-Password"] = $Password } @@ -1551,6 +1575,16 @@ function Set-ProvisioningMode { Write-HostGreen 'Configured device for manual provisioning.' return $configurationYaml } + elseif ($External -or $ExternalProvisioningEndpoint){ + $selectionRegex = '(?:[^\S\n]*#[^\S\n]*)?provisioning:\s*#?\s*source:\s*".*"\s*#?\s*endpoint:\s*".*"' + $replacementContent = @( + 'provisioning:', + ' source: ''external''', + " endpoint: '$ExternalProvisioningEndpoint'") + $configurationYaml = ($configurationYaml -replace $selectionRegex, ($replacementContent -join "`n")) + Write-HostGreen 'Configured device for external provisioning.' + return $configurationYaml + } else { $attestationMethod = Get-DpsProvisioningSettings $selectionRegex = '(?:[^\S\n]*#[^\S\n]*)?provisioning:\s*#?\s*source:\s*".*"\s*#?\s*global_endpoint:\s*".*"\s*#?\s*scope_id:\s*".*"\s*#?\s*attestation:\s*#?\s*method:\s*"' + $attestationMethod + '"\s*#?\s*registration_id:\s*".*"\s*#?\s*device_id:\s*".*"'