#![cfg_attr(docsrs, doc(cfg(feature = "std")))] use crate::errors::Error; #[cfg(feature = "paserk")] use crate::paserk::{FormatAsPaserk, Id}; use regex::Regex; use serde_json::Value; use std::collections::HashMap; #[derive(Debug, PartialEq, Eq, Clone)] /// A footer with optional claims that are JSON-encoded. pub struct Footer { list_of: HashMap, max_keys: usize, max_len: usize, } impl Default for Footer { fn default() -> Self { Self::new() } } impl Footer { /// Keys for registered claims in the footer, that are reserved for usage by PASETO in top-level. pub const REGISTERED_CLAIMS: [&'static str; 2] = ["kid", "wpk"]; /// All PASERK types that are (implemented in this library) unsafe in the footer. pub const DISALLOWED_FOOTER: [&'static str; 8] = [ "k2.local.", "k4.local.", "k2.secret.", "k3.secret.", "k4.secret.", "k2.public.", "k3.public.", "k4.public.", ]; /// See [PASETO docs] for the reason behind this limit. /// /// Maximum number of named keys within an object. /// /// [PASETO docs]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-depth-without-parsing-the-json-string pub const DEFAULT_MAX_KEYS: usize = 512; /// See [PASETO docs] for the reason behind this limit. /// /// Maximum length of the JSON-encoded string. /// /// [PASETO docs]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-depth-without-parsing-the-json-string pub const DEFAULT_MAX_LEN: usize = 8192; /// See [PASETO docs] for the reason behind this limit. /// /// This value has been set by `serde_json` and cannot be changed. /// /// [PASETO docs]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-depth-without-parsing-the-json-string pub const MAX_RECURSION_DEPTH: usize = 128; /// Create a new `Footer` instance. pub fn new() -> Self { Self { list_of: HashMap::new(), max_keys: Self::DEFAULT_MAX_KEYS, max_len: Self::DEFAULT_MAX_LEN, } } /// Change the default (512) amount of maximum number of named keys within an object. /// /// __NOTE__: There should be no need to change this if you don't know this is a specific problem for you. pub fn max_keys(&mut self, max_keys: usize) { self.max_keys = max_keys; } /// Change the default (8192) amount of maximum number of named keys within an object. /// /// __NOTE__: There should be no need to change this if you don't know this is a specific problem for you. pub fn max_len(&mut self, max_len: usize) { self.max_len = max_len; } /// Add additional claims. If `claim` already exists, it is replaced with the new. /// /// Errors: /// - `claim` is a reserved claim (see [`Self::REGISTERED_CLAIMS`]) /// - `value` is any of (starts with) the disallowed PASERK types (see [`Self::DISALLOWED_FOOTER`]). pub fn add_additional(&mut self, claim: &str, value: &str) -> Result<(), Error> { for unsafe_value in Self::DISALLOWED_FOOTER { if value.starts_with(unsafe_value) { return Err(Error::InvalidClaim); } } if !Self::REGISTERED_CLAIMS.contains(&claim) { self.list_of.insert(claim.into(), value.into()); Ok(()) } else { Err(Error::InvalidClaim) } } /// Checks whether a specific claim has been added to the list. /// /// E.g `contains_claim("kid") == true` if `kid` has been added before. pub fn contains_claim(&self, claim: &str) -> bool { self.list_of.contains_key(claim) } /// Return Some(claim value) if claims list contains the `claim`. /// None otherwise. pub fn get_claim(&self, claim: &str) -> Option<&Value> { self.list_of.get(claim) } #[cfg(feature = "paserk")] /// Set the `kid` claim. If it already exists, replace it with the new. pub fn key_id(&mut self, id: &Id) { let mut paserk_kid = String::new(); id.fmt(&mut paserk_kid).unwrap(); self.list_of.insert("kid".into(), paserk_kid.into()); } /// Attempt to create `Footer` from a sequence of bytes. /// /// Errors: /// - `bytes` contains non-UTF-8 sequences /// - `bytes` does not decode as valid JSON /// - `bytes` top-most JSON object does not decode to a map /// - if any registered claims exist and they are not a `String` /// - Parsing JSON maps and arrays that are more than 128 layers deep /// - Maximum number of named keys is exceeded /// - Maximum JSON-encoded string length is exceeded pub fn parse_bytes(&mut self, bytes: &[u8]) -> Result<(), Error> { let input = bytes.to_vec(); self.parse_string(&String::from_utf8(input).map_err(|_| Error::FooterParsing)?) } /// Attempt to parse a `Footer` from a string. /// /// Errors: /// - `string` does not decode as valid JSON /// - `string` top-most JSON object does not decode to a map /// - if any registered claims exist and they are not a `String` /// - Parsing JSON maps and arrays that are more than 128 layers deep /// - Maximum number of named keys is exceeded /// - Maximum JSON-encoded string length is exceeded pub fn parse_string(&mut self, string: &str) -> Result<(), Error> { if string.len() > self.max_len { return Err(Error::FooterParsing); } if Regex::new(r#"[^\\]":"#).unwrap().find_iter(string).count() > self.max_keys { return Err(Error::FooterParsing); } self.list_of = serde_json::from_str(string).map_err(|_| Error::FooterParsing)?; Ok(()) } /// Return the JSON serialized representation of `Self`. /// /// Errors: /// - `self` cannot be serialized as JSON pub fn to_string(&self) -> Result { match serde_json::to_string(&self.list_of) { Ok(ret) => Ok(ret), Err(_) => Err(Error::FooterParsing), } } } #[cfg(test)] mod tests { use crate::footer::Footer; use regex::Regex; #[test] fn test_count_keys() { // https://www.rustescaper.com/ let string = r#""name": "3-S-2", "expect-fail": false, "public-key": "02fbcb7c69ee1c60579be7a334134878d9c5c5bf35d552dab63c0140397ed14cef637d7720925c44699ea30e72874c72fb", "secret-key": "20347609607477aca8fbfbc5e6218455f3199669792ef8b466faa87bdc67798144c848dd03661eed5ac62461340cea96", "secret-key-pem": "-----BEGIN EC PRIVATE KEY-----nMIGkAgEBBDAgNHYJYHR3rKj7+8XmIYRV8xmWaXku+LRm+qh73Gd5gUTISN0DZh7tnWsYkYTQM6pagBwYFK4EEACKhZANiAAT7y3xp7hxgV5vnozQTSHjZxcW/NdVS2rY8nAUA5ftFM72N9dyCSXERpnqMOcodMcvt8kgcrB8KcKee0HU23E79/s4CvEs8hBfnjnSUd/gcAm08EjSIz06iWjrNy4NakxR3I=n-----END EC PRIVATE KEY-----", "public-key-pem": "-----BEGIN PUBLIC KEY-----nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+8t8ae4cYFeb56M0E0h42cXFvzXVUtq2nPAFAOX7RTO9jfXcgklxEaZ6jDnKHTHL7fJIHKwfCnCnntB1NtxO/f7OArxLPIQX5n40lHf4HAJtPBI0iM9Oolo6zcuDWpMUdyn-----END PUBLIC KEY-----", "token": "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9ZWrbGZ6L0MDK72skosUaS0Dz7wJ_2bMcM6tOxFuCasO9GhwHrvvchqgXQNLQQyWzGC2wkr-VKII71AvkLpC8tJOrzJV1cap9NRwoFzbcXjzMZyxQ0wkshxZxx8ImmNWP.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", "payload": "{"data":"this is a signed message","exp":"2022-01-01T00:00:00+00:00"}", "footer": "{"kid":"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn"}", "implicit-assertion": """#; assert_eq!( Regex::new(r#"[^\\]":"#).unwrap().find_iter(string).count(), 13 ); } #[test] fn err_on_max_keys() { let mut footer = Footer::default(); for n in 1..=11 { footer .add_additional(format!("{}", n).as_str(), "test") .unwrap(); } let mut footer_parse = Footer::default(); footer_parse.max_keys(10); assert!(footer_parse .parse_bytes(footer.to_string().unwrap().as_bytes()) .is_err()); } #[test] fn err_on_max_len() { let mut footer = Footer::new(); for n in 1..=11 { footer .add_additional(format!("{}", n).as_str(), "test") .unwrap(); } let ser_footer = footer.to_string().unwrap(); let mut footer_parse = Footer::new(); footer_parse.max_len(ser_footer.len() - 1); assert!(footer_parse.parse_bytes(ser_footer.as_bytes()).is_err()); } #[test] fn err_on_custom_with_registered() { let mut footer = Footer::new(); assert!(footer.add_additional("wpk", "test").is_err()); assert!(footer.add_additional("kid", "test").is_err()); assert!(footer.add_additional("custom", "test").is_ok()); } #[test] #[cfg(all(feature = "paserk", feature = "v2", feature = "v3", feature = "v4"))] fn err_on_disallowed_in_footer() { use crate::keys::{AsymmetricKeyPair, Generate, SymmetricKey}; use crate::paserk::FormatAsPaserk; use crate::version2::V2; use crate::version3::V3; use crate::version4::V4; let mut footer = Footer::new(); let kpv2 = AsymmetricKeyPair::::generate().unwrap(); let kpv3 = AsymmetricKeyPair::::generate().unwrap(); let kpv4 = AsymmetricKeyPair::::generate().unwrap(); let skv2 = SymmetricKey::::generate().unwrap(); let skv4 = SymmetricKey::::generate().unwrap(); let mut buf = String::new(); kpv2.secret.fmt(&mut buf).unwrap(); assert!(footer.add_additional("wpk", &buf).is_err()); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_err()); let mut buf = String::new(); kpv2.public.fmt(&mut buf).unwrap(); assert!(footer.add_additional("wpk", &buf).is_err()); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_err()); let mut buf = String::new(); kpv3.secret.fmt(&mut buf).unwrap(); assert!(footer.add_additional("wpk", &buf).is_err()); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_err()); let mut buf = String::new(); kpv3.public.fmt(&mut buf).unwrap(); assert!(footer.add_additional("wpk", &buf).is_err()); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_err()); let mut buf = String::new(); kpv4.secret.fmt(&mut buf).unwrap(); assert!(footer.add_additional("wpk", &buf).is_err()); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_err()); let mut buf = String::new(); kpv4.public.fmt(&mut buf).unwrap(); assert!(footer.add_additional("wpk", &buf).is_err()); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_err()); let mut buf = String::new(); skv2.fmt(&mut buf).unwrap(); assert!(footer.add_additional("wpk", &buf).is_err()); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_err()); let mut buf = String::new(); skv4.fmt(&mut buf).unwrap(); assert!(footer.add_additional("wpk", &buf).is_err()); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_err()); } #[test] #[cfg(all(feature = "paserk", feature = "v2", feature = "v3", feature = "v4"))] fn kid_in_footer() { use crate::keys::{AsymmetricKeyPair, Generate, SymmetricKey}; use crate::paserk::{FormatAsPaserk, Id}; use crate::version2::V2; use crate::version3::V3; use crate::version4::V4; let mut footer = Footer::new(); let kpv2 = AsymmetricKeyPair::::generate().unwrap(); let kpv3 = AsymmetricKeyPair::::generate().unwrap(); let kpv4 = AsymmetricKeyPair::::generate().unwrap(); let skv2 = SymmetricKey::::generate().unwrap(); let skv4 = SymmetricKey::::generate().unwrap(); let mut buf = String::new(); let paserk_id = Id::from(&kpv2.secret); paserk_id.fmt(&mut buf).unwrap(); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_ok()); footer.key_id(&paserk_id); assert!(footer.contains_claim("kid")); assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); let mut buf = String::new(); let paserk_id = Id::from(&kpv2.public); paserk_id.fmt(&mut buf).unwrap(); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_ok()); footer.key_id(&paserk_id); assert!(footer.contains_claim("kid")); assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); let mut buf = String::new(); let paserk_id = Id::from(&kpv3.secret); paserk_id.fmt(&mut buf).unwrap(); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_ok()); footer.key_id(&paserk_id); assert!(footer.contains_claim("kid")); assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); let mut buf = String::new(); let paserk_id = Id::from(&kpv3.public); paserk_id.fmt(&mut buf).unwrap(); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_ok()); footer.key_id(&paserk_id); assert!(footer.contains_claim("kid")); assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); let mut buf = String::new(); let paserk_id = Id::from(&kpv4.secret); paserk_id.fmt(&mut buf).unwrap(); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_ok()); footer.key_id(&paserk_id); assert!(footer.contains_claim("kid")); assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); let mut buf = String::new(); let paserk_id = Id::from(&kpv4.public); paserk_id.fmt(&mut buf).unwrap(); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_ok()); footer.key_id(&paserk_id); assert!(footer.contains_claim("kid")); assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); let mut buf = String::new(); let paserk_id = Id::from(&skv2); paserk_id.fmt(&mut buf).unwrap(); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_ok()); footer.key_id(&paserk_id); assert!(footer.contains_claim("kid")); assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); let mut buf = String::new(); let paserk_id = Id::from(&skv4); paserk_id.fmt(&mut buf).unwrap(); assert!(footer.add_additional("kid", &buf).is_err()); assert!(footer.add_additional("custom", &buf).is_ok()); footer.key_id(&paserk_id); assert!(footer.contains_claim("kid")); assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); } }