objectstore_server/auth/key_directory.rs
1use std::collections::{BTreeMap, HashSet};
2use std::path::Path;
3
4use anyhow::Context;
5use ed25519_dalek::VerifyingKey;
6use ed25519_dalek::pkcs8::DecodePublicKey;
7use jsonwebtoken::DecodingKey;
8use objectstore_types::auth::Permission;
9
10use crate::config::{AuthZ, AuthZVerificationKey};
11
12/// A single version of a public key, usable for both JWT verification (via
13/// [`DecodingKey`]) and pre-signed URL verification (via [`VerifyingKey`]).
14///
15/// Both are parsed from the same EdDSA PEM file.
16#[derive(Debug)]
17pub struct PublicKey {
18 /// Key material for verifying JWT signatures.
19 pub decoding_key: DecodingKey,
20 /// Key material for verifying pre-signed URL signatures.
21 pub verifying_key: VerifyingKey,
22}
23
24async fn read_key_from_file(filename: &Path) -> anyhow::Result<PublicKey> {
25 let key_content = tokio::fs::read_to_string(filename)
26 .await
27 .with_context(|| format!("reading key from {filename:?}"))?;
28 let decoding_key = DecodingKey::from_ed_pem(key_content.as_bytes())
29 .with_context(|| format!("parsing decoding key from {filename:?}"))?;
30 let verifying_key = VerifyingKey::from_public_key_pem(&key_content)
31 .with_context(|| format!("parsing verifying key from {filename:?}"))?;
32 Ok(PublicKey {
33 decoding_key,
34 verifying_key,
35 })
36}
37
38/// Configures the EdDSA public key(s) and permissions used to verify tokens from a single `kid`.
39///
40/// Note: [`jsonwebtoken::DecodingKey`] redacts key content in its `Debug` implementation.
41#[derive(Debug)]
42pub struct PublicKeyConfig {
43 /// Versions of this key's key material which may be used to verify signatures.
44 ///
45 /// If a key is being rotated, the old and new versions of that key should both be
46 /// configured so objectstore can verify signatures while the updated key is still
47 /// rolling out. Otherwise, this should only contain the most recent version of a key.
48 pub key_versions: Vec<PublicKey>,
49
50 /// The maximum set of permissions that this key's signer is authorized to grant.
51 ///
52 /// If a request's auth token grants full permission but it was signed by a key that
53 /// is only allowed to grant read permission, then the request only has read
54 /// permission.
55 pub max_permissions: HashSet<Permission>,
56}
57
58impl PublicKeyConfig {
59 /// Loads key material and permissions from an [`AuthZVerificationKey`] configuration.
60 pub async fn from_config(key_config: &AuthZVerificationKey) -> anyhow::Result<Self> {
61 let mut key_versions = Vec::with_capacity(key_config.key_files.len());
62 for filename in &key_config.key_files {
63 let key = read_key_from_file(filename)
64 .await
65 .inspect_err(|e| objectstore_log::error!("{:?}", e))?;
66 key_versions.push(key);
67 }
68
69 Ok(Self {
70 max_permissions: key_config.max_permissions.clone(),
71 key_versions,
72 })
73 }
74}
75
76/// Directory of keys that may be used to verify a request's auth token.
77///
78/// The auth token is read from the `X-Os-Auth` header (preferred) or the
79/// standard `Authorization` header (fallback). This directory contains a map keyed
80/// on a key's ID. When verifying a JWT, the `kid` field should be read from the
81/// JWT header and used to index into this directory to select the appropriate key.
82#[derive(Debug)]
83pub struct PublicKeyDirectory {
84 /// Mapping from key ID to key configuration.
85 pub keys: BTreeMap<String, PublicKeyConfig>,
86}
87
88impl PublicKeyDirectory {
89 /// Loads the full key directory from an [`AuthZ`] configuration.
90 pub async fn from_config(auth_config: &AuthZ) -> anyhow::Result<Self> {
91 let mut keys = BTreeMap::new();
92 for (kid, key_config) in &auth_config.keys {
93 let config = PublicKeyConfig::from_config(key_config).await?;
94 keys.insert(kid.clone(), config);
95 }
96
97 Ok(Self { keys })
98 }
99}