Skip to content

Commit a9d04fc

Browse files
committed
[auto-rotation feature] feat(rekey): Manual rotation for all key types + test vectors (#969)
* feat(rekey): implement symmetric key ReKey with wrapping key re-wrap - Implement KMIP ReKey for symmetric keys with name transfer per §4.4 - Support re-wrapping dependent keys when a wrapping key is rekeyed - Add find_wrapped_by() to ObjectsStore trait (SQLite, PostgreSQL, MySQL) - Fix: transfer Name attribute from old to new key during ReKey - Fix: error on self-wrap when wrapping_key_id is user-supplied - Fix: bypass ownership check for server-configured KEK Tested with 37 vector tests (9 symmetric + 27 keypair + 1 security) * fix: consolidate rekey operations using trait * feat: consolidate Recertify operation
1 parent 62f9018 commit a9d04fc

32 files changed

Lines changed: 2495 additions & 760 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
## Features
2+
3+
- Implement KMIP ReKey operation for symmetric keys with name transfer per §4.4
4+
- Support re-wrapping of dependent keys when a wrapping key is rekeyed
5+
- Add `find_wrapped_by()` method to `ObjectsStore` trait (SQLite, PostgreSQL, MySQL implementations)
6+
- Implement KMIP `ReCertify` operation (§4.7) — certificate rotation with new UID and replacement links
7+
- Add proper `ReCertify` and `ReCertifyResponse` KMIP 2.1 types compliant with both KMIP 1.x and 2.x
8+
- Introduce `RekeyOperation` trait to unify symmetric, keypair, and certificate rotation logic
9+
- Add `offset` field to `ReCertify` struct per KMIP 2.1 §6.1.45 for date-based activation scheduling
10+
11+
## Refactor
12+
13+
- 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`)
14+
- Extract `RekeyOperation` trait into `common.rs` with `execute_rekey()` orchestrator — shared 2-phase commit logic
15+
- 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`
16+
- 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
17+
- Move `compute_rotation_uid` and `rewrap_dependants` from `symmetric.rs` to `common.rs`; keypair rekey now uses name-preserving UIDs
18+
- Convert `ReKeyKeyPair` to 2-phase commit (matching symmetric) to support dependant re-wrapping on public keys
19+
- Set rotation metadata (`rotate_generation`, `rotate_date`, `rotate_latest`, `rotate_interval`) on new keys during `ReKeyKeyPair`
20+
- Clear rotation flags on old keys during `ReKeyKeyPair` to prevent scheduler re-triggering
21+
- 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
22+
- Extract `extract_rewrap_spec`, `extract_wrapping_key_uid`, and `retrieve_eligible_keys` into `common.rs` as shared helpers — removes 40+ lines of duplicated logic
23+
- 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
24+
- Refactor `prepare_attributes` in `keypair.rs` — extract `finalize_replacement_key` helper to eliminate SK/PK code duplication
25+
- Move `setup_new_key` and `finalize_replacement_key` from keypair.rs to common.rs as shared helpers
26+
- Extract `preserve_wrapping_key_link` into common.rs — copies WrappingKeyLink from old to new key
27+
- Split `rewrap_dependants` (70→25 lines) by extracting `rewrap_single_dependant` helper
28+
- Split `relink_keys_to_new_certificate` by extracting `relink_single_key` helper
29+
30+
## Bug Fixes
31+
32+
- Transfer `Name` attribute from old key to new key during ReKey per KMIP §4.4
33+
- Return error instead of silently skipping when a user-supplied wrapping key ID equals the key being wrapped
34+
- Bypass ownership check for server-configured KEK during wrapping operations
35+
- Fix symmetric ReKey missing server-wide KEK wrapping and unwrapped-cache insert (now consistent with keypair rekey via shared default)
36+
- Fix keypair rekey not preserving WrappingKeyLink on replacement keys
37+
- Fix symmetric rekey hardcoding `State::Active` — now uses `setup_object_lifecycle` for date-based state computation
38+
- Fix `setup_object_lifecycle` not storing `activation_date` for `PreActive` keys — offset-based activation scheduling now works correctly
39+
- Add `ReCertify` request/response deserialization to KMIP 2.1 message handler
40+
- Fix `ReCertify.generate_replacement` passing empty user to `get_subject`/`get_issuer` — use certificate owner instead
41+
- Fix `ReCertify` not computing lifecycle state from offset — certificates with future activation_date are now `PreActive`
42+
43+
## Documentation
44+
45+
- 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
46+
47+
## Testing
48+
49+
- Add 9 symmetric ReKey test vectors (basic, wrapped, wrapping-key re-wrap, name transfer, offset, links)
50+
- Add 27 ReKeyKeyPair test vectors (RSA, EC, ML-KEM, ML-DSA, SLH-DSA, X25519, secp256k1)
51+
- Add Covercrypt ReKeyKeyPair test vector (in-place attribute rekey with same UIDs)
52+
- Add access privilege escalation test vector for ReKey
53+
- Add 4 ReCertify test vectors (self-signed, chain, with-links, with-offset)
54+
- Add 3 negative ReCertify test vectors (missing UID, non-existent, not a certificate)
55+
- Add 2 offset state verification vectors (rekey + rekey-keypair: Offset=0 → Active, Offset=86400 → PreActive)

crate/interfaces/src/stores/objects_store.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,19 @@ pub trait ObjectsStore {
102102
user_must_be_owner: bool,
103103
vendor_id: &str,
104104
) -> InterfaceResult<Vec<(String, State, Attributes)>>;
105+
106+
/// Return (uid, state, attributes) for every object whose
107+
/// `key_wrapping_data.encryption_key_information.unique_identifier` equals
108+
/// `wrapping_key_uid`. Used by key rotation to re-wrap all objects protected by
109+
/// the rotated key.
110+
///
111+
/// The default implementation returns an empty list; backends that support
112+
/// JSON-based object storage should override this with an efficient query.
113+
async fn find_wrapped_by(
114+
&self,
115+
_wrapping_key_uid: &str,
116+
_user: &str,
117+
) -> InterfaceResult<Vec<(String, State, Attributes)>> {
118+
Ok(vec![])
119+
}
105120
}

crate/kmip/src/kmip_1_4/kmip_operations.rs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,60 @@ pub struct ReCertifyResponse {
498498
pub template_attribute: Option<TemplateAttribute>,
499499
}
500500

501+
impl From<ReCertify> for kmip_2_1::kmip_operations::ReCertify {
502+
fn from(recertify: ReCertify) -> Self {
503+
let cert_req_type = match recertify.certificate_request_type {
504+
CertificateRequestType::CRMF => kmip_2_1::kmip_types::CertificateRequestType::CRMF,
505+
CertificateRequestType::PKCS10 => kmip_2_1::kmip_types::CertificateRequestType::PKCS10,
506+
CertificateRequestType::PEM => kmip_2_1::kmip_types::CertificateRequestType::PEM,
507+
};
508+
Self {
509+
unique_identifier: Some(recertify.unique_identifier.into()),
510+
certificate_request_type: Some(cert_req_type),
511+
certificate_request_value: Some(recertify.certificate_request_value),
512+
offset: None,
513+
attributes: recertify.template_attribute.map(Into::into),
514+
protection_storage_masks: None,
515+
}
516+
}
517+
}
518+
519+
impl TryFrom<kmip_2_1::kmip_operations::ReCertifyResponse> for ReCertifyResponse {
520+
type Error = KmipError;
521+
522+
fn try_from(value: kmip_2_1::kmip_operations::ReCertifyResponse) -> Result<Self, Self::Error> {
523+
Ok(Self {
524+
unique_identifier: value.unique_identifier.to_string(),
525+
template_attribute: None,
526+
})
527+
}
528+
}
529+
530+
impl From<kmip_2_1::kmip_operations::ReCertify> for ReCertify {
531+
fn from(recertify: kmip_2_1::kmip_operations::ReCertify) -> Self {
532+
let cert_req_type = match recertify.certificate_request_type {
533+
Some(kmip_2_1::kmip_types::CertificateRequestType::CRMF) => {
534+
CertificateRequestType::CRMF
535+
}
536+
Some(kmip_2_1::kmip_types::CertificateRequestType::PKCS10) => {
537+
CertificateRequestType::PKCS10
538+
}
539+
Some(kmip_2_1::kmip_types::CertificateRequestType::PEM) | None => {
540+
CertificateRequestType::PEM
541+
}
542+
};
543+
Self {
544+
unique_identifier: recertify
545+
.unique_identifier
546+
.map_or_else(String::new, |u| u.to_string()),
547+
certificate_request_type: cert_req_type,
548+
certificate_request_value: recertify.certificate_request_value.unwrap_or_default(),
549+
template_attribute: None,
550+
// KMIP 1.4 does not support offset; it is dropped during downgrade.
551+
}
552+
}
553+
}
554+
501555
/// 4.9 Locate
502556
/// This operation requests that the server search for one or more Managed Objects.
503557
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
@@ -2647,9 +2701,7 @@ impl TryFrom<Operation> for kmip_2_1::kmip_operations::Operation {
26472701
// }
26482702
// Operation::Poll(poll) => Self::Poll(poll.into()),
26492703
Operation::Query(query) => Self::Query(query.into()),
2650-
// Operation::ReCertify(recertify) => {
2651-
// Self::ReCertify(recertify.into())
2652-
// }
2704+
Operation::ReCertify(recertify) => Self::ReCertify(Box::new(recertify.into())),
26532705
// Operation::Recover(recover) => {
26542706
// Self::Recover(recover.into())
26552707
// }
@@ -2803,9 +2855,9 @@ impl TryFrom<kmip_2_1::kmip_operations::Operation> for Operation {
28032855
(*query_response).try_into().context("QueryResponse")?,
28042856
))
28052857
}
2806-
// Operation::ReCertifyResponse(recertify_response) => {
2807-
// Self::ReCertifyResponse(recertify_response.into())
2808-
// }
2858+
kmip_2_1::kmip_operations::Operation::ReCertifyResponse(recertify_response) => {
2859+
Self::ReCertifyResponse(recertify_response.try_into().context("ReCertifyResponse")?)
2860+
}
28092861
// Operation::RecoverResponse(recover_response) => {
28102862
// Self::RecoverResponse(recover_response.into())
28112863
// }

crate/kmip/src/kmip_2_1/kmip_messages.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,9 @@ impl<'de> Deserialize<'de> for RequestMessageBatchItem {
354354
OperationEnumeration::ReKeyKeyPair => {
355355
Operation::ReKeyKeyPair(map.next_value()?)
356356
}
357+
OperationEnumeration::ReCertify => {
358+
Operation::ReCertify(map.next_value()?)
359+
}
357360
x => {
358361
return Err(de::Error::custom(format!(
359362
"Request Message Batch Item: unsupported operation: {x:?}"
@@ -792,6 +795,9 @@ impl<'de> Deserialize<'de> for ResponseMessageBatchItem {
792795
OperationEnumeration::ReKeyKeyPair => {
793796
Operation::ReKeyKeyPairResponse(map.next_value()?)
794797
}
798+
OperationEnumeration::ReCertify => {
799+
Operation::ReCertifyResponse(map.next_value()?)
800+
}
795801
x => {
796802
return Err(de::Error::custom(format!(
797803
"KMIP 2 response message payload: unsupported operation: \

crate/kmip/src/kmip_2_1/kmip_operations.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ pub enum Operation {
193193
PKCS11Response(PKCS11Response),
194194
Query(Query),
195195
QueryResponse(Box<QueryResponse>),
196+
ReCertify(Box<ReCertify>),
197+
ReCertifyResponse(ReCertifyResponse),
196198
ReKey(ReKey),
197199
ReKeyKeyPair(Box<ReKeyKeyPair>),
198200
ReKeyKeyPairResponse(ReKeyKeyPairResponse),
@@ -277,6 +279,8 @@ impl Display for Operation {
277279
Self::PKCS11Response(op) => write!(f, "{op}")?,
278280
Self::Query(op) => write!(f, "{op}")?,
279281
Self::QueryResponse(op) => write!(f, "{op}")?,
282+
Self::ReCertify(op) => write!(f, "{op}")?,
283+
Self::ReCertifyResponse(op) => write!(f, "{op}")?,
280284
Self::ReKey(op) => write!(f, "{op}")?,
281285
Self::ReKeyKeyPair(op) => write!(f, "{op}")?,
282286
Self::ReKeyKeyPairResponse(op) => write!(f, "{op}")?,
@@ -333,6 +337,7 @@ impl Operation {
333337
| Self::ModifyAttributeResponse(_)
334338
| Self::PKCS11Response(_)
335339
| Self::QueryResponse(_)
340+
| Self::ReCertifyResponse(_)
336341
| Self::ReKeyKeyPairResponse(_)
337342
| Self::ReKeyResponse(_)
338343
| Self::RegisterResponse(_)
@@ -393,6 +398,7 @@ impl Operation {
393398
}
394399
Self::PKCS11(_) | Self::PKCS11Response(_) => OperationEnumeration::PKCS11,
395400
Self::Query(_) | Self::QueryResponse(_) => OperationEnumeration::Query,
401+
Self::ReCertify(_) | Self::ReCertifyResponse(_) => OperationEnumeration::ReCertify,
396402
Self::Register(_) | Self::RegisterResponse(_) => OperationEnumeration::Register,
397403
Self::ReKey(_) | Self::ReKeyResponse(_) => OperationEnumeration::ReKey,
398404
Self::ReKeyKeyPair(_) | Self::ReKeyKeyPairResponse(_) => {
@@ -1021,6 +1027,66 @@ pub struct CertifyResponse {
10211027

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

1030+
/// `ReCertify`
1031+
///
1032+
/// This operation requests the server to generate a new certificate for an
1033+
/// existing public key whose certificate has expired or is about to expire.
1034+
/// The request contains the Unique Identifier of the existing certificate to be
1035+
/// renewed, an optional certificate request, and optional attributes for the new
1036+
/// certificate.
1037+
///
1038+
/// The server creates a new Certificate object with a fresh Unique Identifier,
1039+
/// sets a `ReplacedObjectLink` on the new certificate pointing to the old one,
1040+
/// and sets a `ReplacementObjectLink` on the old certificate pointing to the new one.
1041+
///
1042+
/// KMIP 2.1 §6.1.8 / KMIP 1.4 §4.8
1043+
#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
1044+
#[serde(rename_all = "PascalCase")]
1045+
pub struct ReCertify {
1046+
/// The Unique Identifier of the existing Certificate to be re-certified.
1047+
/// If omitted, the ID Placeholder value is used.
1048+
#[serde(skip_serializing_if = "Option::is_none")]
1049+
pub unique_identifier: Option<UniqueIdentifier>,
1050+
/// An Enumeration object specifying the type of certificate request.
1051+
/// Required if Certificate Request Value is present.
1052+
#[serde(skip_serializing_if = "Option::is_none")]
1053+
pub certificate_request_type: Option<CertificateRequestType>,
1054+
/// A Byte String object with the certificate request.
1055+
#[serde(skip_serializing_if = "Option::is_none")]
1056+
pub certificate_request_value: Option<Vec<u8>>,
1057+
/// An Offset MAY be used to indicate the difference between the Initial Date
1058+
/// and the Activation Date of the new certificate. Per KMIP 2.1 §6.1.45,
1059+
/// the new certificate's Activation Date = Initial Date + Offset.
1060+
#[serde(skip_serializing_if = "Option::is_none")]
1061+
pub offset: Option<i32>,
1062+
/// Specifies desired attributes to be associated with the new certificate.
1063+
#[serde(skip_serializing_if = "Option::is_none")]
1064+
pub attributes: Option<Attributes>,
1065+
/// Specifies all permissible Protection Storage Mask selections for the new
1066+
/// object.
1067+
#[serde(skip_serializing_if = "Option::is_none")]
1068+
pub protection_storage_masks: Option<ProtectionStorageMasks>,
1069+
}
1070+
1071+
impl_display!(ReCertify, "ReCertify", {
1072+
opt unique_identifier,
1073+
opt certificate_request_type,
1074+
opt_b64 certificate_request_value,
1075+
opt offset,
1076+
opt attributes,
1077+
opt protection_storage_masks,
1078+
});
1079+
1080+
/// Response to a `ReCertify` request.
1081+
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
1082+
#[serde(rename_all = "PascalCase")]
1083+
pub struct ReCertifyResponse {
1084+
/// The Unique Identifier of the newly created replacement certificate.
1085+
pub unique_identifier: UniqueIdentifier,
1086+
}
1087+
1088+
impl_display!(ReCertifyResponse, "ReCertifyResponse", { req unique_identifier });
1089+
10241090
/// Create
10251091
///
10261092
/// This operation requests the server to generate a new symmetric key or

crate/server/src/core/kms/kmip.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use cosmian_kms_server_database::reexport::cosmian_kmip::{
1111
GetAttributesResponse, GetResponse, Hash, HashResponse, Import, ImportResponse, Locate,
1212
LocateResponse, MAC, MACResponse, MACVerify, MACVerifyResponse, ModifyAttribute,
1313
ModifyAttributeResponse, PKCS11, PKCS11Response, Query, QueryResponse, RNGRetrieve,
14-
RNGRetrieveResponse, RNGSeed, RNGSeedResponse, ReKey, ReKeyKeyPair, ReKeyKeyPairResponse,
15-
ReKeyResponse, Register, RegisterResponse, Revoke, RevokeResponse, SetAttribute,
16-
SetAttributeResponse, Sign, SignResponse, SignatureVerify, SignatureVerifyResponse,
17-
Validate, ValidateResponse,
14+
RNGRetrieveResponse, RNGSeed, RNGSeedResponse, ReCertify, ReCertifyResponse, ReKey,
15+
ReKeyKeyPair, ReKeyKeyPairResponse, ReKeyResponse, Register, RegisterResponse, Revoke,
16+
RevokeResponse, SetAttribute, SetAttributeResponse, Sign, SignResponse, SignatureVerify,
17+
SignatureVerifyResponse, Validate, ValidateResponse,
1818
},
1919
};
2020
use tracing::Instrument;
@@ -669,6 +669,24 @@ impl KMS {
669669
.await
670670
}
671671

672+
/// `ReCertify` — certificate rotation with a new UID.
673+
///
674+
/// Creates a fresh certificate for the same subject/issuer and links old → new
675+
/// via `ReplacementObjectLink`. Keys referencing the old certificate are updated
676+
/// to point to the new one.
677+
pub(crate) async fn recertify(
678+
&self,
679+
request: ReCertify,
680+
user: &str,
681+
privileged_users: Option<Vec<String>>,
682+
) -> KResult<ReCertifyResponse> {
683+
let span = tracing::span!(tracing::Level::ERROR, "recertify");
684+
685+
Box::pin(operations::recertify(self, request, user, privileged_users))
686+
.instrument(span)
687+
.await
688+
}
689+
672690
/// This operation requests the server to modify a single attribute on an existing Managed Object.
673691
/// Per KMIP spec §3.22, modifying `ActivationDate` on a Pre-Active object to a date in the past
674692
/// or the present triggers an automatic transition to the Active state.

crate/server/src/core/operations/certify/build_certificate.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use crate::{
3737

3838
const X509_VERSION3: i32 = 2;
3939

40-
pub(super) fn build_and_sign_certificate(
40+
pub(crate) fn build_and_sign_certificate(
4141
vendor_id: &str,
4242
issuer: &Issuer,
4343
subject: &Subject,

crate/server/src/core/operations/certify/issuer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use openssl::{
88
/// A certificate Issuer is constructed from a unique identifier and
99
/// - either a private key and a certificate.
1010
/// - or a private key, a subject name and a certificate.
11-
pub(super) enum Issuer<'a> {
11+
pub(crate) enum Issuer<'a> {
1212
PrivateKeyAndCertificate(
1313
UniqueIdentifier,
1414
/// Private key

crate/server/src/core/operations/certify/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ mod tests;
1717

1818
// Re-export the public API of this module.
1919
// Re-export helpers used by sibling RFC submodules via `super::`.
20+
pub(crate) use build_certificate::build_and_sign_certificate;
2021
use build_certificate::extension_config_is_ca;
2122
#[cfg(feature = "non-fips")]
2223
use build_certificate::pqc_signing_key_usage;
2324
pub(crate) use certify_op::certify;
25+
pub(crate) use resolve_issuer::get_issuer;
26+
pub(crate) use resolve_subject::get_subject;

crate/server/src/core/operations/certify/resolve_issuer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use crate::{
2727

2828
/// Determine the issuer of the issued certificate.
2929
/// The issuer can be recovered from different sources or be self-signed.
30-
pub(super) async fn get_issuer<'a>(
30+
pub(crate) async fn get_issuer<'a>(
3131
subject: &'a Subject,
3232
kms: &KMS,
3333
request: &Certify,

0 commit comments

Comments
 (0)