///|
/// A trusted root text that cannot be read.
pub suberror TrustedRootError {
/// The media type is not a trusted root version that the library reads.
UnsupportedTrustedRootMediaType(String)
/// The field at `field` (a dotted JSON path) breaks `rule`.
InvalidTrustedRoot(field~ : String, rule~ : String)
} derive(Debug)
///|
pub extend TrustedRootError with @debug.Debug::{to_repr}
///|
/// Text for people that names the field and the rule.
pub fn TrustedRootError::message(self : TrustedRootError) -> String {
match self {
UnsupportedTrustedRootMediaType(media_type) =>
"Unsupported trusted root media type \{media_type.escape()}"
InvalidTrustedRoot(field~, rule~) =>
if field.is_empty() {
"Invalid trusted root: \{rule}"
} else {
"Invalid trusted root field \{field}: \{rule}"
}
}
}
///|
/// Whether the verifier can use a key of the trusted root.
pub enum KeyStatus {
/// The verifier can use the key.
Supported
/// The key type, for example `Ed25519`, that the verifier does not support.
Unsupported(String)
} derive(Eq, Debug)
///|
pub extend KeyStatus with Eq::{equal, not_equal}
///|
pub extend KeyStatus with @debug.Debug::{to_repr}
///|
priv enum TrustedKey {
Usable(@signature.PublicKey)
Unusable(String)
}
///|
fn TrustedKey::status(self : TrustedKey) -> KeyStatus {
match self {
Usable(_) => Supported
Unusable(kind) => Unsupported(kind)
}
}
///|
/// A validity period. Both bounds are inclusive; an absent bound is open.
priv struct Period {
start : Instant?
end : Instant?
}
///|
fn Period::contains(self : Period, time : Instant) -> Bool {
(self.start is None || self.start.unwrap() <= time) &&
(self.end is None || time <= self.end.unwrap())
}
///|
/// A transparency log or a certificate-transparency log.
priv struct TrustedLog {
base_url : String
log_id : Bytes
key : TrustedKey
valid : Period
}
///|
/// A certificate authority or a timestamp authority.
priv struct Authority {
uri : String
/// The chain from the first intermediate to the root.
chain : Array[@x509.Certificate]
valid : Period
}
///|
/// The keys and certificates that verification trusts. Only
/// `TrustedRoot::parse` and `TrustedRoot::public_good` make it.
pub struct TrustedRoot {
priv tlogs : Array[TrustedLog]
priv authorities : Array[Authority]
priv ctlogs : Array[TrustedLog]
priv timestamp_authorities : Array[Authority]
}
///|
/// A log of the trusted root, for display.
pub struct TrustedLogSummary {
base_url : String
key : KeyStatus
}
///|
/// What a trusted root holds, for display and tests.
pub struct TrustedRootSummary {
transparency_logs : Array[TrustedLogSummary]
certificate_authorities : Array[String]
certificate_transparency_logs : Array[TrustedLogSummary]
timestamp_authorities : Array[String]
}
///|
/// The logs and authorities of the root, and whether each log key is usable.
pub fn TrustedRoot::summary(self : TrustedRoot) -> TrustedRootSummary {
fn log_summary(log : TrustedLog) -> TrustedLogSummary {
{ base_url: log.base_url, key: log.key.status() }
}
{
transparency_logs: self.tlogs.map(log_summary),
certificate_authorities: self.authorities.map(ca => ca.uri),
certificate_transparency_logs: self.ctlogs.map(log_summary),
timestamp_authorities: self.timestamp_authorities.map(tsa => tsa.uri),
}
}
///|
let trusted_root_media_type : String = "application/vnd.dev.sigstore.trustedroot+json;version=0.1"
///|
/// Read a trusted root from its protobuf JSON text, as sigstore-go reads it.
pub fn TrustedRoot::parse(text : StringView) -> TrustedRoot raise TrustedRootError {
parse_root(text) catch {
Shape(field, rule) => raise InvalidTrustedRoot(field~, rule~)
UnsupportedTrustedRootMediaType(_) as err => raise err
InvalidTrustedRoot(..) as err => raise err
err => raise InvalidTrustedRoot(field="", rule=err.to_string())
}
}
///|
/// The pinned Sigstore public-good trusted root (sigstore/root-signing
/// `c9bda74`, SHA-256 `6494e21e…0b66`). It needs no I/O.
pub fn TrustedRoot::public_good() -> TrustedRoot {
TrustedRoot::parse(public_good_document) catch {
err => abort("the pinned public-good root does not parse: \{err.message()}")
}
}
///|
/// The pinned public-good `trusted_root.json` text.
pub fn public_good_trusted_root_json() -> String {
public_good_document
}
///|
fn parse_root(text : StringView) -> TrustedRoot raise {
let root = fields(parse_json(text), "", [
"mediaType", "tlogs", "certificateAuthorities", "ctlogs", "timestampAuthorities",
])
let media_type = root.require_string("mediaType")
guard media_type == trusted_root_media_type else {
raise UnsupportedTrustedRootMediaType(media_type)
}
let tlogs = root
.array("tlogs")
.mapi((index, json) => read_log(json, "\{root.at("tlogs")}[\{index}]", true))
let ctlogs = root
.array("ctlogs")
.mapi((index, json) => read_log(json, "\{root.at("ctlogs")}[\{index}]", false))
let authorities = root
.array("certificateAuthorities")
.mapi((index, json) => read_authority(json, "\{root.at("certificateAuthorities")}[\{index}]"))
let timestamp_authorities = root
.array("timestampAuthorities")
.mapi((index, json) => read_authority(json, "\{root.at("timestampAuthorities")}[\{index}]"))
{ tlogs, authorities, ctlogs, timestamp_authorities }
}
///|
fn read_period(parent : Fields, name : String) -> Period raise Shape {
match parent.object(name, ["start", "end"]) {
Some(range) => {
fn bound(field : String) -> Instant? raise Shape {
match range.string(field) {
Some(text) =>
match parse_timestamp(text) {
Some(time) => Some(time)
None => raise Shape(range.at(field), "not an RFC 3339 time")
}
None => None
}
}
{ start: bound("start"), end: bound("end") }
}
None => { start: None, end: None }
}
}
///|
fn read_log(json : Json, path : String, require_start : Bool) -> TrustedLog raise Shape {
let log = fields(json, path, [
"baseUrl", "hashAlgorithm", "publicKey", "logId", "checkpointKeyId", "operator",
])
let key = log.require_object("publicKey", ["rawBytes", "keyDetails", "validFor"])
let valid = read_period(key, "validFor")
if require_start {
guard valid.start is Some(_) else {
raise Shape(key.at("validFor.start"), "the log \{log.string("baseUrl").unwrap_or("")} has no validity start")
}
}
let raw = key.require_bytes("rawBytes")
// The key details name the encoding: an ECDSA key must be a valid DER
// SubjectPublicKeyInfo; another type, such as a PKCS#1 RSA key, is kept but
// is not usable.
let details = match key.get("keyDetails") {
Some(String(name)) => Some(name)
Some(Number(_, ..)) => Some("key details \{key.get("keyDetails").unwrap().stringify()}")
_ => None
}
let trusted_key = try @signature.PublicKey::from_spki(raw) catch {
UnsupportedKey(kind) => Unusable(kind)
BadKey(rule) =>
match details {
Some(name) if !name.has_prefix("PKIX_ECDSA") => Unusable(name)
_ => raise Shape(key.at("rawBytes"), rule)
}
} noraise {
parsed => Usable(parsed)
}
{
base_url: log.string("baseUrl").unwrap_or(""),
log_id: log.require_object("logId", ["keyId"]).require_bytes("keyId"),
key: trusted_key,
valid,
}
}
///|
fn read_authority(json : Json, path : String) -> Authority raise Shape {
let authority = fields(json, path, ["subject", "uri", "certChain", "validFor", "operator"])
ignore(authority.object("subject", ["organization", "commonName"]))
let chain = authority.require_object("certChain", ["certificates"])
let certificates = chain
.array("certificates")
.mapi((index, item) => {
let at = "\{chain.at("certificates")}[\{index}]"
let raw = fields(item, at, ["rawBytes"]).require_bytes("rawBytes")
@x509.parse(raw) catch {
err => raise Shape(at, "\{err.message()} in \{authority.string("uri").unwrap_or(path)}")
}
})
guard !certificates.is_empty() else {
raise Shape(chain.at("certificates"), "the chain is empty")
}
{
uri: authority.string("uri").unwrap_or(""),
chain: certificates,
valid: read_period(authority, "validFor"),
}
}
///|
fn TrustedRoot::tlog(self : TrustedRoot, log_id : BytesView) -> TrustedLog? {
self.tlogs.iter().find_first(log => log.log_id[:] == log_id)
}
///|
fn TrustedRoot::ctlog(self : TrustedRoot, log_id : BytesView) -> TrustedLog? {
self.ctlogs.iter().find_first(log => log.log_id[:] == log_id)
}