gl_client/pairing/
attestation_device.rs

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        // Gather data to sign over.
96        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        // Sign data.
110        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        // Send approval.
127        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    /// Checks that a restriction string only contains a pubkey field exactly
179    /// once that is not preceded or followed by a '|' to ensure that it is
180    /// not part of an alternative but a restriction by itself.
181    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                // Check if 'pubkey=<pubkey>' is not preceded by '|'
186                if index > 0 && s.chars().nth(index - 1) == Some('|') {
187                    return false;
188                }
189
190                // Check if 'pubkey=<pubkey>' is not followed by '|'
191                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                // Check if 'pubkey=<pubkey>' appears exactly once
197                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        // Check with public key as session id.
222        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        // Check with different "pubkey" restriction than session id.
232        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        // Check with second "pubkey" in same alternative.
242        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        // Check with different public key as session id.
252        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}