authgraph: add placeholder VTS test am: c8625a8963 am: ad80c36b3c
Original change: https://android-review.googlesource.com/c/platform/hardware/interfaces/+/2750487
Change-Id: I177bc256fd4047e9530dbc1ff4c49583492ca464
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/compatibility_matrices/compatibility_matrix.9.xml b/compatibility_matrices/compatibility_matrix.9.xml
index c79c68f..5ae169c 100644
--- a/compatibility_matrices/compatibility_matrix.9.xml
+++ b/compatibility_matrices/compatibility_matrix.9.xml
@@ -513,7 +513,7 @@
<version>1</version>
<interface>
<name>IAuthGraphKeyExchange</name>
- <instance>default</instance>
+ <instance>nonsecure</instance>
</interface>
</hal>
<hal format="aidl" optional="true" updatable-via-apex="true">
diff --git a/security/authgraph/aidl/vts/functional/AuthGraphSessionTest.cpp b/security/authgraph/aidl/vts/functional/AuthGraphSessionTest.cpp
index cab8074..d9dea77 100644
--- a/security/authgraph/aidl/vts/functional/AuthGraphSessionTest.cpp
+++ b/security/authgraph/aidl/vts/functional/AuthGraphSessionTest.cpp
@@ -166,6 +166,171 @@
// encoded as `sink_info.sharedKeys` and `source_info.sharedKeys`.
}
+TEST_P(AuthGraphSessionTest, ParallelSink) {
+ std::shared_ptr<IAuthGraphKeyExchange> source = authNode_;
+ std::shared_ptr<IAuthGraphKeyExchange> sink1 = authNode_;
+ std::shared_ptr<IAuthGraphKeyExchange> sink2 = authNode_;
+
+ // Step 1: create ephemeral ECDH keys at the source.
+ SessionInitiationInfo source_init1_info;
+ ASSERT_EQ(OK, GetReturnError(source->create(&source_init1_info)));
+ ASSERT_TRUE(source_init1_info.key.pubKey.has_value());
+ ASSERT_TRUE(source_init1_info.key.arcFromPBK.has_value());
+ SessionInitiationInfo source_init2_info;
+ ASSERT_EQ(OK, GetReturnError(source->create(&source_init2_info)));
+ ASSERT_TRUE(source_init2_info.key.pubKey.has_value());
+ ASSERT_TRUE(source_init2_info.key.arcFromPBK.has_value());
+
+ // Step 2: pass the source's ECDH public keys and other session info to the sinks.
+ KeInitResult init1_result;
+ ASSERT_EQ(OK, GetReturnError(sink1->init(source_init1_info.key.pubKey.value(),
+ source_init1_info.identity, source_init1_info.nonce,
+ source_init1_info.version, &init1_result)));
+ SessionInitiationInfo sink1_init_info = init1_result.sessionInitiationInfo;
+ ASSERT_TRUE(sink1_init_info.key.pubKey.has_value());
+
+ SessionInfo sink1_info = init1_result.sessionInfo;
+ ASSERT_EQ((int)sink1_info.sharedKeys.size(), 2) << "Expect two symmetric keys from init()";
+ ASSERT_GT((int)sink1_info.sessionId.size(), 0) << "Expect non-empty session ID from sink";
+ std::vector<uint8_t> sink1_signing_key = SigningKeyFromIdentity(sink1_init_info.identity);
+ CheckSignature(sink1_signing_key, sink1_info.sessionId, sink1_info.signature);
+ KeInitResult init2_result;
+ ASSERT_EQ(OK, GetReturnError(sink2->init(source_init2_info.key.pubKey.value(),
+ source_init2_info.identity, source_init2_info.nonce,
+ source_init2_info.version, &init2_result)));
+ SessionInitiationInfo sink2_init_info = init2_result.sessionInitiationInfo;
+ ASSERT_TRUE(sink2_init_info.key.pubKey.has_value());
+
+ SessionInfo sink2_info = init2_result.sessionInfo;
+ ASSERT_EQ((int)sink2_info.sharedKeys.size(), 2) << "Expect two symmetric keys from init()";
+ ASSERT_GT((int)sink2_info.sessionId.size(), 0) << "Expect non-empty session ID from sink";
+ std::vector<uint8_t> sink2_signing_key = SigningKeyFromIdentity(sink2_init_info.identity);
+ CheckSignature(sink2_signing_key, sink2_info.sessionId, sink2_info.signature);
+
+ // Step 3: pass each sink's ECDH public key and other session info to the source, so it can
+ // calculate the same pair of symmetric keys.
+ SessionInfo source_info1;
+ ASSERT_EQ(OK, GetReturnError(source->finish(sink1_init_info.key.pubKey.value(),
+ sink1_init_info.identity, sink1_info.signature,
+ sink1_init_info.nonce, sink1_init_info.version,
+ source_init1_info.key, &source_info1)));
+ ASSERT_EQ((int)source_info1.sharedKeys.size(), 2) << "Expect two symmetric keys from finsh()";
+ ASSERT_GT((int)source_info1.sessionId.size(), 0) << "Expect non-empty session ID from source";
+ std::vector<uint8_t> source_signing_key1 = SigningKeyFromIdentity(source_init1_info.identity);
+ CheckSignature(source_signing_key1, source_info1.sessionId, source_info1.signature);
+ SessionInfo source_info2;
+ ASSERT_EQ(OK, GetReturnError(source->finish(sink2_init_info.key.pubKey.value(),
+ sink2_init_info.identity, sink2_info.signature,
+ sink2_init_info.nonce, sink2_init_info.version,
+ source_init2_info.key, &source_info2)));
+ ASSERT_EQ((int)source_info2.sharedKeys.size(), 2) << "Expect two symmetric keys from finsh()";
+ ASSERT_GT((int)source_info2.sessionId.size(), 0) << "Expect non-empty session ID from source";
+ std::vector<uint8_t> source_signing_key2 = SigningKeyFromIdentity(source_init2_info.identity);
+ CheckSignature(source_signing_key2, source_info2.sessionId, source_info2.signature);
+
+ // Both ends should agree on the session ID.
+ ASSERT_EQ(source_info1.sessionId, sink1_info.sessionId);
+ ASSERT_EQ(source_info2.sessionId, sink2_info.sessionId);
+
+ // Step 4: pass the source's session ID info back to the sink, so it can check it and
+ // update the symmetric keys so they're marked as authentication complete.
+ std::array<Arc, 2> auth_complete_result1;
+ ASSERT_EQ(OK, GetReturnError(sink1->authenticationComplete(
+ source_info1.signature, sink1_info.sharedKeys, &auth_complete_result1)));
+ ASSERT_EQ((int)auth_complete_result1.size(), 2)
+ << "Expect two symmetric keys from authComplete()";
+ sink1_info.sharedKeys = auth_complete_result1;
+ std::array<Arc, 2> auth_complete_result2;
+ ASSERT_EQ(OK, GetReturnError(sink2->authenticationComplete(
+ source_info2.signature, sink2_info.sharedKeys, &auth_complete_result2)));
+ ASSERT_EQ((int)auth_complete_result2.size(), 2)
+ << "Expect two symmetric keys from authComplete()";
+ sink2_info.sharedKeys = auth_complete_result2;
+}
+
+TEST_P(AuthGraphSessionTest, ParallelSource) {
+ std::shared_ptr<IAuthGraphKeyExchange> source1 = authNode_;
+ std::shared_ptr<IAuthGraphKeyExchange> source2 = authNode_;
+ std::shared_ptr<IAuthGraphKeyExchange> sink = authNode_;
+
+ // Step 1: create an ephemeral ECDH key at each of the sources.
+ SessionInitiationInfo source1_init_info;
+ ASSERT_EQ(OK, GetReturnError(source1->create(&source1_init_info)));
+ ASSERT_TRUE(source1_init_info.key.pubKey.has_value());
+ ASSERT_TRUE(source1_init_info.key.arcFromPBK.has_value());
+ SessionInitiationInfo source2_init_info;
+ ASSERT_EQ(OK, GetReturnError(source1->create(&source2_init_info)));
+ ASSERT_TRUE(source2_init_info.key.pubKey.has_value());
+ ASSERT_TRUE(source2_init_info.key.arcFromPBK.has_value());
+
+ // Step 2: pass each source's ECDH public key and other session info to the sink.
+ KeInitResult init1_result;
+ ASSERT_EQ(OK, GetReturnError(sink->init(source1_init_info.key.pubKey.value(),
+ source1_init_info.identity, source1_init_info.nonce,
+ source1_init_info.version, &init1_result)));
+ SessionInitiationInfo sink_init1_info = init1_result.sessionInitiationInfo;
+ ASSERT_TRUE(sink_init1_info.key.pubKey.has_value());
+
+ SessionInfo sink_info1 = init1_result.sessionInfo;
+ ASSERT_EQ((int)sink_info1.sharedKeys.size(), 2) << "Expect two symmetric keys from init()";
+ ASSERT_GT((int)sink_info1.sessionId.size(), 0) << "Expect non-empty session ID from sink";
+ std::vector<uint8_t> sink_signing_key1 = SigningKeyFromIdentity(sink_init1_info.identity);
+ CheckSignature(sink_signing_key1, sink_info1.sessionId, sink_info1.signature);
+
+ KeInitResult init2_result;
+ ASSERT_EQ(OK, GetReturnError(sink->init(source2_init_info.key.pubKey.value(),
+ source2_init_info.identity, source2_init_info.nonce,
+ source2_init_info.version, &init2_result)));
+ SessionInitiationInfo sink_init2_info = init2_result.sessionInitiationInfo;
+ ASSERT_TRUE(sink_init2_info.key.pubKey.has_value());
+
+ SessionInfo sink_info2 = init2_result.sessionInfo;
+ ASSERT_EQ((int)sink_info2.sharedKeys.size(), 2) << "Expect two symmetric keys from init()";
+ ASSERT_GT((int)sink_info2.sessionId.size(), 0) << "Expect non-empty session ID from sink";
+ std::vector<uint8_t> sink_signing_key2 = SigningKeyFromIdentity(sink_init2_info.identity);
+ CheckSignature(sink_signing_key2, sink_info2.sessionId, sink_info2.signature);
+
+ // Step 3: pass the sink's ECDH public keys and other session info to the each of the sources.
+ SessionInfo source1_info;
+ ASSERT_EQ(OK, GetReturnError(source1->finish(sink_init1_info.key.pubKey.value(),
+ sink_init1_info.identity, sink_info1.signature,
+ sink_init1_info.nonce, sink_init1_info.version,
+ source1_init_info.key, &source1_info)));
+ ASSERT_EQ((int)source1_info.sharedKeys.size(), 2) << "Expect two symmetric keys from finsh()";
+ ASSERT_GT((int)source1_info.sessionId.size(), 0) << "Expect non-empty session ID from source";
+ std::vector<uint8_t> source1_signing_key = SigningKeyFromIdentity(source1_init_info.identity);
+ CheckSignature(source1_signing_key, source1_info.sessionId, source1_info.signature);
+
+ SessionInfo source2_info;
+ ASSERT_EQ(OK, GetReturnError(source2->finish(sink_init2_info.key.pubKey.value(),
+ sink_init2_info.identity, sink_info2.signature,
+ sink_init2_info.nonce, sink_init2_info.version,
+ source2_init_info.key, &source2_info)));
+ ASSERT_EQ((int)source2_info.sharedKeys.size(), 2) << "Expect two symmetric keys from finsh()";
+ ASSERT_GT((int)source2_info.sessionId.size(), 0) << "Expect non-empty session ID from source";
+ std::vector<uint8_t> source2_signing_key = SigningKeyFromIdentity(source2_init_info.identity);
+ CheckSignature(source2_signing_key, source2_info.sessionId, source2_info.signature);
+
+ // Both ends should agree on the session ID.
+ ASSERT_EQ(source1_info.sessionId, sink_info1.sessionId);
+ ASSERT_EQ(source2_info.sessionId, sink_info2.sessionId);
+
+ // Step 4: pass the each source's session ID info back to the sink, so it can check it and
+ // update the symmetric keys so they're marked as authentication complete.
+ std::array<Arc, 2> auth_complete_result1;
+ ASSERT_EQ(OK, GetReturnError(sink->authenticationComplete(
+ source1_info.signature, sink_info1.sharedKeys, &auth_complete_result1)));
+ ASSERT_EQ((int)auth_complete_result1.size(), 2)
+ << "Expect two symmetric keys from authComplete()";
+ sink_info1.sharedKeys = auth_complete_result1;
+ std::array<Arc, 2> auth_complete_result2;
+ ASSERT_EQ(OK, GetReturnError(sink->authenticationComplete(
+ source2_info.signature, sink_info2.sharedKeys, &auth_complete_result2)));
+ ASSERT_EQ((int)auth_complete_result2.size(), 2)
+ << "Expect two symmetric keys from authComplete()";
+ sink_info2.sharedKeys = auth_complete_result2;
+}
+
TEST_P(AuthGraphSessionTest, FreshNonces) {
std::shared_ptr<IAuthGraphKeyExchange> source = authNode_;
std::shared_ptr<IAuthGraphKeyExchange> sink = authNode_;
diff --git a/security/authgraph/default/Android.bp b/security/authgraph/default/Android.bp
new file mode 100644
index 0000000..9de3bc1
--- /dev/null
+++ b/security/authgraph/default/Android.bp
@@ -0,0 +1,46 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+//
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "hardware_interfaces_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["hardware_interfaces_license"],
+}
+
+rust_binary {
+ name: "android.hardware.security.authgraph-service.nonsecure",
+ relative_install_path: "hw",
+ vendor: true,
+ init_rc: ["authgraph.rc"],
+ vintf_fragments: ["authgraph.xml"],
+ defaults: [
+ "authgraph_use_latest_hal_aidl_rust",
+ ],
+ rustlibs: [
+ "libandroid_logger",
+ "libauthgraph_core",
+ "libauthgraph_boringssl",
+ "libauthgraph_hal",
+ "libbinder_rs",
+ "liblibc",
+ "liblog_rust",
+ ],
+ srcs: [
+ "src/main.rs",
+ ],
+}
diff --git a/security/authgraph/default/authgraph.rc b/security/authgraph/default/authgraph.rc
new file mode 100644
index 0000000..0222994
--- /dev/null
+++ b/security/authgraph/default/authgraph.rc
@@ -0,0 +1,5 @@
+service vendor.authgraph /vendor/bin/hw/android.hardware.security.authgraph-service.nonsecure
+ interface aidl android.hardware.security.authgraph.IAuthGraph/nonsecure
+ class hal
+ user nobody
+ group nobody
diff --git a/security/authgraph/default/authgraph.xml b/security/authgraph/default/authgraph.xml
new file mode 100644
index 0000000..9529a0a
--- /dev/null
+++ b/security/authgraph/default/authgraph.xml
@@ -0,0 +1,10 @@
+<manifest version="1.0" type="device">
+ <hal format="aidl">
+ <name>android.hardware.security.authgraph</name>
+ <version>1</version>
+ <interface>
+ <name>IAuthGraphKeyExchange</name>
+ <instance>nonsecure</instance>
+ </interface>
+ </hal>
+</manifest>
diff --git a/security/authgraph/default/src/main.rs b/security/authgraph/default/src/main.rs
new file mode 100644
index 0000000..2112e58
--- /dev/null
+++ b/security/authgraph/default/src/main.rs
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+//! Default implementation of the AuthGraph key exchange HAL.
+//!
+//! This implementation of the HAL is only intended to allow testing and policy compliance. A real
+//! implementation of the AuthGraph HAL would be implemented in a secure environment, and would not
+//! be independently registered with service manager (a secure component that uses AuthGraph would
+//! expose an entrypoint that allowed retrieval of the specific IAuthGraphKeyExchange instance that
+//! is correlated with the component).
+
+use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph::{
+ Arc::Arc, IAuthGraphKeyExchange::BnAuthGraphKeyExchange,
+ IAuthGraphKeyExchange::IAuthGraphKeyExchange, Identity::Identity, KeInitResult::KeInitResult,
+ Key::Key, PubKey::PubKey, SessionIdSignature::SessionIdSignature, SessionInfo::SessionInfo,
+ SessionInitiationInfo::SessionInitiationInfo,
+};
+use authgraph_boringssl as boring;
+use authgraph_core::{key::MillisecondsSinceEpoch, keyexchange as ke, traits};
+use authgraph_hal::{err_to_binder, Innto, TryInnto};
+use log::{error, info};
+use std::ffi::CString;
+use std::sync::Mutex;
+
+static SERVICE_NAME: &str = "android.hardware.security.authgraph.IAuthGraphKeyExchange";
+static SERVICE_INSTANCE: &str = "nonsecure";
+
+/// Local error type for failures in the HAL service.
+#[derive(Debug, Clone)]
+struct HalServiceError(String);
+
+impl From<String> for HalServiceError {
+ fn from(s: String) -> Self {
+ Self(s)
+ }
+}
+
+fn main() {
+ if let Err(e) = inner_main() {
+ panic!("HAL service failed: {:?}", e);
+ }
+}
+
+fn inner_main() -> Result<(), HalServiceError> {
+ // Initialize Android logging.
+ android_logger::init_once(
+ android_logger::Config::default()
+ .with_tag("authgraph-hal-nonsecure")
+ .with_min_level(log::Level::Info)
+ .with_log_id(android_logger::LogId::System),
+ );
+ // Redirect panic messages to logcat.
+ std::panic::set_hook(Box::new(|panic_info| {
+ error!("{}", panic_info);
+ }));
+
+ info!("Insecure AuthGraph key exchange HAL service is starting.");
+
+ info!("Starting thread pool now.");
+ binder::ProcessState::start_thread_pool();
+
+ // Register the service
+ let service = AuthGraphService::new_as_binder();
+ let service_name = format!("{}/{}", SERVICE_NAME, SERVICE_INSTANCE);
+ binder::add_service(&service_name, service.as_binder()).map_err(|e| {
+ format!(
+ "Failed to register service {} because of {:?}.",
+ service_name, e
+ )
+ })?;
+
+ info!("Successfully registered AuthGraph HAL services.");
+ binder::ProcessState::join_thread_pool();
+ info!("AuthGraph HAL service is terminating."); // should not reach here
+ Ok(())
+}
+
+/// Non-secure implementation of the AuthGraph key exchange service.
+struct AuthGraphService {
+ imp: Mutex<traits::TraitImpl>,
+}
+
+impl AuthGraphService {
+ /// Create a new instance.
+ fn new() -> Self {
+ Self {
+ imp: Mutex::new(traits::TraitImpl {
+ aes_gcm: Box::new(boring::BoringAes),
+ ecdh: Box::new(boring::BoringEcDh),
+ ecdsa: Box::new(boring::BoringEcDsa),
+ hmac: Box::new(boring::BoringHmac),
+ hkdf: Box::new(boring::BoringHkdf),
+ sha256: Box::new(boring::BoringSha256),
+ rng: Box::new(boring::BoringRng),
+ device: Box::<boring::test_device::AgDevice>::default(),
+ clock: Some(Box::new(StdClock)),
+ }),
+ }
+ }
+
+ /// Create a new instance wrapped in a proxy object.
+ pub fn new_as_binder() -> binder::Strong<dyn IAuthGraphKeyExchange> {
+ BnAuthGraphKeyExchange::new_binder(Self::new(), binder::BinderFeatures::default())
+ }
+}
+
+impl binder::Interface for AuthGraphService {}
+
+/// Extract (and require) an unsigned public key as bytes from a [`PubKey`].
+fn unsigned_pub_key(pub_key: &PubKey) -> binder::Result<&[u8]> {
+ match pub_key {
+ PubKey::PlainKey(key) => Ok(&key.plainPubKey),
+ PubKey::SignedKey(_) => Err(binder::Status::new_exception(
+ binder::ExceptionCode::ILLEGAL_ARGUMENT,
+ Some(&CString::new("expected unsigned public key").unwrap()),
+ )),
+ }
+}
+
+/// This nonsecure implementation of the AuthGraph HAL interface directly calls the AuthGraph
+/// reference implementation library code; a real implementation requires the AuthGraph
+/// code to run in a secure environment, not within Android.
+impl IAuthGraphKeyExchange for AuthGraphService {
+ fn create(&self) -> binder::Result<SessionInitiationInfo> {
+ info!("create()");
+ let mut imp = self.imp.lock().unwrap();
+ let info = ke::create(&mut *imp).map_err(err_to_binder)?;
+ Ok(info.innto())
+ }
+ fn init(
+ &self,
+ peer_pub_key: &PubKey,
+ peer_id: &Identity,
+ peer_nonce: &[u8],
+ peer_version: i32,
+ ) -> binder::Result<KeInitResult> {
+ info!("init(v={peer_version})");
+ let mut imp = self.imp.lock().unwrap();
+ let peer_pub_key = unsigned_pub_key(peer_pub_key)?;
+ let result = ke::init(
+ &mut *imp,
+ peer_pub_key,
+ &peer_id.identity,
+ &peer_nonce,
+ peer_version,
+ )
+ .map_err(err_to_binder)?;
+ Ok(result.innto())
+ }
+
+ fn finish(
+ &self,
+ peer_pub_key: &PubKey,
+ peer_id: &Identity,
+ peer_signature: &SessionIdSignature,
+ peer_nonce: &[u8],
+ peer_version: i32,
+ own_key: &Key,
+ ) -> binder::Result<SessionInfo> {
+ info!("finish(v={peer_version})");
+ let mut imp = self.imp.lock().unwrap();
+ let peer_pub_key = unsigned_pub_key(peer_pub_key)?;
+ let own_key: Key = own_key.clone();
+ let own_key: authgraph_core::key::Key = own_key.try_innto()?;
+ let session_info = ke::finish(
+ &mut *imp,
+ peer_pub_key,
+ &peer_id.identity,
+ &peer_signature.signature,
+ &peer_nonce,
+ peer_version,
+ own_key,
+ )
+ .map_err(err_to_binder)?;
+ Ok(session_info.innto())
+ }
+
+ fn authenticationComplete(
+ &self,
+ peer_signature: &SessionIdSignature,
+ shared_keys: &[Arc; 2],
+ ) -> binder::Result<[Arc; 2]> {
+ info!("authComplete()");
+ let mut imp = self.imp.lock().unwrap();
+ let shared_keys = [shared_keys[0].arc.clone(), shared_keys[1].arc.clone()];
+ let arcs = ke::authentication_complete(&mut *imp, &peer_signature.signature, shared_keys)
+ .map_err(err_to_binder)?;
+ Ok(arcs.map(|arc| Arc { arc }))
+ }
+}
+
+/// Monotonic clock.
+#[derive(Default)]
+pub struct StdClock;
+
+impl traits::MonotonicClock for StdClock {
+ fn now(&self) -> authgraph_core::key::MillisecondsSinceEpoch {
+ let mut time = libc::timespec {
+ tv_sec: 0, // libc::time_t
+ tv_nsec: 0, // libc::c_long
+ };
+ let rc =
+ // Safety: `time` is a valid structure.
+ unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, &mut time as *mut libc::timespec) };
+ if rc < 0 {
+ log::warn!("failed to get time!");
+ return MillisecondsSinceEpoch(0);
+ }
+ // The types in `libc::timespec` may be different on different architectures,
+ // so allow conversion to `i64`.
+ #[allow(clippy::unnecessary_cast)]
+ MillisecondsSinceEpoch((time.tv_sec as i64 * 1000) + (time.tv_nsec as i64 / 1000 / 1000))
+ }
+}