biscuit_auth/
lib.rs

1/*
2 * Copyright (c) 2019 Geoffroy Couprie <contact@geoffroycouprie.com> and Contributors to the Eclipse Foundation.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5//! Biscuit authentication and authorization token
6//!
7//! Biscuit is an authorization token for microservices architectures with the following properties:
8//!
9//! * decentralized validation: any node could validate the token only with public information;
10//! * offline delegation: a new, valid token can be created from another one by attenuating its rights, by its holder, without communicating with anyone;
11//! * capabilities based: authorization in microservices should be tied to rights related to the request, instead of relying to an identity that might not make sense to the authorizer;
12//! * flexible rights managements: the token uses a logic language to specify attenuation and add bounds on ambient data;
13//! * small enough to fit anywhere (cookies, etc).
14//!
15//! Non goals:
16//!
17//! * This is not a new authentication protocol. Biscuit tokens can be used as opaque tokens delivered by other systems such as OAuth.
18//! * Revocation: while tokens come with expiration dates, revocation requires external state management.
19//!
20//! # Usage
21//!
22//! Most of the interaction with this library is done through the
23//! [Biscuit](`crate::token::Biscuit`) structure, that represents a valid
24//! token, and the [Authorizer](`crate::token::authorizer::Authorizer`), used to
25//! check authorization policies on a token.
26//!
27//! In this example we will see how we can create a token, add some checks,
28//! serialize and deserialize a token, append more checks, and validate
29//! those checks in the context of a request:
30//!
31//! ```rust
32//! extern crate biscuit_auth as biscuit;
33//!
34//! use biscuit::{KeyPair, Biscuit, Authorizer, builder::*, error, macros::*};
35//!
36//! fn main() -> Result<(), error::Token> {
37//!   // let's generate the root key pair. The root public key will be necessary
38//!   // to verify the token
39//!   let root = KeyPair::new();
40//!   let public_key = root.public();
41//!
42//!   // creating a first token
43//!   let token1 = {
44//!     // the first block of the token is the authority block. It contains global
45//!     // information like which operation types are available
46//!     let biscuit = biscuit!(r#"
47//!           right("/a/file1.txt", "read");
48//!           right("/a/file1.txt", "write");
49//!           right("/a/file2.txt", "read");
50//!           right("/b/file3.txt", "write");
51//!     "#)
52//!       .build(&root)?; // the first block is signed
53//!
54//!     println!("biscuit (authority): {}", biscuit);
55//!
56//!     biscuit.to_vec()?
57//!   };
58//!
59//!   // this token is only 249 bytes, holding the authority data and the signature
60//!   assert_eq!(token1.len(), 249);
61//!
62//!   // now let's add some restrictions to this token
63//!   // we want to limit access to `/a/file1.txt` and to read operations
64//!   let token2 = {
65//!     // the token is deserialized, the signature is verified
66//!     let deser = Biscuit::from(&token1,  root.public())?;
67//!
68//!     // biscuits can be attenuated by appending checks
69//!     let biscuit = deser.append(block!(r#"
70//!       // checks are implemented as logic rules. If the rule produces something,
71//!       // the check is successful
72//!       // here we verify the presence of a `resource` fact with a path set to "/a/file1.txt"
73//!       // and a read operation
74//!       check if resource("/a/file1.txt"), operation("read");
75//!     "#))?;
76//!
77//!     println!("biscuit (authority): {}", biscuit);
78//!
79//!     biscuit.to_vec()?
80//!   };
81//!
82//!   // this new token fits in 385 bytes
83//!   assert_eq!(token2.len(), 385);
84//!
85//!   /************** VERIFICATION ****************/
86//!
87//!   // let's deserialize the token:
88//!   let biscuit2 = Biscuit::from(&token2,  root.public())?;
89//!
90//!   // let's define 3 authorizers (corresponding to 3 different requests):
91//!   // - one for /a/file1.txt and a read operation
92//!   // - one for /a/file1.txt and a write operation
93//!   // - one for /a/file2.txt and a read operation
94//!
95//!   let mut v1 = authorizer!(r#"
96//!      resource("/a/file1.txt");
97//!      operation("read");
98//!      
99//!      // an authorizer can come with allow/deny policies. While checks are all tested
100//!      // and must all succeed, allow/deny policies are tried one by one in order,
101//!      // and we stop verification on the first that matches
102//!      //
103//!      // here we will check that the token has the corresponding right
104//!      allow if right("/a/file1.txt", "read");
105//!      // explicit catch-all deny. here it is not necessary: if no policy
106//!      // matches, a default deny applies
107//!      deny if true;
108//!   "#)
109//!   .build(&biscuit2)?;
110//!
111//!   let mut v2 = authorizer!(r#"
112//!      resource("/a/file1.txt");
113//!      operation("write");
114//!      allow if right("/a/file1.txt", "write");
115//!   "#)
116//!   .build(&biscuit2)?;
117//!
118//!   let mut v3 = authorizer!(r#"
119//!      resource("/a/file2.txt");
120//!      operation("read");
121//!      allow if right("/a/file2.txt", "read");
122//!   "#)
123//!   .build(&biscuit2)?;
124//!
125//!   // the token restricts to read operations:
126//!   assert!(v1.authorize().is_ok());
127//!   // the second authorizer requested a read operation
128//!   assert!(v2.authorize().is_err());
129//!   // the third authorizer requests /a/file2.txt
130//!   assert!(v3.authorize().is_err());
131//!
132//!   Ok(())
133//! }
134//! ```
135//!
136//! # Concepts
137//!
138//! ## Blocks
139//!
140//! A Biscuit token is made with a list of blocks defining data and checks that
141//! must be validated upon reception with a request. Any failed check will invalidate
142//! the entire token.
143//!
144//! If you hold a valid token, it is possible to add a new block to restrict further
145//! the token, like limiting access to one particular resource, or adding a short
146//! expiration date. This will generate a new, valid token. This can be done offline,
147//! without asking the original token creator.
148//!
149//! On the other hand, if a block is modified or removed, the token will fail the
150//! cryptographic signature verification.
151//!
152//! ## Cryptography
153//!
154//! Biscuit tokens get inspiration from macaroons and JSON Web Tokens, reproducing
155//! useful features from both:
156//!
157//! - offline delegation like macaroons
158//! - based on public key cryptography like JWT, so any application holding the root public key can verify a token (while macaroons are based on a root shared secret)
159//!
160//! Blocks are signed in a chain, starting with the root key, with each block signature
161//! covering the block content, and the next block's public key.
162//! Signatures can be generated either with Ed25519, or with ECDSA over P256.
163//!
164//! ## A logic language for authorization policies: Datalog with constraints
165//!
166//! We rely on a modified version of Datalog, that can represent complex behaviours
167//! in a compact form, and add flexible constraints on data.
168//!
169//! Here are examples of checks that can be implemented with that language:
170//!
171//! - valid if the requested resource is "file.txt" and the operation is "read": `check if resource("file.txt"), operation("read")`
172//! - valid if current time is before January 1st 2030, 00h00mn00s UTC: `check if time($0), $0 < 2030-01-01T00:00:00Z`
173//! - source IP is in set [1.2.3.4, 5.6.7.8]: `check if ip($0), $0 in {"1.2.3.4", "5.6.7.8"}`
174//! - resource matches prefix "/home/biscuit/data/": `check if resource($0), $0.starts_with("home/biscuit/data/")`
175//!
176//! But it can also combine into more complex patterns, like: we can read a resource
177//! **if** the user has the read right, **or** the user is member of an organisation
178//! and that organisation has the read right:
179//!
180//! ```ignore
181//! allow if right($0, "read");
182//! allow if organisation($1), right($1, "read");
183//! ```
184//!
185//! Like Datalog, this language is based around facts and rules, but with some
186//! slight modifications: a block's rules and checks can only apply to facts
187//! from the current or previous blocks. The authorizer executes its checks and
188//! policies in the context of the first block. This allows Biscuit to carry
189//! basic rights in the first block while preventing later blocks from
190//! increasing the token's rights.
191//!
192//! ### Checks
193//!
194//! A check requires the presence of one or more facts, and can have additional
195//! constraints on these facts (the constraints are implemented separately to simplify
196//! the language implementation: among other things, it avoids implementing negation).
197//! It is possible to create rules like these ones:
198//!
199//! - `check if resource("file1")`
200//! - `check if resource($0), owner("user1", $0)` the $0 represents a "hole" that must be filled with the correct value
201//! - `check if time($0), $0 < 2019-02-05T23:00:00Z` expiration date
202//! - `check if application($0), operation($1), user($2), right(#app, $0, $1), owner($2, $0), credit($2, $3), $3 > 0` verifies that the user owns the applications, the application has the right on the operation, there's a credit information for the operation, and the credit is larger than 0
203//!
204//! It is also possible to refuse a request if a condition is met, using `reject`:
205//! - `reject if resource("file1")`
206//!
207//! ### Allow/deny policies
208//!
209//! On the verification side, we can define *allow/deny policies*, which are tested
210//! after all checks passed, one by one in order until one of them matches.
211//!
212//! * if an *allow* matches, verification succeeds
213//! * if a *deny* matches, verification fails
214//! * if there's no *allow* or *deny*, verification fails
215//!
216//! They can be written as follows:
217//!
218//! ```ignore
219//! // verify that we have the right for this request
220//! allow if
221//!   resource($res),
222//!   operation($op),
223//!   right($res, $op);
224//!
225//! deny if true;
226//! ```
227//!
228//! When processing the request, the authorizer will make sure that all checks succeeds
229//! and none of the reject rules matches, otherwise it will retur an error with the list
230//! of failures.
231//!
232//! Then it tries the allow/deny policis and will return the index of the policy that matched.
233//!
234//! ## Symbol table
235//!
236//! To reduce the size of tokens, the language uses string interning: strings are
237//! serialized as an index in a list of strings. Any repetition of the string will
238//! then use reduced space.
239//!
240//! They can be used for pretty printing of a fact or rule. As an example, with a table
241//! containing `["resource", "operation", "read", "rule1", "file1.txt"]`, we could have the following rule:
242//! `#3() <- #0(#4), #1(#2)` that would be printed as `rule1() <- resource("file.txt"), operation("read")`
243//!
244//! biscuit implementations come with a default symbol table to avoid transmitting
245//! frequent values with every token.
246
247mod crypto;
248pub mod datalog;
249pub mod error;
250pub mod format;
251pub mod parser;
252mod token;
253
254pub use crypto::{KeyPair, PrivateKey, PublicKey};
255pub use token::authorizer::{Authorizer, AuthorizerLimits};
256pub use token::builder;
257pub use token::builder::{Algorithm, AuthorizerBuilder, BiscuitBuilder, BlockBuilder};
258pub use token::builder_ext;
259pub use token::unverified::UnverifiedBiscuit;
260pub use token::Biscuit;
261pub use token::RootKeyProvider;
262pub use token::{ThirdPartyBlock, ThirdPartyRequest};
263
264#[cfg(feature = "bwk")]
265mod bwk;
266#[cfg(feature = "bwk")]
267pub use bwk::*;
268
269mod time;
270
271/// Procedural macros to construct Datalog policies
272#[cfg(feature = "datalog-macro")]
273#[cfg_attr(feature = "docsrs", doc(cfg(feature = "datalog-macro")))]
274pub mod macros;