Skip to content
Closed
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
22 changes: 22 additions & 0 deletions crates/oe-ca-core/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,28 @@ pub(crate) fn crl_issued_serials(serials: &[Vec<u8>]) -> Result<Extension, CaErr
build(oe_conformance::OID_CRL_ISSUED_SERIALS, false, &values?)
}

const OID_PRIVATE_KEY_USAGE_PERIOD: &str = "2.5.29.16";

/// Constat T-3 de l'audit du 2026-09-25 (EN 319 421 `TIS-7.6.7-*`) : la clé
/// privée d'une TSU doit avoir sa propre date d'expiration, plus courte que
/// celle du certificat (`profile.private_key_validity`). Non critique,
/// comme le recommande RFC 5280 pour cette extension.
pub(crate) fn private_key_usage_period(
not_after: time::OffsetDateTime,
) -> Result<Extension, CaError> {
let not_after = der::asn1::GeneralizedTime::from_unix_duration(
std::time::Duration::from_secs(not_after.unix_timestamp().max(0) as u64),
)?;
build(
OID_PRIVATE_KEY_USAGE_PERIOD,
false,
&x509_cert::ext::pkix::PrivateKeyUsagePeriod {
not_before: None,
not_after: Some(not_after),
},
)
}

pub(crate) fn ocsp_no_check() -> Extension {
// La valeur est un NULL DER : l'extension vaut par sa seule présence.
Extension {
Expand Down
3 changes: 3 additions & 0 deletions crates/oe-ca-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ impl Issuer {
if let Some(policy) = profile.policy_oid {
exts.push(extensions::certificate_policy(policy)?);
}
if let Some(key_validity) = profile.private_key_validity {
exts.push(extensions::private_key_usage_period(now + key_validity)?);
}
if profile.san_dns_from_cn {
exts.push(extensions::subject_alt_name_dns(subject_cn)?);
}
Expand Down
14 changes: 13 additions & 1 deletion crates/oe-ca-core/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ pub struct Profile {
pub san_dns_from_cn: bool,
/// Politique de certification gravée dans `certificatePolicies`.
pub policy_oid: Option<&'static str>,
/// Durée de vie de la **clé privée**, plus courte que `validity`
/// (constat T-3 de l'audit du 2026-09-25, EN 319 421 `TIS-7.6.7-*`) :
/// gravée dans l'extension `privateKeyUsagePeriod`. `None` pour un
/// profil qui n'a pas cette exigence.
pub private_key_validity: Option<time::Duration>,

/// Applique les règles ETSI propres à ce profil au certificat
/// réellement signé — reproduit le champ `Check` de `ca.Profile` (Go).
Expand Down Expand Up @@ -83,7 +88,10 @@ pub fn tsa_signer() -> Profile {
organizational_unit: "Time Stamping Authority",
organization: "Open eIDAS",
country: "FR",
validity: time::Duration::days(365),
// 2 ans de certificat, 1 an de clé (constat T-3) : la clé expire
// avant lui, imposant un renouvellement de bi-clé à mi-vie du
// certificat plutôt qu'une clé unique reconduite indéfiniment.
validity: time::Duration::days(2 * 365),
// nonRepudiation (contentCommitment) accompagne digitalSignature :
// un jeton d'horodatage engage l'autorité sur la date.
key_usages: KeyUsages::DigitalSignature | KeyUsages::NonRepudiation,
Expand All @@ -96,6 +104,7 @@ pub fn tsa_signer() -> Profile {
required_cn: None,
san_dns_from_cn: false,
policy_oid: None,
private_key_validity: Some(time::Duration::days(365)),
check: oe_conformance::check_tsu_certificate,
}
}
Expand All @@ -121,6 +130,7 @@ pub fn ocsp_responder() -> Profile {
required_cn: None,
san_dns_from_cn: false,
policy_oid: None,
private_key_validity: None,
check: oe_conformance::check_ocsp_responder_certificate,
}
}
Expand All @@ -147,6 +157,7 @@ pub fn internal_client() -> Profile {
required_cn: Some(INTERNAL_CLIENT_CN),
san_dns_from_cn: false,
policy_oid: Some(OID_POLICY_INTERNAL_CLIENT),
private_key_validity: None,
check: oe_conformance::check_internal_client_certificate,
}
}
Expand All @@ -171,6 +182,7 @@ pub fn internal_server() -> Profile {
required_cn: None,
san_dns_from_cn: true,
policy_oid: Some(OID_POLICY_INTERNAL_SERVER),
private_key_validity: None,
check: oe_conformance::check_internal_server_certificate,
}
}
Expand Down
44 changes: 42 additions & 2 deletions crates/oe-conformance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,12 +345,19 @@ pub fn check_signature_algorithm(subject: &str, algorithm_oid: &str) -> Result<(
Ok(())
}

fn x509_time_to_offset_date_time(t: &x509_cert::time::Time) -> time::OffsetDateTime {
let dt = t.to_date_time();
fn der_date_time_to_offset_date_time(dt: der::DateTime) -> time::OffsetDateTime {
time::OffsetDateTime::from_unix_timestamp(dt.unix_duration().as_secs() as i64)
.unwrap_or(time::OffsetDateTime::UNIX_EPOCH)
}

fn x509_time_to_offset_date_time(t: &x509_cert::time::Time) -> time::OffsetDateTime {
der_date_time_to_offset_date_time(t.to_date_time())
}

fn generalized_time_to_offset_date_time(t: &der::asn1::GeneralizedTime) -> time::OffsetDateTime {
der_date_time_to_offset_date_time(t.to_date_time())
}

fn find_extension<'a>(
cert: &'a x509_cert::Certificate,
oid: &str,
Expand Down Expand Up @@ -423,9 +430,42 @@ pub fn check_tsu_certificate(subject: &str, cert: &x509_cert::Certificate) -> Re
));
}

// Constat T-3 de l'audit du 2026-09-25 (EN 319 421 TIS-7.6.7-01/-02/-04/-05) :
// la clé de signature d'une TSU doit avoir sa propre date d'expiration,
// plus courte que celle du certificat — sans quoi rien n'empêche de
// signer indéfiniment avec la même clé.
let key_not_after = tsu_private_key_not_after(subject, cert)?;
if key_not_after > not_after {
return Err(format!(
"{subject}: la clé privée expire après le certificat lui-même"
));
}

Ok(())
}

/// Date d'expiration de la clé privée d'un certificat TSU, gravée dans son
/// extension `privateKeyUsagePeriod` (constat T-3) — appelé par
/// `check_tsu_certificate`, et par `oe_tsa_core::Authority` pour refuser de
/// signer une fois cette date dépassée (`TIS-7.7.1-09`, « le système rejette
/// toute émission une fois la date d'expiration de la clé atteinte »).
pub fn tsu_private_key_not_after(
subject: &str,
cert: &x509_cert::Certificate,
) -> Result<time::OffsetDateTime, String> {
use der::Decode;

let pkup_ext = find_extension(cert, "2.5.29.16")
.ok_or_else(|| format!("{subject}: extension privateKeyUsagePeriod absente"))?;
let pkup =
x509_cert::ext::pkix::PrivateKeyUsagePeriod::from_der(pkup_ext.extn_value.as_bytes())
.map_err(|e| format!("{subject}: privateKeyUsagePeriod illisible: {e}"))?;
let key_not_after = pkup
.not_after
.ok_or_else(|| format!("{subject}: privateKeyUsagePeriod sans notAfter"))?;
Ok(generalized_time_to_offset_date_time(&key_not_after))
}

/// RFC 6960 §4.2.2.2 : le certificat de signature du répondeur OCSP relu et
/// re-contrôlé doit porter CA:FALSE, `extendedKeyUsage` id-kp-OCSPSigning,
/// l'extension `id-pkix-ocsp-nocheck`, `keyUsage` incluant digitalSignature,
Expand Down
57 changes: 47 additions & 10 deletions crates/oe-conformance/tests/tsu_certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use oe_castore::Memory;
use oe_hsm::testing::SoftwareToken;
use oe_hsm::SigningToken;

async fn issue_with_profile(p: oe_ca_core::Profile) -> x509_cert::Certificate {
async fn build_issuer() -> Issuer {
let store = Arc::new(Memory::new());
let root_signer = Arc::new(SoftwareToken::generate(3072));
let issuing_signer = Arc::new(SoftwareToken::generate(3072));
Expand All @@ -36,7 +36,7 @@ async fn issue_with_profile(p: oe_ca_core::Profile) -> x509_cert::Certificate {
.await
.unwrap();

let issuer = Issuer::new(Options {
Issuer::new(Options {
signer: issuing_signer,
certificate: hierarchy.issuing,
chain: vec![],
Expand All @@ -47,19 +47,23 @@ async fn issue_with_profile(p: oe_ca_core::Profile) -> x509_cert::Certificate {
crl_grace: time::Duration::hours(1),
recorder: None,
})
.unwrap();
.unwrap()
}

async fn issue_with_profile(p: oe_ca_core::Profile) -> x509_cert::Certificate {
let issuer = build_issuer().await;
try_issue(&issuer, &p).await.unwrap()
}

async fn try_issue(
issuer: &Issuer,
p: &oe_ca_core::Profile,
) -> Result<x509_cert::Certificate, oe_ca_core::CaError> {
let end_entity = SoftwareToken::generate(3072);
let public_key_der = end_entity.public_key_der().unwrap();
issuer
.issue(
&public_key_der,
"entity.example.test",
&p,
"txn-conformance",
)
.issue(&public_key_der, "entity.example.test", p, "txn-conformance")
.await
.unwrap()
}

#[tokio::test]
Expand All @@ -69,6 +73,39 @@ async fn accepts_a_certificate_issued_with_the_tsa_signer_profile() {
.expect("un certificat émis avec le profil tsa_signer doit être accepté");
}

/// Constat T-3 de l'audit du 2026-09-25 : sans `privateKeyUsagePeriod`, rien
/// ne borne la durée de vie de la clé — le certificat seul ne suffit pas.
/// `check_tsu_certificate` est aussi le `Profile::check` de `tsa_signer` :
/// l'émission elle-même doit être refusée, pas seulement l'affichage après
/// coup (même discipline que le reste du profil, voir `Issuer::issue`).
#[tokio::test]
async fn issuance_is_refused_without_a_private_key_usage_period() {
let issuer = build_issuer().await;
let profile = oe_ca_core::Profile {
private_key_validity: None,
..profile::tsa_signer()
};
let err = try_issue(&issuer, &profile).await.unwrap_err();
assert!(err.to_string().contains("privateKeyUsagePeriod"), "{err}");
}

/// La clé ne doit jamais valoir plus longtemps que le certificat qui la
/// porte (EN 319 421 `TIS-7.6.7-02/-04`).
#[tokio::test]
async fn issuance_is_refused_when_the_key_outlives_the_certificate() {
let issuer = build_issuer().await;
let profile = oe_ca_core::Profile {
// La clé (10 ans) dépasse largement la validité du certificat (2 ans).
private_key_validity: Some(time::Duration::days(10 * 365)),
..profile::tsa_signer()
};
let err = try_issue(&issuer, &profile).await.unwrap_err();
assert!(
err.to_string().contains("expire après le certificat"),
"{err}"
);
}

#[tokio::test]
async fn rejects_a_certificate_issued_with_the_ocsp_responder_profile() {
// Bon profil de PKI, mauvais usage : sert à vérifier que la fonction
Expand Down
32 changes: 30 additions & 2 deletions crates/oe-tsa-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ pub struct Options {

pub struct Authority {
opts: Options,
/// Constat T-3 de l'audit du 2026-09-25 (EN 319 421 `TIS-7.7.1-09`) :
/// mise en cache à la construction (`privateKeyUsagePeriod` du
/// certificat, `oe_conformance::tsu_private_key_not_after`) — relue à
/// chaque signature dans [`Self::timestamp`], jamais recalculée depuis
/// le certificat à chaque appel.
key_not_after: time::OffsetDateTime,
}

impl Authority {
Expand Down Expand Up @@ -149,8 +155,15 @@ impl Authority {
// confiance parce qu'il a été émis un jour — reproduit
// `CheckTSUCertificate` appelée par `cmd/tsa-server` (Go).
oe_conformance::check_tsu_certificate("certificat TSU", &opts.certificate)?;

Ok(Authority { opts })
// Garanti lisible : `check_tsu_certificate` ci-dessus vient de
// vérifier que l'extension existe et est bien formée.
let key_not_after =
oe_conformance::tsu_private_key_not_after("certificat TSU", &opts.certificate)?;

Ok(Authority {
opts,
key_not_after,
})
}

pub fn certificate(&self) -> &Certificate {
Expand Down Expand Up @@ -244,6 +257,21 @@ impl Authority {
)
})?;

// Constat T-3 de l'audit du 2026-09-25 (EN 319 421 `TIS-7.7.1-09`) :
// la clé de signature a sa propre date d'expiration, plus courte que
// celle du certificat — vérifiée à *chaque* signature, pas seulement
// au démarrage, un processus de longue durée pouvant la dépasser en
// cours de route.
if gen_time >= self.key_not_after {
return Err(reject(
FailureInfo::SystemFailure,
format!(
"clé de signature TSU expirée depuis le {}",
self.key_not_after
),
));
}

let tst_info = self.build_tst_info(&req, gen_time)?;
let tst_info_der = tst_info.to_der()?;

Expand Down
42 changes: 41 additions & 1 deletion crates/oe-tsa-core/tests/end_to_end.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ impl oe_hsm::SigningToken for CountingSigner {

fn load_authority_with_signer(
recorder: Option<Arc<dyn oe_tsa_core::Recorder>>,
) -> (Authority, Arc<CountingSigner>) {
load_authority_with_signer_and_clock(recorder, Arc::new(FixedClock))
}

fn load_authority_with_signer_and_clock(
recorder: Option<Arc<dyn oe_tsa_core::Recorder>>,
clock: Arc<dyn Clock>,
) -> (Authority, Arc<CountingSigner>) {
let dir = fixtures_dir();
let key_pem =
Expand All @@ -214,13 +221,46 @@ fn load_authority_with_signer(
policy: der::asn1::ObjectIdentifier::new("1.3.6.1.4.1.99999.1.1.1").unwrap(),
accuracy: std::time::Duration::from_secs(1),
signing_digest: DigestAlg::Sha256,
clock: Arc::new(FixedClock),
clock,
recorder,
})
.expect("construction de l'autorité");
(authority, signer)
}

/// Après l'expiration de la clé de la fixture (`privateKeyUsagePeriod`,
/// notAfter `2028-09-26`, voir `tests/fixtures/tsa/README.md`), mais avant
/// celle du certificat lui-même (`2029-09-25`) : isole le seul cas qui
/// intéresse le constat T-3.
struct KeyExpiredClock;
impl Clock for KeyExpiredClock {
fn now(&self) -> Result<time::OffsetDateTime, String> {
let date = time::Date::from_calendar_date(2029, time::Month::January, 1)
.map_err(|e| e.to_string())?;
Ok(date
.with_hms(0, 0, 0)
.map_err(|e| e.to_string())?
.assume_utc())
}
}

/// Constat T-3 de l'audit du 2026-09-25 (EN 319 421 `TIS-7.7.1-09`) : passé
/// la date d'expiration de la **clé** (pas celle du certificat, encore
/// valide), l'horodatage doit être refusé — et surtout, aucune signature ne
/// doit avoir lieu (pas seulement une erreur rendue après coup).
#[test]
fn a_timestamp_is_refused_once_the_signing_key_has_expired() {
let (authority, signer) = load_authority_with_signer_and_clock(None, Arc::new(KeyExpiredClock));
let req_der = any_granted_request();
let err = authority.timestamp(&req_der).unwrap_err();
assert!(err.to_string().contains("expirée"), "{err}");
assert_eq!(
signer.calls.load(std::sync::atomic::Ordering::SeqCst),
0,
"aucune signature ne doit avoir eu lieu : la clé a expiré avant"
);
}

#[test]
fn a_journal_failure_blocks_the_timestamp_before_any_signature() {
let (authority, signer) = load_authority_with_signer(Some(Arc::new(FailingRecorder)));
Expand Down
Loading
Loading