Skip to content

Commit e1914ec

Browse files
committed
Auto-generate kid if not given
1 parent 34a4c66 commit e1914ec

File tree

5 files changed

+124
-45
lines changed

5 files changed

+124
-45
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ features = ["std"]
474474
# PKCS#8 encoding
475475
[workspace.dependencies.pkcs8]
476476
version = "0.10.2"
477-
features = ["std", "pkcs5", "encryption"]
477+
features = ["alloc", "std", "pkcs5", "encryption"]
478478

479479
# Public Suffix List
480480
[workspace.dependencies.psl]

crates/config/src/sections/secrets.rs

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,12 @@ impl From<Key> for KeyRaw {
132132
#[serde_as]
133133
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
134134
pub struct KeyConfig {
135-
kid: String,
135+
/// The key ID `kid` of the key as used by JWKs.
136+
///
137+
/// If not given, `kid` will be derived from the key by hex-encoding the
138+
/// first four bytes of the key’s fingerprint.
139+
#[serde(skip_serializing_if = "Option::is_none")]
140+
kid: Option<String>,
136141

137142
#[schemars(with = "PasswordRaw")]
138143
#[serde_as(as = "serde_with::TryFromInto<PasswordRaw>")]
@@ -178,12 +183,24 @@ impl KeyConfig {
178183
None => PrivateKey::load(&key)?,
179184
};
180185

186+
let kid = match self.kid.clone() {
187+
Some(kid) => kid,
188+
None => kid_from_key(&private_key)?,
189+
};
190+
181191
Ok(JsonWebKey::new(private_key)
182-
.with_kid(self.kid.clone())
192+
.with_kid(kid)
183193
.with_use(mas_iana::jose::JsonWebKeyUse::Sig))
184194
}
185195
}
186196

197+
/// Returns a kid derived from the given key.
198+
fn kid_from_key(private_key: &PrivateKey) -> anyhow::Result<String> {
199+
let fingerprint = private_key.fingerprint()?;
200+
let head = fingerprint.first_chunk::<4>().unwrap();
201+
Ok(hex::encode(head))
202+
}
203+
187204
/// Encryption config option.
188205
#[derive(Debug, Clone)]
189206
pub enum Encryption {
@@ -322,7 +339,7 @@ impl SecretsConfig {
322339
.await
323340
.context("could not join blocking task")?;
324341
let rsa_key = KeyConfig {
325-
kid: Alphanumeric.sample_string(&mut rng, 10),
342+
kid: Some(Alphanumeric.sample_string(&mut rng, 10)),
326343
password: None,
327344
key: Key::Value(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
328345
};
@@ -338,7 +355,7 @@ impl SecretsConfig {
338355
.await
339356
.context("could not join blocking task")?;
340357
let ec_p256_key = KeyConfig {
341-
kid: Alphanumeric.sample_string(&mut rng, 10),
358+
kid: Some(Alphanumeric.sample_string(&mut rng, 10)),
342359
password: None,
343360
key: Key::Value(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
344361
};
@@ -354,7 +371,7 @@ impl SecretsConfig {
354371
.await
355372
.context("could not join blocking task")?;
356373
let ec_p384_key = KeyConfig {
357-
kid: Alphanumeric.sample_string(&mut rng, 10),
374+
kid: Some(Alphanumeric.sample_string(&mut rng, 10)),
358375
password: None,
359376
key: Key::Value(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
360377
};
@@ -370,7 +387,7 @@ impl SecretsConfig {
370387
.await
371388
.context("could not join blocking task")?;
372389
let ec_k256_key = KeyConfig {
373-
kid: Alphanumeric.sample_string(&mut rng, 10),
390+
kid: Some(Alphanumeric.sample_string(&mut rng, 10)),
374391
password: None,
375392
key: Key::Value(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
376393
};
@@ -383,7 +400,7 @@ impl SecretsConfig {
383400

384401
pub(crate) fn test() -> Self {
385402
let rsa_key = KeyConfig {
386-
kid: "abcdef".to_owned(),
403+
kid: Some("abcdef".to_owned()),
387404
password: None,
388405
key: Key::Value(
389406
indoc::indoc! {r"
@@ -402,7 +419,7 @@ impl SecretsConfig {
402419
),
403420
};
404421
let ecdsa_key = KeyConfig {
405-
kid: "ghijkl".to_owned(),
422+
kid: Some("ghijkl".to_owned()),
406423
password: None,
407424
key: Key::Value(
408425
indoc::indoc! {r"
@@ -422,3 +439,68 @@ impl SecretsConfig {
422439
}
423440
}
424441
}
442+
443+
#[cfg(test)]
444+
mod tests {
445+
use figment::{
446+
Figment, Jail,
447+
providers::{Format, Yaml},
448+
};
449+
use mas_jose::constraints::Constrainable;
450+
use tokio::{runtime::Handle, task};
451+
452+
use super::*;
453+
454+
#[tokio::test]
455+
async fn load_config_inline_secrets() {
456+
task::spawn_blocking(|| {
457+
Jail::expect_with(|jail| {
458+
jail.create_file(
459+
"config.yaml",
460+
indoc::indoc! {r"
461+
secrets:
462+
encryption: >-
463+
0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
464+
keys:
465+
- kid: lekid0
466+
key: |
467+
-----BEGIN EC PRIVATE KEY-----
468+
MHcCAQEEIOtZfDuXZr/NC0V3sisR4Chf7RZg6a2dpZesoXMlsPeRoAoGCCqGSM49
469+
AwEHoUQDQgAECfpqx64lrR85MOhdMxNmIgmz8IfmM5VY9ICX9aoaArnD9FjgkBIl
470+
fGmQWxxXDSWH6SQln9tROVZaduenJqDtDw==
471+
-----END EC PRIVATE KEY-----
472+
- key: |
473+
-----BEGIN EC PRIVATE KEY-----
474+
MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
475+
AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
476+
h27LAir5RqxByHvua2XsP46rSTChof78uw==
477+
-----END EC PRIVATE KEY-----
478+
"},
479+
)?;
480+
481+
let config = Figment::new()
482+
.merge(Yaml::file("config.yaml"))
483+
.extract_inner::<SecretsConfig>("secrets")?;
484+
485+
Handle::current().block_on(async move {
486+
assert_eq!(
487+
config.encryption().await.unwrap(),
488+
[
489+
0, 0, 17, 17, 34, 34, 51, 51, 68, 68, 85, 85, 102, 102, 119, 119, 136,
490+
136, 153, 153, 170, 170, 187, 187, 204, 204, 221, 221, 238, 238, 255,
491+
255
492+
]
493+
);
494+
495+
let key_store = config.key_store().await.unwrap();
496+
assert!(key_store.iter().any(|k| k.kid() == Some("lekid0")));
497+
assert!(key_store.iter().any(|k| k.kid() == Some("040b0ab8")));
498+
});
499+
500+
Ok(())
501+
});
502+
})
503+
.await
504+
.unwrap();
505+
}
506+
}

crates/keystore/src/lib.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
use std::{ops::Deref, sync::Arc};
1010

1111
use der::{Decode, Encode, EncodePem, zeroize::Zeroizing};
12-
use elliptic_curve::{pkcs8::EncodePrivateKey, sec1::ToEncodedPoint};
12+
use elliptic_curve::{
13+
pkcs8::{EncodePrivateKey, EncodePublicKey},
14+
sec1::ToEncodedPoint,
15+
};
16+
use k256::sha2::{Digest, Sha256};
1317
use mas_iana::jose::{JsonWebKeyType, JsonWebSignatureAlg};
1418
pub use mas_jose::jwk::{JsonWebKey, JsonWebKeySet};
1519
use mas_jose::{
@@ -179,6 +183,24 @@ impl PrivateKey {
179183
}
180184
}
181185

186+
/// Returns the fingerprint of the private key.
187+
///
188+
/// The fingerprint is calculated as the SHA256 sum over the PKCS#8 ASN.1
189+
/// DER-encoded bytes of the private key’s corresponding public key.
190+
///
191+
/// # Errors
192+
///
193+
/// Errors if the DER representation of the public key can’t be derived.
194+
pub fn fingerprint(&self) -> Result<[u8; 32], pkcs1::Error> {
195+
let bytes = match self {
196+
PrivateKey::Rsa(key) => key.to_public_key().to_public_key_der()?,
197+
PrivateKey::EcP256(key) => key.public_key().to_public_key_der()?,
198+
PrivateKey::EcP384(key) => key.public_key().to_public_key_der()?,
199+
PrivateKey::EcK256(key) => key.public_key().to_public_key_der()?,
200+
};
201+
Ok(Sha256::digest(bytes).into())
202+
}
203+
182204
/// Serialize the key as a DER document
183205
///
184206
/// It will use the most common format depending on the key type: PKCS1 for

docs/config.schema.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,11 +1553,9 @@
15531553
"KeyConfig": {
15541554
"description": "A single key with its key ID and optional password.",
15551555
"type": "object",
1556-
"required": [
1557-
"kid"
1558-
],
15591556
"properties": {
15601557
"kid": {
1558+
"description": "The key ID `kid` of the key as used by JWKs.\n\nIf not given, `kid` will be derived from the key by hex-encoding the first four bytes of the key’s fingerprint.",
15611559
"type": "string"
15621560
},
15631561
"password_file": {

docs/reference/configuration.md

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -197,35 +197,7 @@ secrets:
197197
# Signing keys
198198
keys:
199199
# It needs at least an RSA key to work properly
200-
- kid: "ahM2bien"
201-
key: |
202-
-----BEGIN RSA PRIVATE KEY-----
203-
MIIEowIBAAKCAQEAuf28zPUp574jDRdX6uN0d7niZCIUpACFo+Po/13FuIGsrpze
204-
yMX6CYWVPalgXW9FCrhxL+4toJRy5npjkgsLFsknL5/zXbWKFgt69cMwsWJ9Ra57
205-
bonSlI7SoCuHhtw7j+sAlHAlqTOCAVz6P039Y/AGvO6xbC7f+9XftWlbbDcjKFcb
206-
pQilkN9qtkdEH7TLayMAFOsgNvBlwF9+oj9w5PIk3veRTdBXI4GlHjhhzqGZKiRp
207-
oP9HnycHHveyT+C33vuhQso5a3wcUNuvDVOixSqR4kvSt4UVWNK/KmEQmlWU1/m9
208-
ClIwrs8Q79q0xkGaSa0iuG60nvm7tZez9TFkxwIDAQABAoIBAHA5YkppQ7fJSm0D
209-
wNDCHeyABNJWng23IuwZAOXVNxB1bjSOAv8yNgS4zaw/Hx5BnW8yi1lYZb+W0x2u
210-
i5X7g91j0nkyEi5g88kJdFAGTsM5ok0BUwkHsEBjTUPIACanjGjya48lfBP0OGWK
211-
LJU2Acbjda1aeUPFpPDXw/w6bieEthQwroq3DHCMnk6i9bsxgIOXeN04ij9XBmsH
212-
KPCP2hAUnZSlx5febYfHK7/W95aJp22qa//eHS8cKQZCJ0+dQuZwLhlGosTFqLUm
213-
qhPlt/b1EvPPY0cq5rtUc2W31L0YayVEHVOQx1fQIkH2VIUNbAS+bfVy+o6WCRk6
214-
s1XDhsECgYEA30tykVTN5LncY4eQIww2mW8v1j1EG6ngVShN3GuBTuXXaEOB8Duc
215-
yT7yJt1ZhmaJwMk4agmZ1/f/ZXBtfLREGVzVvuwqRZ+LHbqIyhi0wQJA0aezPote
216-
uTQnFn+IveHGtpQNDYGL/UgkexuCxbc2HOZG51JpunCK0TdtVfO/9OUCgYEA1TuS
217-
2WAXzNudRG3xd/4OgtkLD9AvfSvyjw2LkwqCMb3A5UEqw7vubk/xgnRvqrAgJRWo
218-
jndgRrRnikHCavDHBO0GAO/kzrFRfw+e+r4jcLl0Yadke8ndCc7VTnx4wQCrMi5H
219-
7HEeRwaZONoj5PAPyA5X+N/gT0NNDA7KoQT45DsCgYBt+QWa6A5jaNpPNpPZfwlg
220-
9e60cAYcLcUri6cVOOk9h1tYoW7cdy+XueWfGIMf+1460Z90MfhP8ncZaY6yzUGA
221-
0EUBO+Tx10q3wIfgKNzU9hwgZZyU4CUtx668mOEqy4iHoVDwZu4gNyiobPsyDzKa
222-
dxtSkDc8OHNV6RtzKpJOtQKBgFoRGcwbnLH5KYqX7eDDPRnj15pMU2LJx2DJVeU8
223-
ERY1kl7Dke6vWNzbg6WYzPoJ/unrJhFXNyFmXj213QsSvN3FyD1pFvp/R28mB/7d
224-
hVa93vzImdb3wxe7d7n5NYBAag9+IP8sIJ/bl6i9619uTxwvgtUqqzKPuOGY9dnh
225-
oce1AoGBAKZyZc/NVgqV2KgAnnYlcwNn7sRSkM8dcq0/gBMNuSZkfZSuEd4wwUzR
226-
iFlYp23O2nHWggTkzimuBPtD7Kq4jBey3ZkyGye+sAdmnKkOjNILNbpIZlT6gK3z
227-
fBaFmJGRJinKA+BJeH79WFpYN6SBZ/c3s5BusAbEU7kE5eInyazP
228-
-----END RSA PRIVATE KEY-----
200+
- key_file: keys/rsa_key
229201
- kid: "iv1aShae"
230202
key: |
231203
-----BEGIN EC PRIVATE KEY-----
@@ -260,9 +232,7 @@ The following key types are supported:
260232
- ECDSA with the P-384 (`secp384r1`) curve
261233
- ECDSA with the K-256 (`secp256k1`) curve
262234

263-
Each entry must have a unique `kid`, plus the key itself.
264-
The `kid` can be any case-sensitive string value as long as it is unique to this list;
265-
a key’s `kid` value must be stable across restarts.
235+
Each entry in the list corresponds to one signing key used by MAS.
266236
The key can either be specified inline (with the `key` property),
267237
or loaded from a file (with the `key_file` property).
268238
The following key formats are supported:
@@ -271,8 +241,15 @@ The following key formats are supported:
271241
- PKCS#8 PEM or DER-encoded RSA or ECDSA private key, encrypted or not
272242
- SEC1 PEM or DER-encoded ECDSA private key
273243

244+
A [JWK Key ID] is automatically derived from each key.
245+
To override this default, set `kid` to a custom value.
246+
The `kid` can be any case-sensitive string value as long as it is unique to this list;
247+
a key’s `kid` value must be stable across restarts.
248+
274249
For PKCS#8 encoded keys, the `password` or `password_file` properties can be used to decrypt the key.
275250

251+
[JWK Key ID]: <https://datatracker.ietf.org/doc/html/rfc7517#section-4.5>
252+
276253
## `passwords`
277254

278255
Settings related to the local password database

0 commit comments

Comments
 (0)