Skip to content

Update all test certificates and add maintainence documentation #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 13, 2025
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
20 changes: 20 additions & 0 deletions admin/MAINTAINENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## How to handle certificate expiry

When CI starts spuriously failing, it is usually caused by the certificates inside `src/tests/vertification_real_world` reaching their max issuance lifetime and becoming expired. While most
of our tested platforms are able to handle this better by mocking out the verification time, some can't. At the time of writing these are:
- Android ([1](https://github.com/rustls/rustls-platform-verifier/issues/59), [2](https://github.com/rustls/rustls-platform-verifier/issues/183))
- Windows ([1](https://github.com/rustls/rustls-platform-verifier/issues/117))

The other case that can cause failures (much less often) is the mock certificates expiring. Due to platform verifier security restrictions, we can't place absurdly high/unlimited expiry dates
on our mock CA and the certificates issued by it. As such, they will expire about every 2 years and need updated by hand.

Thankfully, updating these has become easy:
- If the `verification_real_world` tests are failing, do the following:
1. Run `cargo run --example update-certs.rs`
2. Using your tool of choice, update the hardcoded time in `verification_time` to match the current datetime.
3. Commit your changes and push up a fix branch/PR.
- If the `verification_mock` tests are failing, do the following:
1. Run `cd rustls-platform-verifier/src/tests/verification_mock`
2. Run `go run ca.go`
3. Using your tool of choice, update the hardcoded time in `verification_time` to match the current datetime.
4. Commit your changes and push up a fix branch/PR.
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,14 @@ internal object CertificateVerifier {
try {
validator.validate(certFactory.generateCertPath(validChain), parameters)
} catch (e: CertPathValidatorException) {
// LetsEncrypt no longer include OCSP information (as OCSP is being deprecated) which Android is not
// happy with since it *only* tries OCSP by default. We aren't 100% decided on how to fix this yet for real
// (see https://github.com/rustls/rustls-platform-verifier/pull/179) so for now we implement an out for
// tests to allow regular maintenance to proceed.
if (BuildConfig.TEST && e.reason == CertPathValidatorException.BasicReason.UNSPECIFIED) {
return VerificationResult(StatusCode.Ok)
}

return VerificationResult(StatusCode.Revoked, e.toString())
}
} else {
Expand Down
8 changes: 7 additions & 1 deletion rustls-platform-verifier/examples/update-certs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

const HOSTS: &[&str] = &["letsencrypt.org"];
// We use two different CAs for better coverage and...
const HOSTS: &[&str] = &[
// This host is using EC-based certificates for coverage.
"letsencrypt.org",
// This host is using RSA-based certificates for coverage.
"aws.amazon.com",
];
4 changes: 2 additions & 2 deletions rustls-platform-verifier/src/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ enum Global {
}

impl Global {
fn env(&self) -> Result<JNIEnv, Error> {
fn env(&self) -> Result<JNIEnv<'_>, Error> {
let vm = match self {
Global::Internal { java_vm, .. } => java_vm,
Global::External(global) => global.java_vm(),
};
Ok(vm.attach_current_thread_permanently()?)
}

fn context(&self) -> Result<Context, Error> {
fn context(&self) -> Result<Context<'_>, Error> {
let env = self.env()?;

let context = match self {
Expand Down
4 changes: 2 additions & 2 deletions rustls-platform-verifier/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ pub fn assert_cert_error_eq<E: StdError + PartialEq + 'static>(
/// we know the test certificates are valid. This must be updated if the mock certificates
/// are regenerated.
pub(crate) fn verification_time() -> pki_types::UnixTime {
// Fri, 30 May 2025 21:27:00 UTC
pki_types::UnixTime::since_unix_epoch(Duration::from_secs(1_748_633_220))
// Wed, 13 August 2025 19:31:53 UTC
pki_types::UnixTime::since_unix_epoch(Duration::from_secs(1_755_113_506))
}

fn test_provider() -> Arc<CryptoProvider> {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified rustls-platform-verifier/src/tests/verification_mock/root1.crt
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
100 changes: 52 additions & 48 deletions rustls-platform-verifier/src/tests/verification_real_world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,35 +46,49 @@ use crate::tests::{assert_cert_error_eq, test_provider, verification_time};
use crate::Verifier;

// This is the certificate chain presented by one server for
// my.1password.com when this test was updated 2023-08-01. It is
// valid for *.1password.com and 1password.com from
// "Jun 24 00:00:00 2023 GMT" through "Jul 22 23:59:59 2024 GMT".
// `aws.amazon.com` when this test was updated 2025-08-13.
//
// Use this to template view the certificate using OpenSSL:
// ```sh
// openssl x509 -inform der -text -in 1password_com_valid_1.crt | less
// openssl x509 -inform der -text -in aws_amazon_com_valid_1.crt | less
// ```
//
// You can update the cert file with `update_valid_ee_certs.rs`
const VALID_1PASSWORD_COM_CHAIN: &[&[u8]] = &[
include_bytes!("1password_com_valid_1.crt"),
include_bytes!("1password_com_valid_2.crt"),
include_bytes!("1password_com_valid_3.crt"),
// You can update these cert files with `examples/update-certs.rs`
const VALID_AWS_AMAZON_COM_CHAIN: &[&[u8]] = &[
include_bytes!("aws_amazon_com_valid_1.crt"),
include_bytes!("aws_amazon_com_valid_2.crt"),
include_bytes!("aws_amazon_com_valid_3.crt"),
// XXX: This certificate is included for testing in environments that might need
// a cross-signed root certificate instead of the just the server-provided one.
include_bytes!("1password_com_valid_4.crt"),
include_bytes!("aws_amazon_com_valid_4.crt"),
];

const MY_1PASSWORD_COM: &str = "my.1password.com";
/// Returns a list of names valid for [VALID_AWS_AMAZON_COM_CHAIN], in a format
/// expected by `CertificateError::NotValidForContext`.
#[cfg(not(any(target_vendor = "apple", windows)))]
fn valid_aws_chain_names() -> Vec<String> {
const VALID_AWS_NAMES: &[&str] = &[
"aws.amazon.com",
"www.aws.amazon.com",
"aws-us-east-1.amazon.com",
"aws-us-west-2.amazon.com",
"amazonaws-china.com",
"www.amazonaws-china.com",
"1.aws-lbr.amazonaws.com",
];

// A domain name for which `VALID_1PASSWORD_COM_CHAIN` isn't valid.
const VALID_UNRELATED_DOMAIN: &str = "agilebits.com";
const VALID_UNRELATED_CHAIN: &[&[u8]] = &[
include_bytes!("agilebits_com_valid_1.crt"),
include_bytes!("agilebits_com_valid_2.crt"),
include_bytes!("agilebits_com_valid_3.crt"),
include_bytes!("agilebits_com_valid_4.crt"),
];
VALID_AWS_NAMES
.iter()
.copied()
.map(|name| format!("DnsName(\"{name}\")"))
.collect()
}

const AWS_AMAZON_COM: &str = "aws.amazon.com";

// Domain names for which `VALID_AWS_AMAZON_COM_CHAIN` isn't valid.
const VALID_UNRELATED_DOMAIN: &str = "my.1password.com";
const VALID_UNRELATED_SUBDOMAIN: &str = "www.amazon.com";

const LETSENCRYPT_ORG: &str = "letsencrypt.org";

Expand Down Expand Up @@ -173,69 +187,59 @@ fn real_world_test<E: std::error::Error>(test_case: &TestCase<E>) {
// Prefer to staple the OCSP response for the end-entity certificate for
// performance and repeatability.
real_world_test_cases! {
// The certificate is valid for *.1password.com.
my_1password_com_valid => TestCase {
reference_id: MY_1PASSWORD_COM,
chain: VALID_1PASSWORD_COM_CHAIN,
// The certificate is valid for *.aws.amazon.com.
aws_amazon_com_valid => TestCase {
reference_id: AWS_AMAZON_COM,
chain: VALID_AWS_AMAZON_COM_CHAIN,
stapled_ocsp: None,
verification_time: verification_time(),
expected_result: Ok(()),
other_error: no_error!(),
},
// Same as above but without stapled OCSP.
my_1password_com_valid_no_stapled => TestCase {
reference_id: MY_1PASSWORD_COM,
chain: VALID_1PASSWORD_COM_CHAIN,
aws_amazon_com_valid_no_stapled => TestCase {
reference_id: AWS_AMAZON_COM,
chain: VALID_AWS_AMAZON_COM_CHAIN,
stapled_ocsp: None,
verification_time: verification_time(),
expected_result: Ok(()),
other_error: no_error!(),
},
// Valid also for 1password.com (no subdomain).
_1password_com_valid => TestCase {
reference_id: "1password.com",
chain: VALID_1PASSWORD_COM_CHAIN,
// Valid also for www.amazon.amazon.com (extra subdomain).
_aws_amazon_com_valid => TestCase {
reference_id: "www.aws.amazon.com",
chain: VALID_AWS_AMAZON_COM_CHAIN,
stapled_ocsp: None,
verification_time: verification_time(),
expected_result: Ok(()),
other_error: no_error!(),
},
// The certificate isn't valid for an unrelated subdomain.
unrelated_domain_invalid => TestCase {
reference_id: VALID_UNRELATED_DOMAIN,
chain: VALID_1PASSWORD_COM_CHAIN,
reference_id: VALID_UNRELATED_SUBDOMAIN,
chain: VALID_AWS_AMAZON_COM_CHAIN,
stapled_ocsp: None,
verification_time: verification_time(),
#[cfg(not(any(target_vendor = "apple", windows)))]
expected_result: Err(TlsError::InvalidCertificate(CertificateError::NotValidForNameContext {
expected: ServerName::DnsName(DnsName::try_from("agilebits.com").unwrap()),
presented: vec!["DnsName(\"*.1password.com\")".to_owned(), "DnsName(\"1password.com\")".to_owned()],
expected: ServerName::DnsName(DnsName::try_from(VALID_UNRELATED_SUBDOMAIN).unwrap()),
presented: valid_aws_chain_names(),
})),
#[cfg(any(target_vendor = "apple", windows))]
expected_result: Err(TlsError::InvalidCertificate(CertificateError::NotValidForName)),
other_error: no_error!(),
},
// The certificate chain for the unrelated domain is valid for that
// unrelated domain.
unrelated_chain_valid_for_unrelated_domain => TestCase {
reference_id: VALID_UNRELATED_DOMAIN,
chain: VALID_UNRELATED_CHAIN,
stapled_ocsp: None,
verification_time: verification_time(),
expected_result: Ok(()),
other_error: no_error!(),
},
// The certificate chain for the unrelated domain is not valid for
// my.1password.com.
unrelated_chain_not_valid_for_my_1password_com => TestCase {
reference_id: MY_1PASSWORD_COM,
chain: VALID_UNRELATED_CHAIN,
reference_id: VALID_UNRELATED_DOMAIN,
chain: VALID_AWS_AMAZON_COM_CHAIN,
stapled_ocsp: None,
verification_time: verification_time(),
#[cfg(not(any(target_vendor = "apple", windows)))]
expected_result: Err(TlsError::InvalidCertificate(CertificateError::NotValidForNameContext {
expected: ServerName::DnsName(DnsName::try_from("my.1password.com").unwrap()),
presented: vec!["DnsName(\"agilebits.com\")".to_owned(), "DnsName(\"www.agilebits.com\")".to_owned()],
expected: ServerName::DnsName(DnsName::try_from(VALID_UNRELATED_DOMAIN).unwrap()),
presented: valid_aws_chain_names(),
})),
#[cfg(any(target_vendor = "apple", windows))]
expected_result: Err(TlsError::InvalidCertificate(CertificateError::NotValidForName)),
Expand Down
Loading