1use std::time::{SystemTime, UNIX_EPOCH};
2
3use super::{into_approve_pairing_error, into_verify_pairing_data_error, Error};
4use crate::{
5 credentials::{NodeIdProvider, RuneProvider, TlsConfigProvider},
6 pb::{
7 self,
8 scheduler::{
9 pairing_client::PairingClient, ApprovePairingRequest, GetPairingDataRequest,
10 GetPairingDataResponse,
11 },
12 },
13};
14use bytes::BufMut as _;
15use picky::{pem::Pem, x509::Csr};
16use picky_asn1_x509::{PublicKey, SubjectPublicKeyInfo};
17use ring::{
18 rand,
19 signature::{self, EcdsaKeyPair, KeyPair},
20};
21use rustls_pemfile as pemfile;
22use tonic::transport::Channel;
23
24type Result<T, E = super::Error> = core::result::Result<T, E>;
25
26pub struct Connected(PairingClient<Channel>);
27pub struct Unconnected();
28
29pub struct Client<T, C: TlsConfigProvider + RuneProvider + NodeIdProvider> {
30 inner: T,
31 uri: String,
32 creds: C,
33}
34
35impl<C: TlsConfigProvider + RuneProvider + NodeIdProvider> Client<Unconnected, C> {
36 pub fn new(creds: C) -> Result<Client<Unconnected, C>> {
37 Ok(Self {
38 inner: Unconnected(),
39 uri: crate::utils::scheduler_uri(),
40 creds,
41 })
42 }
43
44 pub fn with_uri(mut self, uri: String) -> Client<Unconnected, C> {
45 self.uri = uri;
46 self
47 }
48
49 pub async fn connect(self) -> Result<Client<Connected, C>> {
50 let tls = self.creds.tls_config();
51 let channel = tonic::transport::Endpoint::from_shared(self.uri.clone())?
52 .tls_config(tls.inner)?
53 .tcp_keepalive(Some(crate::TCP_KEEPALIVE))
54 .http2_keep_alive_interval(crate::TCP_KEEPALIVE)
55 .keep_alive_timeout(crate::TCP_KEEPALIVE_TIMEOUT)
56 .keep_alive_while_idle(true)
57 .connect_lazy();
58
59 let inner = PairingClient::new(channel);
60
61 Ok(Client {
62 inner: Connected(inner),
63 uri: self.uri,
64 creds: self.creds,
65 })
66 }
67}
68
69impl<C: TlsConfigProvider + RuneProvider + NodeIdProvider> Client<Connected, C> {
70 pub async fn get_pairing_data(&self, device_id: &str) -> Result<GetPairingDataResponse> {
71 Ok(self
72 .inner
73 .0
74 .clone()
75 .get_pairing_data(GetPairingDataRequest {
76 device_id: device_id.to_string(),
77 })
78 .await?
79 .into_inner())
80 }
81
82 pub async fn approve_pairing(
83 &self,
84 device_id: &str,
85 device_name: &str,
86 restrs: &str,
87 ) -> Result<pb::greenlight::Empty> {
88 let timestamp = SystemTime::now()
89 .duration_since(UNIX_EPOCH)
90 .map_err(into_approve_pairing_error)?
91 .as_secs();
92
93 let node_id = self.creds.node_id()?;
94
95 let mut buf = vec![];
97 buf.put(device_id.as_bytes());
98 buf.put_u64(timestamp);
99 buf.put(&node_id[..]);
100 buf.put(device_name.as_bytes());
101 buf.put(restrs.as_bytes());
102
103 let tls = self.creds.tls_config();
104 let tls_key = tls
105 .clone()
106 .private_key
107 .ok_or(Error::BuildClientError("empty tls private key".to_string()))?;
108
109 let key = {
111 let mut key = std::io::Cursor::new(&tls_key);
112 pemfile::pkcs8_private_keys(&mut key)
113 .map_err(into_approve_pairing_error)?
114 .remove(0)
115 };
116 let kp =
117 EcdsaKeyPair::from_pkcs8(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key.as_ref())
118 .map_err(into_approve_pairing_error)?;
119 let rng = rand::SystemRandom::new();
120 let sig = kp
121 .sign(&rng, &buf)
122 .map_err(into_approve_pairing_error)?
123 .as_ref()
124 .to_vec();
125
126 Ok(self
128 .inner
129 .0
130 .clone()
131 .approve_pairing(ApprovePairingRequest {
132 device_id: device_id.to_string(),
133 timestamp,
134 device_name: device_name.to_string(),
135 restrictions: restrs.to_string(),
136 sig: sig,
137 rune: self.creds.rune(),
138 pubkey: kp.public_key().as_ref().to_vec(),
139 })
140 .await?
141 .into_inner())
142 }
143
144 pub fn verify_pairing_data(data: GetPairingDataResponse) -> Result<()> {
145 let mut crs = std::io::Cursor::new(&data.csr);
146 let pem = Pem::read_from(&mut crs).map_err(into_verify_pairing_data_error)?;
147 let csr = Csr::from_pem(&pem).map_err(into_verify_pairing_data_error)?;
148 let sub_pk_der = csr
149 .public_key()
150 .to_der()
151 .map_err(into_verify_pairing_data_error)?;
152 let sub_pk_info: SubjectPublicKeyInfo =
153 picky_asn1_der::from_bytes(&sub_pk_der).map_err(into_verify_pairing_data_error)?;
154
155 if let PublicKey::Ec(bs) = sub_pk_info.subject_public_key {
156 let pk = hex::encode(bs.0.payload_view());
157
158 if pk == data.device_id
159 && Self::restriction_contains_pubkey_exactly_once(
160 &data.restrictions,
161 &data.device_id,
162 )
163 {
164 Ok(())
165 } else {
166 Err(Error::VerifyPairingDataError(format!(
167 "device id {} does not match public key {}",
168 data.device_id, pk
169 )))
170 }
171 } else {
172 Err(Error::VerifyPairingDataError(format!(
173 "public key is not ecdsa"
174 )))
175 }
176 }
177
178 fn restriction_contains_pubkey_exactly_once(s: &str, pubkey: &str) -> bool {
182 let search_field = format!("pubkey={}", pubkey);
183 match s.find(&search_field) {
184 Some(index) => {
185 if index > 0 && s.chars().nth(index - 1) == Some('|') {
187 return false;
188 }
189
190 let end_index = index + search_field.len();
192 if end_index < s.len() && s.chars().nth(end_index) == Some('|') {
193 return false;
194 }
195
196 s.matches(&search_field).count() == 1
198 }
199 None => false,
200 }
201 }
202}
203
204#[cfg(test)]
205pub mod tests {
206 use super::*;
207 use crate::{credentials, tls};
208
209 #[test]
210 fn test_verify_pairing_data() {
211 let kp = tls::generate_ecdsa_key_pair();
212 let device_cert = tls::generate_self_signed_device_cert(
213 &hex::encode("00"),
214 "my-device",
215 vec!["localhost".into()],
216 Some(kp),
217 );
218 let csr = device_cert.serialize_request_pem().unwrap();
219 let pk = hex::encode(device_cert.get_key_pair().public_key_raw());
220
221 let pd = GetPairingDataResponse {
223 device_id: pk.clone(),
224 csr: csr.clone().into_bytes(),
225 device_name: "my-device".to_string(),
226 description: "".to_string(),
227 restrictions: format!("pubkey={}", pk.clone()),
228 };
229 assert!(Client::<Connected, credentials::Device>::verify_pairing_data(pd).is_ok());
230
231 let pd = GetPairingDataResponse {
233 device_id: pk.clone(),
234 csr: csr.clone().into_bytes(),
235 device_name: "my-device".to_string(),
236 description: "".to_string(),
237 restrictions: format!("pubkey={}", "02000000"),
238 };
239 assert!(Client::<Connected, credentials::Device>::verify_pairing_data(pd).is_err());
240
241 let pd = GetPairingDataResponse {
243 device_id: pk.clone(),
244 csr: csr.clone().into_bytes(),
245 device_name: "my-device".to_string(),
246 description: "".to_string(),
247 restrictions: format!("pubkey={}|pubkey=02000000", pk),
248 };
249 assert!(Client::<Connected, credentials::Device>::verify_pairing_data(pd).is_err());
250
251 let pd = GetPairingDataResponse {
253 device_id: "00".to_string(),
254 csr: csr.into_bytes(),
255 device_name: "my-device".to_string(),
256 description: "".to_string(),
257 restrictions: format!("pubkey={}", pk.clone()),
258 };
259 assert!(Client::<Connected, credentials::Device>::verify_pairing_data(pd).is_err());
260 }
261}