Skip to content

Commit d843e12

Browse files
committed
feat: Key-Set@Version + Server-Side Decrypt-With-Keyset
1 parent 7bfdf61 commit d843e12

47 files changed

Lines changed: 2486 additions & 134 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG/docs_key-autorotation-spec.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,24 @@
88
- Introduce `RekeyOperation` trait to unify symmetric, keypair, and certificate rotation logic ([#968](https://github.com/Cosmian/kms/pull/968))
99
- Add `offset` field to `ReCertify` struct per KMIP 2.1 §6.1.45 for date-based activation scheduling ([#968](https://github.com/Cosmian/kms/pull/968))
1010
- Implement KMIP §4.57 transition 6 auto-deactivation: Active keys automatically transition to Deactivated when their DeactivationDate is reached ([#968](https://github.com/Cosmian/kms/pull/968))
11+
- Implement keyset resolution: `name@latest`, `name@first`, `name@N` syntax to address specific key generations by `rotate_name` ([#968](https://github.com/Cosmian/kms/pull/968))
12+
- Implement try-each-key decryption: Decrypt, SignatureVerify, and MACVerify operations with a bare keyset name walk the rotation chain (newest→oldest) until one key succeeds ([#968](https://github.com/Cosmian/kms/pull/968))
13+
- Add `--keyset-decrypt-max-attempts` server config flag (default: 100) to cap rotation chain traversal depth ([#968](https://github.com/Cosmian/kms/pull/968))
14+
- Add `find_by_rotate_name()` to `ObjectsStore` trait with SQLite, PostgreSQL, and MySQL implementations for keyset lookup ([#968](https://github.com/Cosmian/kms/pull/968))
15+
- Inherit `rotate_name` from old key to new key during ReKey so keyset resolution works across generations ([#968](https://github.com/Cosmian/kms/pull/968))
16+
- Implement `ckms sym keys set-rotation-policy` CLI command with `--interval`, `--offset`, `--rotation-name` flags ([#968](https://github.com/Cosmian/kms/pull/968))
17+
- Implement `ckms sym keys get-rotation-policy` CLI command to display interval, offset, keyset name, generation, and last rotation date ([#968](https://github.com/Cosmian/kms/pull/968))
18+
- Add Web UI components for Set Rotation Policy, Get Rotation Policy, and Re-Key under Symmetric Keys ([#968](https://github.com/Cosmian/kms/pull/968))
19+
- Add HSM keyset support: store keyset metadata in `CKA_LABEL` (`name::gen::base_id[::latest]`); `SetAttribute rotate_name` writes `CKA_LABEL`; `SetAttribute rotate_interval` writes `CKA_START_DATE`/`CKA_END_DATE`; `Re-Key` on HSM UIDs generates a new HSM key and updates both CKA_LABELs; keyset resolution via `find_by_rotate_name` enumerates PKCS#11 objects and sorts by generation ([#968](https://github.com/Cosmian/kms/pull/968))
20+
- Enrich `HsmStore::retrieve()` export path with CKA_LABEL keyset metadata (`rotate_name`, `rotate_generation`, `rotate_latest`) so the non-latest guard works for extractable HSM keys ([#968](https://github.com/Cosmian/kms/pull/968))
1121

1222
## Security
1323

1424
- Mark `x-rotate-generation` and `x-rotate-date` as server-managed read-only attributes: reject user modifications via AddAttribute, SetAttribute, ModifyAttribute, and DeleteAttribute ([#968](https://github.com/Cosmian/kms/pull/968))
15-
- Reject `Re-Key` and `Re-Key Key Pair` on HSM-managed keys (`hsm::` UID prefix) with an explicit `Not Supported` error instead of silently failing deep in the pipeline ([#968](https://github.com/Cosmian/kms/pull/968))
25+
- Guard `Re-Key` / `Re-Key Key Pair` to reject rotation of non-latest keyset members (`x-rotate-latest = false` and `x-rotate-name` set) with a clear error; keys without a keyset name are unaffected ([#968](https://github.com/Cosmian/kms/pull/968))
26+
- Reject `SetAttribute rotate_offset` on HSM keys with `NotSupported` — HSM rotation scheduling uses `CKA_START_DATE`/`CKA_END_DATE`, not SQL-managed offset windows ([#968](https://github.com/Cosmian/kms/pull/968))
1627
- Restrict rotation to Active or Deactivated keys: `Re-Key`, `Re-Key Key Pair`, and `ReCertify` now reject PreActive, Compromised, Destroyed, and Destroyed_Compromised objects with an explicit error (KMIP §6.1.46 does not list `Wrong_Key_Lifecycle_State` for Re-Key) ([#968](https://github.com/Cosmian/kms/pull/968))
28+
- Reject `@` character in `rotate_name` attribute values to prevent keyset versioning syntax injection ([#968](https://github.com/Cosmian/kms/pull/968))
1729

1830
## Bug Fixes
1931

@@ -33,6 +45,9 @@
3345
- Fix symmetric rekey hardcoding `State::Active` — now uses `setup_object_lifecycle` for date-based state computation ([#968](https://github.com/Cosmian/kms/pull/968))
3446
- Fix `setup_object_lifecycle` not storing `activation_date` for `PreActive` keys — offset-based activation scheduling now works correctly ([#968](https://github.com/Cosmian/kms/pull/968))
3547
- Add `ReCertify` request/response deserialization to KMIP 2.1 message handler ([#968](https://github.com/Cosmian/kms/pull/968))
48+
- Fix `find_by_rotate_name` SQL queries using wrong JSON path (`$.rotate_name``$.RotateName`) matching PascalCase serde serialization ([#968](https://github.com/Cosmian/kms/pull/968))
49+
- Fix `GetAttributes` not returning rotation policy fields (interval, offset, name, generation, date) because they lack `Tag` enum entries ([#968](https://github.com/Cosmian/kms/pull/968))
50+
- Implement `find_by_rotate_name()` on Redis-Findex backend and index `rotate_name` attribute in Findex keywords ([#968](https://github.com/Cosmian/kms/pull/968))
3651
- Fix `ReCertify.generate_replacement` passing empty user to `get_subject`/`get_issuer` — use certificate owner instead ([#968](https://github.com/Cosmian/kms/pull/968))
3752
- Fix `ReCertify` not computing lifecycle state from offset — certificates with future activation_date are now `PreActive` ([#968](https://github.com/Cosmian/kms/pull/968))
3853

@@ -71,6 +86,9 @@
7186
- Add 3 negative ReCertify test vectors (missing UID, non-existent, not a certificate) ([#968](https://github.com/Cosmian/kms/pull/968))
7287
- Add 2 offset state verification vectors (rekey + rekey-keypair: Offset=0 → Active, Offset=86400 → PreActive) ([#968](https://github.com/Cosmian/kms/pull/968))
7388
- Add 2 negative state restriction vectors: `rekey_preactive_fails`, `rekey_keypair_preactive_fails` ([#968](https://github.com/Cosmian/kms/pull/968))
89+
- Add 7 keyset resolution test vectors: `keyset_encrypt_latest`, `keyset_encrypt_bare_name`, `keyset_encrypt_latest_after_rotation`, `keyset_decrypt_try_each`, `keyset_decrypt_double_rotation`, `keyset_decrypt_at_latest`, `keyset_rotate_name_at_rejected` ([#968](https://github.com/Cosmian/kms/pull/968))
90+
- Add HSM keyset support: `find_due_for_rotation` on HsmStore reads `CKA_START_DATE`/`CKA_END_DATE`; keyset metadata stored in `CKA_LABEL` (`name::gen::key_id[::latest]`); `SetAttribute rotate_name` writes `CKA_LABEL` via `C_SetAttributeValue`; `SetAttribute rotate_interval` writes `CKA_START_DATE`/`CKA_END_DATE`; `Re-Key` on HSM UIDs generates a new HSM key and updates both CKA_LABELs; `walk_keyset_chain` resolves HSM keysets by `CKA_LABEL` without `ReplacedObjectLink` ([#968](https://github.com/Cosmian/kms/pull/968))
91+
- Add 3 HSM keyset test vectors: `resident_keyset_set_rotate_name`, `resident_keyset_rekey_and_decrypt`, `resident_keyset_double_rotation` + 1 negative: `hsm_rotate_offset_rejected` ([#968](https://github.com/Cosmian/kms/pull/968))
7492

7593
## Documentation
7694

@@ -81,6 +99,7 @@
8199
- Correct HSM key rotation section: the KMS cannot use KMIP `Re-Key` on
82100
HSM-managed keys (no SQL attribute storage, non-extractable key material);
83101
use PKCS#11 vendor tools instead ([#968](https://github.com/Cosmian/kms/pull/968))
102+
- Add HSM keyset section to `key_auto_rotation.md`: CKA_LABEL convention, UID generation format, supported/unsupported attributes, example workflow, and keyset resolution description ([#968](https://github.com/Cosmian/kms/pull/968))
84103
- Document `x-rotate-generation` and `x-rotate-date` invariants: monotonically
85104
increasing counter unique within a key-set, authoritative last-rotation
86105
timestamp relied on by `is_due_for_rotation` ([#968](https://github.com/Cosmian/kms/pull/968))

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub(crate) mod create_key;
22
pub(crate) mod encrypt_decrypt;
33
pub(crate) mod rekey;
4+
pub(crate) mod rotation_policy;
45

56
pub(crate) const SUB_COMMAND: &str = "sym";
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use test_kms_server::start_default_test_kms_server;
2+
3+
use crate::{
4+
error::result::CosmianResult,
5+
tests::{
6+
symmetric::create_key::create_symmetric_key,
7+
utils::{owner_config, run_ckms, run_ckms_expect_error},
8+
},
9+
};
10+
11+
/// Set the rotation policy for a symmetric key via the CLI.
12+
pub(crate) fn set_rotation_policy(
13+
cli_conf_path: &str,
14+
key_id: &str,
15+
interval: i64,
16+
offset: Option<i64>,
17+
rotate_name: Option<&str>,
18+
) -> CosmianResult<String> {
19+
let mut args = vec!["sym", "keys", "set-rotation-policy", "--key-id", key_id];
20+
let interval_str = interval.to_string();
21+
args.extend(["--interval", &interval_str]);
22+
let offset_str;
23+
if let Some(o) = offset {
24+
offset_str = o.to_string();
25+
args.extend(["--offset", &offset_str]);
26+
}
27+
if let Some(name) = rotate_name {
28+
args.extend(["--rotation-name", name]);
29+
}
30+
run_ckms(cli_conf_path, &args)
31+
}
32+
33+
/// Get the rotation policy for a symmetric key via the CLI.
34+
pub(crate) fn get_rotation_policy(cli_conf_path: &str, key_id: &str) -> CosmianResult<String> {
35+
let args = vec!["sym", "keys", "get-rotation-policy", "--key-id", key_id];
36+
run_ckms(cli_conf_path, &args)
37+
}
38+
39+
#[tokio::test]
40+
pub(crate) async fn test_set_and_get_rotation_policy() -> CosmianResult<()> {
41+
let ctx = start_default_test_kms_server().await;
42+
let owner_client_conf_path = owner_config(ctx);
43+
44+
// Create a symmetric key
45+
let key_id = create_symmetric_key(&owner_client_conf_path, &["--number-of-bits", "256"])?;
46+
47+
// Set rotation policy with interval, offset, and name
48+
let output = set_rotation_policy(
49+
&owner_client_conf_path,
50+
&key_id,
51+
86400,
52+
Some(3600),
53+
Some("test-keyset"),
54+
)?;
55+
assert!(output.contains("Rotation policy set successfully"));
56+
57+
// Get rotation policy and verify
58+
let output = get_rotation_policy(&owner_client_conf_path, &key_id)?;
59+
assert!(
60+
output.contains("86400"),
61+
"expected interval=86400 in: {output}"
62+
);
63+
assert!(output.contains("3600"), "expected offset=3600 in: {output}");
64+
assert!(
65+
output.contains("test-keyset"),
66+
"expected name=test-keyset in: {output}"
67+
);
68+
69+
Ok(())
70+
}
71+
72+
#[tokio::test]
73+
pub(crate) async fn test_set_rotation_policy_name_rejects_at() -> CosmianResult<()> {
74+
let ctx = start_default_test_kms_server().await;
75+
let owner_client_conf_path = owner_config(ctx);
76+
77+
// Create a symmetric key
78+
let key_id = create_symmetric_key(&owner_client_conf_path, &["--number-of-bits", "256"])?;
79+
80+
// Try to set rotation policy with a name containing '@' — should fail
81+
let args = vec![
82+
"sym",
83+
"keys",
84+
"set-rotation-policy",
85+
"--key-id",
86+
&key_id,
87+
"--interval",
88+
"86400",
89+
"--rotation-name",
90+
"bad@name",
91+
];
92+
let stderr = run_ckms_expect_error(&owner_client_conf_path, &args)?;
93+
assert!(
94+
stderr.contains('@'),
95+
"expected error mentioning '@' in: {stderr}"
96+
);
97+
98+
Ok(())
99+
}
100+
101+
#[tokio::test]
102+
pub(crate) async fn test_set_rotation_policy_interval_only() -> CosmianResult<()> {
103+
let ctx = start_default_test_kms_server().await;
104+
let owner_client_conf_path = owner_config(ctx);
105+
106+
// Create a symmetric key
107+
let key_id = create_symmetric_key(&owner_client_conf_path, &["--number-of-bits", "256"])?;
108+
109+
// Set rotation policy with interval only (no offset, no name)
110+
let output = set_rotation_policy(&owner_client_conf_path, &key_id, 43200, None, None)?;
111+
assert!(output.contains("Rotation policy set successfully"));
112+
113+
// Get rotation policy and verify
114+
let output = get_rotation_policy(&owner_client_conf_path, &key_id)?;
115+
assert!(
116+
output.contains("43200"),
117+
"expected interval=43200 in: {output}"
118+
);
119+
assert!(
120+
output.contains("not set") || !output.contains("offset"),
121+
"expected no offset set"
122+
);
123+
124+
Ok(())
125+
}
Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
use clap::Parser;
2-
use cosmian_kms_client::KmsClient;
2+
use cosmian_kms_client::{
3+
KmsClient,
4+
kmip_2_1::{kmip_operations::GetAttributes, kmip_types::UniqueIdentifier},
5+
};
36

4-
use crate::error::result::KmsCliResult;
7+
use crate::{
8+
actions::console,
9+
error::result::{KmsCliResult, KmsCliResultHelper},
10+
};
511

612
/// Get the automatic rotation policy for a symmetric key.
13+
///
14+
/// Displays: rotation interval, offset, keyset name, generation, and last rotation date.
715
#[derive(Parser, Debug)]
816
#[clap(verbatim_doc_comment)]
917
pub struct GetRotationPolicyAction {
@@ -13,9 +21,45 @@ pub struct GetRotationPolicyAction {
1321
}
1422

1523
impl GetRotationPolicyAction {
16-
#[allow(clippy::unused_async)]
17-
pub async fn run(&self, _kms_rest_client: KmsClient) -> KmsCliResult<()> {
18-
// TODO: implement KMIP Get Attributes call to retrieve rotation policy
24+
pub async fn run(&self, kms_rest_client: KmsClient) -> KmsCliResult<()> {
25+
let uid = UniqueIdentifier::TextString(self.key_id.clone());
26+
27+
let response = kms_rest_client
28+
.get_attributes(GetAttributes {
29+
unique_identifier: Some(uid),
30+
attribute_reference: None,
31+
})
32+
.await
33+
.with_context(|| "failed retrieving attributes")?;
34+
35+
let attrs = &response.attributes;
36+
37+
let interval = attrs
38+
.rotate_interval
39+
.map_or_else(|| "not set".to_owned(), |v| v.to_string());
40+
let offset = attrs
41+
.rotate_offset
42+
.map_or_else(|| "not set".to_owned(), |v| v.to_string());
43+
let name = attrs.rotate_name.as_deref().unwrap_or("not set");
44+
let generation = attrs
45+
.rotate_generation
46+
.map_or_else(|| "not set".to_owned(), |v| v.to_string());
47+
let date = attrs
48+
.rotate_date
49+
.map_or_else(|| "never".to_owned(), |d| d.to_string());
50+
51+
let output = format!(
52+
"Rotation policy for key: {}\n\
53+
\x20 Interval (seconds): {interval}\n\
54+
\x20 Offset (seconds): {offset}\n\
55+
\x20 Keyset name: {name}\n\
56+
\x20 Generation: {generation}\n\
57+
\x20 Last rotation date: {date}",
58+
response.unique_identifier
59+
);
60+
61+
console::Stdout::new(&output).write()?;
62+
1963
Ok(())
2064
}
2165
}

crate/clients/clap/src/actions/symmetric/keys/set_rotation_policy.rs

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
use clap::Parser;
2-
use cosmian_kms_client::KmsClient;
2+
use cosmian_kms_client::{
3+
KmsClient,
4+
kmip_2_1::{
5+
kmip_attributes::Attribute, kmip_operations::SetAttribute, kmip_types::UniqueIdentifier,
6+
},
7+
};
38

4-
use crate::error::result::KmsCliResult;
9+
use crate::{
10+
actions::console,
11+
error::result::{KmsCliResult, KmsCliResultHelper},
12+
};
513

614
/// Set the automatic rotation policy for a symmetric key.
15+
///
16+
/// This configures:
17+
/// - The rotation interval (how often the key is automatically re-keyed)
18+
/// - An optional offset (delay before first rotation)
19+
/// - An optional keyset name (for addressing key generations via name@version syntax)
720
#[derive(Parser, Debug)]
821
#[clap(verbatim_doc_comment)]
922
pub struct SetRotationPolicyAction {
@@ -18,12 +31,52 @@ pub struct SetRotationPolicyAction {
1831
/// Offset in seconds from the initial date before the first rotation occurs.
1932
#[clap(long = "offset", short = 'o')]
2033
offset_secs: Option<i64>,
34+
35+
/// A keyset name for addressing key generations via name@latest, name@first, name@N syntax.
36+
/// Must not contain the '@' character.
37+
#[clap(long = "rotation-name", short = 'n')]
38+
rotate_name: Option<String>,
2139
}
2240

2341
impl SetRotationPolicyAction {
24-
#[allow(clippy::unused_async)]
25-
pub async fn run(&self, _kms_rest_client: KmsClient) -> KmsCliResult<()> {
26-
// TODO: implement KMIP Modify Attribute call to set rotation policy
42+
pub async fn run(&self, kms_rest_client: KmsClient) -> KmsCliResult<()> {
43+
let uid = UniqueIdentifier::TextString(self.key_id.clone());
44+
45+
// Set the rotation interval
46+
kms_rest_client
47+
.set_attribute(SetAttribute {
48+
unique_identifier: Some(uid.clone()),
49+
new_attribute: Attribute::RotateInterval(self.interval_secs),
50+
})
51+
.await
52+
.with_context(|| "failed setting RotateInterval attribute")?;
53+
54+
// Set the rotation offset if provided
55+
if let Some(offset) = self.offset_secs {
56+
kms_rest_client
57+
.set_attribute(SetAttribute {
58+
unique_identifier: Some(uid.clone()),
59+
new_attribute: Attribute::RotateOffset(offset),
60+
})
61+
.await
62+
.with_context(|| "failed setting RotateOffset attribute")?;
63+
}
64+
65+
// Set the rotation name if provided
66+
if let Some(ref name) = self.rotate_name {
67+
kms_rest_client
68+
.set_attribute(SetAttribute {
69+
unique_identifier: Some(uid.clone()),
70+
new_attribute: Attribute::RotateName(name.clone()),
71+
})
72+
.await
73+
.with_context(|| "failed setting RotateName attribute")?;
74+
}
75+
76+
let mut stdout = console::Stdout::new("Rotation policy set successfully.");
77+
stdout.set_unique_identifier(&uid);
78+
stdout.write()?;
79+
2780
Ok(())
2881
}
2982
}

crate/hsm/base_hsm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ lru = { workspace = true }
2626
pkcs11-sys = { workspace = true }
2727
rand = { workspace = true }
2828
thiserror = { workspace = true }
29+
time = { workspace = true }
2930
uuid = { workspace = true, features = ["v4"] }
3031
zeroize = { workspace = true }

crate/hsm/base_hsm/src/hsm_lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ use pkcs11_sys::{
88
CK_C_Finalize, CK_C_FindObjects, CK_C_FindObjectsFinal, CK_C_FindObjectsInit, CK_C_GenerateKey,
99
CK_C_GenerateKeyPair, CK_C_GenerateRandom, CK_C_GetAttributeValue, CK_C_GetInfo,
1010
CK_C_GetMechanismInfo, CK_C_GetMechanismList, CK_C_INITIALIZE_ARGS, CK_C_Initialize,
11-
CK_C_Login, CK_C_Logout, CK_C_OpenSession, CK_C_SeedRandom, CK_C_Sign, CK_C_SignInit,
12-
CK_C_UnwrapKey, CK_C_WrapKey, CKF_OS_LOCKING_OK, CKR_CRYPTOKI_ALREADY_INITIALIZED, CKR_OK,
11+
CK_C_Login, CK_C_Logout, CK_C_OpenSession, CK_C_SeedRandom, CK_C_SetAttributeValue, CK_C_Sign,
12+
CK_C_SignInit, CK_C_UnwrapKey, CK_C_WrapKey, CKF_OS_LOCKING_OK,
13+
CKR_CRYPTOKI_ALREADY_INITIALIZED, CKR_OK,
1314
};
1415

1516
use crate::{HResult, hsm_call};
@@ -85,6 +86,7 @@ pub struct HsmLib {
8586
pub(crate) C_SeedRandom: CK_C_SeedRandom,
8687

8788
pub(crate) C_GetAttributeValue: CK_C_GetAttributeValue,
89+
pub(crate) C_SetAttributeValue: CK_C_SetAttributeValue,
8890

8991
pub(crate) C_GetInfo: CK_C_GetInfo,
9092
pub(crate) C_GetMechanismList: CK_C_GetMechanismList,
@@ -130,6 +132,7 @@ impl HsmLib {
130132
C_GenerateRandom: Some(*library.get(b"C_GenerateRandom")?),
131133
C_SeedRandom: Some(*library.get(b"C_SeedRandom")?),
132134
C_GetAttributeValue: Some(*library.get(b"C_GetAttributeValue")?),
135+
C_SetAttributeValue: Some(*library.get(b"C_SetAttributeValue")?),
133136
C_GetInfo: Some(*library.get(b"C_GetInfo")?),
134137
C_GetMechanismList: Some(*library.get(b"C_GetMechanismList")?),
135138
C_GetMechanismInfo: Some(*library.get(b"C_GetMechanismInfo")?),

0 commit comments

Comments
 (0)