nyantec_cert_auth/
lib.rs

1//! # nyantec-cert-auth
2//!
3//! A library for parsing X.509 Client Certificates
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use hyper::{header, Body, Request};
7use jsonwebtoken;
8use serde_derive::{Deserialize, Serialize};
9use thiserror::Error;
10use x509_parser::pem::Pem;
11
12use crate::certificate_parser::{get_email_from_dn, get_email_from_san, get_uid};
13
14mod certificate_parser;
15
16#[derive(Error, Debug)]
17pub enum CustomError {
18	#[error("System time is before unix time")]
19	SystemTimeError(#[from] std::time::SystemTimeError),
20	#[error("JWT Error: {0}")]
21	JWTError(#[from] jsonwebtoken::errors::Error),
22	#[error("Error converting header value to string: {0}")]
23	ToStrError(#[from] header::ToStrError),
24	#[error("Missing x-ssl-client-escaped-cert header")]
25	MissingHeader,
26	#[error("No certificate given")]
27	NoCertificate,
28	#[error("SAN exists but could not be parsed")]
29	InvalidSAN,
30	#[error("Could not get email from certificate")]
31	NoEmail,
32	#[error("Certificate has no Common Name")]
33	NoCommonName,
34	#[error("Invalid urlencoding {0}")]
35	UrlEncoding(#[from] urlencoding::FromUrlEncodingError),
36	#[error("Decoding cert: {0}")]
37	PEM(#[from] x509_parser::prelude::PEMError),
38	#[error("Decoding cert: {0}")]
39	X509(#[from] x509_parser::prelude::X509Error),
40	#[error("Decoding cert: {0}")]
41	Nom(#[from] x509_parser::nom::Err<x509_parser::prelude::X509Error>),
42	#[error("HttpError: {0}")]
43	HttpError(#[from] hyper::http::Error),
44	#[error("")]
45	Infallible(#[from] std::convert::Infallible),
46	#[error("Reqwest Error")]
47	Reqwest(#[from] reqwest::Error),
48	#[error("Hyper Error")]
49	HyperError(#[from] hyper::Error),
50	#[error("Supplied permissions are empty")]
51	PermissionEmptyError,
52	#[error("Supplied entity does not match the List of allowed entities")]
53	PermissionNotMatchedError,
54}
55
56/// Custom Error Wrapper Type
57pub type Result<T> = std::result::Result<T, CustomError>;
58
59/// A struct holding essential information about a parsed X.509 Client Certificate.
60#[derive(Clone, Debug, Serialize)]
61pub struct Claims {
62	/// Email Address (found in the *Subject Alternative Name* field).
63	pub email: String,
64
65	/// Full name of the holder of the client certificate.
66	pub name: String,
67
68	/// User id of the holder of the client certificate.
69	pub uid: String,
70
71	/// Identifies the expiration time on or after which the JWT **must not** be accepted for processing.
72	///
73	/// Needed for the generation of JSON Web Tokens. See also: [RFC Section 4.1.4]
74	///
75	/// [RFC Section 4.1.4]: https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#rfc.section.4.1.4
76	pub exp: u64,
77
78	/// Identifies the time at which the client certificate has been parsed.
79	///
80	/// Needed for the generation of JSON Web Tokens. See also: [RFC Section 4.1.6]
81	///
82	/// [RFC Section 4.1.6]: https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#rfc.section.4.1.6
83	pub iat: u64,
84}
85
86/// Represents a set of permissions.
87///
88/// A user is allowed by the client certificate validation if the client certificate's uid matches
89/// any of the `allowed_uids`.
90#[derive(Clone, Debug, Deserialize)]
91pub struct Permissions {
92	/// Represents a list of allowed uids.
93	#[serde(default)]
94	pub allowed_uids: Vec<String>,
95}
96
97/// Parses a given X.509 Client Certificate and returns a Struct of parsed claims.
98pub fn get_claims(req: Request<Body>) -> crate::Result<Claims> {
99	let escaped_cert_str = req
100		.headers()
101		.get("x-ssl-client-escaped-cert")
102		.ok_or(CustomError::MissingHeader)?
103		.to_str()?;
104
105	let cert_str = urlencoding::decode(escaped_cert_str)?;
106
107	let cert_pem = Pem::iter_from_buffer(&cert_str.as_bytes())
108		.next()
109		.ok_or(CustomError::NoCertificate)??;
110
111	let cert = cert_pem.parse_x509()?;
112
113	let email = get_email_from_san(&cert)
114		.transpose()
115		.or_else(|| get_email_from_dn(&cert).transpose())
116		.ok_or(CustomError::NoEmail)??;
117
118	let name = cert
119		.subject()
120		.iter_common_name()
121		.next()
122		.map(|x| Ok::<_, CustomError>(x.as_str()?))
123		.transpose()?
124		.ok_or(CustomError::NoCommonName)?;
125
126	let uid = get_uid(&cert)?.unwrap_or(name);
127
128	let iat = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
129	let exp = iat + 3600;
130
131	Ok(Claims {
132		email: email.to_string(),
133		name: name.to_string(),
134		uid: uid.to_string(),
135		exp,
136		iat,
137	})
138}
139
140/// Returns Ok if and only if the user matches any of the provided allowed user ids.
141pub fn is_allowed_by_uid(user: &Claims, permissions: &Permissions) -> crate::Result<()> {
142	if permissions.allowed_uids.is_empty() {
143		return Err(CustomError::PermissionEmptyError);
144	}
145
146	if permissions.allowed_uids.iter().any(|u| u.eq(&user.uid)) {
147		Ok(())
148	} else {
149		Err(CustomError::PermissionNotMatchedError)
150	}
151}
152
153#[cfg(test)]
154mod tests {
155	use crate::{is_allowed_by_uid, Claims, CustomError, Permissions};
156
157	#[test]
158	fn test_is_allowed_by_uid() {
159		let user_allowed = Claims {
160			email: "nya@nyantec.com".to_string(),
161			name: "Some Name".to_string(),
162			uid: "nya".to_string(),
163			exp: 0,
164			iat: 0,
165		};
166		let user_denied = Claims {
167			email: "nyet@nyantec.com".to_string(),
168			name: "Some Name".to_string(),
169			uid: "nyet".to_string(),
170			exp: 0,
171			iat: 0,
172		};
173
174		let permissions = Permissions {
175			allowed_uids: vec![user_allowed.uid.clone()],
176		};
177		let permissions_empty = Permissions {
178			allowed_uids: vec![],
179		};
180
181		assert_eq!(
182			is_allowed_by_uid(&user_allowed, &permissions).is_ok(),
183			Ok::<_, CustomError>(()).is_ok()
184		);
185		assert_eq!(
186			is_allowed_by_uid(&user_denied, &permissions).is_ok(),
187			Err::<(), CustomError>(CustomError::PermissionNotMatchedError).is_ok()
188		);
189		assert_eq!(
190			is_allowed_by_uid(&user_allowed, &permissions_empty).is_ok(),
191			Err::<(), CustomError>(CustomError::PermissionEmptyError).is_ok()
192		)
193	}
194}