diff --git a/ca/src/ca.rs b/ca/src/ca.rs index 418f1776..dd11dc3b 100644 --- a/ca/src/ca.rs +++ b/ca/src/ca.rs @@ -13,7 +13,7 @@ use rpki::csr::Csr; use rpki::uri; use rpki::x509::{Serial, Validity, Time, Name}; -use krill_commons::api::{self, DFLT_CLASS, EncodedHash, EntitlementClass, Entitlements, IssuanceRequest, SigningCert, RequestResourceLimit}; +use krill_commons::api::{self, DFLT_CLASS, EncodedHash, EntitlementClass, Entitlements, IssuanceRequest, SigningCert, RequestResourceLimit, IssuanceResponse}; use krill_commons::api::admin::{ Handle, ParentCaContact, @@ -134,39 +134,50 @@ impl CaIniDet { pub type CaEvt = StoredEvent; +//------------ CertIssued --------------------------------------------------- + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CertIssued { + child: Handle, + response: IssuanceResponse +} + +impl CertIssued { + pub fn unwrap(self) -> (Handle, IssuanceResponse) { + (self.child, self.response) + } +} + + //------------ CertRequested ----------------------------------------------- #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CertRequested { parent: ParentCaContact, - class_name: String, - resource_limit: RequestResourceLimit, - csr: Csr + request: IssuanceRequest } impl CertRequested { - pub fn unwrap(self) -> (ParentCaContact, String, RequestResourceLimit, Csr) { - (self.parent, self.class_name, self.resource_limit, self.csr) + pub fn unwrap(self) -> (ParentCaContact, IssuanceRequest) { + (self.parent, self.request) } pub fn parent(&self) -> &ParentCaContact { &self.parent } pub fn class_name(&self) -> &str { - &self.class_name + self.request.class_name() } - pub fn resource_limit(&self) -> &RequestResourceLimit { - &self.resource_limit + pub fn limit(&self) -> &RequestResourceLimit { + self.request.limit() } pub fn csr(&self) -> &Csr { - &self.csr + self.request.csr() } } impl Into for CertRequested { fn into(self) -> IssuanceRequest { - IssuanceRequest::new( - self.class_name, self.resource_limit, self.csr - ) + self.request } } @@ -189,7 +200,7 @@ pub struct CertReceived { pub enum CaEvtDet { // Being a parent Events ChildAdded(ChildCa), - CertificateIssued(Handle, String, IssuedCert), + CertificateIssued(CertIssued), // Being a child Events ParentAdded(ParentHandle, ParentCaContact), @@ -324,18 +335,12 @@ impl CaEvtDet { fn certificate_issued( handle: &Handle, version: u64, - child_handle: Handle, - class_name: &str, - cert: IssuedCert + cert_issued: CertIssued ) -> CaEvt { StoredEvent::new( handle, version, - CaEvtDet::CertificateIssued( - child_handle, - class_name.to_string(), - cert - ) + CaEvtDet::CertificateIssued(cert_issued) ) } @@ -365,7 +370,7 @@ type ResourceClassName = String; pub enum CaCmdDet { // Being a parent AddChild(Handle, Token, ResourceSet), - CertifyChild(Handle, Csr, RequestResourceLimit, Token, Arc>), + CertifyChild(Handle, IssuanceRequest, Token, Arc>), // Being a child AddParent(ParentHandle, ParentCaContact), @@ -409,15 +414,14 @@ impl CaCmdDet { pub fn certify_child( handle: &Handle, child_handle: Handle, - csr: Csr, - limit: RequestResourceLimit, + request: IssuanceRequest, token: Token, signer: Arc> ) -> CaCmd { SentCommand::new( handle, None, - CaCmdDet::CertifyChild(child_handle, csr, limit, token, signer) + CaCmdDet::CertifyChild(child_handle, request, token, signer) ) } @@ -642,9 +646,13 @@ impl Aggregate for CertAuth { let (handle, details) = child.unwrap(); self.children.insert(handle, details); }, - CaEvtDet::CertificateIssued(child, class_name, issued_cert) => { - let child = self.children.get_mut(&child).unwrap(); - child.add_cert(&class_name, issued_cert); + CaEvtDet::CertificateIssued(cert_issued) => { + let (child_hndl, response) = cert_issued.unwrap(); + let (class_name, _, _, issued) = response.unwrap(); + + let child = self.children.get_mut(&child_hndl).unwrap(); + + child.add_cert(&class_name, issued); }, // Being a child @@ -660,7 +668,7 @@ impl Aggregate for CertAuth { CaEvtDet::CertificateRequested(req) => { info!( "Certificate requested for class {} from {}", - req.class_name, + req.class_name(), req.parent ); // do nothing, this should be picked up by listener and sent @@ -696,8 +704,8 @@ impl Aggregate for CertAuth { CaCmdDet::AddChild(child, token, resources) => { self.add_child(child, token, resources) }, - CaCmdDet::CertifyChild(child, csr, limit, token, signer) => { - self.certify_child(child, csr, limit, token, signer) + CaCmdDet::CertifyChild(child, request, token, signer) => { + self.certify_child(child, request, token, signer) } // being a child @@ -843,9 +851,47 @@ impl CertAuth { child_handle: &Handle, token: &Token ) -> CaRes { + // TODO: Support arbitrary resource classes. See issue #25. + let dflt_entitlement_class = self.entitlement_class( + child_handle, + DFLT_CLASS, + token + )?; + + Ok(Entitlements::new(vec![dflt_entitlement_class])) + } + + /// Returns an issuance response for a child and a specific resource + /// class name and public key for the issued certificate. + pub fn issuance_response( + &self, + child_handle: &Handle, + class_name: &str, + pub_key: &PublicKey, + token: &Token + ) -> CaRes { + let entitlement_class = self.entitlement_class( + child_handle, + class_name, + token + )?; + + entitlement_class + .into_issuance_response(pub_key) + .ok_or_else(|| Error::NoIssuedCert) + } + + + /// Returns the EntitlementClass for this child for the given class name. + fn entitlement_class( + &self, + child_handle: &Handle, + class_name: &str, + token: &Token + ) -> CaRes { let child = self.get_authorised_child(child_handle, token)?; - let child_resources = child.resources_for_class(DFLT_CLASS) + let child_resources = child.resources_for_class(class_name) .ok_or_else(|| Error::MissingResources)?; let until = child_resources.not_after(); @@ -858,8 +904,12 @@ impl CertAuth { let resources = cert.resources().clone(); let cert = SigningCert::new(cert.uri().clone(), cert.cert().clone()); - Ok(Entitlements::with_default_class( - cert, resources, until, issued + Ok(EntitlementClass::new( + class_name.to_string(), + cert, + resources, + until, + issued )) } @@ -942,11 +992,12 @@ impl CertAuth { fn certify_child( &self, child: Handle, - csr: Csr, - limit: RequestResourceLimit, + request: IssuanceRequest, token: Token, signer: Arc> ) -> CaEvtsRes { + let (class_name, limit, csr) = request.unwrap(); + let issuing_key = match &self.parents { CaParents::SelfSigned(key, _tal) => key, CaParents::Parents(_) => unimplemented!("Issue #25 (delegate from CA)") @@ -956,7 +1007,7 @@ impl CertAuth { // verify child and resources let child_resources = self.get_authorised_child(&child, &token)? - .resources_for_class(DFLT_CLASS) + .resources_for_class(&class_name) .ok_or_else(|| Error::MissingResources)?; let resources = limit.resolve(child_resources.resources()) @@ -1034,12 +1085,23 @@ impl CertAuth { let version = self.version; let cert_object = CurrentObject::from(issued_cert.cert()); + let signing_cert = SigningCert::from(issuing_cert); + + let cert_issued = CertIssued { + child, + response: IssuanceResponse::new( + DFLT_CLASS.to_string(), + signing_cert, + resources, + issued_cert.cert().validity().not_after(), + issued_cert.clone() + ) + }; + let issued_event = CaEvtDet::certificate_issued( &self.handle, version, - child, - DFLT_CLASS, - issued_cert.clone() + cert_issued ); let delta = { @@ -1145,7 +1207,7 @@ impl CertAuth { let mut version = self.version; for ent in entitlements.classes() { - let name = ent.name(); + let name = ent.class_name(); if let Some(_rc) = parent.resources.get(name) { // Check whether a new certificate @@ -1179,9 +1241,11 @@ impl CertAuth { let cert_issue_req = CertRequested { parent: parent.contact.clone(), - class_name: ent.name().to_string(), - resource_limit: RequestResourceLimit::default(), - csr + request: IssuanceRequest::new( + ent.class_name().to_string(), + RequestResourceLimit::default(), + csr + ) }; let added = CaEvtDet::resource_class_added( @@ -1731,6 +1795,9 @@ pub enum Error { #[display(fmt = "Child CA MUST have resources.")] MusthaveResources, + #[display(fmt = "No issued cert matching pub key in resource class.")] + NoIssuedCert, + #[display(fmt = "Invalidly CSR for child {}: {}.", _0, _1)] InvalidCsr(Handle, String), diff --git a/ca/src/caserver.rs b/ca/src/caserver.rs index 5de27df3..89e3052a 100644 --- a/ca/src/caserver.rs +++ b/ca/src/caserver.rs @@ -6,11 +6,7 @@ use std::ops::Deref; use rpki::crypto::PublicKeyFormat; use rpki::uri; -use krill_commons::api::{ - DFLT_CLASS, - Entitlements, - IssuanceRequest, -}; +use krill_commons::api::{DFLT_CLASS, Entitlements, IssuanceRequest, IssuanceResponse}; use krill_commons::api::admin::{ AddChildRequest, Handle, @@ -213,16 +209,14 @@ impl CaServer { child: &Handle, issue_req: IssuanceRequest, token: Token, - ) -> CaResult { + ) -> CaResult { if parent != & ta_handle() { unimplemented!("https://github.com/NLnetLabs/krill/issues/25"); } else { let ta = self.get_trust_anchor()?; - // class name can be ignored for TA, only uses one. - let (class_name, limit, csr) = issue_req.unwrap(); - - let pub_key = csr.public_key().clone(); + let class_name = issue_req.class_name(); + let pub_key = issue_req.csr().public_key(); if class_name != DFLT_CLASS { unimplemented!("Issue for multiple classes from CAs, issue #25") @@ -231,21 +225,24 @@ impl CaServer { let cmd = CaCmdDet::certify_child( parent, child.clone(), - csr, - limit, - token, + issue_req.clone(), + token.clone(), self.signer.clone() ); let events = ta.process_command(cmd)?; let ta = self.ca_store.update(parent, ta, events)?; - // Get the newly issued cert for the child. Unwrap here is safe. - let child = ta.get_child(child).unwrap(); - let resources = child.resources_for_class(&class_name).unwrap(); - let issued_cert = resources.cert(&pub_key).unwrap(); + // New entitlements will include this resource class, and + // the newly issued certificate. + let response = ta.issuance_response( + child, + &class_name, + &pub_key, + &token + )?; - Ok(issued_cert.clone()) + Ok(response) } } @@ -369,12 +366,15 @@ impl CaServer { ParentCaContact::Embedded(parent_handle, token) => { for cert_req in cert_reqs { let class_name = cert_req.class_name().to_string(); - let issued = self.issue( + let issue_res = self.issue( parent_handle, &handle, cert_req, token.clone() )?; + + let (_,_,_, issued) = issue_res.unwrap(); + issued_certs.push((class_name, issued)); } } diff --git a/ca/src/lib.rs b/ca/src/lib.rs index 2eb139dd..e9ca34f8 100644 --- a/ca/src/lib.rs +++ b/ca/src/lib.rs @@ -40,7 +40,7 @@ mod tests { use rpki::crypto::signer::Signer; use rpki::crypto::PublicKeyFormat; - use krill_commons::api::DFLT_CLASS; + use krill_commons::api::{DFLT_CLASS, IssuanceRequest}; use krill_commons::api::admin::{ Handle, Token, @@ -212,7 +212,8 @@ mod tests { _ => panic!("Expected Csr") }; - let (parent_info, class_name, limit, csr) = req.unwrap(); + let (parent_info, issuance_req) = req.unwrap(); + let (class_name, limit, csr) = issuance_req.unwrap(); assert_eq!("all", &class_name); assert!(limit.is_empty()); if let ParentCaContact::Embedded(handle, token) = parent_info { @@ -230,11 +231,14 @@ mod tests { // - Publication // + let request = IssuanceRequest::new( + DFLT_CLASS.to_string(), limit, csr + ); + let ta_cmd = CaCmdDet::certify_child( &ta_handle, child_handle.clone(), - csr, - limit, + request, child_token.clone(), signer.clone() ); @@ -243,12 +247,17 @@ mod tests { let issued_evt = ta_events[0].clone().into_details(); let _ta = ca_store.update(&ta_handle, ta, ta_events).unwrap(); - let (h, n, issued) = match issued_evt { - CaEvtDet::CertificateIssued(h, n, c) => (h, n, c), + let issued = match issued_evt { + CaEvtDet::CertificateIssued(issued) => issued, _ => panic!("Expected issued certificate.") }; - assert_eq!(child_handle, h); - assert_eq!(DFLT_CLASS, n); + + let (handle, issuance_res) = issued.unwrap(); + + let (class_name, _, _, issued) = issuance_res.unwrap(); + + assert_eq!(child_handle, handle); + assert_eq!(DFLT_CLASS, class_name); // // --- Return issued certificate to child CA diff --git a/cms_proxy/src/rfc6492.rs b/cms_proxy/src/rfc6492.rs index 51f7a281..85b5afdc 100644 --- a/cms_proxy/src/rfc6492.rs +++ b/cms_proxy/src/rfc6492.rs @@ -1,18 +1,20 @@ use std::io; use std::str::FromStr; +use std::convert::TryFrom; use chrono::{Utc, DateTime, SecondsFormat}; +use serde::export::fmt::Display; +use rpki::cert::Cert; +use rpki::crypto::KeyIdentifier; +use rpki::csr::Csr; +use rpki::resources::{AsResources, Ipv4Resources, Ipv6Resources}; use rpki::uri; use rpki::x509::Time; -use krill_commons::util::xml::{XmlReader, XmlReaderErr, AttributesError, XmlWriter}; -use krill_commons::api::{Entitlements, EntitlementClass, SigningCert, RequestResourceLimit, IssuanceRequest}; +use krill_commons::api::{Entitlements, EntitlementClass, SigningCert, RequestResourceLimit, IssuanceRequest, IssuanceResponse, RevocationRequest}; use krill_commons::api::ca::{ResourceSet, ResSetErr, IssuedCert}; -use rpki::cert::Cert; -use rpki::resources::{AsResources, Ipv4Resources, Ipv6Resources}; -use serde::export::fmt::Display; -use rpki::csr::Csr; +use krill_commons::util::xml::{XmlReader, XmlReaderErr, AttributesError, XmlWriter}; //------------ Consts -------------------------------------------------------- @@ -23,6 +25,8 @@ const NS: &str = "http://www.apnic.net/specs/rescerts/up-down/"; const TYPE_LIST_QRY: &str = "list"; const TYPE_LIST_RES: &str = "list_response"; const TYPE_ISSUE_QRY: &str = "issue"; +const TYPE_ISSUE_RES: &str = "issue_response"; +const TYPE_REVOKE_QRY: &str = "revoke"; //------------ Message ------------------------------------------------------- @@ -47,9 +51,19 @@ impl Message { let content = Content::Res(Res::List(entitlements)); Message { sender, recipient, content} } + + pub fn revoke( + sender: String, + recipient: String, + revocation: RevocationRequest + ) -> Self { + let content = Content::Qry(Qry::Revoke(revocation)); + Message { sender, recipient, content } + } } #[derive(Clone, Debug, Eq, PartialEq)] +#[allow(clippy::large_enum_variant)] pub enum Content { Qry(Qry), Res(Res) @@ -82,10 +96,10 @@ impl Message { a.exhausted()?; let content = match msg_type.as_ref() { - TYPE_LIST_QRY | TYPE_ISSUE_QRY => { + TYPE_LIST_QRY | TYPE_ISSUE_QRY | TYPE_REVOKE_QRY => { Ok(Content::Qry(Qry::decode(&msg_type, r)?)) }, - TYPE_LIST_RES => { + TYPE_LIST_RES | TYPE_ISSUE_RES => { Ok(Content::Res(Res::decode(&msg_type, r)?)) } _ => Err(Error::UnknownMessageType) @@ -139,9 +153,11 @@ impl Message { /// This type defines the various RFC6492 queries. #[derive(Clone, Debug, Eq, PartialEq)] +#[allow(clippy::large_enum_variant)] pub enum Qry { List, - Issue(IssuanceRequest) + Issue(IssuanceRequest), + Revoke(RevocationRequest) } @@ -149,7 +165,8 @@ impl Qry { fn msg_type(&self) -> &str { match self { Qry::List => TYPE_LIST_QRY, - Qry::Issue(_) => TYPE_ISSUE_QRY + Qry::Issue(_) => TYPE_ISSUE_QRY, + Qry::Revoke(_) => TYPE_REVOKE_QRY } } @@ -159,14 +176,31 @@ impl Qry { ) -> Result where R: io::Read { match msg_type { TYPE_LIST_QRY => Ok(Qry::List), - TYPE_ISSUE_QRY => Self::decode_issue(r), + TYPE_ISSUE_QRY => Ok(Qry::Issue(Self::decode_issue(r)?)), + TYPE_REVOKE_QRY => Ok(Qry::Revoke(Self::decode_revoke(r)?)), _ => Err(Error::UnknownMessageType) } } + fn decode_revoke( + r: &mut XmlReader + ) -> Result where R: io::Read { + r.take_named_element("key", |mut a, r| { + let class_name = a.take_req("class_name")?; + a.exhausted()?; + + let ski_bytes = r.take_bytes_url_safe_pad()?; + + let ski = KeyIdentifier::try_from(ski_bytes.as_ref()) + .map_err(|_| Error::InvalidSki)?; + + Ok(RevocationRequest::new(class_name.to_string(), ski)) + }) + } + fn decode_issue( r: &mut XmlReader - ) -> Result where R: io::Read { + ) -> Result where R: io::Read { r.take_named_element("request", |mut a, r|{ let class_name = a.take_req("class_name")?; let mut limit = RequestResourceLimit::default(); @@ -192,11 +226,11 @@ impl Qry { let csr_bytes = r.take_bytes_characters()?; let csr = Csr::decode(csr_bytes).map_err(|_| Error::InvalidCsr)?; - Ok(Qry::Issue(IssuanceRequest::new( + Ok(IssuanceRequest::new( class_name.to_string(), limit, csr - ))) + )) }) } @@ -206,7 +240,8 @@ impl Qry { ) -> Result<(), io::Error> { match self { Qry::List => w.empty(), - Qry::Issue(issue_req) => Self::encode_issue(issue_req, w) + Qry::Issue(issue_req) => Self::encode_issue(issue_req, w), + Qry::Revoke(rev) => Self::encode_revoke(rev, w) } } @@ -235,11 +270,22 @@ impl Qry { let mut attrs_str = vec![]; attrs_str.push(("class_name", class_name)); for (k, v) in &attrs_strings { - attrs_str.push((k.clone(), v.as_str())); + attrs_str.push((k, v.as_str())); } w.put_element("request", Some(attrs_str.as_slice()), |w| { - w.put_blob(&csr) + w.put_base64_std(&csr) + }) + } + + fn encode_revoke( + rev: &RevocationRequest, + w: &mut XmlWriter + ) -> Result<(), io::Error> { + let att = [ ("class_name", rev.class_name() )]; + let bytes = rev.key().as_slice(); + w.put_element("key", Some(&att), |w| { + w.put_base64_url_safe(bytes) }) } } @@ -249,14 +295,17 @@ impl Qry { /// This type defines the various RFC6492 queries. #[derive(Clone, Debug, Eq, PartialEq)] +#[allow(clippy::large_enum_variant)] pub enum Res { - List(Entitlements) + List(Entitlements), + Issue(IssuanceResponse) } impl Res { fn msg_type(&self) -> &str { match self { - Res::List(_) => TYPE_LIST_RES + Res::List(_) => TYPE_LIST_RES, + Res::Issue(_) => TYPE_ISSUE_RES } } @@ -269,10 +318,53 @@ impl Res { let entitlements = Self::decode_entitlements(r)?; Ok(Res::List(entitlements)) }, + TYPE_ISSUE_RES => { + let issuance_response = Self::decode_issue_response(r)?; + Ok(Res::Issue(issuance_response)) + } _ => Err(Error::UnknownMessageType) } } + fn decode_issue_response( + r: &mut XmlReader + ) -> Result where R: io::Read { + r.take_named_element("class", |mut a, r| { + let name = a.take_req("class_name")?; + let cert_url = uri::Rsync::from_str( + &a.take_req("cert_url")? + )?; + + let asn = a.take_req("resource_set_as")?; + let v4 = a.take_req("resource_set_ipv4")?; + let v6 = a.take_req("resource_set_ipv6")?; + let resource_set = ResourceSet::from_strs(&asn, &v4, &v6)?; + + let not_after = a.take_req("resource_set_notafter")?; + let not_after = DateTime::::from_str(¬_after)?; + let not_after = Time::new(not_after); + + a.exhausted()?; + + let issued = Self::decode_issued_cert(r)?; + + let cert = r.take_named_element("issuer", |a, r| { + a.exhausted()?; + Self::decode_cert(r) + })?; + + let issuer = SigningCert::new(cert_url, cert); + + Ok(IssuanceResponse::new( + name, + issuer, + resource_set, + not_after, + issued + )) + }) + } + fn decode_entitlements( r: &mut XmlReader ) -> Result where R: io::Read { @@ -387,7 +479,8 @@ impl Res { w: &mut XmlWriter ) -> Result<(), io::Error> { match self { - Res::List(ents) => Self::encode_entitlements(ents, w) + Res::List(ents) => Self::encode_entitlements(ents, w), + Res::Issue(response) => Self::encode_issuance_response(response, w) } } @@ -401,14 +494,47 @@ impl Res { Ok(()) } + fn encode_issuance_response( + res: &IssuanceResponse, + w: &mut XmlWriter + ) -> Result<(), io::Error> { + Self::encode_class( + res.class_name(), + res.issuer().uri(), + res.not_after(), + res.resource_set(), + [res.issued().clone()].iter(), + res.issuer(), + w + ) + } + fn encode_entitlement_class( c: &EntitlementClass, w: &mut XmlWriter ) -> Result<(), io::Error> { - let cert_url = c.issuer().uri().to_string(); - let not_after = c.not_after() - .to_rfc3339_opts(SecondsFormat::Secs, true); - let inrs = c.resource_set(); + Self::encode_class( + c.class_name(), + c.issuer().uri(), + c.not_after(), + c.resource_set(), + c.issued().iter(), + c.issuer(), + w + ) + } + + fn encode_class<'a, W: io::Write>( + class_name: &str, + cert_url: &uri::Rsync, + not_after: Time, + inrs: &ResourceSet, + issued: impl Iterator, + issuer: &SigningCert, + w: &mut XmlWriter + ) -> Result<(), io::Error> { + let cert_url = cert_url.to_string(); + let not_after = not_after.to_rfc3339_opts(SecondsFormat::Secs, true); let asn = inrs.asn().to_string(); let v4 = inrs.v4().to_string(); @@ -417,21 +543,23 @@ impl Res { let mut attrs = vec![]; attrs.push(("cert_url", cert_url.as_str())); - attrs.push(("class_name", c.name())); + attrs.push(("class_name", class_name)); attrs.push(("resource_set_as", asn.as_str())); attrs.push(("resource_set_ipv4", v4.as_str())); attrs.push(("resource_set_ipv6", v6.as_str())); attrs.push(("resource_set_notafter", not_after.as_str())); w.put_element("class", Some(&attrs), |w| { - for issued in c.issued() { + for issued in issued { Self::encode_issued(issued, w)?; } - let issuer_cert = c.issuer().cert().to_captured().into_bytes(); - w.put_element("issuer", None, |w| { w.put_blob(&issuer_cert) }) + let issuer_cert = issuer.cert().to_captured().into_bytes(); + w.put_element("issuer", None, |w| { w.put_base64_std(&issuer_cert) }) }) } + + fn encode_issued( issued: &IssuedCert, w: &mut XmlWriter @@ -456,13 +584,13 @@ impl Res { attrs_strings.push(("resource_set_ipv6", v6.to_string())); } - let mut attrs_str = vec![]; + let mut attrs_str: Vec<(&str, &str)> = vec![]; for (k, v) in &attrs_strings { - attrs_str.push((k.clone(), v.as_str())); + attrs_str.push((k, v.as_str())); } w.put_element("certificate", Some(attrs_str.as_slice()), |w| { - w.put_blob(&cert_bytes) + w.put_base64_std(&cert_bytes) }) } @@ -500,10 +628,12 @@ pub enum Error { #[display(fmt = "Could not parse encoded certificate.")] InvalidCert, - #[display(fmt = "Could not parse encoded certificate request.")] InvalidCsr, + #[display(fmt = "Could not parse SKI in revoke request.")] + InvalidSki, + #[display(fmt = "{}", _0)] InrSyntax(String), } @@ -547,11 +677,14 @@ impl From for Error { #[cfg(test)] mod tests { - use super::*; use std::str; - use sigmsg::SignedMessage; use std::str::from_utf8_unchecked; + use crate::sigmsg::SignedMessage; + use crate::id::tests::test_id_certificate; + + use super::*; + /// Test that the we can re-encode the object to xml, parse that /// xml, and end up with an equal object. fn assert_re_encode_equals(object: Message) { @@ -598,10 +731,39 @@ mod tests { } #[test] - #[ignore] + fn parse_and_encode_issue_response() { + let xml = extract_xml( + include_bytes!("../test/remote/rpkid-rfc6492-issue_response.der") + ); + let issue = Message::decode(xml.as_bytes()).unwrap(); + assert_re_encode_equals(issue); + } + + #[test] + fn encode_and_parse_revocation_request() { + // No example CMS found for this one, so just composing and + // reading the XML based on the RFC spec only. + let cert = test_id_certificate(); + + let sender = "child".to_string(); + let rcpt = "parent".to_string(); + let class = "all".to_string(); + + let ski = cert.subject_public_key_info().key_identifier(); + let revocation = RevocationRequest::new(class, ski); + + let rev = Message::revoke(sender, rcpt, revocation); + + let decoded_rev = Message::decode(rev.encode_vec().as_slice()).unwrap(); + + assert_eq!(rev, decoded_rev); + } + + #[test] +// #[ignore] fn print_cms_content() { let xml = extract_xml( - include_bytes!("../test/remote/rpkid-rfc6492-list_response.der") + include_bytes!("../test/remote/rpkid-rfc6492-issue_response.der") ); eprintln!("{}", xml); diff --git a/cms_proxy/src/rfc8183.rs b/cms_proxy/src/rfc8183.rs index abaca303..053afb8c 100644 --- a/cms_proxy/src/rfc8183.rs +++ b/cms_proxy/src/rfc8183.rs @@ -115,7 +115,7 @@ impl PublisherRequest { "publisher_bpki_ta", None, |w| { - w.put_blob(&self.id_cert.to_bytes()) + w.put_base64_std(&self.id_cert.to_bytes()) } ) } @@ -341,7 +341,7 @@ impl RepositoryResponse { "repository_bpki_ta", None, |w| { - w.put_blob(&self.id_cert.to_bytes()) + w.put_base64_std(&self.id_cert.to_bytes()) } ) } diff --git a/cms_proxy/test/remote/rpkid-rfc6492-issue_response.der b/cms_proxy/test/remote/rpkid-rfc6492-issue_response.der new file mode 100644 index 00000000..b0a1eb46 Binary files /dev/null and b/cms_proxy/test/remote/rpkid-rfc6492-issue_response.der differ diff --git a/commons/src/api/provisioning.rs b/commons/src/api/provisioning.rs index 77555271..0c7b0137 100644 --- a/commons/src/api/provisioning.rs +++ b/commons/src/api/provisioning.rs @@ -1,9 +1,10 @@ -use api::ca::{ResourceSet, IssuedCert}; +use api::ca::{ResourceSet, IssuedCert, RcvdCert}; use rpki::x509::Time; use rpki::cert::{Cert, Overclaim}; use rpki::csr::Csr; use rpki::uri; use rpki::resources::{AsResources, Ipv4Resources, Ipv6Resources}; +use rpki::crypto::{PublicKey, KeyIdentifier}; pub const DFLT_CLASS: &str = "all"; @@ -48,7 +49,7 @@ impl Entitlements { ) -> Self { let name = DFLT_CLASS.to_string(); Entitlements { classes: vec![ - EntitlementClass { name, issuer, resource_set, not_after, issued } + EntitlementClass { class_name: name, issuer, resource_set, not_after, issued } ]} } pub fn new(classes: Vec) -> Self { @@ -63,7 +64,7 @@ impl Entitlements { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct EntitlementClass { - name: String, + class_name: String, issuer: SigningCert, resource_set: ResourceSet, not_after: Time, @@ -72,20 +73,51 @@ pub struct EntitlementClass { impl EntitlementClass { pub fn new( - name: String, + class_name: String, issuer: SigningCert, resource_set: ResourceSet, not_after: Time, issued: Vec ) -> Self { - EntitlementClass { name, issuer, resource_set, not_after, issued } + EntitlementClass { class_name, issuer, resource_set, not_after, issued } } - pub fn name(&self) -> &str { &self.name } + fn unwrap( + self + ) -> (String, SigningCert, ResourceSet, Time, Vec) { + ( + self.class_name, + self.issuer, + self.resource_set, + self.not_after, + self.issued + ) + } + + pub fn class_name(&self) -> &str { &self.class_name } pub fn issuer(&self) -> &SigningCert { &self.issuer } pub fn resource_set(&self) -> &ResourceSet { &self.resource_set } pub fn not_after(&self) -> Time { self.not_after } pub fn issued(&self) -> &Vec { &self.issued } + + /// Converts this into an IssuanceResponse for the given key. I.e. includes + /// the issued certificate matching the given public key only. Returns a + /// None if no match is found. + pub fn into_issuance_response(self, key: &PublicKey) -> Option { + let (class_name, issuer, resource_set, not_after, issued) = self.unwrap(); + + issued.into_iter() + .find(|issued| issued.cert().subject_public_key_info() == key) + .map(|issued| { + IssuanceResponse::new( + class_name, + issuer, + resource_set, + not_after, + issued + ) + }) + } } @@ -116,6 +148,15 @@ impl PartialEq for SigningCert { impl Eq for SigningCert {} +impl From<&RcvdCert> for SigningCert { + fn from(c: &RcvdCert) -> Self { + SigningCert { + uri: c.uri().clone(), + cert: c.cert().clone() + } + } +} + //------------ IssuanceRequest ----------------------------------------------- @@ -157,6 +198,45 @@ impl PartialEq for IssuanceRequest { impl Eq for IssuanceRequest {} +//------------ IssuanceResponse ---------------------------------------------- + +/// A Certificate Issuance Response equivalent to the one defined in +/// section 3.4.2 of RFC6492. +/// +/// Note that this is like a single EntitlementClass response, except that +/// it includes the one certificate which has just been issued only. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct IssuanceResponse { + class_name: String, + issuer: SigningCert, + resource_set: ResourceSet, // resources allowed on a cert + not_after: Time, + issued: IssuedCert +} + +impl IssuanceResponse { + pub fn new( + class_name: String, + issuer: SigningCert, + resource_set: ResourceSet, // resources allowed on a cert + not_after: Time, + issued: IssuedCert + ) -> Self { + IssuanceResponse { class_name, issuer, resource_set, not_after, issued } + } + + pub fn unwrap(self) -> (String, SigningCert, ResourceSet, IssuedCert) { + (self.class_name, self.issuer, self.resource_set, self.issued) + } + + pub fn class_name(&self) -> &str { &self.class_name } + pub fn issuer(&self) -> &SigningCert { &self.issuer } + pub fn resource_set(&self) -> &ResourceSet { &self.resource_set } + pub fn not_after(&self) -> Time { self.not_after } + pub fn issued(&self) -> &IssuedCert { &self.issued } +} + + //------------ RequestResourceLimit ------------------------------------------ /// The scope of resources that a child CA wants to have certified. By default @@ -286,3 +366,25 @@ impl Default for RequestResourceLimit { } } } + + +//------------ RevocationRequest --------------------------------------------- + +/// This type represents a Certificate Revocation Request as +/// defined in section 3.5.1 of RFC6492. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct RevocationRequest { + class_name: String, + key: KeyIdentifier +} + +impl RevocationRequest { + pub fn new(class_name: String, key: KeyIdentifier) -> Self { + RevocationRequest { class_name, key} + } + + pub fn class_name(&self) -> &str { &self.class_name } + pub fn key(&self) -> &KeyIdentifier { &self.key } +} + + diff --git a/commons/src/util/xml.rs b/commons/src/util/xml.rs index 33be4338..9285e398 100644 --- a/commons/src/util/xml.rs +++ b/commons/src/util/xml.rs @@ -218,11 +218,32 @@ impl XmlReader { /// Takes base64 encoded bytes from the next 'characters' event. pub fn take_bytes_characters(&mut self) -> Result { - let chars = Self::strip_whitespace(self.take_chars()?); - let b64 = base64::decode_config(&chars, base64::STANDARD_NO_PAD)?; + self.take_bytes(base64::STANDARD_NO_PAD) + } + + /// Takes base64 encoded bytes from the next 'characters' event. + pub fn take_bytes_url_safe_pad(&mut self) -> Result { + self.take_bytes(base64::URL_SAFE_NO_PAD) + } + + fn take_bytes( + &mut self, + config: base64::Config + ) -> Result { + let chars = self.take_chars()?; + // strip whitespace and padding (we are liberal in what we accept here) + // TODO: Avoid allocation, pass in an AsRef<[u8]> that + // removes any whitespace on the fly. + let chars: Vec = chars.into_bytes() + .into_iter() + .filter(|c| !b" \n\t\r\x0b\x0c=".contains(c)) + .collect(); + + let b64 = base64::decode_config(&chars, config)?; Ok(Bytes::from(b64)) } + pub fn take_empty(&mut self) -> Result<(), XmlReaderErr> { Ok(()) } @@ -247,18 +268,6 @@ impl XmlReader { } } } - - /// Convenience function to strip all whitespace from chars. E.g. - /// prior to decoding base64. I am quite sure this could be done - /// more efficiently. - /// - /// Asked the - fn strip_whitespace(s: String) -> Vec { - s.into_bytes() - .into_iter() - .filter(|c| !b" \n\t\r\x0b\x0c".contains(c)) - .collect() - } } impl XmlReader { @@ -473,12 +482,17 @@ impl XmlWriter { Ok(()) } - /// Converts bytes to base64 encoded Characters as the content. Note - /// that you cannot have both Characters and other included elements. - /// This would be valid XML, but it's not used by any of the RPKI XML - /// structures. - pub fn put_blob(&mut self, bytes: &Bytes) -> Result<(), io::Error> { - let b64 = base64::encode(bytes); + /// Converts bytes to base64 encoded Characters as the content, using the + /// Standard character set, without padding. + pub fn put_base64_std(&mut self, bytes: &Bytes) -> Result<(), io::Error> { + let b64 = base64::encode_config(bytes, base64::STANDARD); + self.put_text(b64.as_ref()) + } + + /// Converts bytes to base64 encoded Characters as the content, using the + /// URL safe character set and padding. + pub fn put_base64_url_safe(&mut self, bytes: &[u8]) -> Result<(), io::Error> { + let b64 = base64::encode_config(bytes, base64::URL_SAFE); self.put_text(b64.as_ref()) } @@ -547,7 +561,7 @@ mod tests { ]), |w| { w.put_element("b", None, |w| { - w.put_blob(&Bytes::from("X")) + w.put_base64_std(&Bytes::from("X")) }) } ) diff --git a/daemon/src/krillserver.rs b/daemon/src/krillserver.rs index 68a70eae..379633e4 100644 --- a/daemon/src/krillserver.rs +++ b/daemon/src/krillserver.rs @@ -7,10 +7,10 @@ use bcder::Captured; use rpki::uri; use krill_ca::{ta_handle, CaServer, CaServerError, PubClients, PubClientError}; -use krill_commons::api::{publication, Entitlements, IssuanceRequest}; +use krill_commons::api::{publication, Entitlements, IssuanceRequest, IssuanceResponse}; use krill_commons::api::admin; use krill_commons::api::admin::{Handle, Token, PubServerInfo, CertAuthInit, CertAuthPubMode, ParentCaContact, AddChildRequest, ParentCaReq}; -use krill_commons::api::ca::{TrustAnchorInfo, RcvdCert, IssuedCert, CertAuthList, CertAuthInfo}; +use krill_commons::api::ca::{TrustAnchorInfo, RcvdCert, CertAuthList, CertAuthInfo}; use krill_commons::util::softsigner::{OpenSslSigner, SignerError}; use krill_cms_proxy::api::ClientInfo; use krill_cms_proxy::proxy; @@ -432,7 +432,7 @@ impl KrillServer { child: &Handle, issue_req: IssuanceRequest, auth: Auth - ) -> KrillRes { + ) -> KrillRes { Ok(self.caserver.issue( parent, child,