sdkms/
client.rs

1/* Copyright (c) Fortanix, Inc.
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7use crate::api_model::*;
8use crate::operations::*;
9
10use headers::{ContentType, HeaderMap, HeaderMapExt, HeaderValue};
11use serde::{Deserialize, Serialize};
12use simple_hyper_client::blocking::Client as HttpClient;
13use simple_hyper_client::hyper::header::AUTHORIZATION;
14use simple_hyper_client::{Bytes, Method, StatusCode};
15use uuid::Uuid;
16
17use std::fmt;
18use std::io::Read;
19use std::marker::PhantomData;
20use std::sync::atomic::{AtomicU64, Ordering};
21use std::time::{SystemTime, UNIX_EPOCH};
22
23pub const DEFAULT_API_ENDPOINT: &'static str = "https://sdkms.fortanix.com";
24
25pub type Result<T> = ::std::result::Result<T, Error>;
26
27enum Auth {
28    Basic(String),
29    Bearer(String),
30}
31
32impl Auth {
33    fn from_api_key(api_key: &str) -> Self {
34        Auth::Basic(api_key.to_owned())
35    }
36
37    fn from_user_pass<T: fmt::Display>(username: T, password: &str) -> Self {
38        Auth::Basic(base64::encode(format!("{}:{}", username, password)))
39    }
40
41    fn format_header(&self) -> HeaderValue {
42        let value = match *self {
43            Auth::Basic(ref basic) => format!("Basic {}", basic),
44            Auth::Bearer(ref bearer) => format!("Bearer {}", bearer),
45        };
46        let bytes = Bytes::from(value);
47        // TODO: return error instead of expect
48        HeaderValue::from_maybe_shared(bytes).expect("invalid characters in auth header")
49    }
50}
51
52/// A builder for [`SdkmsClient`](./struct.SdkmsClient.html)
53pub struct SdkmsClientBuilder {
54    client: Option<HttpClient>,
55    api_endpoint: Option<String>,
56    auth: Option<Auth>,
57}
58
59impl SdkmsClientBuilder {
60    /// This can be used to customize the underlying HTTP client if desired.
61    pub fn with_http_client(mut self, client: HttpClient) -> Self {
62        self.client = Some(client);
63        self
64    }
65    /// This can be used to set the API endpoint. Otherwise the [default endpoint](./constant.DEFAULT_API_ENDPOINT.html) is used.
66    pub fn with_api_endpoint(mut self, api_endpoint: &str) -> Self {
67        self.api_endpoint = Some(api_endpoint.to_owned());
68        self
69    }
70    /// This can be used to make API calls without establishing a session.
71    /// The API key will be passed along as HTTP Basic auth header on all API calls.
72    pub fn with_api_key(mut self, api_key: &str) -> Self {
73        self.auth = Some(Auth::from_api_key(api_key));
74        self
75    }
76    /// This can be used to restore an established session.
77    pub fn with_access_token(mut self, access_token: &str) -> Self {
78        self.auth = Some(Auth::Bearer(access_token.to_owned()));
79        self
80    }
81    /// Build [`SdkmsClient`](./struct.SdkmsClient.html)
82    pub fn build(self) -> Result<SdkmsClient> {
83        let client = match self.client {
84            Some(client) => client,
85            None => {
86                #[cfg(feature = "native-tls")]
87                {
88                    use simple_hyper_client::HttpsConnector;
89                    use tokio_native_tls::native_tls::TlsConnector;
90
91                    let ssl = TlsConnector::new()?;
92                    let connector = HttpsConnector::new(ssl.into());
93                    HttpClient::with_connector(connector)
94                }
95                #[cfg(not(feature = "native-tls"))]
96                panic!("You should either provide an HTTP Client or compile this crate with native-tls feature");
97            }
98        };
99
100        Ok(SdkmsClient {
101            client,
102            api_endpoint: self
103                .api_endpoint
104                .unwrap_or_else(|| DEFAULT_API_ENDPOINT.to_owned()),
105            auth: self.auth,
106            last_used: AtomicU64::new(0),
107            auth_response: None,
108        })
109    }
110}
111
112/// A client session with SDKMS.
113///
114/// REST APIs are exposed as methods on this type. Communication with SDKMS API endpoint is protected with TLS and this
115/// type uses [`simple_hyper_client::blocking::Client`] along with [`tokio_native_tls::TlsConnector`] for HTTP/TLS.
116///
117/// When making crypto API calls using an API key, it is possible to pass the API key as an HTTP Basic Authorization
118/// header along with each request. This can be achieved by setting the API key using
119/// [`SdkmsClientBuilder::with_api_key()`]. Note that some features, e.g. transient keys, may not be available when
120/// using this authentication method. To be able to use such features, you can establish a session using any of the
121/// following methods:
122/// - [`authenticate_with_api_key()`](#method.authenticate_with_api_key)
123/// - [`authenticate_with_cert()`](#method.authenticate_with_cert)
124/// - [`authenticate_app()`](#method.authenticate_app)
125///
126/// Note that certain non-cryptographic APIs require a user session, which can be established using
127/// [`authenticate_user()`](#method.authenticate_user). This includes many APIs such as:
128/// - [`create_group()`](#method.create_group)
129/// - [`create_app()`](#method.create_app)
130/// - ...
131///
132/// Also note that a user session is generally not permitted to call crypto APIs. In case your current authorization
133/// is not appropriate for a particular API call, you'll get an error to that effect from SDKMS.
134///
135/// Certain APIs are "approvable", i.e. they can be subject to an approval policy. In such cases there are two methods
136/// on [`SdkmsClient`], e.g. [`encrypt()`] / [`request_approval_to_encrypt()`]. Whether or not you need to call
137/// [`request_approval_to_encrypt()`] depends on the approval policy that is applicable to the security object being
138/// used in your request. You can find out if a particular request is subject to an approval policy by first calling
139/// the regular API, e.g. [`encrypt()`] and checking if the response indicates that an approval request is needed at
140/// which point you can call [`request_approval_to_encrypt()`]. There is an example of how to do this in
141/// [the repository](https://github.com/fortanix/sdkms-client-rust/blob/master/examples/approval_request.rs).
142///
143/// [`simple_hyper_client::blocking::Client`]: https://docs.rs/simple-hyper-client/0.1.0/simple_hyper_client/blocking/struct.Client.html
144/// [`tokio_native_tls::TlsConnector`]: https://docs.rs/tokio-native-tls/0.3.0/tokio_native_tls/struct.TlsConnector.html
145/// [`SdkmsClientBuilder::with_api_key()`]: ./struct.SdkmsClientBuilder.html#method.with_api_key
146/// [`SdkmsClient`]: ./struct.SdkmsClient.html
147/// [`encrypt()`]: #method.encrypt
148/// [`request_approval_to_encrypt()`]: #method.request_approval_to_encrypt
149pub struct SdkmsClient {
150    auth: Option<Auth>,
151    api_endpoint: String,
152    client: HttpClient,
153    last_used: AtomicU64, // Time.0
154    auth_response: Option<AuthResponse>,
155}
156
157impl SdkmsClient {
158    pub fn builder() -> SdkmsClientBuilder {
159        SdkmsClientBuilder {
160            client: None,
161            api_endpoint: None,
162            auth: None,
163        }
164    }
165
166    fn authenticate(&self, auth: Option<&Auth>) -> Result<Self> {
167        let auth_response: AuthResponse = json_request_with_auth(
168            &self.client,
169            &self.api_endpoint,
170            Method::POST,
171            "/sys/v1/session/auth",
172            auth,
173            None::<&()>,
174        )?;
175        Ok(SdkmsClient {
176            client: self.client.clone(),
177            api_endpoint: self.api_endpoint.clone(),
178            auth: Some(Auth::Bearer(auth_response.access_token.clone())),
179            last_used: AtomicU64::new(now().0),
180            auth_response: Some(auth_response),
181        })
182    }
183
184    pub fn authenticate_with_api_key(&self, api_key: &str) -> Result<Self> {
185        self.authenticate(Some(Auth::from_api_key(api_key)).as_ref())
186    }
187
188    pub fn authenticate_with_cert(&self, app_id: Option<&Uuid>) -> Result<Self> {
189        self.authenticate(app_id.map(|id| Auth::from_user_pass(id, "")).as_ref())
190    }
191
192    pub fn authenticate_app(&self, app_id: &Uuid, app_secret: &str) -> Result<Self> {
193        self.authenticate(Some(Auth::from_user_pass(app_id, app_secret)).as_ref())
194    }
195
196    pub fn authenticate_user(&self, email: &str, password: &str) -> Result<Self> {
197        self.authenticate(Some(Auth::from_user_pass(email, password)).as_ref())
198    }
199
200    pub fn api_endpoint(&self) -> &str {
201        &self.api_endpoint
202    }
203
204    pub fn auth_response(&self) -> Option<&AuthResponse> {
205        self.auth_response.as_ref()
206    }
207
208    pub fn entity_id(&self) -> Option<Uuid> {
209        self.auth_response().map(|ar| ar.entity_id)
210    }
211
212    pub fn has_session(&self) -> bool {
213        match self.auth {
214            Some(Auth::Bearer(_)) => true,
215            _ => false,
216        }
217    }
218
219    fn json_request<E, D>(&self, method: Method, uri: &str, req: Option<&E>) -> Result<D>
220    where
221        E: Serialize,
222        D: for<'de> Deserialize<'de>,
223    {
224        let Self {
225            ref client,
226            ref api_endpoint,
227            ref auth,
228            ..
229        } = *self;
230        let result = json_request_with_auth(client, api_endpoint, method, uri, auth.as_ref(), req)?;
231        self.last_used.store(now().0, Ordering::Relaxed);
232        Ok(result)
233    }
234}
235
236impl Drop for SdkmsClient {
237    fn drop(&mut self) {
238        let _ = self.terminate();
239    }
240}
241
242impl SdkmsClient {
243    pub fn terminate(&mut self) -> Result<()> {
244        if let Some(Auth::Bearer(_)) = self.auth {
245            self.json_request(Method::POST, "/sys/v1/session/terminate", None::<&()>)?;
246            self.auth = None;
247        }
248        Ok(())
249    }
250
251    pub fn invoke_plugin_nice<I, O>(&self, id: &Uuid, req: &I) -> Result<O>
252    where
253        I: Serialize,
254        O: for<'de> Deserialize<'de>,
255    {
256        let req = serde_json::to_value(req)?;
257        let output = self.execute::<OperationInvokePlugin>(&req, (id,), None)?;
258        Ok(serde_json::from_value(output)?)
259    }
260
261    pub fn execute<O: Operation>(
262        &self,
263        body: &O::Body,
264        p: <O::PathParams as TupleRef>::Ref,
265        q: Option<&O::QueryParams>,
266    ) -> Result<O::Output> {
267        self.json_request(O::method(), &O::path(p, q), O::to_body(body).as_ref())
268    }
269
270    pub fn request_approval<O: Operation>(
271        &self,
272        body: &O::Body,
273        p: <O::PathParams as TupleRef>::Ref,
274        q: Option<&O::QueryParams>,
275        description: Option<String>,
276    ) -> Result<PendingApproval<O>> {
277        let request = self.create_approval_request(&ApprovalRequestRequest {
278            operation: Some(O::path(p, q)),
279            method: Some(format!("{}", O::method())),
280            body: O::to_body(body),
281            description,
282        })?;
283        Ok(PendingApproval::from_request_id(request.request_id))
284    }
285
286    pub fn expires_in(&self) -> Option<u64> {
287        let expires_at = self.last_used.load(Ordering::Relaxed)
288            + self.auth_response().map_or(0, |ar| ar.expires_in as u64);
289        expires_at.checked_sub(now().0)
290    }
291}
292
293pub struct PendingApproval<O: Operation>(Uuid, PhantomData<O>);
294
295impl<O: Operation> fmt::Debug for PendingApproval<O> {
296    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
297        fmt::Debug::fmt(&self.0, formatter)
298    }
299}
300
301impl<O: Operation> PendingApproval<O> {
302    pub fn from_request_id(request_id: Uuid) -> Self {
303        PendingApproval(request_id, PhantomData)
304    }
305
306    pub fn request_id(&self) -> Uuid {
307        self.0
308    }
309
310    pub fn get(&self, sdkms: &SdkmsClient) -> Result<ApprovalRequest> {
311        sdkms.get_approval_request(&self.0)
312    }
313
314    pub fn status(&self, sdkms: &SdkmsClient) -> Result<ApprovalStatus> {
315        Ok(self.get(sdkms)?.status)
316    }
317
318    pub fn result(&self, sdkms: &SdkmsClient) -> Result<Result<O::Output>> {
319        let result = sdkms.get_approval_request_result(&self.0)?;
320        Ok(if result.is_ok() {
321            serde_json::from_value::<O::Output>(result.body).map_err(Error::EncoderError)
322        } else {
323            let msg: String = serde_json::from_value(result.body).map_err(Error::EncoderError)?;
324            Err(Error::from_status(
325                StatusCode::from_u16(result.status).unwrap(),
326                msg,
327            ))
328        })
329    }
330}
331
332impl<O: Operation> Clone for PendingApproval<O> {
333    fn clone(&self) -> Self {
334        PendingApproval(self.0, PhantomData)
335    }
336}
337
338fn now() -> Time {
339    Time(
340        SystemTime::now()
341            .duration_since(UNIX_EPOCH)
342            .expect("Invalid system time")
343            .as_secs(),
344    )
345}
346
347fn json_decode_reader<R: Read, T: for<'de> Deserialize<'de>>(rdr: &mut R) -> serde_json::Result<T> {
348    match serde_json::from_reader(rdr) {
349        // When the body of the response is empty, attempt to deserialize null value instead
350        Err(ref e) if e.is_eof() && e.line() == 1 && e.column() == 0 => {
351            serde_json::from_value(serde_json::Value::Null)
352        }
353        v => v,
354    }
355}
356
357fn json_request_with_auth<E, D>(
358    client: &HttpClient,
359    api_endpoint: &str,
360    method: Method,
361    path: &str,
362    auth: Option<&Auth>,
363    body: Option<&E>,
364) -> Result<D>
365where
366    E: Serialize,
367    D: for<'de> Deserialize<'de>,
368{
369    let url = format!("{}{}", api_endpoint, path);
370    let mut req = client.request(method.clone(), &url)?;
371    let mut headers = HeaderMap::new();
372    if let Some(auth) = auth {
373        headers.insert(AUTHORIZATION, auth.format_header());
374    }
375    if let Some(request_body) = body {
376        headers.typed_insert(ContentType::json());
377        let body = serde_json::to_string(request_body).map_err(Error::EncoderError)?;
378        req = req.body(body);
379    }
380    req = req.headers(headers);
381    match req.send() {
382        Err(e) => {
383            info!("Error {} {}", method, url);
384            Err(Error::NetworkError(e))
385        }
386        Ok(ref mut res) if res.status().is_success() => {
387            info!("{} {} {}", res.status().as_u16(), method, url);
388            json_decode_reader(res.body_mut()).map_err(|err| Error::EncoderError(err))
389        }
390        Ok(ref mut res) => {
391            info!("{} {} {}", res.status().as_u16(), method, url);
392            let mut buffer = String::new();
393            res.body_mut()
394                .read_to_string(&mut buffer)
395                .map_err(|err| Error::IoError(err))?;
396            Err(Error::from_status(res.status(), buffer))
397        }
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    fn assert_send<T: Send>() {}
406    fn assert_sync<T: Sync>() {}
407
408    #[test]
409    fn client_is_send_and_sync() {
410        assert_send::<SdkmsClient>();
411        assert_sync::<SdkmsClient>();
412
413        assert_send::<SdkmsClientBuilder>();
414        assert_sync::<SdkmsClientBuilder>();
415    }
416}