1 //! Generate and verify Authentication tickets
5 use std
::marker
::PhantomData
;
7 use anyhow
::{bail, format_err, Error}
;
8 use openssl
::hash
::MessageDigest
;
9 use openssl
::pkey
::{HasPublic, PKey, Private}
;
10 use openssl
::sign
::{Signer, Verifier}
;
11 use percent_encoding
::{percent_decode_str, percent_encode, AsciiSet}
;
13 use crate::api2
::types
::Userid
;
15 pub const TICKET_LIFETIME
: i64 = 3600 * 2; // 2 hours
17 pub const TERM_PREFIX
: &str = "PBSTERM";
19 /// Stringified ticket data must not contain colons...
20 const TICKET_ASCIISET
: &AsciiSet
= &percent_encoding
::CONTROLS
.add(b'
:'
);
22 /// An empty type implementing [`ToString`] and [`FromStr`](std::str::FromStr), used for tickets
26 impl ToString
for Empty
{
27 fn to_string(&self) -> String
{
32 impl std
::str::FromStr
for Empty
{
35 fn from_str(s
: &str) -> Result
<Self, Error
> {
37 bail
!("unexpected ticket data, should be empty");
43 /// An API ticket consists of a ticket type (prefix), type-dependent data, optional additional
44 /// authenticaztion data, a timestamp and a signature. We store these values in the form
45 /// `<prefix>:<stringified data>:<timestamp>::<signature>`.
47 /// The signature is made over the string consisting of prefix, data, timestamp and aad joined
48 /// together by colons. If there is no additional authentication data it will be skipped together
49 /// with the colon separating it from the timestamp.
52 T
: ToString
+ std
::str::FromStr
,
54 prefix
: Cow
<'
static, str>,
57 signature
: Option
<Vec
<u8>>,
58 _type_marker
: PhantomData
<T
>,
63 T
: ToString
+ std
::str::FromStr
,
64 <T
as std
::str::FromStr
>::Err
: std
::fmt
::Debug
,
66 /// Prepare a new ticket for signing.
67 pub fn new(prefix
: &'
static str, data
: &T
) -> Result
<Self, Error
> {
69 prefix
: Cow
::Borrowed(prefix
),
70 data
: data
.to_string(),
71 time
: proxmox
::tools
::time
::epoch_i64(),
73 _type_marker
: PhantomData
,
77 /// Get the ticket prefix.
78 pub fn prefix(&self) -> &str {
82 /// Get the ticket's time stamp in seconds since the unix epoch.
83 pub fn time(&self) -> i64 {
87 /// Get the raw string data contained in the ticket. The `verify` method will call `parse()`
88 /// this in the end, so using this method directly is discouraged as it does not verify the
90 pub fn raw_data(&self) -> &str {
94 /// Serialize the ticket into a writer.
96 /// This only writes a string. We use `io::write` instead of `fmt::Write` so we can reuse the
97 /// same function for openssl's `Verify`, which only implements `io::Write`.
98 fn write_data(&self, f
: &mut dyn io
::Write
) -> Result
<(), Error
> {
102 percent_encode(self.prefix
.as_bytes(), &TICKET_ASCIISET
),
103 percent_encode(self.data
.as_bytes(), &TICKET_ASCIISET
),
106 .map_err(Error
::from
)
109 /// Write additional authentication data to the verifier.
110 fn write_aad(f
: &mut dyn io
::Write
, aad
: Option
<&str>) -> Result
<(), Error
> {
111 if let Some(aad
) = aad
{
112 write
!(f
, ":{}", percent_encode(aad
.as_bytes(), &TICKET_ASCIISET
))?
;
117 /// Change the ticket's time, used mostly for testing.
119 fn change_time(&mut self, time
: i64) -> &mut Self {
125 pub fn sign(&mut self, keypair
: &PKey
<Private
>, aad
: Option
<&str>) -> Result
<String
, Error
> {
126 let mut output
= Vec
::<u8>::new();
127 let mut signer
= Signer
::new(MessageDigest
::sha256(), &keypair
)
128 .map_err(|err
| format_err
!("openssl error creating signer for ticket: {}", err
))?
;
130 self.write_data(&mut output
)
131 .map_err(|err
| format_err
!("error creating ticket: {}", err
))?
;
135 .map_err(Error
::from
)
136 .and_then(|()| Self::write_aad(&mut signer
, aad
))
137 .map_err(|err
| format_err
!("error signing ticket: {}", err
))?
;
139 // See `Self::write_data` for why this is safe
140 let mut output
= unsafe { String::from_utf8_unchecked(output) }
;
142 let signature
= signer
144 .map_err(|err
| format_err
!("error finishing ticket signature: {}", err
))?
;
150 base64
::encode_config(&signature
, base64
::STANDARD_NO_PAD
),
153 self.signature
= Some(signature
);
158 /// `verify` with an additional time frame parameter, not usually required since we always use
159 /// the same time frame.
160 pub fn verify_with_time_frame
<P
: HasPublic
>(
165 time_frame
: std
::ops
::Range
<i64>,
166 ) -> Result
<T
, Error
> {
167 if self.prefix
!= prefix
{
168 bail
!("ticket with invalid prefix");
171 let signature
= match self.signature
.as_ref() {
173 None
=> bail
!("invalid ticket without signature"),
176 let age
= proxmox
::tools
::time
::epoch_i64() - self.time
;
177 if age
< time_frame
.start
{
178 bail
!("invalid ticket - timestamp newer than expected");
180 if age
> time_frame
.end
{
181 bail
!("invalid ticket - expired");
184 let mut verifier
= Verifier
::new(MessageDigest
::sha256(), &keypair
)?
;
186 self.write_data(&mut verifier
)
187 .and_then(|()| Self::write_aad(&mut verifier
, aad
))
188 .map_err(|err
| format_err
!("error verifying ticket: {}", err
))?
;
190 let is_valid
: bool
= verifier
192 .map_err(|err
| format_err
!("openssl error verifying ticket: {}", err
))?
;
195 bail
!("ticket with invalid signature");
200 .map_err(|err
| format_err
!("failed to parse contained ticket data: {:?}", err
))
203 /// Verify the ticket with the provided key pair. The additional authentication data needs to
204 /// match the one used when generating the ticket, and the ticket's age must fall into the time
206 pub fn verify
<P
: HasPublic
>(
211 ) -> Result
<T
, Error
> {
212 self.verify_with_time_frame(keypair
, prefix
, aad
, -300..TICKET_LIFETIME
)
215 /// Parse a ticket string.
216 pub fn parse(ticket
: &str) -> Result
<Self, Error
> {
217 let mut parts
= ticket
.splitn(4, '
:'
);
219 let prefix
= percent_decode_str(
222 .ok_or_else(|| format_err
!("ticket without prefix"))?
,
225 .map_err(|err
| format_err
!("invalid ticket, error decoding prefix: {}", err
))?
;
227 let data
= percent_decode_str(
230 .ok_or_else(|| format_err
!("ticket without data"))?
,
233 .map_err(|err
| format_err
!("invalid ticket, error decoding data: {}", err
))?
;
235 let time
= i64::from_str_radix(
238 .ok_or_else(|| format_err
!("ticket without timestamp"))?
,
241 .map_err(|err
| format_err
!("ticket with bad timestamp: {}", err
))?
;
243 let remainder
= parts
245 .ok_or_else(|| format_err
!("ticket without signature"))?
;
246 // <prefix>:<data>:<time>::signature - the 4th `.next()` swallows the first colon in the
248 if !remainder
.starts_with('
:'
) {
249 bail
!("ticket without signature separator");
251 let signature
= base64
::decode_config(&remainder
[1..], base64
::STANDARD_NO_PAD
)
252 .map_err(|err
| format_err
!("ticket with bad signature: {}", err
))?
;
255 prefix
: Cow
::Owned(prefix
.into_owned()),
256 data
: data
.into_owned(),
258 signature
: Some(signature
),
259 _type_marker
: PhantomData
,
264 pub fn term_aad(userid
: &Userid
, path
: &str, port
: u16) -> String
{
265 format
!("{}{}{}", userid
, path
, port
)
270 use openssl
::pkey
::{PKey, Private}
;
273 use crate::api2
::types
::Userid
;
275 fn simple_test
<F
>(key
: &PKey
<Private
>, aad
: Option
<&str>, modify
: F
)
277 F
: FnOnce(&mut Ticket
<Userid
>) -> bool
,
279 let userid
= Userid
::root_userid();
281 let mut ticket
= Ticket
::new("PREFIX", userid
).expect("failed to create Ticket struct");
282 let should_work
= modify(&mut ticket
);
283 let ticket
= ticket
.sign(key
, aad
).expect("failed to sign test ticket");
286 Ticket
::<Userid
>::parse(&ticket
).expect("failed to parse generated test ticket");
288 let check
: Userid
= parsed
289 .verify(key
, "PREFIX", aad
)
290 .expect("failed to verify test ticket");
292 assert_eq
!(*userid
, check
);
295 .verify(key
, "PREFIX", aad
)
296 .expect_err("failed to verify test ticket");
302 // first we need keys, for testing we use small keys for speed...
304 openssl
::rsa
::Rsa
::generate(1024).expect("failed to generate RSA key for testing");
305 let key
= openssl
::pkey
::PKey
::<openssl
::pkey
::Private
>::from_rsa(rsa
)
306 .expect("failed to create PKey for RSA key");
308 simple_test(&key
, Some("secret aad data"), |_
| true);
309 simple_test(&key
, None
, |_
| true);
310 simple_test(&key
, None
, |t
| {
314 simple_test(&key
, None
, |t
| {
315 t
.change_time(proxmox
::tools
::time
::epoch_i64() + 0x1000_0000);