Skip to content

Commit

Permalink
mdoc auth and iaca certificate validation
Browse files Browse the repository at this point in the history
  • Loading branch information
justAnIdentity committed Nov 24, 2023
1 parent 3183341 commit 10867e6
Show file tree
Hide file tree
Showing 14 changed files with 1,241 additions and 45 deletions.
9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ homepage = "https://github.com/spruceid/isomdl"
repository = "https://github.com/spruceid/isomdl"
license = "Apache-2.0"
exclude = ["test/"]

[dependencies]
anyhow = "1.0"
ecdsa = { version = "0.16.0", features = ["serde"] }
Expand All @@ -35,12 +35,15 @@ async-signature = "0.3.0"
#tracing = "0.1"
base64 = "0.13"
pem-rfc7468 = "0.7.0"
x509-cert = { version = "0.1.1", features = ["pem"] }

x509-cert = {version = "0.2.3", features = ["std"]}
const-oid = "0.9.2"
ssi-jwk = { version = "0.1" }
isomdl-macros = { version = "0.1.0", path = "macros" }
clap = { version = "4", features = ["derive"] }
clap-stdin = "0.2.1"
der = { version = "0.7", features = ["std", "derive", "alloc"] }
hex = "0.4.3"
asn1-rs = { version = "0.5.2", features = ["bits"]}

[dependencies.cose-rs]
git = "https://github.com/spruceid/cose-rs"
Expand Down
2 changes: 1 addition & 1 deletion src/definitions/helpers/non_empty_vec.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use std::ops::Deref;

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(try_from = "Vec<T>", into = "Vec<T>")]
pub struct NonEmptyVec<T: Clone>(Vec<T>);

Expand Down
12 changes: 6 additions & 6 deletions src/definitions/namespaces/latin1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ mod test {
#[test]
fn upper_latin() {
let upper_latin_chars = vec![
' ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '­', '®', ', ',
'±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', ', ',
'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', ', ',
'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', ', ',
'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', ', ',
'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ',
' ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '\u{AD}', '®', '¯',
'°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À',
'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ',
'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â',
'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó',
'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ',
];
assert!(upper_latin_chars.iter().all(is_upper_latin));
}
Expand Down
107 changes: 101 additions & 6 deletions src/issuance/x5chain.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,50 @@
use crate::definitions::helpers::NonEmptyVec;
use crate::presentation::reader::find_anchor;

use crate::presentation::reader::Error;
use crate::presentation::trust_anchor::validate_with_trust_anchor;
use crate::presentation::trust_anchor::TrustAnchorRegistry;
use crate::{definitions::helpers::NonEmptyVec, presentation::trust_anchor::check_validity_period};
use anyhow::{anyhow, Result};

use const_oid::AssociatedOid;

use elliptic_curve::{
sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint},
AffinePoint, CurveArithmetic, FieldBytesSize, PublicKey,
};
use p256::NistP256;
use serde::{Deserialize, Serialize};
use serde_cbor::Value as CborValue;
use signature::Verifier;
use std::{fs::File, io::Read};
use x509_cert::der::Encode;
use x509_cert::{
certificate::Certificate,
der::{Decode, Encode},
der::{referenced::OwnedToRef, Decode},
};

pub const X5CHAIN_HEADER_LABEL: i128 = 33;

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)]
pub struct X509 {
bytes: Vec<u8>,
pub bytes: Vec<u8>,
}

impl X509 {
pub fn public_key<C>(&self) -> Result<PublicKey<C>, Error>
where
C: AssociatedOid + CurveArithmetic,
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
FieldBytesSize<C>: ModulusSize,
{
let cert = x509_cert::Certificate::from_der(&self.bytes)?;
cert.tbs_certificate
.subject_public_key_info
.owned_to_ref()
.try_into()
.map_err(|e| format!("could not parse public key from pkcs8 spki: {e}"))
.map_err(|_e| Error::MdocAuth("could not parse public key from pkcs8 spki".to_string()))
}
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -41,6 +74,63 @@ impl X5Chain {
),
}
}

pub fn validate(
&self,
trust_anchor_registry: Option<TrustAnchorRegistry>,
) -> Result<Vec<Error>, Error> {
let x5chain = self.0.as_ref();
let mut results: Vec<Result<(), Error>> = x5chain
.windows(2)
.map(|chain_link| {
let target = &chain_link[0];
let issuer = &chain_link[1];
check_signature(target, issuer)
})
.collect();

for x509 in x5chain {
let cert = x509_cert::Certificate::from_der(&x509.bytes)?;
results.push(check_validity_period(&cert))
}

let mut errors: Vec<Error> = vec![];

//validate the last certificate in the chain against trust anchor
let last_in_chain = x5chain.to_vec().pop();
if let Some(x509) = last_in_chain {
let inner = x509_cert::Certificate::from_der(&x509.bytes)?;
if let Some(trust_anchor) = find_anchor(inner, trust_anchor_registry)? {
errors.append(&mut validate_with_trust_anchor(x509, trust_anchor)?);
} else {
errors.push(Error::MdocAuth(
"No matching trust anchor found".to_string(),
));
};
} else {
errors.push(Error::MdocAuth("Empty certificate chain".to_string()))
}

let mut sig_errors = results
.into_iter()
.filter(|result| result.is_err())
.collect::<Vec<Result<(), Error>>>()
.into_iter()
.map(|e| e.expect_err("something went wrong"))
.collect::<Vec<Error>>();

errors.append(&mut sig_errors);
Ok(errors)
}
}

pub fn check_signature(target: &X509, issuer: &X509) -> Result<(), Error> {
let parent_public_key = ecdsa::VerifyingKey::from(issuer.public_key()?);
let child_cert = x509_cert::Certificate::from_der(&target.bytes)?;
let sig: ecdsa::Signature<NistP256> =
ecdsa::Signature::from_der(child_cert.signature.raw_bytes())?;
let bytes = child_cert.tbs_certificate.to_der()?;
Ok(parent_public_key.verify(&bytes, &sig)?)
}

#[derive(Default, Debug, Clone)]
Expand All @@ -57,7 +147,8 @@ impl Builder {
.map_err(|e| anyhow!("unable to parse certificate from der: {}", e))?;
let x509 = X509 {
bytes: cert
.to_vec()
.encode_to_vec(&mut vec![])?
.to_der()
.map_err(|e| anyhow!("unable to convert certificate to bytes: {}", e))?,
};
self.certs.push(x509);
Expand All @@ -68,7 +159,8 @@ impl Builder {
.map_err(|e| anyhow!("unable to parse certificate from der encoding: {}", e))?;
let x509 = X509 {
bytes: cert
.to_vec()
.encode_to_vec(&mut vec![])?
.to_der()
.map_err(|e| anyhow!("unable to convert certificate to bytes: {}", e))?,
};
self.certs.push(x509);
Expand Down Expand Up @@ -179,4 +271,7 @@ pub mod test {
// Algorithm::ES512
//));
}

#[test]
pub fn validate_x5chain() {}
}
28 changes: 14 additions & 14 deletions src/presentation/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,20 @@ pub struct Document {

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreparedDeviceResponse {
prepared_documents: Vec<PreparedDocument>,
signed_documents: Vec<DeviceResponseDoc>,
document_errors: Option<DocumentErrors>,
status: Status,
pub prepared_documents: Vec<PreparedDocument>,
pub signed_documents: Vec<DeviceResponseDoc>,
pub document_errors: Option<DocumentErrors>,
pub status: Status,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct PreparedDocument {
id: Uuid,
doc_type: String,
issuer_signed: IssuerSigned,
device_namespaces: DeviceNamespacesBytes,
prepared_cose_sign1: PreparedCoseSign1,
errors: Option<NamespaceErrors>,
pub struct PreparedDocument {
pub id: Uuid,
pub doc_type: String,
pub issuer_signed: IssuerSigned,
pub device_namespaces: DeviceNamespacesBytes,
pub prepared_cose_sign1: PreparedCoseSign1,
pub errors: Option<NamespaceErrors>,
}

type Namespaces = NonEmptyMap<Namespace, NonEmptyMap<ElementIdentifier, IssuerSignedItemBytes>>;
Expand Down Expand Up @@ -631,7 +631,7 @@ impl From<Mdoc> for Document {
}

/// Filter permitted items to only permit the items that were requested.
fn filter_permitted(request: &RequestedItems, permitted: PermittedItems) -> PermittedItems {
pub fn filter_permitted(request: &RequestedItems, permitted: PermittedItems) -> PermittedItems {
permitted
.into_iter()
.filter_map(|(doc_type, namespaces)| {
Expand Down Expand Up @@ -832,9 +832,9 @@ mod test {
let issuer_item1 = Tag24::new(issuer_signed_item1).unwrap();
let issuer_item2 = Tag24::new(issuer_signed_item2).unwrap();
let issuer_item3 = Tag24::new(issuer_signed_item3).unwrap();
let mut issuer_items = NonEmptyMap::new(element_identifier1, issuer_item1.clone());
let mut issuer_items = NonEmptyMap::new(element_identifier1, issuer_item1);
issuer_items.insert(element_identifier2, issuer_item2.clone());
issuer_items.insert(element_identifier3, issuer_item3.clone());
issuer_items.insert(element_identifier3, issuer_item3);

let result = nearest_age_attestation(requested_element_identifier, issuer_items)
.expect("failed to process age attestation request");
Expand Down
124 changes: 124 additions & 0 deletions src/presentation/mdoc_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use crate::definitions::device_response::Document;
use crate::definitions::issuer_signed;
use crate::definitions::DeviceAuth;
use crate::definitions::Mso;
use crate::definitions::{
device_signed::DeviceAuthentication, helpers::Tag24, SessionTranscript180135,
};
use crate::presentation::reader::Error;
use crate::presentation::reader::Error as ReaderError;
use anyhow::Result;
use elliptic_curve::generic_array::GenericArray;
use issuer_signed::IssuerSigned;
use p256::ecdsa::Signature;
use p256::ecdsa::VerifyingKey;
use p256::pkcs8::DecodePublicKey;
use serde_cbor::Value as CborValue;
use ssi_jwk::Params;
use ssi_jwk::JWK as SsiJwk;
use x509_cert::der::Decode;

pub fn issuer_authentication(x5chain: CborValue, issuer_signed: IssuerSigned) -> Result<(), Error> {
let signer_key = get_signer_key(&x5chain)?;
let issuer_auth = issuer_signed.issuer_auth;
let verification_result: cose_rs::sign1::VerificationResult =
issuer_auth.verify::<VerifyingKey, Signature>(&signer_key, None, None);
if !verification_result.success() {
Err(ReaderError::ParsingError)?
} else {
Ok(())
}
}

pub fn device_authentication(
mso: Tag24<Mso>,
document: Document,
session_transcript: SessionTranscript180135,
) -> Result<(), Error> {
let device_key = mso.into_inner().device_key_info.device_key;
let jwk = SsiJwk::try_from(device_key)?;
match jwk.params {
Params::EC(p) => {
let x_coordinate = p.x_coordinate.clone();
let y_coordinate = p.y_coordinate.clone();
let (Some(x), Some(y)) = (x_coordinate, y_coordinate) else {
return Err(ReaderError::MdocAuth(
"device key jwk is missing coordinates".to_string(),
));
};
let encoded_point = p256::EncodedPoint::from_affine_coordinates(
GenericArray::from_slice(x.0.as_slice()),
GenericArray::from_slice(y.0.as_slice()),
false,
);
let verifying_key = VerifyingKey::from_encoded_point(&encoded_point)?;
let namespaces_bytes = document.device_signed.namespaces;
let device_auth: DeviceAuth = document.device_signed.device_auth;

//TODO: fix for attended use case:
match device_auth {
DeviceAuth::Signature { device_signature } => {
let detached_payload = Tag24::new(DeviceAuthentication::new(
session_transcript,
document.doc_type,
namespaces_bytes,
))
.map_err(|_| ReaderError::CborDecodingError)?;
let external_aad = None;
let cbor_payload = serde_cbor::to_vec(&detached_payload)?;
let result = device_signature.verify::<VerifyingKey, Signature>(
&verifying_key,
Some(cbor_payload),
external_aad,
);
if !result.success() {
Err(ReaderError::ParsingError)?
} else {
Ok(())
}
}
DeviceAuth::Mac { .. } => {
Err(ReaderError::Unsupported)
// send not yet supported error
}
}
}
_ => Err(Error::MdocAuth("Unsupported device_key type".to_string())),
}
}

fn get_signer_key(x5chain: &CborValue) -> Result<VerifyingKey, Error> {
let signer = match x5chain {
CborValue::Text(t) => {
let x509 = x509_cert::Certificate::from_der(t.as_bytes())?;

x509.tbs_certificate
.subject_public_key_info
.subject_public_key
}
CborValue::Array(a) => match a.first() {
Some(CborValue::Text(t)) => {
let x509 = x509_cert::Certificate::from_der(t.as_bytes())?;

x509.tbs_certificate
.subject_public_key_info
.subject_public_key
}
_ => return Err(ReaderError::CborDecodingError)?,
},
CborValue::Bytes(b) => {
let x509 = x509_cert::Certificate::from_der(b)?;

x509.tbs_certificate
.subject_public_key_info
.subject_public_key
}
_ => {
return Err(ReaderError::MdocAuth(format!(
"Unexpected type for x5chain header: {:?} ",
x5chain
)))
}
};
Ok(VerifyingKey::from_public_key_der(signer.raw_bytes())?)
}
2 changes: 2 additions & 0 deletions src/presentation/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod device;
pub mod mdoc_auth;
pub mod reader;
pub mod trust_anchor;

use anyhow::Result;
use base64::{decode, encode};
Expand Down
Loading

0 comments on commit 10867e6

Please sign in to comment.