sqrl_protocol/
client_request.rs

1//! All of the code needed for sending client requests to a SQRL server
2
3use crate::{
4    decode_public_key, decode_signature, encode_newline_data,
5    error::SqrlError,
6    get_or_error, parse_newline_data, parse_query_data,
7    server_response::{ServerResponse, TIFValue},
8    ProtocolVersion, Result, SqrlUrl, PROTOCOL_VERSIONS,
9};
10use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
11use ed25519_dalek::{Signature, VerifyingKey};
12use std::{collections::HashMap, convert::TryFrom, fmt, result, str::FromStr};
13
14// Keys used for encoding ClientRequest
15const CLIENT_PARAMETERS_KEY: &str = "client";
16const SERVER_DATA_KEY: &str = "server";
17const IDENTITY_SIGNATURE_KEY: &str = "ids";
18const PREVIOUS_IDENTITY_SIGNATURE_KEY: &str = "pids";
19const UNLOCK_REQUEST_SIGNATURE_KEY: &str = "urs";
20
21// Keys used for encoding ClientParameters
22const PROTOCOL_VERSION_KEY: &str = "ver";
23const COMMAND_KEY: &str = "cmd";
24const IDENTITY_KEY_KEY: &str = "idk";
25const OPTIONS_KEY: &str = "opt";
26const BUTTON_KEY: &str = "btn";
27const PREVIOUS_IDENTITY_KEY_KEY: &str = "pidk";
28const INDEX_SECRET_KEY: &str = "ins";
29const PREVIOUS_INDEX_SECRET_KEY: &str = "pins";
30const SERVER_UNLOCK_KEY_KEY: &str = "suk";
31const VERIFY_UNLOCK_KEY_KEY: &str = "vuk";
32
33/// A client request to a server
34pub struct ClientRequest {
35    /// The client parameters
36    pub client_params: ClientParameters,
37    /// The previous server response, or the sqrl url if the first request
38    pub server_data: ServerData,
39    /// The signature of this request (ids)
40    pub identity_signature: Signature,
41    /// The signature of this request using a previous identity (pids)
42    pub previous_identity_signature: Option<Signature>,
43    /// The unlock request signature for an identity unlock (urs)
44    pub unlock_request_signature: Option<String>,
45}
46
47impl ClientRequest {
48    /// Generate a new client request
49    pub fn new(
50        client_params: ClientParameters,
51        server_data: ServerData,
52        identity_signature: Signature,
53    ) -> Self {
54        ClientRequest {
55            client_params,
56            server_data,
57            identity_signature,
58            previous_identity_signature: None,
59            unlock_request_signature: None,
60        }
61    }
62
63    /// Parse a client request from a query string
64    pub fn from_query_string(query_string: &str) -> Result<Self> {
65        let map = parse_query_data(query_string)?;
66        let client_parameters_string = get_or_error(
67            &map,
68            CLIENT_PARAMETERS_KEY,
69            "Invalid client request: No client parameters",
70        )?;
71        let client_params = ClientParameters::from_base64(&client_parameters_string)?;
72        let server_string = get_or_error(
73            &map,
74            SERVER_DATA_KEY,
75            "Invalid client request: No server value",
76        )?;
77        let server_data = ServerData::from_base64(&server_string)?;
78        let ids_string = get_or_error(
79            &map,
80            IDENTITY_SIGNATURE_KEY,
81            "Invalid client request: No ids value",
82        )?;
83        let identity_signature = decode_signature(&ids_string)?;
84        let previous_identity_signature = match map.get(PREVIOUS_IDENTITY_SIGNATURE_KEY) {
85            Some(x) => Some(decode_signature(x)?),
86            None => None,
87        };
88
89        let unlock_request_signature = map.get(UNLOCK_REQUEST_SIGNATURE_KEY).map(|x| x.to_string());
90
91        Ok(ClientRequest {
92            client_params,
93            server_data,
94            identity_signature,
95            previous_identity_signature,
96            unlock_request_signature,
97        })
98    }
99
100    /// Convert a client request to the query string to add in the request
101    pub fn to_query_string(&self) -> String {
102        let mut result = format!(
103            "{}={}",
104            CLIENT_PARAMETERS_KEY,
105            self.client_params.to_base64()
106        );
107        result += &format!("&{}={}", SERVER_DATA_KEY, self.server_data);
108        result += &format!(
109            "&{}={}",
110            IDENTITY_SIGNATURE_KEY,
111            BASE64_URL_SAFE_NO_PAD.encode(self.identity_signature.to_bytes())
112        );
113
114        if let Some(pids) = &self.previous_identity_signature {
115            result += &format!(
116                "&{}={}",
117                PREVIOUS_IDENTITY_SIGNATURE_KEY,
118                BASE64_URL_SAFE_NO_PAD.encode(pids.to_bytes())
119            );
120        }
121        if let Some(urs) = &self.unlock_request_signature {
122            result += &format!(
123                "&{}={}",
124                UNLOCK_REQUEST_SIGNATURE_KEY,
125                BASE64_URL_SAFE_NO_PAD.encode(urs)
126            );
127        }
128
129        result
130    }
131
132    /// Get the portion of the client request that is signed
133    pub fn get_signed_string(&self) -> String {
134        format!(
135            "{}{}",
136            self.client_params.to_base64(),
137            &self.server_data.to_base64()
138        )
139    }
140
141    /// Validate that the values input in the client request are valid
142    pub fn validate(&self) -> Result<()> {
143        self.client_params.validate()?;
144
145        // If the pik is set the pids must also (and vice-versa)
146        if self.previous_identity_signature.is_some()
147            && self.client_params.previous_identity_key.is_none()
148        {
149            return Err(SqrlError::new(
150                "Previous identity signature set, but no previous identity key set".to_owned(),
151            ));
152        } else if self.previous_identity_signature.is_none()
153            && self.client_params.previous_identity_key.is_some()
154        {
155            return Err(SqrlError::new(
156                "Previous identity key set, but no previous identity signature".to_owned(),
157            ));
158        }
159
160        // If the enable or remove commands are set, the unlock request signature must also be set
161        if (self.client_params.command == ClientCommand::Enable
162            || self.client_params.command == ClientCommand::Remove)
163            && self.unlock_request_signature.is_none()
164        {
165            return Err(SqrlError::new(
166                "When attempting to enable identity, unlock request signature (urs) must be set"
167                    .to_owned(),
168            ));
169        }
170
171        match &self.server_data {
172            ServerData::ServerResponse {
173                server_response, ..
174            } if !server_response
175                .transaction_indication_flags
176                .contains(&TIFValue::CurrentIdMatch) =>
177            {
178                if self.client_params.server_unlock_key.is_none() {
179                    return Err(SqrlError::new("If attempting to re-enable identity (cmd=enable), must include server unlock key (suk)".to_owned()));
180                } else if self.client_params.verify_unlock_key.is_none() {
181                    return Err(SqrlError::new("If attempting to re-enable identity (cmd=enable), must include verify unlock key (vuk)".to_owned()));
182                }
183            }
184            _ => (),
185        }
186
187        Ok(())
188    }
189}
190
191/// Parameters used for sending requests to the client
192#[derive(Debug, PartialEq)]
193pub struct ClientParameters {
194    /// The supported protocol versions of the client (ver)
195    pub protocol_version: ProtocolVersion,
196    /// The client command requested to be performed (cmd)
197    pub command: ClientCommand,
198    /// The client identity used to sign the request (idk)
199    pub identity_key: VerifyingKey,
200    /// Optional options requested by the client (opt)
201    pub options: Option<Vec<ClientOption>>,
202    /// The button pressed in response to a server query (btn)
203    pub button: Option<u8>,
204    /// A previous client identity used to sign the request (pidk)
205    pub previous_identity_key: Option<VerifyingKey>,
206    /// The current identity indexed secret in response to a server query (ins)
207    pub index_secret: Option<String>,
208    /// The previous identity indexed secret in response to a server query (pins)
209    pub previous_index_secret: Option<String>,
210    /// The server unlock key used for unlocking an identity (suk)
211    pub server_unlock_key: Option<String>,
212    /// The verify unlock key used for unlocking an identity (vuk)
213    pub verify_unlock_key: Option<String>,
214}
215
216impl ClientParameters {
217    /// Create a new client parameter using the command and verifying key
218    pub fn new(command: ClientCommand, identity_key: VerifyingKey) -> ClientParameters {
219        ClientParameters {
220            protocol_version: ProtocolVersion::new(PROTOCOL_VERSIONS).unwrap(),
221            command,
222            identity_key,
223            options: None,
224            button: None,
225            previous_identity_key: None,
226            index_secret: None,
227            previous_index_secret: None,
228            server_unlock_key: None,
229            verify_unlock_key: None,
230        }
231    }
232
233    /// Parse a base64-encoded client parameter value
234    pub fn from_base64(base64_string: &str) -> Result<Self> {
235        let query_string = String::from_utf8(BASE64_URL_SAFE_NO_PAD.decode(base64_string)?)?;
236        Self::from_str(&query_string)
237    }
238
239    /// base64-encode this client parameter object
240    pub fn to_base64(&self) -> String {
241        BASE64_URL_SAFE_NO_PAD.encode(self.to_string().as_bytes())
242    }
243
244    /// Verify the client request is valid
245    pub fn validate(&self) -> Result<()> {
246        Ok(())
247    }
248}
249
250impl fmt::Display for ClientParameters {
251    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
252        let mut map = HashMap::<&str, &str>::new();
253        let protocol = self.protocol_version.to_string();
254        map.insert(PROTOCOL_VERSION_KEY, &protocol);
255        let command = self.command.to_string();
256        map.insert(COMMAND_KEY, &command);
257
258        let identity_key = BASE64_URL_SAFE_NO_PAD.encode(self.identity_key.as_bytes());
259        map.insert(IDENTITY_KEY_KEY, &identity_key);
260
261        let options_string: String;
262        if let Some(options) = &self.options {
263            options_string = ClientOption::to_option_string(options);
264            map.insert(OPTIONS_KEY, &options_string);
265        }
266        let button_string: String;
267        if let Some(button) = &self.button {
268            button_string = button.to_string();
269            map.insert(BUTTON_KEY, &button_string);
270        }
271        let previous_identity_key_string: String;
272        if let Some(previous_identity_key) = &self.previous_identity_key {
273            previous_identity_key_string =
274                BASE64_URL_SAFE_NO_PAD.encode(previous_identity_key.as_bytes());
275            map.insert(PREVIOUS_IDENTITY_KEY_KEY, &previous_identity_key_string);
276        }
277        if let Some(index_secret) = &self.index_secret {
278            map.insert(INDEX_SECRET_KEY, index_secret);
279        }
280        if let Some(previous_index_secret) = &self.previous_index_secret {
281            map.insert(PREVIOUS_INDEX_SECRET_KEY, previous_index_secret);
282        }
283        if let Some(server_unlock_key) = &self.server_unlock_key {
284            map.insert(SERVER_UNLOCK_KEY_KEY, server_unlock_key);
285        }
286        if let Some(verify_unlock_key) = &self.verify_unlock_key {
287            map.insert(VERIFY_UNLOCK_KEY_KEY, verify_unlock_key);
288        }
289
290        write!(f, "{}", &encode_newline_data(&map))
291    }
292}
293
294impl FromStr for ClientParameters {
295    type Err = SqrlError;
296
297    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
298        let map = parse_newline_data(s)?;
299        // Validate the protocol version is supported
300        let ver_string = get_or_error(
301            &map,
302            PROTOCOL_VERSION_KEY,
303            "Invalid client request: No version number",
304        )?;
305        let protocol_version = ProtocolVersion::new(&ver_string)?;
306
307        let cmd_string = get_or_error(&map, COMMAND_KEY, "Invalid client request: No cmd value")?;
308        let command = ClientCommand::from(cmd_string);
309        let idk_string = get_or_error(
310            &map,
311            IDENTITY_KEY_KEY,
312            "Invalid client request: No idk value",
313        )?;
314        let identity_key = decode_public_key(&idk_string)?;
315
316        let button = match map.get(BUTTON_KEY) {
317            Some(s) => match s.parse::<u8>() {
318                Ok(b) => Some(b),
319                Err(_) => {
320                    return Err(SqrlError::new(format!(
321                        "Invalid client request: Unable to parse btn {}",
322                        s
323                    )))
324                }
325            },
326            None => None,
327        };
328
329        let previous_identity_key = match map.get(PREVIOUS_IDENTITY_KEY_KEY) {
330            Some(x) => Some(decode_public_key(x)?),
331            None => None,
332        };
333
334        let options = match map.get(OPTIONS_KEY) {
335            Some(x) => Some(ClientOption::from_option_string(x)?),
336            None => None,
337        };
338
339        let index_secret = map.get(INDEX_SECRET_KEY).map(|x| x.to_string());
340        let previous_index_secret = map.get(PREVIOUS_INDEX_SECRET_KEY).map(|x| x.to_string());
341        let server_unlock_key = map.get(SERVER_UNLOCK_KEY_KEY).map(|x| x.to_string());
342        let verify_unlock_key = map.get(VERIFY_UNLOCK_KEY_KEY).map(|x| x.to_string());
343
344        Ok(ClientParameters {
345            protocol_version,
346            command,
347            identity_key,
348            options,
349            button,
350            previous_identity_key,
351            index_secret,
352            previous_index_secret,
353            server_unlock_key,
354            verify_unlock_key,
355        })
356    }
357}
358
359/// The commands a client can request of the server
360#[derive(Debug, PartialEq)]
361pub enum ClientCommand {
362    /// A query to determine which client identity the server knows
363    Query,
364    /// A request to verify and accept the client's identity assertion
365    Ident,
366    /// A request to disable the client identity on the server
367    Disable,
368    /// A request to re-enable the client identity on the server
369    Enable,
370    /// A request to remove the client identity from the server
371    Remove,
372}
373
374impl fmt::Display for ClientCommand {
375    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
376        match self {
377            ClientCommand::Query => write!(f, "query"),
378            ClientCommand::Ident => write!(f, "ident"),
379            ClientCommand::Disable => write!(f, "disable"),
380            ClientCommand::Enable => write!(f, "enable"),
381            ClientCommand::Remove => write!(f, "remove"),
382        }
383    }
384}
385
386impl From<String> for ClientCommand {
387    fn from(value: String) -> Self {
388        match value.as_str() {
389            "query" => ClientCommand::Query,
390            "ident" => ClientCommand::Ident,
391            "disable" => ClientCommand::Disable,
392            "enable" => ClientCommand::Enable,
393            "remove" => ClientCommand::Remove,
394            _ => panic!("Not this!"),
395        }
396    }
397}
398
399/// Request options included in a client request
400#[derive(Debug, PartialEq)]
401pub enum ClientOption {
402    /// A request to the server to not restrict client requests from only the
403    /// ip address that initially queried the server
404    NoIPTest,
405    /// A request to the server to only allow SQRL auth for authentication
406    SQRLOnly,
407    /// A request to the server to not allow side-channel auth change requests
408    /// e.g. email, backup code, etc.
409    Hardlock,
410    /// An option to inform the server that the SQRL client has a secure method
411    /// of sending data back to the client's web browser
412    ClientProvidedSession,
413    /// A request to the server to return the client identity's server unlock
414    /// key
415    ServerUnlockKey,
416}
417
418impl ClientOption {
419    fn from_option_string(opt: &str) -> Result<Vec<Self>> {
420        let mut options: Vec<ClientOption> = Vec::new();
421        for option in opt.split('~') {
422            options.push(ClientOption::try_from(option)?)
423        }
424
425        Ok(options)
426    }
427
428    fn to_option_string(opt: &Vec<Self>) -> String {
429        let mut options = "".to_owned();
430        for option in opt {
431            if options.is_empty() {
432                options += &format!("{}", option);
433            } else {
434                options += &format!("~{}", option);
435            }
436        }
437
438        options
439    }
440}
441
442impl fmt::Display for ClientOption {
443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444        match self {
445            ClientOption::NoIPTest => write!(f, "noiptest"),
446            ClientOption::SQRLOnly => write!(f, "sqrlonly"),
447            ClientOption::Hardlock => write!(f, "hardlock"),
448            ClientOption::ClientProvidedSession => write!(f, "cps"),
449            ClientOption::ServerUnlockKey => write!(f, "suk"),
450        }
451    }
452}
453
454impl TryFrom<&str> for ClientOption {
455    type Error = SqrlError;
456
457    fn try_from(value: &str) -> Result<Self> {
458        match value {
459            "noiptest" => Ok(ClientOption::NoIPTest),
460            "sqrlonly" => Ok(ClientOption::SQRLOnly),
461            "hardlock" => Ok(ClientOption::Hardlock),
462            "cps" => Ok(ClientOption::ClientProvidedSession),
463            "suk" => Ok(ClientOption::ServerUnlockKey),
464            _ => Err(SqrlError::new(format!("Invalid client option {}", value))),
465        }
466    }
467}
468
469/// The previous server response to add to the next client request, or the
470/// SQRL url for the first request
471#[derive(Debug, PartialEq)]
472pub enum ServerData {
473    /// During the first request sent to a server, the server data is set as
474    /// the first SQRL protocol url used to auth against the server
475    Url {
476        /// The first SQRL url called
477        url: SqrlUrl,
478    },
479    /// Any request after the first one includes the server response to the
480    /// previous client request
481    ServerResponse {
482        /// The parsed previous response to the client's request
483        server_response: ServerResponse,
484        /// The original previous response to the client's request
485        original_response: String,
486    },
487}
488
489impl ServerData {
490    /// Parse the base64-encoded server data
491    pub fn from_base64(base64_string: &str) -> Result<Self> {
492        let data = String::from_utf8(BASE64_URL_SAFE_NO_PAD.decode(base64_string)?)?;
493        if let Ok(parsed) = SqrlUrl::parse(&data) {
494            return Ok(ServerData::Url { url: parsed });
495        }
496
497        match ServerResponse::from_str(&data) {
498            Ok(server_response) => Ok(ServerData::ServerResponse {
499                server_response,
500                original_response: base64_string.to_owned(),
501            }),
502            Err(_) => Err(SqrlError::new(format!("Invalid server data: {}", &data))),
503        }
504    }
505
506    /// base64-encode the server data
507    pub fn to_base64(&self) -> String {
508        match self {
509            ServerData::Url { url } => BASE64_URL_SAFE_NO_PAD.encode(url.to_string().as_bytes()),
510            ServerData::ServerResponse {
511                original_response, ..
512            } => original_response.clone(),
513        }
514    }
515}
516
517impl fmt::Display for ServerData {
518    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
519        match self {
520            ServerData::Url { url } => {
521                write!(f, "{}", url)
522            }
523            ServerData::ServerResponse {
524                original_response, ..
525            } => {
526                write!(f, "{}", &original_response)
527            }
528        }
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535
536    const TEST_CLIENT_REQUEST: &str = "client=dmVyPTENCmNtZD1xdWVyeQ0KaWRrPWlnZ2N1X2UtdFdxM3NvZ2FhMmFBRENzeFJaRUQ5b245SDcxNlRBeVBSMHcNCnBpZGs9RTZRczJnWDdXLVB3aTlZM0tBbWJrdVlqTFNXWEN0S3lCY3ltV2xvSEF1bw0Kb3B0PWNwc35zdWsNCg&server=c3FybDovL3Nxcmwuc3RldmUuY29tL2NsaS5zcXJsP3g9MSZudXQ9ZTd3ZTZ3Q3RvU3hsJmNhbj1hSFIwY0hNNkx5OXNiMk5oYkdodmMzUXZaR1Z0Ynk1MFpYTjA&ids=hcXWTPx3EgP9R_AjtoCIrie_YgZxVD72nd5_pjMOnhUEYmhdjLUYs3jjcJT_GQuzNKXyAwY1ns1R6QJn1YKzCA";
537    const TEST_CLIENT_PARAMS: &str = "dmVyPTENCmNtZD1xdWVyeQ0KaWRrPWlnZ2N1X2UtdFdxM3NvZ2FhMmFBRENzeFJaRUQ5b245SDcxNlRBeVBSMHcNCnBpZGs9RTZRczJnWDdXLVB3aTlZM0tBbWJrdVlqTFNXWEN0S3lCY3ltV2xvSEF1bw0Kb3B0PWNwc35zdWsNCg";
538    const TEST_SERVER_RESPONSE: &str = "dmVyPTENCm51dD0xV005bGZGMVNULXoNCnRpZj01DQpxcnk9L2NsaS5zcXJsP251dD0xV005bGZGMVNULXoNCnN1az1CTUZEbTdiUGxzUW9qdUpzb0RUdmxTMU1jbndnU2N2a3RGODR2TGpzY0drDQo";
539    const TEST_SQRL_URL: &str = "c3FybDovL3Rlc3R1cmwuY29t";
540    const TEST_INVALID_URL: &str = "aHR0cHM6Ly9nb29nbGUuY29t";
541
542    #[test]
543    fn client_request_validate_example() {
544        ClientRequest::from_query_string(TEST_CLIENT_REQUEST).unwrap();
545    }
546
547    #[test]
548    fn client_parameters_encode_decode() {
549        let mut params = ClientParameters::new(
550            ClientCommand::Query,
551            decode_public_key("iggcu_e-tWq3sogaa2aADCsxRZED9on9H716TAyPR0w").unwrap(),
552        );
553        params.previous_identity_key =
554            Some(decode_public_key("E6Qs2gX7W-Pwi9Y3KAmbkuYjLSWXCtKyBcymWloHAuo").unwrap());
555        params.options = Some(vec![
556            ClientOption::ClientProvidedSession,
557            ClientOption::ServerUnlockKey,
558        ]);
559
560        let decoded = ClientParameters::from_base64(&params.to_base64()).unwrap();
561        assert_eq!(params, decoded);
562    }
563
564    #[test]
565    fn client_parameters_decode_example() {
566        let client_parameters = ClientParameters::from_base64(TEST_CLIENT_PARAMS).unwrap();
567
568        assert_eq!(client_parameters.protocol_version.to_string(), "1");
569        assert_eq!(client_parameters.command, ClientCommand::Query);
570        assert_eq!(
571            BASE64_URL_SAFE_NO_PAD.encode(client_parameters.identity_key.as_bytes()),
572            "iggcu_e-tWq3sogaa2aADCsxRZED9on9H716TAyPR0w"
573        );
574        match &client_parameters.previous_identity_key {
575            Some(s) => assert_eq!(
576                BASE64_URL_SAFE_NO_PAD.encode(s.as_bytes()),
577                "E6Qs2gX7W-Pwi9Y3KAmbkuYjLSWXCtKyBcymWloHAuo"
578            ),
579            None => panic!(),
580        }
581        match &client_parameters.options {
582            Some(s) => assert_eq!(
583                s,
584                &vec![
585                    ClientOption::ClientProvidedSession,
586                    ClientOption::ServerUnlockKey
587                ]
588            ),
589            None => panic!(),
590        }
591    }
592
593    #[test]
594    fn server_data_parse_sqrl_url() {
595        let data = ServerData::from_base64(TEST_SQRL_URL).unwrap();
596        match data {
597            ServerData::Url { url } => assert_eq!(url.to_string(), "sqrl://testurl.com"),
598            ServerData::ServerResponse { .. } => {
599                panic!("Did not expect a ServerResponse");
600            }
601        };
602    }
603
604    #[test]
605    fn server_data_parse_nonsqrl_url() {
606        let result = ServerData::from_base64(TEST_INVALID_URL);
607        if result.is_ok() {
608            panic!("Got back a real result");
609        }
610    }
611
612    #[test]
613    fn server_data_parse_server_data() {
614        let data = ServerData::from_base64(TEST_SERVER_RESPONSE).unwrap();
615        match data {
616            ServerData::Url { url: _ } => panic!("Did not expect a url"),
617            ServerData::ServerResponse {
618                server_response,
619                original_response,
620                ..
621            } => {
622                assert_eq!(server_response.nut, "1WM9lfF1ST-z");
623                assert_eq!(original_response, TEST_SERVER_RESPONSE);
624            }
625        };
626    }
627}