// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! recovery_key_store contains functions for working with Google's
//! recovery key store, also called Vault internally.
use crate::{
debug, get_secret_from_request, pin, Authentication, DirtyFlag, ParsedState, Reauth,
RequestError, COUNTER_ID_KEY, VAULT_HANDLE_WITHOUT_TYPE_KEY, WRAPPED_PIN_DATA_KEY,
};
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use bytes::Bytes;
use cbor::{cbor, MapKey, MapKeyRef, MapLookupKey, Value};
const SECONDS_IN_A_DAY: i64 = 86400;
/// The public keys of the recovery key store cohorts are published in
/// XML files. This module implements a basic XML parser.
mod xml {
use alloc::boxed::Box;
use alloc::collections::btree_map::Entry;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::iter;
/// XML values either contain string content or other tags. This parser
/// doesn't support mixing them like HTML.
#[derive(Debug, PartialEq, Eq)]
pub enum Value {
String(String),
Object(BTreeMap<String, Element>),
}
impl Default for Value {
fn default() -> Self {
Value::String(String::new())
}
}
// The maximum supported nesting of XML tags.
const MAX_DEPTH: i32 = 16;
/// Each XML element is either a single value or a list of them. E.g.
/// in this XML:
///
/// <pantry>
/// <food>carrot</food>
/// </pantry>
///
/// The <pantry> value contains a `food` element that would be `Single`.
/// But if multiple foods are listed then `food` would be a `List`:
///
/// <pantry>
/// <food>carrot</food>
/// <food>cheese</food>
/// </pantry>
#[derive(Debug, PartialEq, Eq)]
pub enum Element {
Single(Value),
List(Vec<Value>),
}
impl Element {
/// iter iterates over the values of the `Element`, whether there be
/// one or several.
pub fn iter(&self) -> Box<dyn iter::Iterator<Item = &Value> + '_> {
match self {
Element::Single(value) => Box::new(iter::once(value)),
Element::List(values) => Box::new(values.iter()),
}
}
}
/// A ByteReader produces bytes one at a time and supports undoing a read.
/// While an arbitrary number of bytes can be unread, the XML parser only
/// requires a single byte lookahead.
struct ByteReader<'a> {
data: &'a [u8],
offset: usize,
}
impl ByteReader<'_> {
fn next(&mut self) -> Option<u8> {
if self.offset >= self.data.len() {
None
} else {
let ret = Some(self.data[self.offset]);
self.offset += 1;
ret
}
}
fn unread(&mut self) {
if self.offset > 0 {
self.offset -= 1;
}
}
}
fn is_whitespace(c: u8) -> bool {
// https://www.w3.org/TR/xml/#NT-S
matches!(c, b' ' | b'\r' | b'\n' | b'\t')
}
fn is_name_start(c: u8) -> bool {
// https://www.w3.org/TR/xml/#NT-NameStartChar
matches!(c, b':' | b'_' | b'a'..=b'z' | b'A'..=b'Z')
}
fn is_name_char(c: u8) -> bool {
// https://www.w3.org/TR/xml/#NT-NameChar
if is_name_start(c) { true } else { matches!(c, b'-' | b'.' | b'0'..=b'9') }
}
struct Parser<'a> {
reader: ByteReader<'a>,
}
impl Parser<'_> {
/// Parse a complete XML document.
fn top_level(&mut self) -> Option<(String, Value)> {
self.skip_whitespace();
self.match_header()?;
self.match_char(b'<')?;
self.parse_tag(/* depth= */ 1)
}
/// Discard bytes until `limit` returns true, then unread the final
/// byte.
fn skip_until<F>(&mut self, limit: F)
where
F: Fn(u8) -> bool,
{
loop {
match self.reader.next() {
None => {
return;
}
Some(byte) => {
if limit(byte) {
self.reader.unread();
return;
}
}
}
}
}
fn skip_whitespace(&mut self) {
self.skip_until(|c| !is_whitespace(c))
}
/// Skip an XML attribute within a tag.
fn skip_attribute(&mut self) -> Option<()> {
// https://www.w3.org/TR/xml/#NT-Attribute
self.skip_until(|c| !is_name_char(c));
self.skip_whitespace();
self.match_char(b'=')?;
self.skip_whitespace();
let delim = self.reader.next()?;
if delim != b'"' && delim != b'\'' {
return None;
}
self.skip_until(|c| c == delim);
self.match_char(delim)?;
self.skip_whitespace();
Some(())
}
/// Skip a series of XML attributes within a tag.
fn skip_attributes(&mut self) -> Option<()> {
loop {
let Some(byte) = self.reader.next() else {
return Some(());
};
if !is_name_start(byte) {
self.reader.unread();
return Some(());
}
self.skip_attribute()?;
}
}
/// Exactly match a single byte.
fn match_char(&mut self, c: u8) -> Option<()> {
match self.reader.next() {
None => None,
Some(byte) => {
if byte == c {
Some(())
} else {
None
}
}
}
}
/// Exactly match the given string.
fn match_string(&mut self, s: &str) -> Option<()> {
for c in s.bytes() {
self.match_char(c)?;
}
Some(())
}
/// Extract a string made from all the bytes until either EOF or until
/// `check` returns false. If `check` returns false, that byte is
/// unread.
fn string_while<F>(&mut self, check: F) -> Option<String>
where
F: Fn(u8) -> bool,
{
let mut vec = Vec::new();
loop {
let Some(byte) = self.reader.next() else {
break;
};
if !check(byte) {
self.reader.unread();
break;
}
vec.push(byte);
}
String::from_utf8(vec).ok()
}
/// Match an XML "header" (called a text declaration in the XML spec).
fn match_header(&mut self) -> Option<()> {
// https://www.w3.org/TR/xml/#NT-TextDecl
self.match_string("<?xml")?;
self.skip_whitespace();
self.skip_attributes()?;
self.match_string("?>")?;
self.skip_whitespace();
Some(())
}
/// Parse a tag. This expects to be positioned immediately after the
/// opening '<'.
fn parse_tag(&mut self, depth: i32) -> Option<(String, Value)> {
if depth > MAX_DEPTH {
return None;
}
self.skip_whitespace();
let tag = self.string_while(is_name_char)?;
self.skip_whitespace();
self.skip_attributes();
self.match_char(b'>')?;
self.skip_whitespace();
let mut dict = BTreeMap::new();
loop {
let byte = self.reader.next()?;
if byte == b'<' {
let byte = self.reader.next()?;
if byte == b'/' {
// This is the closing tag.
self.skip_whitespace();
self.match_string(&tag)?;
self.skip_whitespace();
self.match_char(b'>')?;
self.skip_whitespace();
return Some((tag, Value::Object(dict)));
}
// This is a another tag within this tag.
self.reader.unread();
let (subtag, subvalue) = self.parse_tag(depth + 1)?;
match dict.entry(subtag) {
Entry::Vacant(entry) => {
entry.insert(Element::Single(subvalue));
}
Entry::Occupied(mut entry) => {
let existing = entry.get_mut();
let mut new = match existing {
Element::Single(prev_value) => {
// This subtag has been seen once before. Now there are two
// values.
Element::List(vec![core::mem::take(prev_value), subvalue])
}
Element::List(prev_values) => {
// This subtag has been seen more than once before. Append the
// new value to the list.
prev_values.push(subvalue);
Element::List(core::mem::take(prev_values))
}
};
core::mem::swap(existing, &mut new);
}
}
} else if dict.is_empty() {
// This tag contains string content.
self.reader.unread();
let contents = self.string_while(|c| c != b'<')?;
self.match_string("</")?;
self.skip_whitespace();
self.match_string(&tag)?;
self.skip_whitespace();
self.match_char(b'>')?;
self.skip_whitespace();
return Some((tag, Value::String(String::from(contents.trim()))));
} else {
// This tag contains both tags and string content, which isn't supported.
return None;
}
}
}
}
/// Parse an XML document.
pub fn parse(xml: &[u8]) -> Option<(String, Value)> {
Parser { reader: ByteReader { data: xml, offset: 0 } }.top_level()
}
#[cfg(test)]
mod tests {
use super::Value::Object;
use super::*;
use crate::recovery_key_store::xml::Element::{List, Single};
#[test]
fn test_parse() {
let expected = (
String::from("certificate"),
Object(BTreeMap::from([
(
String::from("endpoints"),
Single(Object(BTreeMap::from([(
String::from("cert"),
List(vec![
Value::String(String::from(
"MIIDOzCCASOgAwIBAgIRALohAkmP2SJK75Xsk8FsngUwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0yMzA5MDUyMTUwNThaFw0yNTA0MDkwMDAwMDBaMDIxMDAuBgNVBAMTJ0dvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBFbmRwb2ludDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMCD3sSR26q9occ1Y/K2SQyIsSJkJtGALvd3t4l9E8ajmOV9fQHp7d4ExmRJIldlFL/Y5i5FBg3NvwK7TLvoAPmjEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD7HLz0sS04rV7BXzrd2KJdMk2fCbrjTPNNUUZu+UbPB0lDvWcP1+uroIOEZuPLUK0EBbQYzCjP/bp7tT4me4myivPbg2IBLvTaOVKbUzi6SqA4X+vyAe3c7Bp6A3hPzxNangk2jmpKdIvLXJ8DHyXVrCXk/dNObnWUDnvbmoXg5yWK/snB5OIysDPUlxUmRspxhRajVgRnDAMTnJ2YZhHC15Jm/neugxVKeSeBb4wamLRibkdWbc4KJTiSjh1CnH1OKsCI8N006Gk+YXHnrY3OmakVg/bSnfAoMWLMDvtXbDbMVYAl9uRLBDwoOS6MFMsrj+Iwniuv4E2Kb+UcWK36AR/KH1/ILFpRUTtfPwIQcvEc2tWkH+W2BJqKOvwGH3rOm2qF88g8/egrHua7jnv8aJlfQ3c3S7ytikxugCQhSAJhVO0kdWXGUut78UzBrhMEvBqHlQtZnyPSEWd6bJKdGqwmbQwdKoou5HCu0YQxanmzENR9PmDs6+AMN0xJDcb9TOBQsvQW+vY3D34U61izaU2xytglgRzjSlBwFYDP75VgsL9gcNlYSt9R1EroPPsaEV1xhW47WpWArLdprVhVX70kPf3fUkcpDXimapFpMWONWlSUCIKPy/q0d2DcamL9HN5sZLyOGPctMTEowPomW8TiISWJFdtSK2fJXkk8s",
)),
Value::String(String::from(
"MIIDOzCCASOgAwIBAgIRALohAkmP2SJK75Xsk8FsngUwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0yMzA5MDUyMTUwNThaFw0yNTA0MDkwMDAwMDBaMDIxMDAuBgNVBAMTJ0dvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBFbmRwb2ludDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOHSWq/RFpU1VnCCCmPcTDeJT3t3+27+BjFOdsC8/hcnbFUKwHt6Tt0uiHV3LP/aO0/DHYC8Kdb/KAMC+ai+aJ2jEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBALz6PK44f46capH7isFvHMdTosG3DIV4QP70zLtGtGBM+57RKU0UYLtgdtKfCCwQVIgru9PfMdNdbxKojI96cfB/QxsH5H/96iUET+EnvvQ63NMSnLtOp7H4UceBujpXeSLN0yRNr59JS+mLtyL5+5KjHgtOM7tpxJ3eP1tx8NnE30TE0BoeTQyoKu0wfHVsc5+Fs3EWJUpgV+Z0/KJFoy3M2Z0DHZxfn6fg+/xYxn8ttkMhlZXhJMjNqtcGmlwLYktmsG5LlsQNimXwGl9olVviEZwcHGUzHw8QWszoKzn+TgTgv76m2eZ5MwJeN1JnaLb+1gQtgKRpnG8TFxWGC/TIHUqLow/GruH2TSlLPr6l6ed+QjG01sAN5cdI7OR84D8W1F0vb8fVOr7kjf7N3qLDNQXDCRUUKHlRVanIt6h+kT1ctlM51+QmRhDsAkzY/3lFrXDySnQk18vlzTyA+QgqmvfNkPhgCp/fpgtWJFaPL9bJWaMaW/soXRUf26F6RMLK43EihdoVMtUAvmCIKUQyI88X6hJxEhWLyy/8Y45nAFk5CgXuzV2doOJTSITtJligTy1IuczH75bmp87c5ZPp51vUO4WYXuwffTCoQ8UYSYbNxxqKOfFkILnM1WoGAzCrVt5aKOyGPILzOsOS8X0EeQ9YF6Mvaf2iFljc2o30",
)),
Value::String(String::from(
"MIIDOzCCASOgAwIBAgIRALohAkmP2SJK75Xsk8FsngUwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0yMzA5MDUyMTUwNThaFw0yNTA0MDkwMDAwMDBaMDIxMDAuBgNVBAMTJ0dvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBFbmRwb2ludDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNeVqPpEctoVzN48WNefTpJEmRrrbpXoWRhHwH/AOYmQgXR6xX/AE1/qeen8fMj4Lnyb8KPveZjXvTlFq2mdBHGjEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAEQIGwhKa7MDq+Wt5p7fvv1AXhX4HxpgkKv5xbuMWCcw6R8zTYQ4hF/XHegIEqjmwWFxEvD95Lu3oLz4gMEoZVywBt2QFb1wkWUjdeT9oy5YbrJiLm9evhMFWyjnu2h9OVqxCVvarVx35ZySThDr2n3CYntLSKyTSdVlzCsdcCOj1UFkqMe73gOUZFMkXETUoINlFYwX6NP5V1Moy8OjsSNa6/8zyYwivm3rQlj3GUEhSlX+0ib+IXYpcrDFF7/6+G8lWBAHmKGwGR6kpAQ7Zg7KEjY0gSYWOr86oJIMFzeXVjaqhwGXK2tO+JBTPZSf4zljke+QCDN1uZjscgpOOXcBvT3LqLDaz2TSen4EMXhD56lYrq/970a1ol7B26nNAjJr1Q2ZyH4kXgBnK/b7AjYzNhTx0k0o7zRdh4tMeNkxhHgpBQ7d8VM81lZJg95n5SuOvJkJlEsPus9nJ1QeKAAjLV+Hp4n+xEImnvwnPEeE9vo07KHeHsCaBFVVan+9VKMiFEnYO+JdA8DwVTwTHHRH2T2OcEF+oo6m9nZZgGZbcovftryoOetJRY8E2JG+j5ScVWwnh5QcWhP1oOqsZdFWbKmJyxbN0qhKRWB1l6xZipMTj4RYzrZtwXNWdJIudC1Lkr6GgMn2UybLPc4xDH5FLWDtLN7griLweFrniuAQ",
)),
]),
)]))),
),
(
String::from("intermediates"),
Single(Object(BTreeMap::from([(
String::from("cert"),
List(vec![
Value::String(String::from(
"MIIFCjCCAvKgAwIBAgIRAN7d1InOjWGTUT558zWPLwEwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UEAxMVR29vZ2xlIENyeXB0QXV0aFZhdWx0MB4XDTE4MDUwOTAxMjAwNloXDTI4MDUxMDAxMjAwNlowOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO9067x94+sxIpqXHN9rcdorqVsH8s3ROZeBI3ORAWM8dGmR+m/yg7rrcLrLZNCKMo5RskjAc/9tWIFnoyJvp3bgJaZO1mOZGB6dF1rc3ZsWZJ5lk6roD3jWXoihI6A5qciG2Ojfn9d4UNkVYflg0xKMEP4tOFgS++XIbIZSBvtwONoOUK+w2RCnU/aCUKpJ7c49HBsieV/AcI3k4ia72JNip/9OeefyqaeuRt0X9vVTz1N4uu5LYQE90mrywaR9N0uFmfkJX6wIhkM4snbc/be5kpNcXn42seWVgLiQHwmynyN1VgHGlK+D+ewc5g3EotI4LNWjN7dgaz3wDEcVr9+cg2Z6wvh4qc5I8gxgXx5hYKIJcoXPXvyo95krrDtEatcILlVyrNoSl0aGhibh7Xt2CMEwtaS856r6JYQ9Zz6F3/KzM4B0c5XPR/Il7IAdae/e+Z4eVgj6zA19ngJmHWtMUzHHE3gcyDNqIcULMZYea7I11TVN4oW1pB6rsyIsBXALZXT93TJLI9HZ/w52A8qJIxIFP89iNtehPd8fYZipBJOj6e6PLf8+pcDE/RSSLs6ezURJ1gkovnubNhOxQ4+ku8WNsxCFB65sLriXNI8yZ8HWftJsop2k5gQ7wV0eXFNXJhAGaIXggKEb/Wf+qAEnMyxdAuLrlXwORl3AJteHAgMBAAGjJjAkMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4ICAQBlbWcXgD4KCBgBpNU6z8675oAiJb4YwrI8GT2Y5lglz6jkmy9gPZdU56PPyXO0MIBCsmmXxEcVURDULuX8DJsbzuqnbM8mEbmK8CVlMhq9NNOFZMCtnhu647lY+ZabBUYr4bSgPiJxwwMor3c15PFx/deZAYeAtbV9zW0Q07yXmjOoQhtgvJjEO9pwxwf1gktD9Wbj7OpSiLNlKGpLFOTjm0ckzIBGgwvYWp+A6LCjmOzuV91hdUF4LErG0Z6GQVllazHSJ5oaNEJx6wyJnt+gL4TDXwgDF7QpkSixBgfx5TY9QVsTi/wLzkDCjl8xuX3YXdlojofksxa83MAF6W8Pua4ZhKFTcnGAFQMTfPMUt0BAEkyTxlAovZ7H+ZXCkD47TkcGI9KWav7dDL9P4IqQljD9fr/R0anlH+rwJn9jJ1UqTbWoHgYr8qNa4SkD3WfZhb7TQJbUD6VocrEqBz6P9WgJFlB0Nn54ue7RlFC5+nlV8m6ZPbf6+f7wVOrVn0Obxq2t9RSiL9AebPDgfts+JgvflmPSOHD5W+4o42S4/huelfFxuIM1aid8lZip0TJBzYXWmOCp2SPHdN0wIp7/m1FjJ5Z7rjqn0dB+oXvHapywAdymEaVm/rs940d50cGg/1RfvAC3oYSyZe99YeK9DEQo1249+0n6QhhoJQJACw==",
)),
Value::String(String::from(
"MIIFGjCCAwKgAwIBAgIQHflnDNWkj2yxeD1IB6GdTTANBgkqhkiG9w0BAQsFADAxMS8wLQYDVQQDEyZHb29nbGUgQ2xvdWQgS2V5IFZhdWx0IFNlcnZpY2UgUm9vdCBDQTAeFw0xODA1MDcxODU4MTBaFw0yODA1MDgxODU4MTBaMDkxNzA1BgNVBAMTLkdvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBJbnRlcm1lZGlhdGUgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDvdOu8fePrMSKalxzfa3HaK6lbB/LN0TmXgSNzkQFjPHRpkfpv8oO663C6y2TQijKOUbJIwHP/bViBZ6Mib6d24CWmTtZjmRgenRda3N2bFmSeZZOq6A941l6IoSOgOanIhtjo35/XeFDZFWH5YNMSjBD+LThYEvvlyGyGUgb7cDjaDlCvsNkQp1P2glCqSe3OPRwbInlfwHCN5OImu9iTYqf/Tnnn8qmnrkbdF/b1U89TeLruS2EBPdJq8sGkfTdLhZn5CV+sCIZDOLJ23P23uZKTXF5+NrHllYC4kB8Jsp8jdVYBxpSvg/nsHOYNxKLSOCzVoze3YGs98AxHFa/fnINmesL4eKnOSPIMYF8eYWCiCXKFz178qPeZK6w7RGrXCC5VcqzaEpdGhoYm4e17dgjBMLWkvOeq+iWEPWc+hd/yszOAdHOVz0fyJeyAHWnv3vmeHlYI+swNfZ4CZh1rTFMxxxN4HMgzaiHFCzGWHmuyNdU1TeKFtaQeq7MiLAVwC2V0/d0ySyPR2f8OdgPKiSMSBT/PYjbXoT3fH2GYqQSTo+nujy3/PqXAxP0Uki7Ons1ESdYJKL57mzYTsUOPpLvFjbMQhQeubC64lzSPMmfB1n7SbKKdpOYEO8FdHlxTVyYQBmiF4IChG/1n/qgBJzMsXQLi65V8DkZdwCbXhwIDAQABoyYwJDAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBATANBgkqhkiG9w0BAQsFAAOCAgEAQ+G3v3JCbzChBs8HUGx6i2TMm1NZM71+chbA2JF9De8kVd/r2CETvvBRLXcTPcWWA0+PRDGaDmi4TR3bJhXgBStecQZkQtzI3ZcdFfI0rTNeCevfHp5nJjtB+AYomCTKNrlNLpk9YbJosqEKVLQBhlLNYm3PT4CQYJ1NubLLtKF1cn4Z+eayxud1kDrZWFyN5CYewOrtXc8oCynj8H0/NydOuCRQU2c/UXWmvsmlRRffHJEXLqCMitTHV9w4VHEVg9YYssxno/jWtp+b4z8JsE2vkJjs2tmOvfiMupbJx9h6zj2j04rjhf/A+vGPRKOD5WtbbX4An2+szsNLmERBfWUNsO1AaSTc3W+AJOjrG30tewS7jFRPluTtgB+kmozSW0MU/BgAYJuNKRVP8zklVmQqJRbrrxSzrvHzJlz/lvFu9MD7nGtiFqT9VggFjqq5vgn5srBp3Dq4GDGerg+HCDCN9qgnL1gBcKzCMK1oT0bCRWZGckT28WMnfcgZ/fuEVNgQcEXLgWiZWZDBEVlMh7u2QoOr2LXwXuXME8k87rAQbxvGLhyxq2uNxUdH16uljm7p5u2Qobyqxqf2rOGJYCBLK2JP74d6Nl6hD5FGBBaO6mN0Ojn/ShJ1Cq9o3wCHoLYn55wJnXYu7QXAX6230h7ekXpbxPPHO4x0Var5p+8=",
)),
]),
)]))),
),
(
String::from("metadata"),
Single(Object(BTreeMap::from([
(
String::from("creation-time"),
Single(Value::String(String::from("1694037058"))),
),
(
String::from("previous"),
Single(Object(BTreeMap::from([
(
String::from("hash"),
Single(Value::String(String::from(
"TQudrujnu1I9bdoDaYxGQYuRN/8SwTLjdk6vzYTOkIU=",
))),
),
(
String::from("serial"),
Single(Value::String(String::from("10015"))),
),
]))),
),
(
String::from("refresh-interval"),
Single(Value::String(String::from("2592000"))),
),
(String::from("serial"), Single(Value::String(String::from("10016")))),
]))),
),
])),
);
assert_eq!(expected, parse(crate::recovery_key_store::SAMPLE_CERTS_XML).unwrap());
}
#[test]
fn test_parse_with_attributes() {
// `SAMPLE_CERTS_XML` doesn't have any attributes in the tags so
// this test checks that they are ignored.
const XML: &[u8] = br#"<?xml ?>< a foo="true" bar='false' >b</ a >"#;
assert_eq!((String::from("a"), Value::String(String::from("b"))), parse(XML).unwrap());
}
#[test]
fn test_parse_with_mixed_tags_and_strings() {
// This parser doesn't support mixing textual contents with string contents in
// the same tag.
const XML: &[u8] = br#"<?xml?><a>b<tag></tag></a>"#;
assert!(parse(XML).is_none());
}
#[test]
fn test_max_depth() {
// The nests in this example exceeds `MAX_DEPTH`.
const XML: &[u8] = br#"<?xml?>
<a><a><a><a><a><a><a><a><a><a><a><a><a><a><a><a><a><a>
</a></a></a></a></a></a></a></a></a></a></a></a></a></a></a></a></a></a>"#;
assert!(parse(XML).is_none());
}
}
}
/// The x509 module provides basic X.509 support since the recovery key store
/// publishes its key in X.509 format.
mod x509 {
use super::SECONDS_IN_A_DAY;
use crate::der;
use alloc::string::String;
/// The parsed form of an X.509 certificate.
#[derive(Debug, PartialEq)]
pub struct Certificate<'a> {
/// The full DER contents of the certificate.
pub der: &'a [u8],
/// The DER bytes, including the header, of the subject of the
/// certificate.
pub subject: &'a [u8],
/// The DER bytes, including the header, of the issuer of the
/// certificate.
pub issuer: &'a [u8],
/// The time before which the certificate is invalid. This is in seconds
/// since the UNIX epoch, but only accurate to 24 hrs.
pub not_before_approx_epoch_seconds: i64,
/// The time after which the certificate is invalid. This is in seconds
/// since the UNIX epoch, but only accurate to 24 hrs.
pub not_after_approx_epoch_seconds: i64,
/// The SubjectPublicKeyInfo from the certificate (including DER
/// header).
pub spki: &'a [u8],
/// The signature by the issuer.
pub signature: &'a [u8],
/// The object identifier bytes (not including the header) of the
/// signature algorithm used to sign this certificate.
pub sig_algo_oid: &'a [u8],
/// True if this is a CA certificate.
pub is_ca: bool,
/// The body of the certificate that is signed.
pub to_be_signed: &'a [u8],
}
/// Parse a DER-encoded, X.509 certificate.
pub fn parse(input: &[u8]) -> Option<Certificate> {
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.1
let (top_level, empty) = der::next_tagged(input, der::SEQUENCE)?;
if !empty.is_empty() {
return None;
}
let (der::SEQUENCE, header_len, to_be_signed, top_level) = der::next_element(top_level)?
else {
return None;
};
let tbs = &to_be_signed[header_len..];
let (_outer_sig_algo, top_level) = der::next_tagged(top_level, der::SEQUENCE)?;
let (sig_bit_string, empty) = der::next_tagged(top_level, der::BIT_STRING)?;
if !empty.is_empty() {
return None;
}
// The first byte of a BIT STRING is the number of unused bits. This is
// always zero for the key types that we deal with.
let (unused_bits, signature) = der::u8_next(sig_bit_string)?;
if unused_bits != 0 {
return None;
}
let (_version, tbs) = der::next_optional(tbs, der::CONTEXT_SPECIFIC | der::CONSTRUCTED)?;
let (_serial_number, tbs) = der::next_tagged(tbs, der::INTEGER)?;
let (sig_algo, tbs) = der::next_tagged(tbs, der::SEQUENCE)?;
let (sig_algo_oid, _) = der::next_tagged(sig_algo, der::OBJECT_IDENTIFIER)?;
let (der::SEQUENCE, _, issuer, tbs) = der::next_element(tbs)? else {
return None;
};
let (validity, tbs) = der::next_tagged(tbs, der::SEQUENCE)?;
let (der::SEQUENCE, _, subject, tbs) = der::next_element(tbs)? else {
return None;
};
let (der::SEQUENCE, _, spki, tbs) = der::next_element(tbs)? else {
return None;
};
let (_issuer_unique_id, tbs) =
der::next_optional(tbs, der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 1)?;
let (_subject_unique_id, tbs) =
der::next_optional(tbs, der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 2)?;
let (extensions, _tbs) =
der::next_optional(tbs, der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 3)?;
// Future versions of X.509 may add further fields so `_tbs` might not be empty
// at this point.
let mut is_ca = false;
if let Some(extensions) = extensions {
let (mut extensions, empty) = der::next_tagged(extensions, der::SEQUENCE)?;
if !empty.is_empty() {
return None;
}
while !extensions.is_empty() {
let (extension, rest) = der::next_tagged(extensions, der::SEQUENCE)?;
extensions = rest;
let (id, rest) = der::next_tagged(extension, der::OBJECT_IDENTIFIER)?;
let (_critical, rest) = der::next_optional(rest, der::BOOLEAN)?;
let (value, rest) = der::next_tagged(rest, der::OCTET_STRING)?;
if !rest.is_empty() {
return None;
}
if id == b"\x55\x1d\x13" {
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9
let (basic_constraints, empty) = der::next_tagged(value, der::SEQUENCE)?;
if !empty.is_empty() {
return None;
}
let (is_ca_bool, _basic_constraints) =
der::next_optional(basic_constraints, der::BOOLEAN)?;
if let Some(b"\xff") = is_ca_bool {
is_ca = true;
}
}
}
}
let (not_before, rest) = parse_time(validity)?;
let (not_after, empty) = parse_time(rest)?;
if !empty.is_empty() {
return None;
}
Some(Certificate {
der: input,
subject,
issuer,
not_before_approx_epoch_seconds: not_before,
not_after_approx_epoch_seconds: not_after,
spki,
signature,
sig_algo_oid,
is_ca,
to_be_signed,
})
}
/// Parse a PKIX timestamp into approximate seconds since the epoch, also
/// returning the remaining input. The parsed timestamp is only accurate to
/// 24 hrs. Note that PKIX imposes additional limits on the format of
/// timestamps on top of ASN.1 and this code assumes those limits.
fn parse_time(der: &[u8]) -> Option<(i64, &[u8])> {
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5
let (tag, contents, rest) = der::next(der)?;
if tag == der::UTC_TIME {
// YYMMDDHHMMSSZ
if contents.len() != 13 {
return None;
}
let contents = String::from_utf8(contents.to_vec()).ok()?;
let two_digit_year = contents[0..2].parse::<u8>().ok()? as u16;
let year =
if two_digit_year >= 50 { 1900 + two_digit_year } else { 2000 + two_digit_year };
let month = contents[2..4].parse::<u8>().ok()?;
let day = contents[4..6].parse::<u8>().ok()?;
Some((epoch_seconds(year, month, day)?, rest))
} else if tag == der::GENERALIZED_TIME {
// YYYYMMDDHHMMSSZ
if contents.len() != 15 {
return None;
}
let contents = String::from_utf8(contents.to_vec()).ok()?;
let year = contents[0..4].parse::<u16>().ok()?;
let month = contents[4..6].parse::<u8>().ok()?;
let day = contents[6..8].parse::<u8>().ok()?;
Some((epoch_seconds(year, month, day)?, rest))
} else {
None
}
}
/// Convert a year/month/day into seconds since the epoch.
fn epoch_seconds(year: u16, month: u8, day: u8) -> Option<i64> {
fn is_leap_year(year: u16) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
fn days_in_year(year: u16) -> u16 {
if is_leap_year(year) { 366 } else { 365 }
}
const DAYS_IN_MONTH: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const DAYS_BEFORE_MONTH: [u16; 12] =
[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
if !(1970..=9999).contains(&year) || !(1..=12).contains(&month) || !(1..=31).contains(&day)
{
return None;
}
let leap_year = is_leap_year(year);
let month_idx = (month - 1) as usize;
let days_in_month = if leap_year && month == 2 { 29 } else { DAYS_IN_MONTH[month_idx] };
if day > days_in_month {
return None;
}
let mut days: i64 = day as i64 - 1;
days += DAYS_BEFORE_MONTH[month_idx] as i64;
if month > 2 && leap_year {
days += 1;
}
for y in 1970..year {
days += days_in_year(y) as i64;
}
Some(days * SECONDS_IN_A_DAY)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::recovery_key_store::ROOT_CERTIFICATE;
#[test]
fn test_parse() {
let result = parse(ROOT_CERTIFICATE).unwrap();
let expected = Certificate {
der: ROOT_CERTIFICATE,
not_before_approx_epoch_seconds: 1525651200,
not_after_approx_epoch_seconds: 2156889600,
is_ca: true,
// These offsets into `ROOT_CERTIFICATE` are derived from
// the output of `openssl asn1parse -i`.
issuer: &ROOT_CERTIFICATE[0x2e..0x2e + 51],
subject: &ROOT_CERTIFICATE[0x81..0x81 + 51],
// The self signature has been removed from the root certificate for size.
signature: b"",
spki: &ROOT_CERTIFICATE[0xb4..0xb4 + 550],
sig_algo_oid: &ROOT_CERTIFICATE[0x23..0x23 + 9],
to_be_signed: &ROOT_CERTIFICATE[0x04..0x04 + 763],
};
assert_eq!(result, expected);
}
#[test]
fn test_epoch_seconds() {
// These second values can be confirmed by evaluating, e.g.,
// `time.gmtime(320630400)` in Python after `import time`.
let expected = [
(1970, 01, 01, 0),
(1980, 02, 29, 320630400),
(1983, 03, 05, 415670400),
(1999, 12, 31, 946598400),
(2000, 01, 01, 946684800),
(2010, 06, 17, 1276732800),
];
for (year, month, day, value) in expected {
let Some(result) = epoch_seconds(year, month, day) else {
panic!("year: {}, month: {}, day: {} panicked", year, month, day);
};
if result != value {
panic!(
"year: {}, month: {}, day: {} -> {}, but expected {}",
year, month, day, result, value
);
}
}
}
#[test]
fn test_invalid_dates() {
let invalid = [
(1969, 12, 31),
(1970, 0, 1),
(1970, 1, 0),
(1970, 13, 1),
(1970, 12, 32),
// 1999 is not a leap year, so no 29th of Feb.
(1999, 02, 29),
// No April 31st in any year.
(2000, 04, 31),
(10000, 1, 1),
];
for (year, month, day) in invalid {
if let Some(_) = epoch_seconds(year, month, day) {
panic!(
"year: {}, month: {}, day: {} unexpectedly didn't panic",
year, month, day
);
}
}
}
}
}
/// The key_distribution module is concerned with getting cohort public keys
/// from the distributed files that contain and validate them.
mod key_distribution {
use super::{x509, xml, ROOT_CERTIFICATE, SECONDS_IN_A_DAY};
use crate::spki;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
#[cfg(feature = "chromium_integration_test")]
use super::TEST_ROOT_CERTIFICATE;
/// A Cohort represents a specific recovery key store cohort. It includes:
/// * The public key of the cohort.
/// * Its certificate chain.
/// * The serial number of the public key update.
type Cohort = ([u8; crypto::P256_X962_LENGTH], Vec<Vec<u8>>, i64);
/// Return an ECDH public key, a certificate path, and the serial
/// number, from an arbitrary recovery key store cohort, given the published
/// XML files. These XML files are found at
/// * https://www.gstatic.com/cryptauthvault/v0/cert.xml
/// * https://www.gstatic.com/cryptauthvault/v0/cert.sig.xml
///
/// The `current_time` is used for certificate validation. The
/// `cohort_selector` is used to deterministically select one of the
/// cohorts from the set.
pub fn get_cohort_key(
cert_xml: &[u8],
sig_xml: &[u8],
current_time_epoch_millis: i64,
cohort_selector: u32,
) -> Result<Cohort, &'static str> {
// The cert.sig.xml contains:
// a) a leaf X.509 certificate.
// b) a signature of the `cert.xml` file by that certificate.
// c) a number of intermediate certificates from which a chain can be
// built to `ROOT_CERTIFICATE`.
let (sig_toplevel, sig_value) = xml::parse(sig_xml).ok_or("failed to parse sig XML")?;
if sig_toplevel != "signature" {
return Err("unexpected toplevel element in sig XML");
}
let xml::Value::Object(sig_value) = sig_value else {
return Err("no child elements in sig XML");
};
#[cfg(not(feature = "chromium_integration_test"))]
let root_cert = ROOT_CERTIFICATE;
#[cfg(feature = "chromium_integration_test")]
let root_cert = if sig_value.contains_key("chromium-test") {
TEST_ROOT_CERTIFICATE
} else {
ROOT_CERTIFICATE
};
let Some(xml::Element::Single(xml::Value::String(leaf_cert_der_b64))) =
sig_value.get("certificate")
else {
return Err("missing <certificate>");
};
let leaf_cert_der = base64::decode(leaf_cert_der_b64)
.map_err(|_| "failed to base64-decode <certificate>")?;
let leaf_cert = x509::parse(&leaf_cert_der).ok_or("failed to parse <certificate>")?;
let (leaf_key_type, leaf_key) =
spki::parse(leaf_cert.spki).ok_or("failed to parse leaf key")?;
if leaf_key_type != spki::PublicKeyType::RSA {
return Err("leaf key is not RSA, but should be");
}
let Some(xml::Element::Single(xml::Value::String(sig_b64))) = sig_value.get("value") else {
return Err("missing <value>");
};
let sig = base64::decode(sig_b64).map_err(|_| "failed to base64-decode <value>")?;
// The public key from <certificate> should only be trusted if it chains
// up to `ROOT_CERTIFICATE`.
build_path(&leaf_cert, sig_value, root_cert, current_time_epoch_millis)?;
// The <value> is an RSA signature of the cert.xml file.
if !crypto::rsa_verify(leaf_key, cert_xml, &sig) {
return Err("signature validation failed");
}
// Now that the `cert.xml` file has been validated, the cohort public keys
// can be extracted from it.
get_public_keys_from_certs(cert_xml, current_time_epoch_millis, root_cert, cohort_selector)
}
/// Return an ECDH public key, a certificate path, and the serial number
/// from an arbitrary recovery key store cohort, given the `cert.xml` file,
/// which must already have been validated.
///
/// The `current_time` is used for certificate validation and also to
/// "randomly" pick a cohort.
fn get_public_keys_from_certs(
cert_xml: &[u8],
current_time_epoch_millis: i64,
root_cert: &[u8],
cohort_selector: u32,
) -> Result<Cohort, &'static str> {
let (certs_toplevel, certs_value) =
xml::parse(cert_xml).ok_or("failed to parse certs XML")?;
if certs_toplevel != "certificate" {
return Err("unexpected toplevel element in certs XML");
}
let xml::Value::Object(certs_value) = certs_value else {
return Err("no child elements in certs XML");
};
let Some(xml::Element::Single(xml::Value::Object(metadata))) = certs_value.get("metadata")
else {
return Err("missing or incorrectly structured <metadata>");
};
let Some(xml::Element::Single(xml::Value::String(serial))) = metadata.get("serial") else {
return Err("missing or incorrectly structured <serial>");
};
let serial = serial.parse::<i64>().map_err(|_| "cannot parse serial number")?;
let Some(xml::Element::Single(xml::Value::Object(endpoints))) =
certs_value.get("endpoints")
else {
return Err("missing or incorrectly structured <endpoints>");
};
let endpoint_ders = endpoints
.get("cert")
.ok_or("missing <cert>")?
.iter()
.map(|cert_value| {
let xml::Value::String(cert_der_b64) = cert_value else {
return Err("unexpected object in <cert>");
};
base64::decode(cert_der_b64).map_err(|_| "failed to base64-decode <cert>")
})
.collect::<Result<Vec<Vec<u8>>, &'static str>>()?;
if endpoint_ders.is_empty() {
// This should be impossible, but is checked here because
// establishing that fact involves aspects of the XML parser.
return Err("no endpoint certificates");
}
// Pick one of the endpoints arbitrarily.
let endpoint_der = &endpoint_ders[cohort_selector as usize % endpoints.len()];
let endpoint_cert = x509::parse(endpoint_der).ok_or("failed to parse <cert>")?;
let (cohort_public_key_type, cohort_public_key) =
spki::parse(endpoint_cert.spki).ok_or("invalid SPKI in leaf certificate")?;
if cohort_public_key_type != spki::PublicKeyType::P256 {
return Err("leaf certificate public key is not P-256");
}
let cohort_public_key: [u8; crypto::P256_X962_LENGTH] =
cohort_public_key.try_into().map_err(|_| "leaf certificate public key invalid")?;
Ok((
cohort_public_key,
// Even though the `cert.xml` file is signed by `cert.sig.xml`,
// the certificates within it also chain to `ROOT_CERTIFICATE` and
// the certificate path is required in futher parts of the protocol.
// Thus it might seem that checking `cert.sig.xml` is superfluous
// but the Android implementation does it and so this code does too.
build_path(&endpoint_cert, certs_value, root_cert, current_time_epoch_millis)?,
serial,
))
}
/// Attempt to build a path from `leaf` to `root_der` using intermediates
/// from `xml`. If successful, returns the DER encoded certificates on
/// the path, not including the root, starting from the leaf.
fn build_path(
leaf: &x509::Certificate,
xml: BTreeMap<String, xml::Element>,
root_der: &[u8],
current_time_epoch_millis: i64,
) -> Result<Vec<Vec<u8>>, &'static str> {
let root = x509::parse(root_der).ok_or("failed to parse root")?;
let Some(xml::Element::Single(xml::Value::Object(intermediates_value))) =
xml.get("intermediates")
else {
return Err("missing <intermediates>");
};
let mut intermediates_der = intermediates_value
.get("cert")
.unwrap_or(&xml::Element::List(Vec::new()))
.iter()
.map(|cert_value| {
let xml::Value::String(cert_der_b64) = cert_value else {
return Err("non-string in intermediates list");
};
base64::decode(cert_der_b64).map_err(|_| "failed to base64-decode <intermediate>")
})
.collect::<Result<Vec<Vec<u8>>, &'static str>>()?;
let intermediates = intermediates_der
.iter()
.map(|der| x509::parse(der))
.collect::<Option<Vec<x509::Certificate>>>()
.ok_or("failed to parse <intermediate>")?;
/// Verify an X.509 signature.
fn verify(
sig_algo_oid: &[u8],
issuer_spki: &[u8],
signed_message: &[u8],
signature: &[u8],
) -> bool {
// This is the DER-encoded OID for RSA signatures using SHA-256.
const RSA_WITH_SHA256: &[u8] = b"\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0b";
if sig_algo_oid != RSA_WITH_SHA256 {
return false;
}
let Some((pub_key_type, pub_key)) = spki::parse(issuer_spki) else {
return false;
};
if pub_key_type != spki::PublicKeyType::RSA {
return false;
}
crypto::rsa_verify(pub_key, signed_message, signature)
}
/// Check that the claimed chain of certificates is valid at the given
/// time.
fn check_path(certs: &[&x509::Certificate], current_time_epoch_secs: i64) -> bool {
if certs.len() < 2 {
return false;
}
certs.windows(2).all(|pair| {
// The timestamps are only accurate to within a day, but that's fine for
// expiration checking. The "not after" timestamp is advanced by a day so that
// to err on the side of validity.
pair[0].not_before_approx_epoch_seconds <= current_time_epoch_secs
&& pair[0].not_after_approx_epoch_seconds + SECONDS_IN_A_DAY
>= current_time_epoch_secs
&& pair[1].is_ca
&& pair[0].issuer == pair[1].subject
&& verify(
pair[0].sig_algo_oid,
pair[1].spki,
pair[0].to_be_signed,
pair[0].signature,
)
})
}
/// Perform one step of path building. The path is built from the leaf
/// upwards by adding a subset of the intermediate certificates. If
/// successful it returns a vector of indexes of the intermediates used,
/// in order from leaf to root.
fn path_step(
leaf: &x509::Certificate,
intermediates: &[x509::Certificate],
root: &x509::Certificate,
current_time_epoch_millis: i64,
// The number of steps performed so far.
steps: &mut usize,
// The indexes of the intermediates in the current path.
path: Vec<usize>,
) -> Option<Vec<usize>> {
// While cycles in the certificate graph are already blocked, this algorithm can
// still be factorial in the number of intermediates. This limits the maximum
// about of work that will be attempted. In practice, the total number of steps
// needed at the time of writing is two.
const MAX_STEPS: usize = 100;
if *steps > MAX_STEPS {
return None;
}
let head = path.last().map_or(leaf, |i| &intermediates[*i]);
if head.issuer == root.subject {
// We can reach the root in a single step from the current head. See whether
// that's a valid chain.
let mut certs = Vec::new();
certs.push(leaf);
for i in &path {
certs.push(&intermediates[*i]);
}
certs.push(root);
return if check_path(&certs, current_time_epoch_millis / 1000) {
Some(path)
} else {
None
};
}
for (i, intermediate) in intermediates.iter().enumerate() {
// An intermediate will not be used twice in any path.
if path.contains(&i) {
continue;
}
if head.issuer == intermediate.subject {
// This intermediate fits on the end of the current path. Recurse to see whether
// a valid path can be found using it.
let mut path = path.clone();
path.push(i);
*steps += 1;
let path = path_step(
leaf,
intermediates,
root,
current_time_epoch_millis,
steps,
path,
);
if path.is_some() {
return path;
}
}
}
// This path is a dead-end. Backtrack to try another.
None
}
let mut steps = 0;
let intermediate_indexes = path_step(
leaf,
&intermediates,
&root,
current_time_epoch_millis,
&mut steps,
Vec::new(),
)
.ok_or("no path found")?;
// Build a vector of the certificates in the valid path by pulling
// the values from `intermediates_der`.
let mut ret = vec![leaf.der.to_vec()];
for intermediate_index in intermediate_indexes {
ret.push(core::mem::take(intermediates_der.get_mut(intermediate_index).unwrap()));
}
Ok(ret)
}
#[cfg(test)]
mod tests {
use super::super::{SAMPLE_CERTS_XML, SAMPLE_SIG_XML, SAMPLE_VALIDATION_EPOCH_MILLIS};
use super::*;
const SAMPLE_COHORT_SELECTOR: u32 = 0;
#[test]
fn test_get_public_keys() {
let (_key, path, serial) = get_cohort_key(
SAMPLE_CERTS_XML,
SAMPLE_SIG_XML,
SAMPLE_VALIDATION_EPOCH_MILLIS,
SAMPLE_COHORT_SELECTOR,
)
.unwrap();
assert_eq!(path.len(), 2);
assert_eq!(serial, 10016);
}
#[test]
fn test_get_public_keys_expired() {
assert!(
get_cohort_key(SAMPLE_CERTS_XML, SAMPLE_SIG_XML, 1, SAMPLE_COHORT_SELECTOR)
.is_err()
)
}
#[test]
fn test_looping() {
// This is a self-signed certificate, thus its issuer and subject names are
// equal. The this is substituted into the template below as the leaf, and as a
// number of intermediates. This creates a factorial number of possible paths to
// explore. This test checks that this fails quickly because of the step limit.
const SELF_SIGNED: &str = "MIIC9TCCAd2gAwIBAgIRANpvKYYQJ4Qj7jmebDBBsnMwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEChMHQWNtZSBDbzAeFw0yNDAyMTAxMzM4MDJaFw0yNTAyMDkxMzM4MDJaMBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC305e6s9JSPDozo3q0XqPeiv4LslYJx23UtJ8/f1EuoxjRCS4lXCAxaj0WBgyNLqShVa7ZJ3BbzfNIcqmDhzEWB8FeyucDutxjjUWmuE6jNd892Daid64tjPuBGVaqPw71zaletuq8CLprzZdTFuW9TNCuCaDaeeibqrfjH8SbUCV1lIMd0wIJ+6QcvoN6OBNSmR/zjjfoJq4U6gt1MpLpdy2kUhdr6gDU2A4JMOUztXk4MBvV2nfbtt8YldTJshukcfgC6Z3y5wXcGI1qsKQPbHYzIXIM2UOSSVIWLjuYWr68yIrQkP+ph1lyBFZo/j4yE5Bi4mleWxJn6K1azvIxAgMBAAGjRjBEMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA8GA1UdEQQIMAaCBHRlc3QwDQYJKoZIhvcNAQELBQADggEBAAK8zjiJ2HCPhdJB+T0526ao0pc08O8B8VZ4fD4Y+dO3REDwqQBvbPKtv9ysF3iSqd6wJwIHw4KpGi6Le557b786ev91mWoInRgEQb+aRKCeGJP2+g1T5JTy5KRfsheeq2ilGorGFTsqz89oDXbGsGilQZS69i/mMFaLketBDChG5XYom5YCtO2F6P++FVWyXCZtSl4roiO6Tol2si2GRmUZMi4YvgG98EbNaXQ0POO785cXSJVkB6Ag8uO7Vll01DcHYfJLkKXRCaetYIdUAlkdiRnJSaXNFKolqK3NO3RbUIBqmYjRU85Fbbun99Y2CbpA2xLRsfMjVabbQFkg/QA=";
const TEMPLATE: &str = r#"
<?xml version="1.0" encoding="UTF-8"?>
<signature>
<intermediates>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
<cert>CERT</cert>
</intermediates>
<certificate>CERT</certificate>
<value>9Et3PY1oHl8=</value>
</signature>
"#;
let sig_xml = TEMPLATE.replace("CERT", SELF_SIGNED);
assert_eq!(
Err("no path found"),
get_cohort_key(b"", &sig_xml.into_bytes(), 1, SAMPLE_COHORT_SELECTOR)
);
}
}
}
/// The securebox module implements the "securebox" construction that is used
/// by the recovery key store.
///
/// Within Google, see go/securebox2
mod securebox {
use alloc::vec::Vec;
const EMPTY: &[u8] = b"";
const VERSION: &[u8] = b"\x02\x00";
const SALT: &[u8] = b"SECUREBOX\x02\x00";
const SHARED_SECRET_MAGIC: &[u8] = b"SHARED HKDF-SHA-256 AES-128-GCM";
const ECDH_MAGIC: &[u8] = b"P256 HKDF-SHA-256 AES-128-GCM";
/// Encrypt `payload`, and authenticate `header`, to a combination of
/// `shared_secret` and a key shared with the optional public key
/// `their_public`. Only fails if `their_public` is invalid.
pub fn encrypt(
their_public: Option<&[u8; crypto::P256_X962_LENGTH]>,
shared_secret: &[u8],
header: &[u8],
payload: &[u8],
) -> Option<Vec<u8>> {
let keying_material_storage;
let my_public_storage;
let (my_public, keying_material, info) = match their_public {
None => (EMPTY, shared_secret, SHARED_SECRET_MAGIC),
Some(their_public) => {
let my_private = crypto::P256Scalar::generate();
my_public_storage = my_private.compute_public_key();
let ecdh_shared_secret =
crypto::p256_scalar_mult(&my_private, their_public).ok()?;
keying_material_storage = [&ecdh_shared_secret, shared_secret].concat();
(my_public_storage.as_ref(), keying_material_storage.as_ref(), ECDH_MAGIC)
}
};
let mut secret_key = [0u8; 16];
crypto::hkdf_sha256(keying_material, SALT, info, &mut secret_key).ok()?;
let mut nonce = [0u8; crypto::NONCE_LEN];
crypto::rand_bytes(&mut nonce);
let mut ciphertext = payload.to_vec();
crypto::aes_128_gcm_seal_in_place(&secret_key, &nonce, header, &mut ciphertext);
Some([VERSION, my_public, nonce.as_ref(), ciphertext.as_ref()].concat())
}
#[cfg(test)]
mod tests {
use super::*;
fn decrypt(
my_private: Option<&[u8; crypto::P256_SCALAR_LENGTH]>,
shared_secret: &[u8],
header: &[u8],
encrypted_payload: &[u8],
) -> Option<Vec<u8>> {
if encrypted_payload.len() < VERSION.len() {
return None;
}
let (version, encrypted_payload) = encrypted_payload.split_at(VERSION.len());
if version != VERSION {
return None;
}
let keying_material_storage;
let (keying_material, info, encrypted_payload) = match my_private {
None => (shared_secret, SHARED_SECRET_MAGIC, encrypted_payload),
Some(my_private) => {
let my_private = crypto::P256Scalar::try_from(my_private).ok()?;
if encrypted_payload.len() < crypto::P256_X962_LENGTH {
return None;
}
let (their_public, encrypted_payload) =
encrypted_payload.split_at(crypto::P256_X962_LENGTH);
let ecdh_shared_secret =
crypto::p256_scalar_mult(&my_private, their_public.try_into().unwrap())
.ok()?;
keying_material_storage = [&ecdh_shared_secret, shared_secret].concat();
(keying_material_storage.as_ref(), ECDH_MAGIC, encrypted_payload)
}
};
if encrypted_payload.len() < crypto::NONCE_LEN {
return None;
}
let (nonce, ciphertext) = encrypted_payload.split_at(crypto::NONCE_LEN);
let ciphertext = ciphertext.to_vec();
let mut secret_key = [0u8; 16];
crypto::hkdf_sha256(keying_material, SALT, info, &mut secret_key).ok()?;
crypto::aes_128_gcm_open_in_place(
&secret_key,
nonce.try_into().unwrap(),
header,
ciphertext,
)
.ok()
}
const TEST_SHARED_SECRET: &[u8] = b"sharedsecret";
const TEST_HEADER: &[u8] = b"header";
const TEST_PAYLOAD: &[u8] = b"payload";
const TEST_PUBLIC_KEY : &[u8; crypto::P256_X962_LENGTH] = b"\x04\x8b\x76\xbe\x02\xa7\xf8\xa4\x03\x3c\x3f\x47\x37\xaa\xd7\x94\x23\x88\x34\xb1\xc4\x47\xab\xa7\xb8\xd8\x99\xd5\xc4\x3d\xc7\x20\x91\x1c\x40\x44\x6b\x4c\x0b\xfe\xe1\x72\x45\x72\x4d\x26\x8e\x9a\x55\x16\xa1\xf1\x98\x8c\x58\x97\xfd\xc0\x13\x22\xc7\x10\x17\x9a\xf1";
const TEST_PRIVATE_KEY : &[u8; crypto::P256_SCALAR_LENGTH] = b"\x2a\x55\xbb\x5f\x96\x08\x60\x8c\xd8\x6b\x09\xbc\xe7\x30\xf7\xc4\xff\x0d\xd5\xe7\x64\xc7\x3c\xf2\x2c\xbb\x3c\x2a\x2b\xba\x83\x20";
#[test]
fn encrypt_symmetric() {
let ciphertext = encrypt(None, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD).unwrap();
let plaintext = decrypt(None, TEST_SHARED_SECRET, TEST_HEADER, &ciphertext).unwrap();
assert_eq!(plaintext, TEST_PAYLOAD);
}
#[test]
fn encrypt_asymmetric() {
let ciphertext =
encrypt(Some(TEST_PUBLIC_KEY), TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD)
.unwrap();
let plaintext =
decrypt(Some(TEST_PRIVATE_KEY), TEST_SHARED_SECRET, TEST_HEADER, &ciphertext)
.unwrap();
assert_eq!(plaintext, TEST_PAYLOAD);
}
#[test]
fn decrypt_known_good() {
// Test a known-good ciphertext generated by another implementation.
const TEST_ENCRYPTED_PAYLOAD : &[u8] = b"\x02\x00\x04\xb5\xd6\xd6\x97\xe2\x8f\x35\x7b\xfe\x19\x22\xa3\xb5\x94\xaa\x32\xae\x82\x61\xfd\xef\xfa\xa8\x73\x91\xc2\x43\xd4\x0e\x53\x6a\xf0\xf1\x3b\xf7\xec\x4b\xdb\x34\x48\xeb\xf2\xa2\xf2\x81\x7e\x78\x5c\x08\x90\x45\x22\x2d\xed\xdf\x17\x12\xb4\xff\x0c\xb1\x27\x9d\x8a\x95\x1e\xc1\x46\xa8\x6b\x30\xc3\x57\xeb\x6d\xd4\xc3\xef\xb2\xc7\x02\x6d\x47\xef\x96\x75\xe4\xba\x5b\x3a\x1a\x9a\x8a\x3f\xcf\x02";
const TEST_HEADER: &[u8] = b"\x01\x02\x03\x04";
const TEST_PAYLOAD: &[u8] = b"\x04\x05\x06\x07";
let plaintext =
decrypt(Some(TEST_PRIVATE_KEY), b"", TEST_HEADER, TEST_ENCRYPTED_PAYLOAD).unwrap();
assert_eq!(plaintext, TEST_PAYLOAD);
}
}
}
fn cohort_selector_from_handle(handle: &[u8; VAULT_HANDLE_LEN]) -> u32 {
// unwrap: the slice will always be four bytes long, as required.
u32::from_le_bytes(handle[VAULT_HANDLE_LEN - 4..VAULT_HANDLE_LEN].try_into().unwrap())
}
/// The maximum number of attempted guesses that the recovery key store will
/// allow for the secret.
const MAX_ATTEMPTS: u32 = 10;
/// The length, in bytes, of a Vault handle.
pub const VAULT_HANDLE_LEN: usize = 17;
/// This identifies a GPM PIN vault.
pub const VAULT_HANDLE_FIRST_BYTE: u8 = 3;
/// The length, in bytes, of a Vault handle.
pub const COUNTER_ID_LEN: usize = 8;
/// This structure holds the many values that result from wrapping a knowledge
/// factor. All these values need to be placed into a `Vault` protobuf for
/// submission to the service.
struct Wrapped {
encrypted_recovery_key: Vec<u8>,
vault_handle: [u8; VAULT_HANDLE_LEN],
counter_id: [u8; COUNTER_ID_LEN],
cohort_public_key: [u8; crypto::P256_X962_LENGTH],
max_attempts: u32,
certs_in_path: Vec<Vec<u8>>,
app_public_key: [u8; crypto::P256_X962_LENGTH],
wrapped_app_private_key: Vec<u8>,
wrapped_wrapping_key: Vec<u8>,
serial: i64,
}
impl From<Wrapped> for cbor::Value {
fn from(value: Wrapped) -> Self {
cbor!({
"cohort_public_key": (&value.cohort_public_key),
"encrypted_recovery_key": (value.encrypted_recovery_key),
"vault_handle": (&value.vault_handle),
"counter_id": (&value.counter_id),
"max_attempts": (value.max_attempts as i64),
"certs_in_path": (value.certs_in_path.into_iter().map(|cert| Value::Bytestring(Bytes::from(cert))).collect::<Vec<Value>>()),
"app_public_key": (&value.app_public_key),
"wrapped_app_private_key": (value.wrapped_app_private_key),
"wrapped_wrapping_key": (value.wrapped_wrapping_key),
})
}
}
/// The Vault parameters. These must be constant when renewing a Vault so that
/// renewal doesn't reset the attempt counters.
struct Parameters {
vault_handle: [u8; VAULT_HANDLE_LEN],
counter_id: [u8; COUNTER_ID_LEN],
}
impl Parameters {
/// Generate a random set of Parameters.
fn random() -> Self {
let mut params = Parameters { vault_handle: [0u8; VAULT_HANDLE_LEN], counter_id: [0u8; 8] };
params.vault_handle[0] = VAULT_HANDLE_FIRST_BYTE;
crypto::rand_bytes(&mut params.vault_handle[1..]);
crypto::rand_bytes(&mut params.counter_id);
params
}
fn from_pin_data(pin_data: &pin::Data) -> Self {
let mut params =
Parameters { counter_id: pin_data.counter_id, vault_handle: [0u8; VAULT_HANDLE_LEN] };
params.vault_handle[0] = VAULT_HANDLE_FIRST_BYTE;
params.vault_handle[1..].copy_from_slice(&pin_data.vault_handle_without_type);
params
}
fn from_request(request: &BTreeMap<cbor::MapKey, cbor::Value>) -> Result<Self, RequestError> {
let Some(Value::Bytestring(counter_id)) = request.get(COUNTER_ID_KEY) else {
return debug("counter ID required");
};
if counter_id.len() != COUNTER_ID_LEN {
return debug("bad counter ID length");
}
let Some(Value::Bytestring(vault_handle_without_type)) =
request.get(VAULT_HANDLE_WITHOUT_TYPE_KEY)
else {
return debug("vault handle required");
};
if vault_handle_without_type.len() != VAULT_HANDLE_LEN - 1 {
return debug("bad vault handle length");
}
let mut params = Parameters {
counter_id: counter_id
.as_ref()
.try_into()
.map_err(|_| RequestError::Debug("incorrect length counter ID"))?,
vault_handle: [0u8; VAULT_HANDLE_LEN],
};
params.vault_handle[0] = VAULT_HANDLE_FIRST_BYTE;
params.vault_handle[1..].copy_from_slice(vault_handle_without_type);
Ok(params)
}
}
fn wrap(
pin_hash: &[u8],
cert_xml: &[u8],
sig_xml: &[u8],
params: Parameters,
current_time_epoch_millis: i64,
) -> Result<Wrapped, &'static str> {
let (cohort_public_key, certs_in_path, serial) = key_distribution::get_cohort_key(
cert_xml,
sig_xml,
current_time_epoch_millis,
cohort_selector_from_handle(¶ms.vault_handle),
)?;
let max_attempts = MAX_ATTEMPTS.to_le_bytes();
let vault_params = [
b"V1 THM_encrypted_recovery_key".as_ref(),
cohort_public_key.as_ref(),
¶ms.counter_id,
max_attempts.as_ref(),
¶ms.vault_handle,
]
.concat();
let mut recovery_key = [0u8; 32];
crypto::rand_bytes(&mut recovery_key);
// Within Google, see go/recovery-key-store-creation
let locally_encrypted_recovery_key = securebox::encrypt(
/* their_public_key= */ None,
/* shared_secret= */ pin_hash,
/* header= */ b"V1 locally_encrypted_recovery_key",
/* payload= */ recovery_key.as_ref(),
)
.ok_or("failed to encrypt PIN hash")?;
let thm_kf_hash = crypto::sha256_two_part(b"THM_KF_hash", pin_hash);
let encrypted_recovery_key = securebox::encrypt(
Some(&cohort_public_key),
/* shared_secret= */ &thm_kf_hash,
/* header= */ &vault_params,
/* payload= */ &locally_encrypted_recovery_key,
)
.ok_or("failed to encrypt encrypted PIN hash")?;
let app_private_key = crypto::P256Scalar::generate();
let app_public_key = app_private_key.compute_public_key();
let mut app_private_key_to_be_wrapped =
[app_private_key.bytes().as_ref(), app_public_key.as_ref()].concat();
let mut wrapping_key = [0u8; 32];
crypto::rand_bytes(&mut wrapping_key);
let wrapped_wrapping_key = securebox::encrypt(
/* their_public= */ None,
&recovery_key,
b"V1 encrypted_application_key",
&wrapping_key,
)
.ok_or("failed to encrypt wrapping key")?;
let mut nonce = [0u8; 12];
crypto::rand_bytes(&mut nonce);
crypto::aes_256_gcm_seal_in_place(
&wrapping_key,
&nonce,
/* aad= */ b"",
&mut app_private_key_to_be_wrapped,
);
let wrapped_app_private_key = [nonce.as_ref(), &app_private_key_to_be_wrapped].concat();
Ok(Wrapped {
cohort_public_key,
encrypted_recovery_key,
vault_handle: params.vault_handle,
counter_id: params.counter_id,
max_attempts: MAX_ATTEMPTS,
certs_in_path,
app_public_key,
wrapped_app_private_key,
wrapped_wrapping_key,
serial,
})
}
/// Return a CBOR response for `wrapped` which includes the fields needed to
/// insert the virtual member into a security domain.
fn include_security_domain_member_fields(
wrapped: Wrapped,
security_domain_secret: &[u8; 32],
) -> Result<cbor::Value, RequestError> {
let header = b"V1 shared_key";
let Some(wrapped_sds) =
securebox::encrypt(Some(&wrapped.app_public_key), &[], header, security_domain_secret)
else {
return debug("generated public key was invalid");
};
let member_proof = crypto::hmac_sha256(security_domain_secret, &wrapped.app_public_key);
let wrapped_cbor: cbor::Value = wrapped.into();
Ok(cbor!({
"wrapped_sds": wrapped_sds,
"member_proof": (&member_proof),
"wrapped": wrapped_cbor,
}))
}
/// Check that the given serial number is at least equal to the greatest serial
/// number used by the given device before. This is done so that devices cannot
/// request rewrapping of PIN hashes under older recovery key store cohorts,
/// which may allow an attacker additional guesses against a PIN.
fn enforce_cert_highwater(
state: &mut DirtyFlag<ParsedState>,
device_id: &[u8],
serial: i64,
) -> Result<(), RequestError> {
let device = state.get_device(device_id).ok_or(RequestError::Debug("missing device"))?;
let current_value: Option<i64> =
device.get(RECOVERY_KEY_STORE_SERIAL_HIGHWATER_KEY).and_then(|v| match v {
Value::Int(current) => Some(*current),
_ => None,
});
// If the serial matches the current highwater exactly, we're done.
match current_value {
Some(current) if current == serial => return Ok(()),
Some(current) if current > serial => return Err(RequestError::RecoveryKeyStoreDowngrade),
_ => (),
}
// Need to update the highwater.
let device =
state.get_mut().get_mut_device(device_id).ok_or(RequestError::Debug("missing device"))?;
device.insert(
MapKey::String(String::from(RECOVERY_KEY_STORE_SERIAL_HIGHWATER)),
Value::Int(serial),
);
Ok(())
}
map_keys! {
PIN_HASH, PIN_HASH_KEY = "pin_hash",
CERT_XML, CERT_XML_KEY = "cert_xml",
SIG_XML, SIG_XML_KEY = "sig_xml",
RECOVERY_KEY_STORE_SERIAL_HIGHWATER, RECOVERY_KEY_STORE_SERIAL_HIGHWATER_KEY = "recovery_key_store_serial_highwater",
}
/// Encrypts a PIN hash to a Vault public key. This is a purely public operation
/// that could be done by the client. It's exposed from the enclave to save
/// having to reimplement this logic in Chromium since it has to live in the
/// enclave in order to support `do_wrap_as_member`, below.
pub(crate) fn do_wrap(
current_time_epoch_millis: i64,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let Some(Value::Bytestring(pin_hash)) = request.get(PIN_HASH_KEY) else {
return debug("PIN hash required");
};
let Some(Value::Bytestring(cert_xml)) = request.get(CERT_XML_KEY) else {
return debug("cert.xml required");
};
let Some(Value::Bytestring(sig_xml)) = request.get(SIG_XML_KEY) else {
return debug("cert.sig.xml required");
};
let wrapped =
wrap(pin_hash, cert_xml, sig_xml, Parameters::random(), current_time_epoch_millis)
.map_err(RequestError::Debug)?;
Ok(wrapped.into())
}
/// Encrypts a PIN hash to a Vault public key and then constructs a security
/// domain member for that PIN hash. This is a sensitive operation because it
/// allows a new PIN to be a member of the domain, thus the client must have
/// done user verification or else reauthenticated very recently.
pub(crate) fn do_wrap_as_member(
auth: &Authentication,
state: &mut DirtyFlag<ParsedState>,
current_time_epoch_millis: i64,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
// Reauth is required to perform this command. UV is not enough.
let device_id = match auth {
Authentication::Device(device_id, _, _, Reauth::Done) => device_id,
_ => return debug("PIN change needs reauth via RAPT token"),
};
let Some(Value::Bytestring(pin_hash)) = request.get(PIN_HASH_KEY) else {
return debug("PIN hash required");
};
let Some(Value::Bytestring(cert_xml)) = request.get(CERT_XML_KEY) else {
return debug("cert.xml required");
};
let Some(Value::Bytestring(sig_xml)) = request.get(SIG_XML_KEY) else {
return debug("cert.sig.xml required");
};
let (security_domain_secret, _) = get_secret_from_request(state, &request, device_id)?;
let wrapped = wrap(
pin_hash,
cert_xml,
sig_xml,
Parameters::from_request(&request)?,
current_time_epoch_millis,
)
.map_err(RequestError::Debug)?;
// Reset the PIN count. This operation allows the client to change the PIN thus
// they could request the security domain secret from Folsom. Getting another
// batch of PIN attempts is less powerful than that and it allows the device to
// use the new PIN immediately, without reregistering with the enclave.
state.get_mut().set_pin_state(device_id, super::PINState { attempts: 0 })?;
include_security_domain_member_fields(wrapped, &security_domain_secret)
}
pub(crate) fn do_rewrap(
auth: &Authentication,
state: &mut DirtyFlag<ParsedState>,
current_time_epoch_millis: i64,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let device_id = match auth {
Authentication::Device(device_id, _, _, _) => device_id,
_ => return debug("not authenticated"),
};
let Some(Value::Bytestring(cert_xml)) = request.get(CERT_XML_KEY) else {
return debug("cert.xml required");
};
let Some(Value::Bytestring(sig_xml)) = request.get(SIG_XML_KEY) else {
return debug("cert.sig.xml required");
};
let Some(Value::Bytestring(wrapped_pin_data)) = request.get(WRAPPED_PIN_DATA_KEY) else {
return debug("wrapped PIN required");
};
let (security_domain_secret, _) = get_secret_from_request(state, &request, device_id)?;
let pin_data = pin::Data::from_wrapped(wrapped_pin_data, &security_domain_secret)?;
let wrapped = wrap(
&pin_data.pin_hash,
cert_xml,
sig_xml,
Parameters::from_pin_data(&pin_data),
current_time_epoch_millis,
)
.map_err(RequestError::Debug)?;
enforce_cert_highwater(state, device_id, wrapped.serial)?;
include_security_domain_member_fields(wrapped, &security_domain_secret)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wrap() {
let pin_hash = [1u8; 32];
assert!(
wrap(
&pin_hash,
SAMPLE_CERTS_XML,
SAMPLE_SIG_XML,
Parameters::random(),
SAMPLE_VALIDATION_EPOCH_MILLIS
)
.is_ok()
);
}
}
#[cfg(fuzzing)]
pub mod fuzzing {
pub fn xml_parse(input: &[u8]) {
super::xml::parse(input);
}
pub fn x509_parse(input: &[u8]) {
super::x509::parse(input);
}
}
// Certificate:
// Data:
// Version: 3 (0x2)
// Serial Number:
// 6c:d7:6e:79:4d:a8:d2:f3:3d:80:6a:b8:37:a6:e1:8f
// Signature Algorithm: sha256WithRSAEncryption
// Issuer: CN=Google Cloud Key Vault Service Root CA
// Validity
// Not Before: May 7 18:24:02 2018 GMT
// Not After : May 8 19:24:02 2038 GMT
// Subject: CN=Google Cloud Key Vault Service Root CA
// Subject Public Key Info:
// Public Key Algorithm: rsaEncryption
// RSA Public-Key: (4096 bit)
// Modulus:
// 00:ad:48:33:bb:ee:28:f7:29:76:d9:ea:a5:d4:18:
// ...
// e7:de:cd
// Exponent: 65537 (0x10001)
// X509v3 extensions:
// X509v3 Key Usage: critical
// Digital Signature, Certificate Sign, CRL Sign
// X509v3 Basic Constraints: critical
// CA:TRUE
// Signature Algorithm: sha256WithRSAEncryption
// ...
//
// The self-signature has been removed from this certificate to make it a bit
// smaller.
const ROOT_CERTIFICATE : &[u8] = b"\x30\x82\x03\x0d\x30\x82\x02\xf7\xa0\x03\x02\x01\x02\x02\x10\x6c\xd7\x6e\x79\x4d\xa8\xd2\xf3\x3d\x80\x6a\xb8\x37\xa6\xe1\x8f\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0b\x05\x00\x30\x31\x31\x2f\x30\x2d\x06\x03\x55\x04\x03\x13\x26\x47\x6f\x6f\x67\x6c\x65\x20\x43\x6c\x6f\x75\x64\x20\x4b\x65\x79\x20\x56\x61\x75\x6c\x74\x20\x53\x65\x72\x76\x69\x63\x65\x20\x52\x6f\x6f\x74\x20\x43\x41\x30\x1e\x17\x0d\x31\x38\x30\x35\x30\x37\x31\x38\x32\x34\x30\x32\x5a\x17\x0d\x33\x38\x30\x35\x30\x38\x31\x39\x32\x34\x30\x32\x5a\x30\x31\x31\x2f\x30\x2d\x06\x03\x55\x04\x03\x13\x26\x47\x6f\x6f\x67\x6c\x65\x20\x43\x6c\x6f\x75\x64\x20\x4b\x65\x79\x20\x56\x61\x75\x6c\x74\x20\x53\x65\x72\x76\x69\x63\x65\x20\x52\x6f\x6f\x74\x20\x43\x41\x30\x82\x02\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00\x03\x82\x02\x0f\x00\x30\x82\x02\x0a\x02\x82\x02\x01\x00\xad\x48\x33\xbb\xee\x28\xf7\x29\x76\xd9\xea\xa5\xd4\x18\x86\x06\xad\xe0\x59\x7a\x28\x87\x6a\xa5\xdc\x9f\xaf\x56\xec\xdf\xfd\x38\x63\xcd\xd2\x20\xd3\x19\x24\x93\x0f\xcd\x00\x5c\x58\x16\x2e\x3d\x12\x8d\x5f\x6b\xf8\x5f\xf3\x00\x88\xa0\x0a\x82\x12\xcd\x65\x0f\xab\x44\xdd\xc0\x83\xdd\x3d\xfe\x11\x03\xea\xba\x1e\x82\x07\x62\xa6\x64\x32\x7a\x98\xf9\xd7\xbd\x55\x25\x52\xe1\x6b\xd0\xed\x8c\xc1\x99\x30\xca\x5a\x81\x11\x3c\xca\xd3\x1e\x4d\x08\x78\x0b\xfe\x9e\x3b\xbe\x48\xe1\x1f\x1e\x8b\xa9\x56\xa8\x6e\x28\x0a\x94\x7c\x6c\xce\xf5\x62\xe1\xf9\x2d\xfe\xaa\xbb\x29\x6d\xd8\x4d\x5c\x61\xc1\xd2\xc6\x11\xa6\xfe\x3a\xa4\x9f\xc0\xcc\x5d\x04\xb8\x4c\x7c\x4d\x0a\xd1\xdb\xc5\xb7\xc6\xec\xf3\x22\x40\x17\x4e\x03\x26\xc3\x1b\x44\x28\x45\x14\x1a\x53\xd7\xb6\x74\xbb\x9d\xe2\x20\x00\x2e\xe6\xa5\x50\x84\xaa\xd0\x5e\x22\x00\xc2\x06\xe8\x66\xa7\x7e\x26\x41\xcb\x5d\x4d\x5f\x25\xe5\x53\xe5\x62\x4e\x26\x0a\x09\x15\x61\xe4\x75\x69\x07\xb5\xae\x26\x49\x89\x52\xef\x62\x75\x43\xdd\xbb\x24\x3d\x49\x74\x44\xf5\x90\x5b\x47\xf8\x40\xed\x60\x0b\x71\xef\x1e\xc5\xf7\x10\x00\x6d\xbd\xad\x30\x84\xf0\xb3\xfc\x30\x77\x6a\xc0\xcd\x94\xd7\xfe\x4c\x51\x6c\x39\x57\x54\xb4\xe8\x53\x4e\x4b\x15\x83\xeb\xf9\xd1\x55\xd7\x0b\xd7\x9a\x2d\x23\x96\x42\x31\x9e\x17\x5a\x54\x1a\x96\x0b\xdd\xe9\xe7\x6f\x14\x89\x47\x0b\xa6\x26\xfe\x1d\x5c\xcc\x58\x67\x58\x23\x71\xb5\x34\xe6\xbf\x95\x3a\x74\x73\xc2\xdc\x6c\x98\xda\xa6\x28\x95\x9d\xe4\x50\x27\x77\x08\xa8\x33\xce\x48\x49\xc4\xab\x8d\x21\xc9\x97\x75\x8f\x1d\xc9\x9c\xec\x49\x33\x01\xec\xf2\xfe\x2c\xf4\x62\x25\x6f\x70\x5c\x3a\x60\xef\x03\xf3\x2e\xd3\xdc\x44\x30\xac\x29\x1c\x19\xb8\x4c\x50\xca\x5d\xe1\x87\x39\x68\x5a\xed\xc7\x16\x10\x40\x9b\xc8\xee\x67\x72\xee\x97\xb8\xdd\xa2\xcb\x3f\x52\xf9\x3b\x8c\xca\x36\x41\x13\x9e\x76\xa7\xa3\xee\xe6\x01\x02\xdb\x19\x3e\xa9\xa6\xf4\x34\x60\xd3\x1d\xd2\xca\x2d\xbc\x96\x9f\x72\x31\x76\x60\x47\xc9\x3a\xfb\x88\xf0\xaa\x9a\x9c\x87\x9e\x09\x02\xfe\x96\xc6\x7e\xf1\xae\xb1\xce\x41\xa4\x1b\xa0\xb0\x1b\x65\xcf\xae\xe6\xe1\x15\x8b\x27\xbd\xb2\x01\xe5\x4f\x3b\xf9\x72\xff\xcc\x38\xa2\xb3\x6c\x19\x68\xe7\xde\xcd\x02\x03\x01\x00\x01\xa3\x23\x30\x21\x30\x0e\x06\x03\x55\x1d\x0f\x01\x01\xff\x04\x04\x03\x02\x01\x86\x30\x0f\x06\x03\x55\x1d\x13\x01\x01\xff\x04\x05\x30\x03\x01\x01\xff\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0b\x05\x00\x03\x01\x00";
// When building this code for integration testing in Chromium, a test root is
// used. The private key for this root, and code for generating it, is in
// `fake_recovery_key_store.cc` in Chromium.
#[cfg(feature = "chromium_integration_test")]
const TEST_ROOT_CERTIFICATE : &[u8] = b"\x30\x82\x02\xac\x30\x82\x01\x94\xa0\x03\x02\x01\x02\x02\x01\x01\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0b\x05\x00\x30\x0f\x31\x0d\x30\x0b\x06\x03\x55\x04\x03\x0c\x04\x52\x6f\x6f\x74\x30\x1e\x17\x0d\x32\x34\x30\x31\x33\x31\x32\x32\x32\x30\x30\x32\x5a\x17\x0d\x32\x34\x30\x32\x31\x34\x32\x32\x32\x30\x30\x32\x5a\x30\x0f\x31\x0d\x30\x0b\x06\x03\x55\x04\x03\x0c\x04\x52\x6f\x6f\x74\x30\x82\x01\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00\x03\x82\x01\x0f\x00\x30\x82\x01\x0a\x02\x82\x01\x01\x00\xc7\x95\x62\x74\xaf\x3f\x50\xb9\xa8\x4f\x55\x12\x0e\x5d\x1f\x89\x67\xdf\x55\x8c\x16\x75\x06\x1c\x33\x01\xa0\xdb\xa4\x88\x39\xdc\xca\x57\xf2\x6b\xd3\x0b\x22\x0d\x79\x2c\xc2\x4c\x1d\xf6\x0c\x32\xf6\x66\xca\x07\x89\x12\x17\x1e\x43\xbb\x16\xda\xe8\x2a\xec\x89\x25\x84\x88\xa5\x5d\xc1\x80\xb1\x9c\xab\xed\x00\x7d\xec\x9d\x4a\x49\xa4\x61\x48\x9e\xab\xb3\x78\x35\xce\x72\x80\x40\x20\x58\xca\x90\xbc\x24\x41\x01\x48\xaf\xc7\x69\x9d\x04\xa0\xa5\x20\xe8\xca\x16\xe5\x96\x6a\x36\x82\x2f\xc6\xe5\x0b\x53\x72\x3d\x89\x78\xc8\x27\xf7\xda\xe1\x6e\x07\xd6\xc1\xad\xaf\xa7\x27\xbb\x4b\x1f\x09\xdc\x41\x2d\x68\x21\xda\x85\x0b\xe1\x6f\x7b\x34\xa3\x45\x80\x43\x4e\x2a\xe7\x17\xd1\xde\xd7\xc0\xde\xf6\x0c\xa2\x50\x41\xd2\xf7\x7b\xb6\x81\x29\xb3\x80\xcc\xbf\x1e\x5a\xeb\xf3\x3c\xd2\xe6\x8d\xfc\x9a\x41\x23\xdc\xcb\x68\xff\x9e\x34\xe9\x25\xd7\xa7\x27\xc4\x68\x72\x38\x0e\xcc\x5b\x48\x40\xbb\x90\xa0\xfd\xd4\xc4\xea\xe9\x11\xd5\x6e\xda\x34\xea\x40\x2b\xcd\x21\x9b\x77\xdc\x05\xc2\xcf\x00\x05\xbc\x28\x5c\x59\xcf\xd9\x67\xcb\x30\x12\x77\x5e\xb6\xe9\x02\x03\x01\x00\x01\xa3\x13\x30\x11\x30\x0f\x06\x03\x55\x1d\x13\x01\x01\xff\x04\x05\x30\x03\x01\x01\xff\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0b\x05\x00\x03\x82\x01\x01\x00\xb1\xaa\xae\x2a\x28\x83\xdb\x56\x02\xdb\x0f\x15\x32\xce\xa6\x9f\xac\xe4\x98\xa7\x08\x99\x24\xae\xe4\x7c\x8d\x2a\x98\x03\x02\xdc\x9d\xca\x58\x8b\xda\x75\xce\xcc\xef\x51\x26\xab\xd7\x14\xcc\x15\x4d\xa4\x66\x46\x03\x81\xf9\x2b\x09\x0b\x01\xf9\x9a\x1a\x39\x79\xec\x99\xee\x59\xbd\x82\x0a\x44\xe0\xc2\x9f\xe4\x0e\x82\x8e\x10\x0e\x5a\xc3\xd5\x7c\x3c\xe7\x21\x78\xd0\xfa\xea\x72\x7a\xf9\x55\x46\x3b\x89\x94\xec\x8e\xb1\x75\xe2\x14\xf8\xf4\x7d\x63\x71\xea\x60\xad\x03\xdc\x7f\xf4\xe6\x98\x0f\xd0\x9f\x1e\x97\x95\xb4\x7d\xea\x89\xb0\xae\x8a\x52\xb7\xe8\x94\x96\x04\x4a\xce\x82\x8f\x9b\xb9\x1b\x5f\xf2\xe3\xc0\x05\x02\x4b\x7b\xfc\x94\x47\x21\x2b\x24\x61\xd1\x81\xea\x93\xf4\x04\x1e\x6c\x2c\xca\xfe\x7b\x05\xf1\x50\x25\x4f\x4c\x61\xf2\x7c\x0d\xef\xaa\x3c\xe9\x0f\xa6\xdc\x78\x78\x72\x78\x7c\x5b\x7c\x63\xc7\x95\x09\x62\xfc\x05\xc6\xde\xdd\x1f\x16\x61\xa8\xb1\x37\x40\xce\xf8\xfd\xae\x9f\x35\xc0\x1d\xb0\x1f\x45\xec\x4f\xb5\x95\x00\xe2\xb0\x24\x75\xe6\x2b\xb8\xe1\xbc\x84\x3f\xf9\xd1\x0a\x18\x72\xa5\xb8\xa1\x28\x73\x69\x4f\x78\x5d\x83";
#[cfg(test)]
pub(crate)
const SAMPLE_CERTS_XML : &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
<certificate>
<metadata>
<serial>10016</serial>
<creation-time>1694037058</creation-time>
<refresh-interval>2592000</refresh-interval>
<previous>
<serial>10015</serial>
<hash>TQudrujnu1I9bdoDaYxGQYuRN/8SwTLjdk6vzYTOkIU=</hash>
</previous>
</metadata>
<intermediates>
<cert>MIIFCjCCAvKgAwIBAgIRAN7d1InOjWGTUT558zWPLwEwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UEAxMVR29vZ2xlIENyeXB0QXV0aFZhdWx0MB4XDTE4MDUwOTAxMjAwNloXDTI4MDUxMDAxMjAwNlowOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO9067x94+sxIpqXHN9rcdorqVsH8s3ROZeBI3ORAWM8dGmR+m/yg7rrcLrLZNCKMo5RskjAc/9tWIFnoyJvp3bgJaZO1mOZGB6dF1rc3ZsWZJ5lk6roD3jWXoihI6A5qciG2Ojfn9d4UNkVYflg0xKMEP4tOFgS++XIbIZSBvtwONoOUK+w2RCnU/aCUKpJ7c49HBsieV/AcI3k4ia72JNip/9OeefyqaeuRt0X9vVTz1N4uu5LYQE90mrywaR9N0uFmfkJX6wIhkM4snbc/be5kpNcXn42seWVgLiQHwmynyN1VgHGlK+D+ewc5g3EotI4LNWjN7dgaz3wDEcVr9+cg2Z6wvh4qc5I8gxgXx5hYKIJcoXPXvyo95krrDtEatcILlVyrNoSl0aGhibh7Xt2CMEwtaS856r6JYQ9Zz6F3/KzM4B0c5XPR/Il7IAdae/e+Z4eVgj6zA19ngJmHWtMUzHHE3gcyDNqIcULMZYea7I11TVN4oW1pB6rsyIsBXALZXT93TJLI9HZ/w52A8qJIxIFP89iNtehPd8fYZipBJOj6e6PLf8+pcDE/RSSLs6ezURJ1gkovnubNhOxQ4+ku8WNsxCFB65sLriXNI8yZ8HWftJsop2k5gQ7wV0eXFNXJhAGaIXggKEb/Wf+qAEnMyxdAuLrlXwORl3AJteHAgMBAAGjJjAkMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4ICAQBlbWcXgD4KCBgBpNU6z8675oAiJb4YwrI8GT2Y5lglz6jkmy9gPZdU56PPyXO0MIBCsmmXxEcVURDULuX8DJsbzuqnbM8mEbmK8CVlMhq9NNOFZMCtnhu647lY+ZabBUYr4bSgPiJxwwMor3c15PFx/deZAYeAtbV9zW0Q07yXmjOoQhtgvJjEO9pwxwf1gktD9Wbj7OpSiLNlKGpLFOTjm0ckzIBGgwvYWp+A6LCjmOzuV91hdUF4LErG0Z6GQVllazHSJ5oaNEJx6wyJnt+gL4TDXwgDF7QpkSixBgfx5TY9QVsTi/wLzkDCjl8xuX3YXdlojofksxa83MAF6W8Pua4ZhKFTcnGAFQMTfPMUt0BAEkyTxlAovZ7H+ZXCkD47TkcGI9KWav7dDL9P4IqQljD9fr/R0anlH+rwJn9jJ1UqTbWoHgYr8qNa4SkD3WfZhb7TQJbUD6VocrEqBz6P9WgJFlB0Nn54ue7RlFC5+nlV8m6ZPbf6+f7wVOrVn0Obxq2t9RSiL9AebPDgfts+JgvflmPSOHD5W+4o42S4/huelfFxuIM1aid8lZip0TJBzYXWmOCp2SPHdN0wIp7/m1FjJ5Z7rjqn0dB+oXvHapywAdymEaVm/rs940d50cGg/1RfvAC3oYSyZe99YeK9DEQo1249+0n6QhhoJQJACw==</cert>
<cert>MIIFGjCCAwKgAwIBAgIQHflnDNWkj2yxeD1IB6GdTTANBgkqhkiG9w0BAQsFADAxMS8wLQYDVQQDEyZHb29nbGUgQ2xvdWQgS2V5IFZhdWx0IFNlcnZpY2UgUm9vdCBDQTAeFw0xODA1MDcxODU4MTBaFw0yODA1MDgxODU4MTBaMDkxNzA1BgNVBAMTLkdvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBJbnRlcm1lZGlhdGUgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDvdOu8fePrMSKalxzfa3HaK6lbB/LN0TmXgSNzkQFjPHRpkfpv8oO663C6y2TQijKOUbJIwHP/bViBZ6Mib6d24CWmTtZjmRgenRda3N2bFmSeZZOq6A941l6IoSOgOanIhtjo35/XeFDZFWH5YNMSjBD+LThYEvvlyGyGUgb7cDjaDlCvsNkQp1P2glCqSe3OPRwbInlfwHCN5OImu9iTYqf/Tnnn8qmnrkbdF/b1U89TeLruS2EBPdJq8sGkfTdLhZn5CV+sCIZDOLJ23P23uZKTXF5+NrHllYC4kB8Jsp8jdVYBxpSvg/nsHOYNxKLSOCzVoze3YGs98AxHFa/fnINmesL4eKnOSPIMYF8eYWCiCXKFz178qPeZK6w7RGrXCC5VcqzaEpdGhoYm4e17dgjBMLWkvOeq+iWEPWc+hd/yszOAdHOVz0fyJeyAHWnv3vmeHlYI+swNfZ4CZh1rTFMxxxN4HMgzaiHFCzGWHmuyNdU1TeKFtaQeq7MiLAVwC2V0/d0ySyPR2f8OdgPKiSMSBT/PYjbXoT3fH2GYqQSTo+nujy3/PqXAxP0Uki7Ons1ESdYJKL57mzYTsUOPpLvFjbMQhQeubC64lzSPMmfB1n7SbKKdpOYEO8FdHlxTVyYQBmiF4IChG/1n/qgBJzMsXQLi65V8DkZdwCbXhwIDAQABoyYwJDAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBATANBgkqhkiG9w0BAQsFAAOCAgEAQ+G3v3JCbzChBs8HUGx6i2TMm1NZM71+chbA2JF9De8kVd/r2CETvvBRLXcTPcWWA0+PRDGaDmi4TR3bJhXgBStecQZkQtzI3ZcdFfI0rTNeCevfHp5nJjtB+AYomCTKNrlNLpk9YbJosqEKVLQBhlLNYm3PT4CQYJ1NubLLtKF1cn4Z+eayxud1kDrZWFyN5CYewOrtXc8oCynj8H0/NydOuCRQU2c/UXWmvsmlRRffHJEXLqCMitTHV9w4VHEVg9YYssxno/jWtp+b4z8JsE2vkJjs2tmOvfiMupbJx9h6zj2j04rjhf/A+vGPRKOD5WtbbX4An2+szsNLmERBfWUNsO1AaSTc3W+AJOjrG30tewS7jFRPluTtgB+kmozSW0MU/BgAYJuNKRVP8zklVmQqJRbrrxSzrvHzJlz/lvFu9MD7nGtiFqT9VggFjqq5vgn5srBp3Dq4GDGerg+HCDCN9qgnL1gBcKzCMK1oT0bCRWZGckT28WMnfcgZ/fuEVNgQcEXLgWiZWZDBEVlMh7u2QoOr2LXwXuXME8k87rAQbxvGLhyxq2uNxUdH16uljm7p5u2Qobyqxqf2rOGJYCBLK2JP74d6Nl6hD5FGBBaO6mN0Ojn/ShJ1Cq9o3wCHoLYn55wJnXYu7QXAX6230h7ekXpbxPPHO4x0Var5p+8=</cert>
</intermediates>
<endpoints>
<cert>MIIDOzCCASOgAwIBAgIRALohAkmP2SJK75Xsk8FsngUwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0yMzA5MDUyMTUwNThaFw0yNTA0MDkwMDAwMDBaMDIxMDAuBgNVBAMTJ0dvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBFbmRwb2ludDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMCD3sSR26q9occ1Y/K2SQyIsSJkJtGALvd3t4l9E8ajmOV9fQHp7d4ExmRJIldlFL/Y5i5FBg3NvwK7TLvoAPmjEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD7HLz0sS04rV7BXzrd2KJdMk2fCbrjTPNNUUZu+UbPB0lDvWcP1+uroIOEZuPLUK0EBbQYzCjP/bp7tT4me4myivPbg2IBLvTaOVKbUzi6SqA4X+vyAe3c7Bp6A3hPzxNangk2jmpKdIvLXJ8DHyXVrCXk/dNObnWUDnvbmoXg5yWK/snB5OIysDPUlxUmRspxhRajVgRnDAMTnJ2YZhHC15Jm/neugxVKeSeBb4wamLRibkdWbc4KJTiSjh1CnH1OKsCI8N006Gk+YXHnrY3OmakVg/bSnfAoMWLMDvtXbDbMVYAl9uRLBDwoOS6MFMsrj+Iwniuv4E2Kb+UcWK36AR/KH1/ILFpRUTtfPwIQcvEc2tWkH+W2BJqKOvwGH3rOm2qF88g8/egrHua7jnv8aJlfQ3c3S7ytikxugCQhSAJhVO0kdWXGUut78UzBrhMEvBqHlQtZnyPSEWd6bJKdGqwmbQwdKoou5HCu0YQxanmzENR9PmDs6+AMN0xJDcb9TOBQsvQW+vY3D34U61izaU2xytglgRzjSlBwFYDP75VgsL9gcNlYSt9R1EroPPsaEV1xhW47WpWArLdprVhVX70kPf3fUkcpDXimapFpMWONWlSUCIKPy/q0d2DcamL9HN5sZLyOGPctMTEowPomW8TiISWJFdtSK2fJXkk8s</cert>
<cert>MIIDOzCCASOgAwIBAgIRALohAkmP2SJK75Xsk8FsngUwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0yMzA5MDUyMTUwNThaFw0yNTA0MDkwMDAwMDBaMDIxMDAuBgNVBAMTJ0dvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBFbmRwb2ludDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOHSWq/RFpU1VnCCCmPcTDeJT3t3+27+BjFOdsC8/hcnbFUKwHt6Tt0uiHV3LP/aO0/DHYC8Kdb/KAMC+ai+aJ2jEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBALz6PK44f46capH7isFvHMdTosG3DIV4QP70zLtGtGBM+57RKU0UYLtgdtKfCCwQVIgru9PfMdNdbxKojI96cfB/QxsH5H/96iUET+EnvvQ63NMSnLtOp7H4UceBujpXeSLN0yRNr59JS+mLtyL5+5KjHgtOM7tpxJ3eP1tx8NnE30TE0BoeTQyoKu0wfHVsc5+Fs3EWJUpgV+Z0/KJFoy3M2Z0DHZxfn6fg+/xYxn8ttkMhlZXhJMjNqtcGmlwLYktmsG5LlsQNimXwGl9olVviEZwcHGUzHw8QWszoKzn+TgTgv76m2eZ5MwJeN1JnaLb+1gQtgKRpnG8TFxWGC/TIHUqLow/GruH2TSlLPr6l6ed+QjG01sAN5cdI7OR84D8W1F0vb8fVOr7kjf7N3qLDNQXDCRUUKHlRVanIt6h+kT1ctlM51+QmRhDsAkzY/3lFrXDySnQk18vlzTyA+QgqmvfNkPhgCp/fpgtWJFaPL9bJWaMaW/soXRUf26F6RMLK43EihdoVMtUAvmCIKUQyI88X6hJxEhWLyy/8Y45nAFk5CgXuzV2doOJTSITtJligTy1IuczH75bmp87c5ZPp51vUO4WYXuwffTCoQ8UYSYbNxxqKOfFkILnM1WoGAzCrVt5aKOyGPILzOsOS8X0EeQ9YF6Mvaf2iFljc2o30</cert>
<cert>MIIDOzCCASOgAwIBAgIRALohAkmP2SJK75Xsk8FsngUwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0yMzA5MDUyMTUwNThaFw0yNTA0MDkwMDAwMDBaMDIxMDAuBgNVBAMTJ0dvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBFbmRwb2ludDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNeVqPpEctoVzN48WNefTpJEmRrrbpXoWRhHwH/AOYmQgXR6xX/AE1/qeen8fMj4Lnyb8KPveZjXvTlFq2mdBHGjEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAEQIGwhKa7MDq+Wt5p7fvv1AXhX4HxpgkKv5xbuMWCcw6R8zTYQ4hF/XHegIEqjmwWFxEvD95Lu3oLz4gMEoZVywBt2QFb1wkWUjdeT9oy5YbrJiLm9evhMFWyjnu2h9OVqxCVvarVx35ZySThDr2n3CYntLSKyTSdVlzCsdcCOj1UFkqMe73gOUZFMkXETUoINlFYwX6NP5V1Moy8OjsSNa6/8zyYwivm3rQlj3GUEhSlX+0ib+IXYpcrDFF7/6+G8lWBAHmKGwGR6kpAQ7Zg7KEjY0gSYWOr86oJIMFzeXVjaqhwGXK2tO+JBTPZSf4zljke+QCDN1uZjscgpOOXcBvT3LqLDaz2TSen4EMXhD56lYrq/970a1ol7B26nNAjJr1Q2ZyH4kXgBnK/b7AjYzNhTx0k0o7zRdh4tMeNkxhHgpBQ7d8VM81lZJg95n5SuOvJkJlEsPus9nJ1QeKAAjLV+Hp4n+xEImnvwnPEeE9vo07KHeHsCaBFVVan+9VKMiFEnYO+JdA8DwVTwTHHRH2T2OcEF+oo6m9nZZgGZbcovftryoOetJRY8E2JG+j5ScVWwnh5QcWhP1oOqsZdFWbKmJyxbN0qhKRWB1l6xZipMTj4RYzrZtwXNWdJIudC1Lkr6GgMn2UybLPc4xDH5FLWDtLN7griLweFrniuAQ</cert>
</endpoints>
</certificate>
"#;
#[cfg(test)]
pub(crate)
const SAMPLE_SIG_XML : &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
<signature>
<intermediates>
<cert>MIIFGjCCAwKgAwIBAgIQHflnDNWkj2yxeD1IB6GdTTANBgkqhkiG9w0BAQsFADAxMS8wLQYDVQQDEyZHb29nbGUgQ2xvdWQgS2V5IFZhdWx0IFNlcnZpY2UgUm9vdCBDQTAeFw0xODA1MDcxODU4MTBaFw0yODA1MDgxODU4MTBaMDkxNzA1BgNVBAMTLkdvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBJbnRlcm1lZGlhdGUgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDvdOu8fePrMSKalxzfa3HaK6lbB/LN0TmXgSNzkQFjPHRpkfpv8oO663C6y2TQijKOUbJIwHP/bViBZ6Mib6d24CWmTtZjmRgenRda3N2bFmSeZZOq6A941l6IoSOgOanIhtjo35/XeFDZFWH5YNMSjBD+LThYEvvlyGyGUgb7cDjaDlCvsNkQp1P2glCqSe3OPRwbInlfwHCN5OImu9iTYqf/Tnnn8qmnrkbdF/b1U89TeLruS2EBPdJq8sGkfTdLhZn5CV+sCIZDOLJ23P23uZKTXF5+NrHllYC4kB8Jsp8jdVYBxpSvg/nsHOYNxKLSOCzVoze3YGs98AxHFa/fnINmesL4eKnOSPIMYF8eYWCiCXKFz178qPeZK6w7RGrXCC5VcqzaEpdGhoYm4e17dgjBMLWkvOeq+iWEPWc+hd/yszOAdHOVz0fyJeyAHWnv3vmeHlYI+swNfZ4CZh1rTFMxxxN4HMgzaiHFCzGWHmuyNdU1TeKFtaQeq7MiLAVwC2V0/d0ySyPR2f8OdgPKiSMSBT/PYjbXoT3fH2GYqQSTo+nujy3/PqXAxP0Uki7Ons1ESdYJKL57mzYTsUOPpLvFjbMQhQeubC64lzSPMmfB1n7SbKKdpOYEO8FdHlxTVyYQBmiF4IChG/1n/qgBJzMsXQLi65V8DkZdwCbXhwIDAQABoyYwJDAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBATANBgkqhkiG9w0BAQsFAAOCAgEAQ+G3v3JCbzChBs8HUGx6i2TMm1NZM71+chbA2JF9De8kVd/r2CETvvBRLXcTPcWWA0+PRDGaDmi4TR3bJhXgBStecQZkQtzI3ZcdFfI0rTNeCevfHp5nJjtB+AYomCTKNrlNLpk9YbJosqEKVLQBhlLNYm3PT4CQYJ1NubLLtKF1cn4Z+eayxud1kDrZWFyN5CYewOrtXc8oCynj8H0/NydOuCRQU2c/UXWmvsmlRRffHJEXLqCMitTHV9w4VHEVg9YYssxno/jWtp+b4z8JsE2vkJjs2tmOvfiMupbJx9h6zj2j04rjhf/A+vGPRKOD5WtbbX4An2+szsNLmERBfWUNsO1AaSTc3W+AJOjrG30tewS7jFRPluTtgB+kmozSW0MU/BgAYJuNKRVP8zklVmQqJRbrrxSzrvHzJlz/lvFu9MD7nGtiFqT9VggFjqq5vgn5srBp3Dq4GDGerg+HCDCN9qgnL1gBcKzCMK1oT0bCRWZGckT28WMnfcgZ/fuEVNgQcEXLgWiZWZDBEVlMh7u2QoOr2LXwXuXME8k87rAQbxvGLhyxq2uNxUdH16uljm7p5u2Qobyqxqf2rOGJYCBLK2JP74d6Nl6hD5FGBBaO6mN0Ojn/ShJ1Cq9o3wCHoLYn55wJnXYu7QXAX6230h7ekXpbxPPHO4x0Var5p+8=</cert>
<cert>MIIFCjCCAvKgAwIBAgIRAN7d1InOjWGTUT558zWPLwEwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UEAxMVR29vZ2xlIENyeXB0QXV0aFZhdWx0MB4XDTE4MDUwOTAxMjAwNloXDTI4MDUxMDAxMjAwNlowOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO9067x94+sxIpqXHN9rcdorqVsH8s3ROZeBI3ORAWM8dGmR+m/yg7rrcLrLZNCKMo5RskjAc/9tWIFnoyJvp3bgJaZO1mOZGB6dF1rc3ZsWZJ5lk6roD3jWXoihI6A5qciG2Ojfn9d4UNkVYflg0xKMEP4tOFgS++XIbIZSBvtwONoOUK+w2RCnU/aCUKpJ7c49HBsieV/AcI3k4ia72JNip/9OeefyqaeuRt0X9vVTz1N4uu5LYQE90mrywaR9N0uFmfkJX6wIhkM4snbc/be5kpNcXn42seWVgLiQHwmynyN1VgHGlK+D+ewc5g3EotI4LNWjN7dgaz3wDEcVr9+cg2Z6wvh4qc5I8gxgXx5hYKIJcoXPXvyo95krrDtEatcILlVyrNoSl0aGhibh7Xt2CMEwtaS856r6JYQ9Zz6F3/KzM4B0c5XPR/Il7IAdae/e+Z4eVgj6zA19ngJmHWtMUzHHE3gcyDNqIcULMZYea7I11TVN4oW1pB6rsyIsBXALZXT93TJLI9HZ/w52A8qJIxIFP89iNtehPd8fYZipBJOj6e6PLf8+pcDE/RSSLs6ezURJ1gkovnubNhOxQ4+ku8WNsxCFB65sLriXNI8yZ8HWftJsop2k5gQ7wV0eXFNXJhAGaIXggKEb/Wf+qAEnMyxdAuLrlXwORl3AJteHAgMBAAGjJjAkMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEBMA0GCSqGSIb3DQEBCwUAA4ICAQBlbWcXgD4KCBgBpNU6z8675oAiJb4YwrI8GT2Y5lglz6jkmy9gPZdU56PPyXO0MIBCsmmXxEcVURDULuX8DJsbzuqnbM8mEbmK8CVlMhq9NNOFZMCtnhu647lY+ZabBUYr4bSgPiJxwwMor3c15PFx/deZAYeAtbV9zW0Q07yXmjOoQhtgvJjEO9pwxwf1gktD9Wbj7OpSiLNlKGpLFOTjm0ckzIBGgwvYWp+A6LCjmOzuV91hdUF4LErG0Z6GQVllazHSJ5oaNEJx6wyJnt+gL4TDXwgDF7QpkSixBgfx5TY9QVsTi/wLzkDCjl8xuX3YXdlojofksxa83MAF6W8Pua4ZhKFTcnGAFQMTfPMUt0BAEkyTxlAovZ7H+ZXCkD47TkcGI9KWav7dDL9P4IqQljD9fr/R0anlH+rwJn9jJ1UqTbWoHgYr8qNa4SkD3WfZhb7TQJbUD6VocrEqBz6P9WgJFlB0Nn54ue7RlFC5+nlV8m6ZPbf6+f7wVOrVn0Obxq2t9RSiL9AebPDgfts+JgvflmPSOHD5W+4o42S4/huelfFxuIM1aid8lZip0TJBzYXWmOCp2SPHdN0wIp7/m1FjJ5Z7rjqn0dB+oXvHapywAdymEaVm/rs940d50cGg/1RfvAC3oYSyZe99YeK9DEQo1249+0n6QhhoJQJACw==</cert>
</intermediates>
<certificate>MIIFGTCCAwGgAwIBAgIRAOUOMMnP/H98t0zAwO3YjxIwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0yMzA5MDUyMTUxMDBaFw0yODA5MDYyMTUxMDBaMDUxMzAxBgNVBAMTKkdvb2dsZSBDbG91ZCBLZXkgVmF1bHQgU2VydmljZSBTaWduaW5nIEtleTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANqoaDjGHUrdnO6raw9omQ+xnhSxqwTSY2dlC83an+F9JNlL/CHjvn+kyKP7rP57k4y9+9REqjvk+zaR6rQjzP6m2FbYf/kXsmS8ohtTXsmI9NTvobGCGZOYwFbB28yxoOiXA2A91cG+Rt/KmetMcGphFE0/9PGZg9JSmWiGLDJEvgG4ckz6fmL/orhbC/V1K3ArNZ2eJ8Sw29eMo62XpJqvmi+6BrFS3edcJNC1dUpC/ixP73G1J5XDVb60no4JolG1N7Utug/WlPr88eI7LdV05sMfRfX+ta4TrIK7yJ1urGuOVsIDBGFjsfgpRTlwiG829D9uGhRSAE8GzVCFiVF8AfQwlEtgahwg23QzWRaKYo6qeRMCw1hNURF31hQ5bgQeKcaS98x6MkzszBOT2aFiK0EWBzwsJLI3KadRYUMcKa3AFXSv7QLGkAU+Ivas/m3Mt0s7KQnIzjsYbOqiC895WsylxaQyMy5xvVKp0gYjmK2YtgfXo59hznqns1FzeR4fBsbKsh+NnWXzcJ8cEg8jbk0nxAz0reMj1IN25Wb1WDfUCiTy+9V6dfFLQFQ6KYDb/bbIRyPk4g176gWK9agVrHrhiQsDVstSN/cAgLBVUFi1oeLzZ0SwB4wCXuP8SmEVrGl3zxxv3szgUxwfm+elaZ0BrA5deSenJdhV1QQ3AgMBAAGjIDAeMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQDuLSK5nov/grmYNc8CTnrKNZ1w8p5Wbi9QThzJXoSV1BuFklXNX4GlgjZ04eS5ns/lUCdqByx0K2ZGX24wzZX0sSUQ+74Fq5uDINm6ESPV46y6hXvqIotLYIrgpl7Z2Ej7D6JT5fPYzAncUQd8Z9LuNMMt/rG8IlfSN6yOuZnAxI8wKtCrp23QugtqYKHyfxCN/HzCMEs1XP7qhgolnmLoTqU9j2HlPPESmH4+St4w7QPVQWARQ2S0hdtT4dhjmkqeDBojBjkGn9fS+vsOKsH3CDTt3A0pFI66xQ9TwT5mHCIIkAxGzc/DzPtpTUz6XBhtWNyI59adbCHfOtWWNjpriYvTbOm1ZZL6DXsaFJIbYX0Cmh6unonuvZ2c1Pu6nnVxR1HamIdtDZjvgbyFRJ4wCWpMhAU9WVJSotz57OXf/CvbBI0gfhl/EmWtKsGiDryPjphILWrnO55V6G6HJgk6xpzcjZzSnWpf5UF9RGjUaZNwOtxma/57pM8o5vTCeaOrq/3dKUWO2JBgxkOG+/ZCOe0E0Q2CwCCWTtf4ReaUIbeYQTj4cfR4eaj6Z8euytwEM2UQCep+HXJdOxv6/eHRXPK21Alt0crWmhZ8J7hZyeZ/24a3in8hqg9X9wxZXPghXo4W3My3Tn+dP2m36RiBQOCHSoYWMRINZccj9284GQ==</certificate>
<value>n6kI2dGZKz5CGbXnbz79m51QTDt+WszzNOvcqXsGm6g3ObmpjkghTU3wPmrJ0c5zUD1l4QQEmTKRBIACgK7Sp64JdC4IGP5y+z8HhXPslP3Dc5aySOk4b++m7AIbkAuw63SbPD8L2nQ20CMNiaVVBqZJ0uWUV04qN8IOll1L8NbeZLhjFUcx9riYBrzWOr9uis5IANkfPTFgFyPFjqFk9XrbVpPcNCRtz7Pew+L7OW5z7sh5rW8iZmjhhV/e4VDTgYBFq/Js5W4yalRI9uuEXLJqG1/US4L5cMnJoZOxPmz48an0ug/Pi8yV9cIq+xvER/XaeeUG53Fqy9cn2qG6ROwxH109toaLx3TZaLjdVh7wcJCLtOY6WngHksQbIyU1mDYzz7uWItCss2Nb0NbZ+QMn3k1GxDGIwlY/HXdt7OihPQWLRM2H/QRqlI9p8i1L+DaPrhyGrGHzYKN8z9qGZYx1AsQUWQCR0YeXvlxjtSvBEPtWkfEE0RrZPJtFh+bvrD55Id7XapnGKKXYMmYf9KbDJ3GMD1aT6xgMhlAhtltN5vNg08LSH5Ma4TXhmNpKny5JQqlAUTby1wIhgdElQSdU0jYpmle8N0wsuLoX+e3bHFKxWVkrwvXDC0v2wqH5mzm8FLhxXZDA2ApnGT+eOC1gjd8qTuouzm5GuMhjvig=</value>
</signature>
"#;
/// This is a timestamp at which the sample XML files are valid.
#[cfg(test)]
pub(crate) const SAMPLE_VALIDATION_EPOCH_MILLIS: i64 = 1707344402000;