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
6 changes: 6 additions & 0 deletions CHANGELOG/docs_key-autorotation-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Documentation

- Add key auto-rotation specification document covering all 6 rotation
scenarios (plain symmetric, wrapping key, wrapped key, asymmetric pair,
wrapped private key, server-wide KEK), rotation policy attributes,
server-side scheduler, KMIP attribute tables, and implementation roadmap.
55 changes: 55 additions & 0 deletions CHANGELOG/feat_key-rotation-manual.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## Features

- Implement KMIP ReKey operation for symmetric keys with name transfer per §4.4
- Support re-wrapping of dependent keys when a wrapping key is rekeyed
- Add `find_wrapped_by()` method to `ObjectsStore` trait (SQLite, PostgreSQL, MySQL implementations)
- Implement KMIP `ReCertify` operation (§4.7) — certificate rotation with new UID and replacement links
- Add proper `ReCertify` and `ReCertifyResponse` KMIP 2.1 types compliant with both KMIP 1.x and 2.x
- Introduce `RekeyOperation` trait to unify symmetric, keypair, and certificate rotation logic
- Add `offset` field to `ReCertify` struct per KMIP 2.1 §6.1.45 for date-based activation scheduling

## Refactor

- Reorganize ReKey modules into `rekey/` folder: `mod.rs`, `symmetric.rs`, `keypair.rs`, `common.rs`; move `ReCertify` handler to `operations/recertify.rs` (top-level, parallel to `certify.rs`)
- Extract `RekeyOperation` trait into `common.rs` with `execute_rekey()` orchestrator — shared 2-phase commit logic
- Extract 6 shared helpers into `common.rs`: `compute_replacement_dates`, `prepare_replacement_attributes`, `update_old_key_after_rekey`, `set_rotation_metadata_on_new_key`, `clear_rotation_flags_on_old_key`, `enforce_privileged_user`
- Add `KeyRetirement` struct + `finalize_rekey` function in `common.rs` — shared Phase 2 logic (retire old keys + rewrap dependants + atomic commit) used by both symmetric and keypair rekey
- Move `compute_rotation_uid` and `rewrap_dependants` from `symmetric.rs` to `common.rs`; keypair rekey now uses name-preserving UIDs
- Convert `ReKeyKeyPair` to 2-phase commit (matching symmetric) to support dependant re-wrapping on public keys
- Set rotation metadata (`rotate_generation`, `rotate_date`, `rotate_latest`, `rotate_interval`) on new keys during `ReKeyKeyPair`
- Clear rotation flags on old keys during `ReKeyKeyPair` to prevent scheduler re-triggering
- Add default implementations to `RekeyOperation` trait for `detect_wrapping`, `persist_new_key`, `finalize_dependants`, and `rewrap_new_objects` — eliminates duplicate code across symmetric.rs, keypair.rs, and recertify.rs
- Extract `extract_rewrap_spec`, `extract_wrapping_key_uid`, and `retrieve_eligible_keys` into `common.rs` as shared helpers — removes 40+ lines of duplicated logic
- Extract shared `validate_no_crypto_param_change` into `common.rs` — validates that ReKey/ReKeyKeyPair requests do not alter algorithm, curve, or key length; now applies to both symmetric and keypair rekey
- Refactor `prepare_attributes` in `keypair.rs` — extract `finalize_replacement_key` helper to eliminate SK/PK code duplication
- Move `setup_new_key` and `finalize_replacement_key` from keypair.rs to common.rs as shared helpers
- Extract `preserve_wrapping_key_link` into common.rs — copies WrappingKeyLink from old to new key
- Split `rewrap_dependants` (70→25 lines) by extracting `rewrap_single_dependant` helper
- Split `relink_keys_to_new_certificate` by extracting `relink_single_key` helper

## Bug Fixes

- Transfer `Name` attribute from old key to new key during ReKey per KMIP §4.4
- Return error instead of silently skipping when a user-supplied wrapping key ID equals the key being wrapped
- Bypass ownership check for server-configured KEK during wrapping operations
- Fix symmetric ReKey missing server-wide KEK wrapping and unwrapped-cache insert (now consistent with keypair rekey via shared default)
- Fix keypair rekey not preserving WrappingKeyLink on replacement keys
- Fix symmetric rekey hardcoding `State::Active` — now uses `setup_object_lifecycle` for date-based state computation
- Fix `setup_object_lifecycle` not storing `activation_date` for `PreActive` keys — offset-based activation scheduling now works correctly
- Add `ReCertify` request/response deserialization to KMIP 2.1 message handler
- Fix `ReCertify.generate_replacement` passing empty user to `get_subject`/`get_issuer` — use certificate owner instead
- Fix `ReCertify` not computing lifecycle state from offset — certificates with future activation_date are now `PreActive`

## Documentation

- Add Certificate Renewal (ReCertify) section to key_auto_rotation.md with RFC references (RFC 4210, 4211, 5280, 2986, 5272), KMIP 2.1 §6.1.45 attribute table, and CMP relationship explanation

## Testing

- Add 9 symmetric ReKey test vectors (basic, wrapped, wrapping-key re-wrap, name transfer, offset, links)
- Add 27 ReKeyKeyPair test vectors (RSA, EC, ML-KEM, ML-DSA, SLH-DSA, X25519, secp256k1)
- Add Covercrypt ReKeyKeyPair test vector (in-place attribute rekey with same UIDs)
- Add access privilege escalation test vector for ReKey
- Add 4 ReCertify test vectors (self-signed, chain, with-links, with-offset)
- Add 3 negative ReCertify test vectors (missing UID, non-existent, not a certificate)
- Add 2 offset state verification vectors (rekey + rekey-keypair: Offset=0 → Active, Offset=86400 → PreActive)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The **Cosmian KMS** presents some unique features, such as:
- **Other integrations**: [OpenSSH](./documentation/docs/integrations/openssh.md), [S/MIME email encryption](./documentation/docs/integrations/smime.md), and [FortiGate / FortiOS](./documentation/docs/integrations/fortigate.md).
- **Security and standards**: [FIPS 140-3](./documentation/docs/certifications_and_compliance/fips.md), [KMIP 1.0-2.1 binary and JSON TTLV support](./documentation/docs/kmip_support/introduction/index.md), [state-of-the-art authentication mechanisms](./documentation/docs/configuration/authentication.md), and native compatibility with network appliances such as [FortiGate / FortiOS](./documentation/docs/integrations/fortigate.md).
- **HSM support**: [Utimaco, SmartCard-HSM/Nitrokey HSM 2, Proteccio, Crypt2pay, and others](./documentation/docs/hsm_support/introduction/index.md), with KMS keys wrapped by HSMs.
- **Operations**: full-featured [CLI and graphical clients](https://docs.cosmian.com/kms_clients/), [high-availability mode](./documentation/docs/installation/high_availability_mode.md), [confidential cloud deployment](./documentation/docs/installation/marketplace_guide.md), [OpenTelemetry integration](./documentation/docs/configuration/logging.md), and [OpenAPI 3.1 spec with Swagger UI](./documentation/docs/kmip_support/openapi.md) for interactive API exploration.
- **Operations**: full-featured [CLI and graphical clients](https://docs.cosmian.com/kms_clients/), [high-availability mode](./documentation/docs/installation/high_availability_mode.md), [confidential cloud deployment](./documentation/docs/installation/marketplace_guide.md), [OpenTelemetry integration](./documentation/docs/configuration/logging.md), [OpenAPI 3.1 spec with Swagger UI](./documentation/docs/kmip_support/openapi.md) for interactive API exploration, and [scheduled key auto-rotation](./documentation/docs/kmip_support/key_auto_rotation.md).

The **Cosmian KMS** is both a Key Management System and a Public Key Infrastructure. As a KMS, it is designed to manage the lifecycle of keys and provide scalable cryptographic services such as on-the-fly key generation, encryption, and decryption operations.

Expand Down
15 changes: 15 additions & 0 deletions crate/interfaces/src/stores/objects_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,19 @@ pub trait ObjectsStore {
user_must_be_owner: bool,
vendor_id: &str,
) -> InterfaceResult<Vec<(String, State, Attributes)>>;

/// Return (uid, state, attributes) for every object whose
/// `key_wrapping_data.encryption_key_information.unique_identifier` equals
/// `wrapping_key_uid`. Used by key rotation to re-wrap all objects protected by
/// the rotated key.
///
/// The default implementation returns an empty list; backends that support
/// JSON-based object storage should override this with an efficient query.
async fn find_wrapped_by(
&self,
_wrapping_key_uid: &str,
_user: &str,
) -> InterfaceResult<Vec<(String, State, Attributes)>> {
Ok(vec![])
}
}
64 changes: 58 additions & 6 deletions crate/kmip/src/kmip_1_4/kmip_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,60 @@ pub struct ReCertifyResponse {
pub template_attribute: Option<TemplateAttribute>,
}

impl From<ReCertify> for kmip_2_1::kmip_operations::ReCertify {
fn from(recertify: ReCertify) -> Self {
let cert_req_type = match recertify.certificate_request_type {
CertificateRequestType::CRMF => kmip_2_1::kmip_types::CertificateRequestType::CRMF,
CertificateRequestType::PKCS10 => kmip_2_1::kmip_types::CertificateRequestType::PKCS10,
CertificateRequestType::PEM => kmip_2_1::kmip_types::CertificateRequestType::PEM,
};
Self {
unique_identifier: Some(recertify.unique_identifier.into()),
certificate_request_type: Some(cert_req_type),
certificate_request_value: Some(recertify.certificate_request_value),
offset: None,
attributes: recertify.template_attribute.map(Into::into),
protection_storage_masks: None,
}
}
}

impl TryFrom<kmip_2_1::kmip_operations::ReCertifyResponse> for ReCertifyResponse {
type Error = KmipError;

fn try_from(value: kmip_2_1::kmip_operations::ReCertifyResponse) -> Result<Self, Self::Error> {
Ok(Self {
unique_identifier: value.unique_identifier.to_string(),
template_attribute: None,
})
}
}

impl From<kmip_2_1::kmip_operations::ReCertify> for ReCertify {
fn from(recertify: kmip_2_1::kmip_operations::ReCertify) -> Self {
let cert_req_type = match recertify.certificate_request_type {
Some(kmip_2_1::kmip_types::CertificateRequestType::CRMF) => {
CertificateRequestType::CRMF
}
Some(kmip_2_1::kmip_types::CertificateRequestType::PKCS10) => {
CertificateRequestType::PKCS10
}
Some(kmip_2_1::kmip_types::CertificateRequestType::PEM) | None => {
CertificateRequestType::PEM
}
};
Self {
unique_identifier: recertify
.unique_identifier
.map_or_else(String::new, |u| u.to_string()),
certificate_request_type: cert_req_type,
certificate_request_value: recertify.certificate_request_value.unwrap_or_default(),
template_attribute: None,
// KMIP 1.4 does not support offset; it is dropped during downgrade.
}
}
}

/// 4.9 Locate
/// This operation requests that the server search for one or more Managed Objects.
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -2647,9 +2701,7 @@ impl TryFrom<Operation> for kmip_2_1::kmip_operations::Operation {
// }
// Operation::Poll(poll) => Self::Poll(poll.into()),
Operation::Query(query) => Self::Query(query.into()),
// Operation::ReCertify(recertify) => {
// Self::ReCertify(recertify.into())
// }
Operation::ReCertify(recertify) => Self::ReCertify(Box::new(recertify.into())),
// Operation::Recover(recover) => {
// Self::Recover(recover.into())
// }
Expand Down Expand Up @@ -2803,9 +2855,9 @@ impl TryFrom<kmip_2_1::kmip_operations::Operation> for Operation {
(*query_response).try_into().context("QueryResponse")?,
))
}
// Operation::ReCertifyResponse(recertify_response) => {
// Self::ReCertifyResponse(recertify_response.into())
// }
kmip_2_1::kmip_operations::Operation::ReCertifyResponse(recertify_response) => {
Self::ReCertifyResponse(recertify_response.try_into().context("ReCertifyResponse")?)
}
// Operation::RecoverResponse(recover_response) => {
// Self::RecoverResponse(recover_response.into())
// }
Expand Down
6 changes: 6 additions & 0 deletions crate/kmip/src/kmip_2_1/kmip_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,9 @@ impl<'de> Deserialize<'de> for RequestMessageBatchItem {
OperationEnumeration::ReKeyKeyPair => {
Operation::ReKeyKeyPair(map.next_value()?)
}
OperationEnumeration::ReCertify => {
Operation::ReCertify(map.next_value()?)
}
x => {
return Err(de::Error::custom(format!(
"Request Message Batch Item: unsupported operation: {x:?}"
Expand Down Expand Up @@ -792,6 +795,9 @@ impl<'de> Deserialize<'de> for ResponseMessageBatchItem {
OperationEnumeration::ReKeyKeyPair => {
Operation::ReKeyKeyPairResponse(map.next_value()?)
}
OperationEnumeration::ReCertify => {
Operation::ReCertifyResponse(map.next_value()?)
}
x => {
return Err(de::Error::custom(format!(
"KMIP 2 response message payload: unsupported operation: \
Expand Down
66 changes: 66 additions & 0 deletions crate/kmip/src/kmip_2_1/kmip_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ pub enum Operation {
PKCS11Response(PKCS11Response),
Query(Query),
QueryResponse(Box<QueryResponse>),
ReCertify(Box<ReCertify>),
ReCertifyResponse(ReCertifyResponse),
ReKey(ReKey),
ReKeyKeyPair(Box<ReKeyKeyPair>),
ReKeyKeyPairResponse(ReKeyKeyPairResponse),
Expand Down Expand Up @@ -277,6 +279,8 @@ impl Display for Operation {
Self::PKCS11Response(op) => write!(f, "{op}")?,
Self::Query(op) => write!(f, "{op}")?,
Self::QueryResponse(op) => write!(f, "{op}")?,
Self::ReCertify(op) => write!(f, "{op}")?,
Self::ReCertifyResponse(op) => write!(f, "{op}")?,
Self::ReKey(op) => write!(f, "{op}")?,
Self::ReKeyKeyPair(op) => write!(f, "{op}")?,
Self::ReKeyKeyPairResponse(op) => write!(f, "{op}")?,
Expand Down Expand Up @@ -333,6 +337,7 @@ impl Operation {
| Self::ModifyAttributeResponse(_)
| Self::PKCS11Response(_)
| Self::QueryResponse(_)
| Self::ReCertifyResponse(_)
| Self::ReKeyKeyPairResponse(_)
| Self::ReKeyResponse(_)
| Self::RegisterResponse(_)
Expand Down Expand Up @@ -393,6 +398,7 @@ impl Operation {
}
Self::PKCS11(_) | Self::PKCS11Response(_) => OperationEnumeration::PKCS11,
Self::Query(_) | Self::QueryResponse(_) => OperationEnumeration::Query,
Self::ReCertify(_) | Self::ReCertifyResponse(_) => OperationEnumeration::ReCertify,
Self::Register(_) | Self::RegisterResponse(_) => OperationEnumeration::Register,
Self::ReKey(_) | Self::ReKeyResponse(_) => OperationEnumeration::ReKey,
Self::ReKeyKeyPair(_) | Self::ReKeyKeyPairResponse(_) => {
Expand Down Expand Up @@ -1021,6 +1027,66 @@ pub struct CertifyResponse {

impl_display!(CertifyResponse, "CertifyResponse", { req unique_identifier });

/// `ReCertify`
///
/// This operation requests the server to generate a new certificate for an
/// existing public key whose certificate has expired or is about to expire.
/// The request contains the Unique Identifier of the existing certificate to be
/// renewed, an optional certificate request, and optional attributes for the new
/// certificate.
///
/// The server creates a new Certificate object with a fresh Unique Identifier,
/// sets a `ReplacedObjectLink` on the new certificate pointing to the old one,
/// and sets a `ReplacementObjectLink` on the old certificate pointing to the new one.
///
/// KMIP 2.1 §6.1.8 / KMIP 1.4 §4.8
#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct ReCertify {
/// The Unique Identifier of the existing Certificate to be re-certified.
/// If omitted, the ID Placeholder value is used.
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_identifier: Option<UniqueIdentifier>,
/// An Enumeration object specifying the type of certificate request.
/// Required if Certificate Request Value is present.
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_request_type: Option<CertificateRequestType>,
/// A Byte String object with the certificate request.
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_request_value: Option<Vec<u8>>,
/// An Offset MAY be used to indicate the difference between the Initial Date
/// and the Activation Date of the new certificate. Per KMIP 2.1 §6.1.45,
/// the new certificate's Activation Date = Initial Date + Offset.
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<i32>,
/// Specifies desired attributes to be associated with the new certificate.
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<Attributes>,
/// Specifies all permissible Protection Storage Mask selections for the new
/// object.
#[serde(skip_serializing_if = "Option::is_none")]
pub protection_storage_masks: Option<ProtectionStorageMasks>,
}

impl_display!(ReCertify, "ReCertify", {
opt unique_identifier,
opt certificate_request_type,
opt_b64 certificate_request_value,
opt offset,
opt attributes,
opt protection_storage_masks,
});

/// Response to a `ReCertify` request.
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct ReCertifyResponse {
/// The Unique Identifier of the newly created replacement certificate.
pub unique_identifier: UniqueIdentifier,
}

impl_display!(ReCertifyResponse, "ReCertifyResponse", { req unique_identifier });

/// Create
///
/// This operation requests the server to generate a new symmetric key or
Expand Down
26 changes: 22 additions & 4 deletions crate/server/src/core/kms/kmip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ use cosmian_kms_server_database::reexport::cosmian_kmip::{
GetAttributesResponse, GetResponse, Hash, HashResponse, Import, ImportResponse, Locate,
LocateResponse, MAC, MACResponse, MACVerify, MACVerifyResponse, ModifyAttribute,
ModifyAttributeResponse, PKCS11, PKCS11Response, Query, QueryResponse, RNGRetrieve,
RNGRetrieveResponse, RNGSeed, RNGSeedResponse, ReKey, ReKeyKeyPair, ReKeyKeyPairResponse,
ReKeyResponse, Register, RegisterResponse, Revoke, RevokeResponse, SetAttribute,
SetAttributeResponse, Sign, SignResponse, SignatureVerify, SignatureVerifyResponse,
Validate, ValidateResponse,
RNGRetrieveResponse, RNGSeed, RNGSeedResponse, ReCertify, ReCertifyResponse, ReKey,
ReKeyKeyPair, ReKeyKeyPairResponse, ReKeyResponse, Register, RegisterResponse, Revoke,
RevokeResponse, SetAttribute, SetAttributeResponse, Sign, SignResponse, SignatureVerify,
SignatureVerifyResponse, Validate, ValidateResponse,
},
};
use tracing::Instrument;
Expand Down Expand Up @@ -669,6 +669,24 @@ impl KMS {
.await
}

/// `ReCertify` — certificate rotation with a new UID.
///
/// Creates a fresh certificate for the same subject/issuer and links old → new
/// via `ReplacementObjectLink`. Keys referencing the old certificate are updated
/// to point to the new one.
pub(crate) async fn recertify(
&self,
request: ReCertify,
user: &str,
privileged_users: Option<Vec<String>>,
) -> KResult<ReCertifyResponse> {
let span = tracing::span!(tracing::Level::ERROR, "recertify");

Box::pin(operations::recertify(self, request, user, privileged_users))
.instrument(span)
.await
}

/// This operation requests the server to modify a single attribute on an existing Managed Object.
/// Per KMIP spec §3.22, modifying `ActivationDate` on a Pre-Active object to a date in the past
/// or the present triggers an automatic transition to the Active state.
Expand Down
Loading
Loading