Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 211 additions & 5 deletions crates/config/src/sections/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,86 @@ impl From<Encryption> for EncryptionRaw {
}
}

/// Description of signing keys.
///
/// It either holds the key config values directly or references a directory
/// where each file contains a key.
#[derive(Debug, Clone)]
pub enum Keys {
Values(Vec<KeyConfig>),
Directory(Utf8PathBuf),
}

impl Keys {
/// Returns a list of key configs.
///
/// If `keys_dir` was given, the keys are read from file.
async fn key_configs(&self) -> anyhow::Result<Vec<KeyConfig>> {
match self {
Keys::Values(key_configs) => Ok(key_configs.clone()),
Keys::Directory(path) => key_configs_from_path(path).await,
}
}
}

/// Reads all keys from the given directory.
async fn key_configs_from_path(path: &Utf8PathBuf) -> anyhow::Result<Vec<KeyConfig>> {
let mut result = vec![];
let mut read_dir = tokio::fs::read_dir(path).await?;
while let Some(dir_entry) = read_dir.next_entry().await? {
if !dir_entry.path().is_file() {
continue;
}
result.push(KeyConfig {
kid: None,
password: None,
key: Key::File(dir_entry.path().try_into()?),
});
}
Ok(result)
}

#[serde_as]
#[derive(JsonSchema, Serialize, Deserialize, Debug, Clone)]
struct KeysRaw {
/// List of private keys to use for signing and encrypting payloads.
#[serde(skip_serializing_if = "Option::is_none")]
keys: Option<Vec<KeyConfig>>,

/// Directory of private keys to use for signing and encrypting payloads.
#[schemars(with = "Option<String>")]
#[serde(skip_serializing_if = "Option::is_none")]
keys_dir: Option<Utf8PathBuf>,
}

impl TryFrom<KeysRaw> for Keys {
type Error = anyhow::Error;

fn try_from(value: KeysRaw) -> Result<Keys, Self::Error> {
match (value.keys, value.keys_dir) {
(None, None) => bail!("Missing `keys` or `keys_dir`"),
(None, Some(path)) => Ok(Keys::Directory(path)),
(Some(keys), None) => Ok(Keys::Values(keys)),
(Some(_), Some(_)) => bail!("Cannot specify both `keys` and `keys_dir`"),
}
}
}

impl From<Keys> for KeysRaw {
fn from(value: Keys) -> Self {
match value {
Keys::Directory(path) => KeysRaw {
keys_dir: Some(path),
keys: None,
},
Keys::Values(keys) => KeysRaw {
keys_dir: None,
keys: Some(keys),
},
}
}
}

/// Application secrets
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
Expand All @@ -255,8 +335,10 @@ pub struct SecretsConfig {
encryption: Encryption,

/// List of private keys to use for signing and encrypting payloads
#[serde(default)]
keys: Vec<KeyConfig>,
#[schemars(with = "KeysRaw")]
#[serde_as(as = "serde_with::TryFromInto<KeysRaw>")]
#[serde(flatten)]
keys: Keys,
}

impl SecretsConfig {
Expand All @@ -267,7 +349,8 @@ impl SecretsConfig {
/// Returns an error when a key could not be imported
#[tracing::instrument(name = "secrets.load", skip_all)]
pub async fn key_store(&self) -> anyhow::Result<Keystore> {
let web_keys = try_join_all(self.keys.iter().map(KeyConfig::json_web_key)).await?;
let key_configs = self.keys.key_configs().await?;
let web_keys = try_join_all(key_configs.iter().map(KeyConfig::json_web_key)).await?;

Ok(Keystore::new(JsonWebKeySet::new(web_keys)))
}
Expand Down Expand Up @@ -382,7 +465,7 @@ impl SecretsConfig {

Ok(Self {
encryption: Encryption::Value(Standard.sample(&mut rng)),
keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key],
keys: Keys::Values(vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key]),
})
}

Expand Down Expand Up @@ -423,7 +506,7 @@ impl SecretsConfig {

Self {
encryption: Encryption::Value([0xEA; 32]),
keys: vec![rsa_key, ecdsa_key],
keys: Keys::Values(vec![rsa_key, ecdsa_key]),
}
}
}
Expand All @@ -439,6 +522,129 @@ mod tests {

use super::*;

#[tokio::test]
async fn load_config() {
task::spawn_blocking(|| {
Jail::expect_with(|jail| {
jail.create_file(
"config.yaml",
indoc::indoc! {r"
secrets:
encryption_file: encryption
keys_dir: keys
"},
)?;
jail.create_file("encryption", example_secret())?;
jail.create_dir("keys")?;
jail.create_file(
"keys/key1",
indoc::indoc! {r"
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
"},
)?;
jail.create_file(
"keys/key2",
indoc::indoc! {r"
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
h27LAir5RqxByHvua2XsP46rSTChof78uw==
-----END EC PRIVATE KEY-----
"},
)?;

let config = Figment::new()
.merge(Yaml::file("config.yaml"))
.extract_inner::<SecretsConfig>("secrets")?;

Handle::current().block_on(async move {
assert!(
matches!(config.encryption, Encryption::File(ref p) if p == "encryption")
);
assert_eq!(
config.encryption().await.unwrap(),
[
0, 0, 17, 17, 34, 34, 51, 51, 68, 68, 85, 85, 102, 102, 119, 119, 136,
136, 153, 153, 170, 170, 187, 187, 204, 204, 221, 221, 238, 238, 255,
255
]
);

let mut key_config = config.keys.key_configs().await.unwrap();
key_config.sort_by_key(|a| {
if let Key::File(p) = &a.key {
Some(p.clone())
} else {
None
}
});
let key_store = config.key_store().await.unwrap();

assert!(key_config[0].kid.is_none());
assert!(matches!(&key_config[0].key, Key::File(p) if p == "keys/key1"));
assert!(key_store.iter().any(|k| k.kid() == Some("xmgGCzGtQFmhEOP0YAqBt-oZyVauSVMXcf4kwcgGZLc")));
assert!(key_config[1].kid.is_none());
assert!(matches!(&key_config[1].key, Key::File(p) if p == "keys/key2"));
assert!(key_store.iter().any(|k| k.kid() == Some("ONUCn80fsiISFWKrVMEiirNVr-QEvi7uQI0QH9q9q4o")));
});

Ok(())
});
})
.await
.unwrap();
}

#[tokio::test]
async fn load_config_inline_secrets() {
task::spawn_blocking(|| {
Expand Down
19 changes: 11 additions & 8 deletions docs/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1528,14 +1528,6 @@
"description": "Application secrets",
"type": "object",
"properties": {
"keys": {
"description": "List of private keys to use for signing and encrypting payloads",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/KeyConfig"
}
},
"encryption_file": {
"description": "File containing the encryption key for secure cookies.",
"type": "string"
Expand All @@ -1547,6 +1539,17 @@
],
"type": "string",
"pattern": "[0-9a-fA-F]{64}"
},
"keys": {
"description": "List of private keys to use for signing and encrypting payloads.",
"type": "array",
"items": {
"$ref": "#/definitions/KeyConfig"
}
},
"keys_dir": {
"description": "Directory of private keys to use for signing and encrypting payloads.",
"type": "string"
}
}
},
Expand Down
19 changes: 15 additions & 4 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ The secret is not updated when the content of the file changes.
> Changing the encryption secret afterwards will lead to a loss of all encrypted
> information in the database.

### `secrets.keys`
### Singing Keys

The service can use a number of key types for signing.
The following key types are supported:
Expand All @@ -232,15 +232,26 @@ The following key types are supported:
- ECDSA with the P-384 (`secp384r1`) curve
- ECDSA with the K-256 (`secp256k1`) curve

Each entry in the list corresponds to one signing key used by MAS.
The key can either be specified inline (with the `key` property),
or loaded from a file (with the `key_file` property).
The following key formats are supported:

- PKCS#1 PEM or DER-encoded RSA private key
- PKCS#8 PEM or DER-encoded RSA or ECDSA private key, encrypted or not
- SEC1 PEM or DER-encoded ECDSA private key

The keys can be given as a directory path via `secrets.keys_dir`
or, alternatively, as an inline configuration list via `secrets.keys`.

#### `secrets.keys_dir`

Path to the directory containing MAS signing key files.
Only keys that don’t require a password are supported.

#### `secrets.keys`

Each entry in the list corresponds to one signing key used by MAS.
The key can either be specified inline (with the `key` property),
or loaded from a file (with the `key_file` property).

A [JWK Key ID] is automatically derived from each key.
To override this default, set `kid` to a custom value.
The `kid` can be any case-sensitive string value as long as it is unique to this list;
Expand Down
Loading