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;