Skip to content
Merged
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
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)
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use crate::{

const X509_VERSION3: i32 = 2;

pub(super) fn build_and_sign_certificate(
pub(crate) fn build_and_sign_certificate(
vendor_id: &str,
issuer: &Issuer,
subject: &Subject,
Expand Down
2 changes: 1 addition & 1 deletion crate/server/src/core/operations/certify/issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use openssl::{
/// A certificate Issuer is constructed from a unique identifier and
/// - either a private key and a certificate.
/// - or a private key, a subject name and a certificate.
pub(super) enum Issuer<'a> {
pub(crate) enum Issuer<'a> {
PrivateKeyAndCertificate(
UniqueIdentifier,
/// Private key
Expand Down
3 changes: 3 additions & 0 deletions crate/server/src/core/operations/certify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ mod tests;

// Re-export the public API of this module.
// Re-export helpers used by sibling RFC submodules via `super::`.
pub(crate) use build_certificate::build_and_sign_certificate;
use build_certificate::extension_config_is_ca;
#[cfg(feature = "non-fips")]
use build_certificate::pqc_signing_key_usage;
pub(crate) use certify_op::certify;
pub(crate) use resolve_issuer::get_issuer;
pub(crate) use resolve_subject::get_subject;
2 changes: 1 addition & 1 deletion crate/server/src/core/operations/certify/resolve_issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::{

/// Determine the issuer of the issued certificate.
/// The issuer can be recovered from different sources or be self-signed.
pub(super) async fn get_issuer<'a>(
pub(crate) async fn get_issuer<'a>(
subject: &'a Subject,
kms: &KMS,
request: &Certify,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ fn cryptographic_usage_mask_public_key(
/// - a certificate
/// - a key pair and a subject name
/// - a CSR
pub(super) async fn get_subject(
pub(crate) async fn get_subject(
kms: &KMS,
request: &cosmian_kms_server_database::reexport::cosmian_kmip::kmip_2_1::kmip_operations::Certify,
user: &str,
Expand Down
4 changes: 2 additions & 2 deletions crate/server/src/core/operations/certify/subject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use openssl::{
use crate::{kms_error, result::KResult};

/// This holds `KeyPair` information when one is created for the subject
pub(super) struct KeyPairData {
pub(crate) struct KeyPairData {
pub(crate) private_key_id: UniqueIdentifier,
pub(crate) private_key_object: Object,
pub(crate) private_key_tags: HashSet<String>,
Expand All @@ -45,7 +45,7 @@ impl Display for KeyPairData {

/// The party that gets signed by the issuer and gets the certificate
#[expect(clippy::large_enum_variant)]
pub(super) enum Subject {
pub(crate) enum Subject {
X509Req(
/// Unique identifier of the certificate to create
UniqueIdentifier,
Expand Down
Loading
Loading