diff --git a/src/vc_util/src/custom.rs b/src/vc_util/src/custom.rs new file mode 100644 index 0000000000..cd2d56e17a --- /dev/null +++ b/src/vc_util/src/custom.rs @@ -0,0 +1,241 @@ +//! Custom utils for handling specific VCs/VPs. + +use crate::issuer_api::{ArgumentValue, CredentialSpec}; +use crate::{ + inconsistent_jwt_claims, validate_claim, validate_claims_match_spec, + verify_ii_presentation_jwt_with_canister_ids, CredentialVerificationError, + PresentationVerificationError, VcFlowSigners, +}; +use candid::Principal; +use identity_credential::validator::JwtValidationError; +use std::collections::HashMap; + +/// Validates the provided presentation `vp_jwt`, checking the following conditions: +/// - the presentation is a valid presentation of the IC Attribute Sharing flow; +/// in particular the presentation contains IdAlias and a matching VerifiedAdult credentials +/// - both credentials are valid wrt. `vc_flow_signers`, `root_pk_raw`, and `current_time_ns` +/// - `effective_vc_subject` matches the principal to which the presentation applies, i.e. +/// the subject of the IdAlias VC (which is linked via id_alias-id to the VerifiedAdult VC) +pub fn validate_verified_adult_presentation( + vp_jwt: &str, + effective_vc_subject: Principal, + vc_flow_signers: &VcFlowSigners, + root_pk_raw: &[u8], + current_time_ns: u128, +) -> Result<(), PresentationVerificationError> { + let (_alias_tuple, claims) = verify_ii_presentation_jwt_with_canister_ids( + vp_jwt, + effective_vc_subject, + vc_flow_signers, + root_pk_raw, + current_time_ns, + )?; + validate_claim("iss", &vc_flow_signers.issuer_origin, claims.iss()) + .map_err(invalid_requested_vc)?; + let vc_claims = claims + .vc() + .ok_or(invalid_requested_vc(inconsistent_jwt_claims( + "missing vc in id_alias JWT claims", + )))?; + validate_claims_match_spec(vc_claims, &verified_adult_vc_spec()) + .map_err(invalid_requested_vc)?; + Ok(()) +} + +fn invalid_requested_vc(e: JwtValidationError) -> PresentationVerificationError { + PresentationVerificationError::InvalidRequestedCredential( + CredentialVerificationError::InvalidClaims(e), + ) +} + +fn verified_adult_vc_spec() -> CredentialSpec { + let mut args = HashMap::new(); + args.insert("age_at_least".to_string(), ArgumentValue::Int(18)); + CredentialSpec { + credential_type: "VerifiedAdult".to_string(), + arguments: Some(args), + } +} + +#[cfg(test)] +mod tests { + use super::super::tests::create_verifiable_presentation_jwt_for_test; + use super::*; + use crate::II_ISSUER_URL; + use assert_matches::assert_matches; + use identity_jose::jwu::decode_b64; + + const TEST_IC_ROOT_RAW_PK_B64URL: &str = "rfZWOKUwVrIiLJG7JFewJ0vKlRmKWsva3-f9chePBpveqNmelHnYCHomhvyBvzxLEf4nVXDUgfFpj3nUaK_g5XrMHimPi2l5jaeokbvsGXCT7F9HWQmSPUi_7WhD2-0f"; + const ID_ALIAS_CREDENTIAL_JWS: &str = "eyJqd2siOnsia3R5Ijoib2N0IiwiYWxnIjoiSWNDcyIsImsiOiJNRHd3REFZS0t3WUJCQUdEdUVNQkFnTXNBQW9BQUFBQUFBQUFBQUVCMGd6TTVJeXFMYUhyMDhtQTRWd2J5SmRxQTFyRVFUX2xNQnVVbmN5UDVVYyJ9LCJraWQiOiJkaWQ6aWNwOnJ3bGd0LWlpYWFhLWFhYWFhLWFhYWFhLWNhaSIsImFsZyI6IkljQ3MifQ.eyJleHAiOjE2MjAzMjk1MzAsImlzcyI6Imh0dHBzOi8vaWRlbnRpdHkuaWMwLmFwcC8iLCJuYmYiOjE2MjAzMjg2MzAsImp0aSI6Imh0dHBzOi8vaWRlbnRpdHkuaWMwLmFwcC9jcmVkZW50aWFsLzE2MjAzMjg2MzAwMDAwMDAwMDAiLCJzdWIiOiJkaWQ6aWNwOnAybmxjLTNzNXVsLWxjdTc0LXQ2cG4yLXVpNWltLWk0YTVmLWE0dGdhLWU2em5mLXRudmxoLXdrbWpzLWRxZSIsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiSW50ZXJuZXRJZGVudGl0eUlkQWxpYXMiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaGFzX2lkX2FsaWFzIjoiZGlkOmljcDpqa2syMi16cWR4Yy1rZ3Blei02c3YybS01cGJ5NC13aTR0Mi1wcm1vcS1nZjJpaC1pMnF0Yy12MzdhYy01YWUifX19.2dn3omtjZXJ0aWZpY2F0ZVkBsdnZ96JkdHJlZYMBgwGDAYMCSGNhbmlzdGVygwGDAkoAAAAAAAAAAAEBgwGDAYMBgwJOY2VydGlmaWVkX2RhdGGCA1gg0zi9m0cg2U6AhUtpoJ7YKvX949iyhX0HssFZavg1DuSCBFgg0sz_P8xdqTDewOhKJUHmWFFrS7FQHnDotBDmmGoFfWCCBFggkzS0s8W3-LMHUd6MNFx8vCZvWiTSFRorIEMWhTwPnnmCBFggb2v6rTj0HcTJloNfmKGknAnxYl3Z7DM2BgMtON2-XyiCBFggMJp7SZcwpVGuz6sLBZpMol2Zn5_ELZ5uqoPUCgWgui2CBFggdfCIVuucl7ykgQOb4p4Cp2hA8C4YKVk3R8YuHCg0Ho6CBFggvJUUsrcvnUQooo4CjuiXSdzpHIMw75qS0VeLqXsfteiDAYIEWCA1U_ZYHVOz3Sdkb2HIsNoLDDiBuFfG3DxH6miIwRPra4MCRHRpbWWCA0mAuK7U3YmkvhZpc2lnbmF0dXJlWDCIiIfdfnkfcdQOsawkjiLy3zZ5vAryQBf6jYl8szyA_oT2sCh8zKNQS6HlIZoegQBkdHJlZYMBggRYIAGrxrjxRQC1-m5yYX0gu_fp5t9vfKWXuij1mbw_X2-8gwJDc2lngwJYIIOQR7wl3Ws9Jb8VP4rhIb37XKLMkkZ2P7WaZ5we60WGgwGCBFggsvWfw_9l6MdknYq3zHvk8WyFan5Rxsb-dfI-p_IA_g6DAlgg_OG2VoT11h2BQfYTnf83Y9blXTdGnmKGRe3SJAvV8h2CA0A"; + const ADULT_CREDENTIAL_JWS: &str = "eyJqd2siOnsia3R5Ijoib2N0IiwiYWxnIjoiSWNDcyIsImsiOiJNRHd3REFZS0t3WUJCQUdEdUVNQkFnTXNBQW9BQUFBQUFBQUFBUUVCMzdLQ29yYjQ2OUVFZ19uYzVvTFI5RHNoSy1SOEhXUUNMN05VMDcwa1R0WSJ9LCJraWQiOiJkaWQ6aWNwOnJya2FoLWZxYWFhLWFhYWFhLWFhYWFxLWNhaSIsImFsZyI6IkljQ3MifQ.eyJleHAiOjE2MjAzMjk1MzAsImlzcyI6Imh0dHBzOi8vYWdlX3ZlcmlmaWVyLmluZm8vIiwibmJmIjoxNjIwMzI4NjMwLCJqdGkiOiJodHRwczovL2FnZV92ZXJpZmllci5pbmZvL2NyZWRlbnRpYWxzLzQyIiwic3ViIjoiZGlkOmljcDpqa2syMi16cWR4Yy1rZ3Blei02c3YybS01cGJ5NC13aTR0Mi1wcm1vcS1nZjJpaC1pMnF0Yy12MzdhYy01YWUiLCJ2YyI6eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlZlcmlmaWVkQWR1bHQiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiYWdlX2F0X2xlYXN0IjoxOH19fQ.2dn3omtjZXJ0aWZpY2F0ZVkBsdnZ96JkdHJlZYMBgwGDAYMCSGNhbmlzdGVygwGCBFggGqA23yasU2u2BherQAbx8y4H6lttXUDVNHAJknxkEcGDAkoAAAAAAAAAAQEBgwGDAYMBgwJOY2VydGlmaWVkX2RhdGGCA1ggAEPaT4CyIbEI6mH0lfgHG1yXPlqUpijL4EYeCJTeI2KCBFgg0sz_P8xdqTDewOhKJUHmWFFrS7FQHnDotBDmmGoFfWCCBFggEKUIjno3uO8-1RpfXTakCG0-J58Sk93bTqYVeU5xlc-CBFgg3Nc5Lp9mVleNY-HHA1ws5XvEGAWHd6Tn45U6R-8dx0WCBFggd8PYYS3mmXQc2roJ34IJMIbDDb4YWB3GZODAFW7AI6yCBFggQ4eqW4cFZ0hQ6C2-tbf4IiKXWNxb_TLP27B23Kgxq8GDAYIEWCA1U_ZYHVOz3Sdkb2HIsNoLDDiBuFfG3DxH6miIwRPra4MCRHRpbWWCA0mAuK7U3YmkvhZpc2lnbmF0dXJlWDCIu9FZjeqZIFpqri2_3JBt2QmnbIsp0Y9O4DGuSF7zhvfyTTegAAhnzJT9E-zoqTdkdHJlZYMBggRYII1hVxROQ2YGU4j3iepO8R7lxxKwSzHdP1lUs1ELgMIngwJDc2lngwJYIPL9DVTa-kaJmcs0Khp5uNdi4aZ5Dnt_kCORxF48UNPjgwJYIK17NMXB0rEaKIbr0yqVwTNJjw85Y53dOxfzqoN8PmgDggNA"; + const EMPLOYEE_CREDENTIAL_JWS: &str = "eyJqd2siOnsia3R5Ijoib2N0IiwiYWxnIjoiSWNDcyIsImsiOiJNRHd3REFZS0t3WUJCQUdEdUVNQkFnTXNBQW9BQUFBQUFBQUFBUUVCMzdLQ29yYjQ2OUVFZ19uYzVvTFI5RHNoSy1SOEhXUUNMN05VMDcwa1R0WSJ9LCJraWQiOiJkaWQ6aWNwOnJya2FoLWZxYWFhLWFhYWFhLWFhYWFxLWNhaSIsImFsZyI6IkljQ3MifQ.eyJleHAiOjE2MjAzMjk1MzAsImlzcyI6Imh0dHBzOi8vZW1wbG95bWVudC5pbmZvLyIsIm5iZiI6MTYyMDMyODYzMCwianRpIjoiaHR0cHM6Ly9lbXBsb3ltZW50LmluZm8vY3JlZGVudGlhbHMvNDIiLCJzdWIiOiJkaWQ6aWNwOmprazIyLXpxZHhjLWtncGV6LTZzdjJtLTVwYnk0LXdpNHQyLXBybW9xLWdmMmloLWkycXRjLXYzN2FjLTVhZSIsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpZWRFbXBsb3llZSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJlbXBsb3llZV9vZiI6eyJlbXBsb3llcklkIjoiZGlkOndlYjpkZmluaXR5Lm9yZyIsImVtcGxveWVyTmFtZSI6IkRGSU5JVFkgRm91bmRhdGlvbiJ9fX19.2dn3omtjZXJ0aWZpY2F0ZVkBsdnZ96JkdHJlZYMBgwGDAYMCSGNhbmlzdGVygwGCBFggGqA23yasU2u2BherQAbx8y4H6lttXUDVNHAJknxkEcGDAkoAAAAAAAAAAQEBgwGDAYMBgwJOY2VydGlmaWVkX2RhdGGCA1ggPmrhfVE6NX9-yRvIyijmJ-8ID6iVSxET45GEWm21bw-CBFgg0sz_P8xdqTDewOhKJUHmWFFrS7FQHnDotBDmmGoFfWCCBFggEKUIjno3uO8-1RpfXTakCG0-J58Sk93bTqYVeU5xlc-CBFggq_ssVg2yMUpmx1c3eKj_W104x6P4eK1HYN5PsBcp3ZCCBFggOR-FhmvNHz9A-lDCTrkhgz0ETI7ePYvn8Nh9x1RARDKCBFggwqOwOs93JEnQT97xRsEHERKf6h6IheMH2hBk3O1b_jyDAYIEWCA1U_ZYHVOz3Sdkb2HIsNoLDDiBuFfG3DxH6miIwRPra4MCRHRpbWWCA0mAuK7U3YmkvhZpc2lnbmF0dXJlWDCJRTFzpJm_9HgCb8gYgnsBee-bN2nqxaFQGmRrqI9a4x8YyFN9HmRqt_rF-vZKVCNkdHJlZYMBggRYII1hVxROQ2YGU4j3iepO8R7lxxKwSzHdP1lUs1ELgMIngwJDc2lngwJYIPL9DVTa-kaJmcs0Khp5uNdi4aZ5Dnt_kCORxF48UNPjgwJYINHVvrQeii1pNBIwX-CyraHaGdwOA-brS3SWGayJeKDjggNA"; + const ID_ALIAS_PRINCIPAL: &str = + "jkk22-zqdxc-kgpez-6sv2m-5pby4-wi4t2-prmoq-gf2ih-i2qtc-v37ac-5ae"; + const RP_PRINCIPAL: &str = "p2nlc-3s5ul-lcu74-t6pn2-ui5im-i4a5f-a4tga-e6znf-tnvlh-wkmjs-dqe"; + const EXPIRY_NS: u128 = 1620329530 * 1_000_000_000; // from ID_ALIAS_CREDENTIAL_JWS + const MINUTE_NS: u128 = 60 * 1_000_000_000; + const CURRENT_TIME_BEFORE_EXPIRY_NS: u128 = EXPIRY_NS - MINUTE_NS; + const CURRENT_TIME_AFTER_EXPIRY_NS: u128 = EXPIRY_NS + MINUTE_NS; + const II_CANISTER_ID: &str = "rwlgt-iiaaa-aaaaa-aaaaa-cai"; + const ISSUER_CANISTER_ID: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai"; + + fn test_ic_root_pk_raw() -> Vec { + decode_b64(TEST_IC_ROOT_RAW_PK_B64URL).expect("failure decoding canister pk") + } + + fn id_alias_principal() -> Principal { + Principal::from_text(ID_ALIAS_PRINCIPAL).expect("wrong principal") + } + + fn rp_principal() -> Principal { + Principal::from_text(RP_PRINCIPAL).expect("wrong principal") + } + + fn ii_canister_id() -> Principal { + Principal::from_text(II_CANISTER_ID).expect("wrong principal") + } + + fn issuer_canister_id() -> Principal { + Principal::from_text(ISSUER_CANISTER_ID).expect("wrong principal") + } + + fn default_test_vc_flow_signers() -> VcFlowSigners { + VcFlowSigners { + ii_canister_id: ii_canister_id(), + ii_origin: II_ISSUER_URL.to_string(), + issuer_canister_id: issuer_canister_id(), + issuer_origin: "https://age_verifier.info/".to_string(), + } + } + + #[test] + fn should_validate_verified_adult_presentation() { + let vp_jwt = create_verifiable_presentation_jwt_for_test( + rp_principal(), + vec![ + ID_ALIAS_CREDENTIAL_JWS.to_string(), + ADULT_CREDENTIAL_JWS.to_string(), + ], + ) + .expect("vp-creation failed"); + validate_verified_adult_presentation( + &vp_jwt, + rp_principal(), + &default_test_vc_flow_signers(), + &test_ic_root_pk_raw(), + CURRENT_TIME_BEFORE_EXPIRY_NS, + ) + .expect("VP verification failed"); + } + + #[test] + fn should_fail_validate_verified_adult_presentation_if_wrong_vc_flow_signers() { + let vp_jwt = create_verifiable_presentation_jwt_for_test( + rp_principal(), + vec![ + ID_ALIAS_CREDENTIAL_JWS.to_string(), + ADULT_CREDENTIAL_JWS.to_string(), + ], + ) + .expect("vp-creation failed"); + + // wrong ii_canister_id + let result = validate_verified_adult_presentation( + &vp_jwt, + rp_principal(), + &VcFlowSigners { + ii_canister_id: issuer_canister_id(), + ..default_test_vc_flow_signers() + }, + &test_ic_root_pk_raw(), + CURRENT_TIME_BEFORE_EXPIRY_NS, + ); + assert_matches!(result, Err(e) if format!("{:?}", e).to_string().contains("InvalidSignature")); + + // wrong issuer_canister_id + let result = validate_verified_adult_presentation( + &vp_jwt, + rp_principal(), + &VcFlowSigners { + issuer_canister_id: ii_canister_id(), + ..default_test_vc_flow_signers() + }, + &test_ic_root_pk_raw(), + CURRENT_TIME_BEFORE_EXPIRY_NS, + ); + assert_matches!(result, Err(e) if format!("{:?}", e).to_string().contains("InvalidSignature")); + + // wrong issuer_origin + let result = validate_verified_adult_presentation( + &vp_jwt, + rp_principal(), + &VcFlowSigners { + issuer_origin: "https://wrong.origin.com".to_string(), + ..default_test_vc_flow_signers() + }, + &test_ic_root_pk_raw(), + CURRENT_TIME_BEFORE_EXPIRY_NS, + ); + assert_matches!(result, Err(e) if format!("{:?}", e).to_string().contains("InconsistentCredentialJwtClaims")); + } + + #[test] + fn should_fail_validate_verified_adult_presentation_if_wrong_effective_subject() { + let vp_jwt = create_verifiable_presentation_jwt_for_test( + rp_principal(), + vec![ + ID_ALIAS_CREDENTIAL_JWS.to_string(), + ADULT_CREDENTIAL_JWS.to_string(), + ], + ) + .expect("vp-creation failed"); + let result = validate_verified_adult_presentation( + &vp_jwt, + id_alias_principal(), // wrong effective subject + &default_test_vc_flow_signers(), + &test_ic_root_pk_raw(), + CURRENT_TIME_BEFORE_EXPIRY_NS, + ); + assert_matches!(result, Err(e) if format!("{:?}", e).to_string().contains("holder does not match subject")); + } + + #[test] + fn should_fail_validate_verified_adult_presentation_if_expired() { + let vp_jwt = create_verifiable_presentation_jwt_for_test( + rp_principal(), + vec![ + ID_ALIAS_CREDENTIAL_JWS.to_string(), + ADULT_CREDENTIAL_JWS.to_string(), + ], + ) + .expect("vp-creation failed"); + let result = validate_verified_adult_presentation( + &vp_jwt, + rp_principal(), + &default_test_vc_flow_signers(), + &test_ic_root_pk_raw(), + CURRENT_TIME_AFTER_EXPIRY_NS, + ); + assert_matches!(result, Err(e) if format!("{:?}", e).to_string().contains("credential expired")); + } + + #[test] + fn should_fail_validate_verified_adult_presentation_if_wrong_vcs() { + let vp_jwt = create_verifiable_presentation_jwt_for_test( + rp_principal(), + vec![ + ID_ALIAS_CREDENTIAL_JWS.to_string(), + EMPLOYEE_CREDENTIAL_JWS.to_string(), // not a VerifiedAdult VC + ], + ) + .expect("vp-creation failed"); + let result = validate_verified_adult_presentation( + &vp_jwt, + rp_principal(), + &default_test_vc_flow_signers(), + &test_ic_root_pk_raw(), + CURRENT_TIME_BEFORE_EXPIRY_NS, + ); + assert_matches!(result, Err(e) if format!("{:?}", e).to_string().contains("InconsistentCredentialJwtClaims")); + } +} diff --git a/src/vc_util/src/issuer_api.rs b/src/vc_util/src/issuer_api.rs index 91a3105f37..1de4110426 100644 --- a/src/vc_util/src/issuer_api.rs +++ b/src/vc_util/src/issuer_api.rs @@ -1,5 +1,6 @@ use candid::{CandidType, Deserialize, Nat}; use serde_bytes::ByteBuf; +use serde_json::Value; use std::collections::HashMap; use std::fmt::{Display, Formatter}; @@ -70,6 +71,27 @@ impl Display for ArgumentValue { } } +impl PartialEq for ArgumentValue { + fn eq(&self, other: &Value) -> bool { + match self { + ArgumentValue::String(ls) => { + if let Some(rs) = other.as_str() { + ls.eq(rs) + } else { + false + } + } + ArgumentValue::Int(li) => { + if let Some(ri) = other.as_i64() { + (*li as i64) == ri + } else { + false + } + } + } + } +} + #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] pub struct CredentialSpec { pub credential_type: String, @@ -135,4 +157,44 @@ mod tests { format!("{}", ArgumentValue::String("some string".to_string())) ); } + + #[test] + fn should_correctly_compare_argument_values() { + assert_eq!(ArgumentValue::Int(42), Value::from(42)); + assert_eq!(ArgumentValue::Int(123456789), Value::from(123456789)); + assert_eq!(ArgumentValue::Int(0), Value::from(0)); + + assert_ne!(ArgumentValue::Int(42), Value::from(11)); + assert_ne!(ArgumentValue::Int(42), Value::from("some string")); + assert_ne!(ArgumentValue::Int(42), Value::from(true)); + assert_ne!(ArgumentValue::Int(42), Value::from(vec![1, 2, 3])); + + assert_eq!( + ArgumentValue::String("same string".to_string()), + Value::from("same string") + ); + let long_string = "this is a bit longer string just for testing purposes"; + assert_eq!( + ArgumentValue::String(long_string.to_string()), + Value::from(long_string) + ); + assert_eq!(ArgumentValue::String("".to_string()), Value::from("")); + + assert_ne!( + ArgumentValue::String("some string".to_string()), + Value::from("different") + ); + assert_ne!( + ArgumentValue::String("a string".to_string()), + Value::from(42) + ); + assert_ne!( + ArgumentValue::String("a string".to_string()), + Value::from(true) + ); + assert_ne!( + ArgumentValue::String("a string".to_string()), + Value::from(vec![1, 2, 3]) + ); + } } diff --git a/src/vc_util/src/lib.rs b/src/vc_util/src/lib.rs index 8089b8076e..454999bb4c 100644 --- a/src/vc_util/src/lib.rs +++ b/src/vc_util/src/lib.rs @@ -1,3 +1,4 @@ +use crate::issuer_api::CredentialSpec; use candid::Principal; use canister_sig_util::{extract_raw_canister_sig_pk_from_der, CanisterSigPublicKey}; use ic_certification::Hash; @@ -15,10 +16,12 @@ use identity_jose::jws::{ }; use identity_jose::jwt::JwtClaims; use identity_jose::jwu::{decode_b64, encode_b64}; -use serde_json::Value; +use serde_json::{Map, Value}; use sha2::{Digest, Sha256}; +use std::collections::HashMap; use std::ops::{Add, Deref, DerefMut}; +pub mod custom; pub mod issuer_api; pub const II_CREDENTIAL_URL_PREFIX: &str = "https://identity.ic0.app/credential/"; @@ -37,10 +40,12 @@ pub struct AliasTuple { } #[derive(Debug, Eq, PartialEq)] -pub struct VcFlowParties { - /// Ids of canisters that issued credentials contained in a verifiable presentation. +/// Parties that signed credentials contained in a verifiable presentation. +pub struct VcFlowSigners { pub ii_canister_id: Principal, + pub ii_origin: String, pub issuer_canister_id: Principal, + pub issuer_origin: String, } #[derive(Debug)] @@ -195,7 +200,8 @@ fn parse_verifiable_presentation_jwt(vp_jwt: &str) -> Result, /// DOES NOT perform semantic validation of the returned claims. pub fn verify_ii_presentation_jwt_with_canister_ids( vp_jwt: &str, - vc_flow_parties: &VcFlowParties, + effective_vc_subject: Principal, + vc_flow_signers: &VcFlowSigners, root_pk_raw: &[u8], current_time_ns: u128, ) -> Result<(AliasTuple, JwtClaims), PresentationVerificationError> { @@ -215,19 +221,21 @@ pub fn verify_ii_presentation_jwt_with_canister_ids( ))?; let alias_tuple = get_verified_id_alias_from_jws( id_alias_vc_jws.as_str(), - &vc_flow_parties.ii_canister_id, + &vc_flow_signers.ii_canister_id, root_pk_raw, current_time_ns, ) .map_err(PresentationVerificationError::InvalidIdAliasCredential)?; - let holder = principal_for_did(&presentation.holder.to_string()).map_err(|e| { - PresentationVerificationError::Unknown(format!("error parsing holder: {}", e)) - })?; - if holder != alias_tuple.id_dapp { + // TODO: change `get_verified_id_alias_from_jws` to additionally take `effective_vc_subject` + // as an argument and to perform the check below internally. + // Also, consider adding `ii_origin`-argument to enable custom values for II-origin, + // and extend should_fail_validate_verified_adult_presentation_if_wrong_vc_flow_signers() + // if needed. + if effective_vc_subject != alias_tuple.id_dapp { return Err(PresentationVerificationError::InvalidPresentationJwt( format!( "holder does not match subject: expected {}, got {}", - holder, alias_tuple.id_dapp + effective_vc_subject, alias_tuple.id_dapp ) .to_string(), )); @@ -241,7 +249,7 @@ pub fn verify_ii_presentation_jwt_with_canister_ids( ))?; let claims = verify_credential_jws_with_canister_id( requested_vc_jws.as_str(), - &vc_flow_parties.issuer_canister_id, + &vc_flow_signers.issuer_canister_id, root_pk_raw, current_time_ns, ) @@ -320,16 +328,16 @@ fn validate_claim + std::fmt::Display, S: std::fmt::Display>( Ok(()) } else { ic_cdk::println!( - "inconsistent claim [{}] in id_alias VC:: expected: {}, actual: {}", + "inconsistent claim [{}] in VC:: expected: {}, actual: {}", label, expected, actual ); - Err(inconsistent_jwt_claims("inconsistent claim in id_alias VC")) + Err(inconsistent_jwt_claims("inconsistent claim in VC")) } } else { - ic_cdk::println!("missing claim [{}] in id_alias VC", label); - Err(inconsistent_jwt_claims("missing claim in id_alias VC")) + ic_cdk::println!("missing claim [{}] in VC", label); + Err(inconsistent_jwt_claims("missing claim in VC")) } } @@ -353,6 +361,35 @@ fn validate_expiration( } } +fn validate_claims_match_spec( + vc_claims: &Map, + spec: &CredentialSpec, +) -> Result<(), JwtValidationError> { + let credential_type = vc_claims + .get("type") + .ok_or(inconsistent_jwt_claims("missing type JWT vc"))?; + let types = credential_type + .as_array() + .ok_or(inconsistent_jwt_claims("wrong types in JWT vc"))?; + if !types.contains(&Value::String(spec.credential_type.clone())) { + return Err(inconsistent_jwt_claims("wrong vc type")); + }; + let credential_subject = vc_claims + .get("credentialSubject") + .ok_or(inconsistent_jwt_claims( + "missing credentialSubject in JWT vc", + ))?; + let subject = Subject::from_json_value(credential_subject.clone()).map_err(|_| { + inconsistent_jwt_claims("missing credentialSubject in VerifiedAdult JWT vc") + })?; + for (key, value) in spec.arguments.as_ref().unwrap_or(&HashMap::new()).iter() { + if *value != subject.properties[key] { + return Err(inconsistent_jwt_claims("wrong VerifiedAdult vc")); + } + } + Ok(()) +} + // Per https://datatracker.ietf.org/doc/html/rfc7518#section-6.4, // JwkParamsOct are for symmetric keys or another key whose value is a single octet sequence. fn canister_sig_pk_jwk(canister_sig_pk_der: &[u8]) -> Result { @@ -500,7 +537,7 @@ mod tests { claims } - fn create_verifiable_presentation_jwt_for_test( + pub(crate) fn create_verifiable_presentation_jwt_for_test( holder: Principal, vcs_jws: Vec, ) -> Result { @@ -700,6 +737,15 @@ mod tests { ); } + fn default_test_vc_flow_signers() -> VcFlowSigners { + VcFlowSigners { + ii_canister_id: test_canister_sig_pk().canister_id, + ii_origin: II_ISSUER_URL.to_string(), + issuer_canister_id: test_issuer_canister_sig_pk().canister_id, + issuer_origin: "https://vc_issuer.com".to_string(), + } + } + #[test] fn should_verify_ii_presentation() { let id_alias = Principal::from_text(ID_ALIAS_FOR_VP).expect("wrong principal"); @@ -714,10 +760,8 @@ mod tests { .expect("vp creation failed"); let (alias_tuple_from_jws, _claims) = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { - ii_canister_id: test_canister_sig_pk().canister_id, - issuer_canister_id: test_issuer_canister_sig_pk().canister_id, - }, + id_dapp, + &default_test_vc_flow_signers(), &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, ) @@ -739,10 +783,8 @@ mod tests { .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { - ii_canister_id: test_canister_sig_pk().canister_id, - issuer_canister_id: test_issuer_canister_sig_pk().canister_id, - }, + id_dapp, + &default_test_vc_flow_signers(), &test_ic_root_pk_raw(), CURRENT_TIME_AFTER_EXPIRY_NS, ); @@ -751,9 +793,9 @@ mod tests { #[test] fn should_fail_verify_ii_presentation_with_extra_vc() { - let holder = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); + let id_dapp = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); let vp_jwt = create_verifiable_presentation_jwt_for_test( - holder, + id_dapp, vec![ ID_ALIAS_VC_FOR_VP_JWS.to_string(), REQUESTED_VC_FOR_VP_JWS.to_string(), @@ -763,10 +805,8 @@ mod tests { .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { - ii_canister_id: test_canister_sig_pk().canister_id, - issuer_canister_id: test_issuer_canister_sig_pk().canister_id, - }, + id_dapp, + &default_test_vc_flow_signers(), &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, ); @@ -775,18 +815,16 @@ mod tests { #[test] fn should_fail_verify_ii_presentation_with_missing_vc() { - let holder = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); + let id_dapp = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); let vp_jwt = create_verifiable_presentation_jwt_for_test( - holder, + id_dapp, vec![ID_ALIAS_VC_FOR_VP_JWS.to_string()], ) .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { - ii_canister_id: test_canister_sig_pk().canister_id, - issuer_canister_id: test_issuer_canister_sig_pk().canister_id, - }, + id_dapp, + &default_test_vc_flow_signers(), &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, ); @@ -794,10 +832,10 @@ mod tests { } #[test] - fn should_fail_verify_ii_presentation_with_wrong_holder() { - let wrong_holder = dapp_principal(); // does not match ID_ALIAS_VC_FOR_VP_JWS + fn should_fail_verify_ii_presentation_with_wrong_effective_subject() { + let wrong_subject = dapp_principal(); // does not match ID_ALIAS_VC_FOR_VP_JWS let vp_jwt = create_verifiable_presentation_jwt_for_test( - wrong_holder, + wrong_subject, vec![ ID_ALIAS_VC_FOR_VP_JWS.to_string(), REQUESTED_VC_FOR_VP_JWS.to_string(), @@ -806,10 +844,8 @@ mod tests { .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { - ii_canister_id: test_canister_sig_pk().canister_id, - issuer_canister_id: test_issuer_canister_sig_pk().canister_id, - }, + wrong_subject, + &default_test_vc_flow_signers(), &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, ); @@ -818,11 +854,11 @@ mod tests { #[test] fn should_fail_verify_ii_presentation_with_non_matching_id_alias_in_vcs() { - let holder = dapp_principal(); // does match ID_ALIAS_CREDENTIAL_JWS + let id_dapp = dapp_principal(); // does match ID_ALIAS_CREDENTIAL_JWS // ID_ALIAS_CREDENTIAL_JWS does not match REQUESTED_VC_FOR_VP_JWS let vp_jwt = create_verifiable_presentation_jwt_for_test( - holder, + id_dapp, vec![ ID_ALIAS_CREDENTIAL_JWS.to_string(), REQUESTED_VC_FOR_VP_JWS.to_string(), @@ -831,10 +867,8 @@ mod tests { .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { - ii_canister_id: test_canister_sig_pk().canister_id, - issuer_canister_id: test_issuer_canister_sig_pk().canister_id, - }, + id_dapp, + &default_test_vc_flow_signers(), &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, ); @@ -843,21 +877,19 @@ mod tests { #[test] fn should_fail_verify_ii_presentation_with_invalid_id_alias_vc() { - let holder = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); + let id_dapp = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); let mut bad_id_alias_vc = ID_ALIAS_VC_FOR_VP_JWS.to_string(); bad_id_alias_vc.insert(42, 'a'); let vp_jwt = create_verifiable_presentation_jwt_for_test( - holder, + id_dapp, vec![bad_id_alias_vc, REQUESTED_VC_FOR_VP_JWS.to_string()], ) .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { - ii_canister_id: test_canister_sig_pk().canister_id, - issuer_canister_id: test_issuer_canister_sig_pk().canister_id, - }, + id_dapp, + &default_test_vc_flow_signers(), &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, ); @@ -866,21 +898,19 @@ mod tests { #[test] fn should_fail_verify_ii_presentation_with_invalid_requested_vc() { - let holder = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); + let id_dapp = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); let mut bad_requested_vc = REQUESTED_VC_FOR_VP_JWS.to_string(); bad_requested_vc.insert(42, 'a'); let vp_jwt = create_verifiable_presentation_jwt_for_test( - holder, + id_dapp, vec![ID_ALIAS_VC_FOR_VP_JWS.to_string(), bad_requested_vc], ) .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { - ii_canister_id: test_canister_sig_pk().canister_id, - issuer_canister_id: test_issuer_canister_sig_pk().canister_id, - }, + id_dapp, + &default_test_vc_flow_signers(), &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, ); @@ -889,10 +919,10 @@ mod tests { #[test] fn should_fail_verify_ii_presentation_with_wrong_ii_canister_id() { - let holder = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); + let id_dapp = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); let vp_jwt = create_verifiable_presentation_jwt_for_test( - holder, + id_dapp, vec![ ID_ALIAS_VC_FOR_VP_JWS.to_string(), REQUESTED_VC_FOR_VP_JWS.to_string(), @@ -901,9 +931,10 @@ mod tests { .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { + id_dapp, + &VcFlowSigners { ii_canister_id: test_issuer_canister_sig_pk().canister_id, - issuer_canister_id: test_issuer_canister_sig_pk().canister_id, + ..default_test_vc_flow_signers() }, &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, @@ -913,10 +944,10 @@ mod tests { #[test] fn should_fail_verify_ii_presentation_with_wrong_issuer_canister_id() { - let holder = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); + let id_dapp = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); let vp_jwt = create_verifiable_presentation_jwt_for_test( - holder, + id_dapp, vec![ ID_ALIAS_VC_FOR_VP_JWS.to_string(), REQUESTED_VC_FOR_VP_JWS.to_string(), @@ -925,9 +956,10 @@ mod tests { .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { - ii_canister_id: test_canister_sig_pk().canister_id, + id_dapp, + &VcFlowSigners { issuer_canister_id: test_canister_sig_pk().canister_id, + ..default_test_vc_flow_signers() }, &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, @@ -937,11 +969,11 @@ mod tests { #[test] fn should_fail_verify_ii_presentation_with_wrong_order_of_vcs() { - let holder = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); + let id_dapp = Principal::from_text(ID_RP_FOR_VP).expect("wrong principal"); // Swap the order of the VCs let vp_jwt = create_verifiable_presentation_jwt_for_test( - holder, + id_dapp, vec![ REQUESTED_VC_FOR_VP_JWS.to_string(), ID_ALIAS_VC_FOR_VP_JWS.to_string(), @@ -950,14 +982,16 @@ mod tests { .expect("vp creation failed"); let result = verify_ii_presentation_jwt_with_canister_ids( &vp_jwt, - &VcFlowParties { + id_dapp, + &VcFlowSigners { // Swap also the order of the canister ids, so that they match the VCs ii_canister_id: test_issuer_canister_sig_pk().canister_id, issuer_canister_id: test_canister_sig_pk().canister_id, + ..default_test_vc_flow_signers() }, &test_ic_root_pk_raw(), CURRENT_TIME_BEFORE_EXPIRY_NS, ); - assert_matches!(result, Err(e) if format!("{:?}", e).contains("inconsistent claim in id_alias VC")); + assert_matches!(result, Err(e) if format!("{:?}", e).contains("inconsistent claim in VC")); } }