diff --git a/Cargo.toml b/Cargo.toml index 6a49b8b4..d8409e58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ path = "src/bin/utils.rs" [dependencies] anyhow = "1.0" -ecdsa = { version = "0.16.0", features = ["serde"] } +ecdsa = { version = "0.16.0", features = ["serde","verifying"] } p256 = { version = "0.13.0", features = ["serde", "ecdh"] } p384 = { version = "0.13.0", features = ["serde", "ecdh"] } rand = { version = "0.8.5", features = ["getrandom"] } @@ -39,12 +39,12 @@ async-signature = "0.3.0" #tracing = "0.1" base64 = "0.13" pem-rfc7468 = "0.7.0" -x509-cert = { version = "0.2.3", features = ["std"] } -const-oid = "0.9.2" +x509-cert = { version = "0.2.4", features = ["pem", "builder"] } ssi-jwk = { version = "0.1" } isomdl-macros = { version = "0.1.0", path = "macros" } clap = { version = "4", features = ["derive"] } clap-stdin = "0.2.1" +const-oid = "0.9.2" der = { version = "0.7", features = ["std", "derive", "alloc"] } hex = "0.4.3" asn1-rs = { version = "0.5.2", features = ["bits"] } diff --git a/src/definitions/mod.rs b/src/definitions/mod.rs index 05f7bd8e..0d1f20ca 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -9,6 +9,7 @@ pub mod mso; pub mod namespaces; pub mod session; pub mod traits; +pub mod validated_request; pub mod validated_response; pub mod validity_info; pub mod x509; diff --git a/src/definitions/validated_request.rs b/src/definitions/validated_request.rs new file mode 100644 index 00000000..311662e8 --- /dev/null +++ b/src/definitions/validated_request.rs @@ -0,0 +1,18 @@ +use crate::{definitions::ValidationErrors, presentation::device::RequestedItems}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Default)] +pub struct ValidatedRequest { + pub items_requests: RequestedItems, + pub common_name: Option, + pub reader_authentication: Status, + pub errors: ValidationErrors, +} + +#[derive(Serialize, Deserialize, Default)] +pub enum Status { + #[default] + Unchecked, + Invalid, + Valid, +} diff --git a/src/definitions/x509/extensions.rs b/src/definitions/x509/extensions.rs index a64a7829..472aa1f7 100644 --- a/src/definitions/x509/extensions.rs +++ b/src/definitions/x509/extensions.rs @@ -14,9 +14,6 @@ const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19"; const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31"; const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37"; -// A Specific OID defined for mDL signing -const VALUE_EXTENDED_KEY_USAGE: &str = "1.0.18013.5.1.2"; - // -- 18013-5 IACA SPECIFIC ROOT EXTENSION VALUE CHECKS -- // // Key Usage: 5, 6 (keyCertSign, crlSign) // Basic Constraints: Pathlen:0 @@ -112,7 +109,10 @@ pub fn validate_iaca_root_extensions(root_extensions: Vec) -> Vec) -> Vec { +pub fn validate_iaca_signer_extensions( + leaf_extensions: Vec, + value_extended_key_usage: &str, +) -> Vec { let disallowed = iaca_disallowed_x509_extensions(); let mut x509_errors: Vec = vec![]; let mut errors: Vec = vec![]; @@ -148,10 +148,11 @@ pub fn validate_iaca_signer_extensions(leaf_extensions: Vec) -> Vec) -> Vec { /* Extended key usage in the signer certificate should be set to this OID meant specifically for mDL signing. Note that this value will be different for other types of mdocs */ -pub fn validate_extended_key_usage(bytes: Vec) -> Vec { +pub fn validate_extended_key_usage(bytes: Vec, value_extended_key_usage: &str) -> Vec { let extended_key_usage = ExtendedKeyUsage::from_der(&bytes); match extended_key_usage { Ok(eku) => { if !eku .0 .into_iter() - .any(|oid| oid.to_string() == VALUE_EXTENDED_KEY_USAGE) + .any(|oid| oid.to_string() == value_extended_key_usage) { return vec![Error::ValidationError( "Invalid extended key usage, expected: 1.0.18013.5.1.2".to_string(), diff --git a/src/definitions/x509/trust_anchor.rs b/src/definitions/x509/trust_anchor.rs index e69b3b67..8ac9a840 100644 --- a/src/definitions/x509/trust_anchor.rs +++ b/src/definitions/x509/trust_anchor.rs @@ -9,11 +9,15 @@ use x509_cert::attr::AttributeTypeAndValue; use x509_cert::certificate::CertificateInner; use x509_cert::der::Decode; +const MDOC_VALUE_EXTENDED_KEY_USAGE: &str = "1.0.18013.5.1.2"; +const READER_VALUE_EXTENDED_KEY_USAGE: &str = "1.0.18013.5.1.6"; + #[derive(Serialize, Deserialize, Clone)] pub enum TrustAnchor { Iaca(X509), Aamva(X509), Custom(X509, ValidationRuleSet), + IacaReader(X509), } #[derive(Serialize, Deserialize, Clone)] @@ -128,13 +132,17 @@ pub fn validate_with_ruleset( } }; } - TrustAnchor::Custom(certificate, ruleset) => { + TrustAnchor::IacaReader(certificate) => { + let rule_set = ValidationRuleSet { + distinguished_names: vec!["2.5.4.3".to_string()], + typ: RuleSetType::ReaderAuth, + }; match x509_cert::Certificate::from_der(&certificate.bytes) { Ok(root_certificate) => { errors.append(&mut process_validation_outcomes( leaf_certificate, root_certificate, - ruleset, + rule_set, )); } Err(e) => { @@ -142,6 +150,9 @@ pub fn validate_with_ruleset( } }; } + TrustAnchor::Custom(_certificate, _ruleset) => { + //TODO + } } errors } @@ -267,7 +278,10 @@ fn apply_ruleset( //Under the IACA ruleset, the values for S or ST should be the same in subject and issuer if they are present in both RuleSetType::IACA => { let mut extension_errors = validate_iaca_root_extensions(root_extensions); - extension_errors.append(&mut validate_iaca_signer_extensions(leaf_extensions)); + extension_errors.append(&mut validate_iaca_signer_extensions( + leaf_extensions, + MDOC_VALUE_EXTENDED_KEY_USAGE, + )); for dn in leaf_distinguished_names { if dn.oid.to_string() == *"2.5.4.8" { let state_or_province = @@ -288,7 +302,10 @@ fn apply_ruleset( //Under the AAMVA ruleset, S/ST is mandatory and should be the same in the subject and issuer RuleSetType::AAMVA => { let mut extension_errors = validate_iaca_root_extensions(root_extensions); - extension_errors.append(&mut validate_iaca_signer_extensions(leaf_extensions)); + extension_errors.append(&mut validate_iaca_signer_extensions( + leaf_extensions, + MDOC_VALUE_EXTENDED_KEY_USAGE, + )); for dn in leaf_distinguished_names { let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); @@ -306,8 +323,10 @@ fn apply_ruleset( } RuleSetType::ReaderAuth => { //TODO - Err(X509Error::ValidationError( - "Unimplemented ruleset".to_string(), + + Ok(validate_iaca_signer_extensions( + leaf_extensions, + READER_VALUE_EXTENDED_KEY_USAGE, )) } } @@ -344,6 +363,12 @@ pub fn find_anchor( Err(_) => false, } } + TrustAnchor::IacaReader(certificate) => { + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, + Err(_) => false, + } + } }) else { return Err(X509Error::ValidationError( diff --git a/src/definitions/x509/x5chain.rs b/src/definitions/x509/x5chain.rs index d41db8f9..72e3d783 100644 --- a/src/definitions/x509/x5chain.rs +++ b/src/definitions/x509/x5chain.rs @@ -67,7 +67,7 @@ impl X509 { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct X5Chain(NonEmptyVec); impl From> for X5Chain { @@ -161,8 +161,7 @@ impl X5Chain { } //validate the last certificate in the chain against trust anchor - let last_in_chain = x5chain.last(); - if let Some(x509) = last_in_chain { + if let Some(x509) = x5chain.last() { match x509_cert::Certificate::from_der(&x509.bytes) { Ok(cert) => { // if the issuer of the signer certificate is known in the trust anchor registry, do the validation. diff --git a/src/presentation/device.rs b/src/presentation/device.rs index 40d63438..8aefad3a 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -1,3 +1,8 @@ +use crate::definitions::validated_request::Status as ValidationStatus; +use crate::definitions::validated_request::ValidatedRequest; +use crate::definitions::x509::error::Error as X509Error; +use crate::definitions::x509::trust_anchor::TrustAnchorRegistry; +use crate::definitions::x509::X5Chain; use crate::definitions::IssuerSignedItem; use crate::{ definitions::{ @@ -21,10 +26,13 @@ use cose_rs::sign1::{CoseSign1, PreparedCoseSign1}; use p256::FieldBytes; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; +use serde_json::json; use session::SessionTranscript180135; use std::collections::BTreeMap; use std::num::ParseIntError; use uuid::Uuid; +use x509_cert::attr::AttributeTypeAndValue; +use x509_cert::der::Decode; #[derive(Serialize, Deserialize)] pub struct SessionManagerInit { @@ -50,6 +58,7 @@ pub struct SessionManager { sk_reader: [u8; 32], reader_message_counter: u32, state: State, + trusted_verifiers: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -76,6 +85,16 @@ pub enum Error { ParsingError(#[from] ParseIntError), #[error("age_over element identifier is malformed")] PrefixError, + #[error("error decoding reader authentication certificate")] + CertificateError, + #[error("error while validating reader authentication certificate")] + ValidationError, +} + +impl From for Error { + fn from(_value: x509_cert::der::Error) -> Self { + Error::CertificateError + } } pub type Documents = NonEmptyMap; @@ -167,7 +186,8 @@ impl SessionManagerEngaged { pub fn process_session_establishment( self, session_establishment: SessionEstablishment, - ) -> anyhow::Result<(SessionManager, RequestedItems)> { + trusted_verifiers: Option, + ) -> anyhow::Result<(SessionManager, ValidatedRequest)> { let e_reader_key = session_establishment.e_reader_key; let session_transcript = SessionTranscript180135(self.device_engagement, e_reader_key.clone(), self.handover); @@ -191,14 +211,15 @@ impl SessionManagerEngaged { sk_reader, reader_message_counter: 0, state: State::AwaitingRequest, + trusted_verifiers, }; - let requested_data = sm.handle_decoded_request(SessionData { + let validated_request = sm.handle_decoded_request(SessionData { data: Some(session_establishment.data), status: None, - })?; + }); - Ok((sm, requested_data)) + Ok((sm, validated_request)) } } @@ -215,24 +236,43 @@ impl SessionManager { }) } - fn validate_request( - &self, - request: DeviceRequest, - ) -> Result, PreparedDeviceResponse> { - if !request.version.starts_with("1.") { + fn validate_request(&self, request: DeviceRequest) -> ValidatedRequest { + let items_requests: Vec = request + .doc_requests + .clone() + .into_inner() + .into_iter() + .map(|DocRequest { items_request, .. }| items_request.into_inner()) + .collect(); + + let mut validated_request = ValidatedRequest { + items_requests, + common_name: None, + reader_authentication: ValidationStatus::Unchecked, + errors: BTreeMap::new(), + }; + + if request.version != DeviceRequest::VERSION { // tracing::error!( // "unsupported DeviceRequest version: {} ({} is supported)", // request.version, // DeviceRequest::VERSION // ); - return Err(PreparedDeviceResponse::empty(Status::GeneralError)); + validated_request.errors.insert( + "parsing_errors".to_string(), + json!(vec!["unsupported DeviceRequest version".to_string()]), + ); } - Ok(request - .doc_requests - .into_inner() - .into_iter() - .map(|DocRequest { items_request, .. }| items_request.into_inner()) - .collect()) + if let Some(doc_request) = request.doc_requests.first() { + let (validation_errors, common_name) = self.reader_authentication(doc_request.clone()); + if validation_errors.is_empty() { + validated_request.reader_authentication = ValidationStatus::Valid; + } + + validated_request.common_name = common_name; + } + + validated_request } pub fn prepare_response(&mut self, requests: &RequestedItems, permitted: PermittedItems) { @@ -240,36 +280,59 @@ impl SessionManager { self.state = State::Signing(prepared_response); } - fn handle_decoded_request(&mut self, request: SessionData) -> anyhow::Result { - let data = request.data.ok_or_else(|| { - anyhow::anyhow!("no mdoc requests received, assume session can be terminated") - })?; - let decrypted_request = session::decrypt_reader_data( + fn handle_decoded_request(&mut self, request: SessionData) -> ValidatedRequest { + let mut validated_request = ValidatedRequest::default(); + let data = match request.data { + Some(d) => d, + None => { + validated_request.errors.insert( + "parsing_errors".to_string(), + json!(vec![ + "no mdoc requests received, assume session can be terminated".to_string() + ]), + ); + return validated_request; + } + }; + let decrypted_request = match session::decrypt_reader_data( &self.sk_reader.into(), data.as_ref(), &mut self.reader_message_counter, ) - .map_err(|e| anyhow::anyhow!("unable to decrypt request: {}", e))?; - let request = match self.parse_request(&decrypted_request) { - Ok(r) => r, + .map_err(|e| anyhow::anyhow!("unable to decrypt request: {}", e)) + { + Ok(decrypted) => decrypted, Err(e) => { - self.state = State::Signing(e); - return Ok(Default::default()); + validated_request + .errors + .insert("decryption_errors".to_string(), json!(vec![e.to_string()])); + return validated_request; } }; - let request = match self.validate_request(request) { + + let request = match self.parse_request(&decrypted_request) { Ok(r) => r, Err(e) => { self.state = State::Signing(e); - return Ok(Default::default()); + return ValidatedRequest::default(); } }; - Ok(request) + + self.validate_request(request) } /// Handle a request from the reader. - pub fn handle_request(&mut self, request: &[u8]) -> anyhow::Result { - let session_data: SessionData = serde_cbor::from_slice(request)?; + pub fn handle_request(&mut self, request: &[u8]) -> ValidatedRequest { + let mut validated_request = ValidatedRequest::default(); + let session_data: SessionData = match serde_cbor::from_slice(request) { + Ok(sd) => sd, + Err(e) => { + validated_request + .errors + .insert("parsing_errors".to_string(), json!(vec![e.to_string()])); + return validated_request; + } + }; self.handle_decoded_request(session_data) } @@ -338,6 +401,76 @@ impl SessionManager { None } } + + pub fn reader_authentication( + &self, + doc_request: DocRequest, + ) -> (Vec, Option) { + //TODO validate the reader authentication. This code only grabs the CN from the x5chain + let mut validation_errors: Vec = vec![]; + if let Some(reader_auth) = doc_request.reader_auth { + if let Some(x5chain_cbor) = reader_auth.unprotected().get_i(33) { + let x5c = x5chain_cbor; + + let x5chain = + X5Chain::from_cbor(x5chain_cbor.clone()).map_err(|_| Error::CertificateError); + match x5chain { + Ok(x5c) => { + if let Some(trusted_verifiers) = &self.trusted_verifiers { + validation_errors + .append(&mut x5c.validate(Some(trusted_verifiers.clone()))); + } + } + Err(e) => { + validation_errors.push(X509Error::ValidationError(e.to_string())); + } + } + + match x5c { + CborValue::Bytes(x509) => { + match x509_cert::Certificate::from_der(&x509) { + Ok(cert) => { + let distinguished_names: Vec = cert + .tbs_certificate + .subject + .0 + .into_iter() + .map(|rdn| { + rdn.0 + .into_vec() + .into_iter() + .filter(|atv| { + //common name + atv.oid.to_string() == "2.5.4.3".to_string() + }) + .collect::>() + }) + .collect::>>() + .into_iter() + .flatten() + .collect(); + + if let Some(common_name) = distinguished_names.first() { + return (validation_errors, Some(common_name.to_string())); + } else { + return (validation_errors, None); + } + } + Err(e) => { + validation_errors.push(X509Error::ValidationError(e.to_string())); + return (validation_errors, None); + } + } + } + _ => return (validation_errors, None), + } + } else { + return (validation_errors, None); + } + } else { + return (validation_errors, None); + } + } } impl PreparedDeviceResponse { diff --git a/src/presentation/reader.rs b/src/presentation/reader.rs index 79712a8b..e1ca11f7 100644 --- a/src/presentation/reader.rs +++ b/src/presentation/reader.rs @@ -4,6 +4,7 @@ use crate::definitions::x509::trust_anchor::TrustAnchorRegistry; use crate::definitions::x509::x5chain::X5CHAIN_HEADER_LABEL; use crate::definitions::x509::X5Chain; use crate::definitions::{Status, ValidatedResponse}; +use crate::presentation::reader::device_request::ItemsRequestBytes; use crate::presentation::reader::Error as ReaderError; use crate::{ definitions::{ @@ -18,7 +19,13 @@ use crate::{ }, definitions::{DeviceEngagement, DeviceResponse, SessionData, SessionTranscript180135}, }; +use aes::cipher::{generic_array::GenericArray, typenum::U32}; use anyhow::{anyhow, Result}; +use cose_rs::algorithm::Algorithm; +use cose_rs::sign1::HeaderMap; +use cose_rs::CoseSign1; +use p256::ecdsa::SigningKey; +use sec1::DecodeEcPrivateKey; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; use serde_json::json; @@ -34,8 +41,17 @@ pub struct SessionManager { sk_reader: [u8; 32], reader_message_counter: u32, trust_anchor_registry: Option, + reader_auth_key: [u8; 32], + reader_x5chain: X5Chain, } +#[derive(Serialize, Deserialize)] +pub struct ReaderAuthentication( + pub String, + pub SessionTranscript180135, + pub ItemsRequestBytes, +); + #[derive(Debug, thiserror::Error, Serialize, Deserialize)] pub enum Error { #[error("Received IssuerAuth had a detached payload.")] @@ -131,6 +147,8 @@ impl SessionManager { qr_code: String, namespaces: device_request::Namespaces, trust_anchor_registry: Option, + reader_x5chain: X5Chain, + reader_key: &str, ) -> Result<(Self, Vec, [u8; 16])> { let device_engagement_bytes = Tag24::::from_qr_code_uri(&qr_code).map_err(|e| anyhow!(e))?; @@ -166,6 +184,9 @@ impl SessionManager { let sk_device = derive_session_key(&shared_secret, &session_transcript_bytes, false)?.into(); + let reader_signing_key: SigningKey = ecdsa::SigningKey::from_sec1_pem(reader_key)?; + let reader_auth_key: GenericArray = reader_signing_key.to_bytes().into(); + let mut session_manager = Self { session_transcript, sk_device, @@ -173,6 +194,8 @@ impl SessionManager { sk_reader, reader_message_counter: 0, trust_anchor_registry, + reader_auth_key: reader_auth_key.into(), + reader_x5chain, }; let request = session_manager.build_request(namespaces)?; @@ -224,8 +247,33 @@ impl SessionManager { namespaces, request_info: None, }; + + //the certificate should be supplied by the reader + //let certificate_cbor = serde_cbor::to_vec(&self.reader_cert_bytes)?; + let mut header_map = HeaderMap::default(); + header_map.insert_i(33, self.reader_x5chain.into_cbor()); + + let algorithm = Algorithm::ES256; + let payload = ReaderAuthentication( + "ReaderAuthentication".to_string(), + self.session_transcript.clone(), + Tag24::new(items_request.clone())?, + ); + + let reader_signing_key = SigningKey::from_slice(&self.reader_auth_key)?; //SigningKey::from_bytes(self.reader_auth_key.to_vec()); + let signature = reader_signing_key.sign_recoverable(&serde_cbor::to_vec(&payload)?)?; + let prepared_cosesign = CoseSign1::builder() + .detached() + .signature_algorithm(algorithm) + .payload(serde_cbor::to_vec(&payload)?) + .unprotected(header_map) + .prepare() + .unwrap(); + + let cose_sign1 = prepared_cosesign.finalize(signature.0.to_vec()); + let doc_request = DocRequest { - reader_auth: None, + reader_auth: Some(cose_sign1), items_request: Tag24::new(items_request)?, }; let device_request = DeviceRequest { @@ -282,10 +330,49 @@ impl SessionManager { return validated_response; } }; - validated_response.parsing = Status::Valid; - validated_response.response = parsed_response; + let header = document.issuer_signed.issuer_auth.unprotected().clone(); + if let Some(x5chain_bytes) = header.get_i(33) { + let x5chain = match X5Chain::from_cbor(x5chain_bytes.clone()) { + Ok(x5chain) => x5chain, + Err(e) => { + validated_response + .errors + .insert("parsing_errors".to_string(), json!(vec![e])); + return validated_response; + } + }; + + match parse_namespaces(&device_response) { + Ok(parsed_response) => { + return self.validate_response(x5chain, document.clone(), parsed_response) + } + Err(e) => { + validated_response + .errors + .insert("parsing_errors".to_string(), json!(vec![e])); + return validated_response; + } + }; + } else { + validated_response + .errors + .insert("parsing_errors".to_string(), json!(vec![Error::X5Chain])); + return validated_response; + } + } + + pub fn validate_response( + &mut self, + x5chain: X5Chain, + document: Document, + parsed_response: BTreeMap, + ) -> ValidatedResponse { + let mut validated_response = ValidatedResponse { + response: parsed_response, + ..Default::default() + }; - match device_authentication(document, self.session_transcript.clone()) { + match device_authentication(&document, self.session_transcript.clone()) { Ok(_) => { validated_response.device_authentication = Status::Valid; } diff --git a/test/presentation/reader_auth.pem b/test/presentation/reader_auth.pem new file mode 100644 index 00000000..3d7ed57c --- /dev/null +++ b/test/presentation/reader_auth.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICfzCCAiWgAwIBAgIUa1sPN12Jdv6KwSjG3DJeK6DCm0cwCgYIKoZIzj0EAwIw +bjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMSAwHgYDVQQKDBdJU09tREwgVGVz +dCBSZWFkZXIgUm9vdDEwMC4GA1UEAwwnSVNPMTgwMTMtNSBUZXN0IENlcnRpZmlj +YXRlIFJlYWRlciBSb290MB4XDTIzMTEyMDEzMjYwMloXDTI0MDUyNDEzMjYwMlow +VDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRcwFQYDVQQKDA5TcHJ1Y2UgU3lz +dGVtczEfMB0GA1UEAwwWSVNPMTgwMTMtNSBUZXN0IFJlYWRlcjBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABCm4PuNX0645fokw5XwZ5MpMtY0G4z+b1PvE/5Zx8As5 +4c9VAeVHb1Mlw59GPNBGU2xzccPZF8qsInT1JBd4cqOjgbowgbcwHQYDVR0OBBYE +FCyPAvWShVVL9dkiTlZQuL7kOtSjMB8GA1UdIwQYMBaAFFhiV3bwFCly/JtNCDvK +NUQxvDVmMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUBAf8ECzAJBgcogYxdBQEGMB0G +A1UdEgQWMBSBEmV4YW1wbGVAaXNvbWRsLmNvbTAvBgNVHR8EKDAmMCSgIqAghh5o +dHRwczovL2V4YW1wbGUuY29tL0lTT21ETC5jcmwwCgYIKoZIzj0EAwIDSAAwRQIg +XB7Y464ffTiQr32lfm/30S6HuvIsghovj1NFWcBGuCECIQCxGGShlVzrjTDsfahx +3LPTEI8prVIfLclczAvOOMq30A== +-----END CERTIFICATE----- diff --git a/test/presentation/reader_key.pem b/test/presentation/reader_key.pem new file mode 100644 index 00000000..c4b18090 --- /dev/null +++ b/test/presentation/reader_key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIFkWRnOYuhN/3iJLTcBXZdvwbnWWAppQSZvc5OlzROK6oAoGCCqGSM49 +AwEHoUQDQgAEKbg+41fTrjl+iTDlfBnkyky1jQbjP5vU+8T/lnHwCznhz1UB5Udv +UyXDn0Y80EZTbHNxw9kXyqwidPUkF3hyow== +-----END EC PRIVATE KEY-----