diff --git a/lib/hpke/include/hpke/userinfo_vc.h b/lib/hpke/include/hpke/userinfo_vc.h index 908451f5..085fd4f5 100644 --- a/lib/hpke/include/hpke/userinfo_vc.h +++ b/lib/hpke/include/hpke/userinfo_vc.h @@ -6,11 +6,51 @@ #include #include #include +#include using namespace MLS_NAMESPACE::bytes_ns; namespace MLS_NAMESPACE::hpke { +struct UserInfoClaimsAddress +{ + std::optional formatted; + std::optional street_address; + std::optional locality; + std::optional region; + std::optional postal_code; + std::optional country; + + static UserInfoClaimsAddress from_json(const nlohmann::json& address_json); +}; + +struct UserInfoClaims +{ + + std::optional sub; + std::optional name; + std::optional given_name; + std::optional family_name; + std::optional middle_name; + std::optional nickname; + std::optional preferred_username; + std::optional profile; + std::optional picture; + std::optional website; + std::optional email; + std::optional email_verified; + std::optional gender; + std::optional birthdate; + std::optional zoneinfo; + std::optional locale; + std::optional phone_number; + std::optional phone_number_verified; + std::optional address; + std::optional updated_at; + + static UserInfoClaims from_json(const nlohmann::json& cred_subject_json); +}; + struct UserInfoVC { private: @@ -29,7 +69,8 @@ struct UserInfoVC std::string key_id() const; std::chrono::system_clock::time_point not_before() const; std::chrono::system_clock::time_point not_after() const; - std::map subject() const; + const std::string& raw_credential() const; + const UserInfoClaims& subject() const; const Signature::PublicJWK& public_key() const; bool valid_from(const Signature::PublicKey& issuer_key) const; diff --git a/lib/hpke/src/userinfo_vc.cpp b/lib/hpke/src/userinfo_vc.cpp index fcbfb5b8..89860d12 100644 --- a/lib/hpke/src/userinfo_vc.cpp +++ b/lib/hpke/src/userinfo_vc.cpp @@ -8,6 +8,44 @@ using nlohmann::json; namespace MLS_NAMESPACE::hpke { +static const std::string name_attr = "name"; +static const std::string sub_attr = "sub"; +static const std::string given_name_attr = "given_name"; +static const std::string family_name_attr = "family_name"; +static const std::string middle_name_attr = "middle_name"; +static const std::string nickname_attr = "nickname"; +static const std::string preferred_username_attr = "preferred_username"; +static const std::string profile_attr = "profile"; +static const std::string picture_attr = "picture"; +static const std::string website_attr = "website"; +static const std::string email_attr = "email"; +static const std::string email_verified_attr = "email_verified"; +static const std::string gender_attr = "gender"; +static const std::string birthdate_attr = "birthdate"; +static const std::string zoneinfo_attr = "zoneinfo"; +static const std::string locale_attr = "locale"; +static const std::string phone_number_attr = "phone_number"; +static const std::string phone_number_verified_attr = "phone_number_verified"; +static const std::string address_attr = "address"; +static const std::string address_formatted_attr = "formatted"; +static const std::string address_street_address_attr = "street_address"; +static const std::string address_locality_attr = "locality"; +static const std::string address_region_attr = "region"; +static const std::string address_postal_code_attr = "postal_code"; +static const std::string address_country_attr = "country"; +static const std::string updated_at_attr = "updated_at"; + +template +static std::optional +get_optional(const json& json_object, const std::string& field_name) +{ + if (!json_object.contains(field_name)) { + return std::nullopt; + } + + return { json_object.at(field_name).get() }; +} + /// /// ParsedCredential /// @@ -130,7 +168,7 @@ struct UserInfoVC::ParsedCredential std::chrono::system_clock::time_point not_after; // `exp` // Credential subject fields - std::map credential_subject; + UserInfoClaims credential_subject; Signature::PublicJWK public_key; // Signature verification information @@ -142,7 +180,7 @@ struct UserInfoVC::ParsedCredential std::string issuer_in, std::chrono::system_clock::time_point not_before_in, std::chrono::system_clock::time_point not_after_in, - std::map credential_subject_in, + UserInfoClaims credential_subject_in, Signature::PublicJWK&& public_key_in, bytes to_be_signed_in, bytes signature_in) @@ -222,7 +260,7 @@ struct UserInfoVC::ParsedCredential epoch_time(payload.at("nbf").get()), epoch_time(payload.at("exp").get()), - vc.at("credentialSubject"), + UserInfoClaims::from_json(vc.at("credentialSubject")), std::move(public_key), to_be_signed, @@ -235,6 +273,59 @@ struct UserInfoVC::ParsedCredential } }; +/// +/// UserInfoClaimsAddress +/// +UserInfoClaimsAddress +UserInfoClaimsAddress::from_json(const nlohmann::json& address_json) +{ + return { + get_optional(address_json, address_formatted_attr), + get_optional(address_json, address_street_address_attr), + get_optional(address_json, address_locality_attr), + get_optional(address_json, address_region_attr), + get_optional(address_json, address_postal_code_attr), + get_optional(address_json, address_country_attr), + }; +} + +/// +/// UserInfoClaims +/// +UserInfoClaims +UserInfoClaims::from_json(const nlohmann::json& cred_subject_json) +{ + std::optional address_opt = {}; + + if (cred_subject_json.contains(address_attr)) { + address_opt = + UserInfoClaimsAddress::from_json(cred_subject_json.at(address_attr)); + } + + return { + get_optional(cred_subject_json, sub_attr), + get_optional(cred_subject_json, name_attr), + get_optional(cred_subject_json, given_name_attr), + get_optional(cred_subject_json, family_name_attr), + get_optional(cred_subject_json, middle_name_attr), + get_optional(cred_subject_json, nickname_attr), + get_optional(cred_subject_json, preferred_username_attr), + get_optional(cred_subject_json, profile_attr), + get_optional(cred_subject_json, picture_attr), + get_optional(cred_subject_json, website_attr), + get_optional(cred_subject_json, email_attr), + get_optional(cred_subject_json, email_verified_attr), + get_optional(cred_subject_json, gender_attr), + get_optional(cred_subject_json, birthdate_attr), + get_optional(cred_subject_json, zoneinfo_attr), + get_optional(cred_subject_json, locale_attr), + get_optional(cred_subject_json, phone_number_attr), + get_optional(cred_subject_json, phone_number_verified_attr), + address_opt, + get_optional(cred_subject_json, updated_at_attr), + }; +} + /// /// UserInfoVC /// @@ -263,7 +354,13 @@ UserInfoVC::valid_from(const Signature::PublicKey& issuer_key) const return parsed_cred->verify(issuer_key); } -std::map +const std::string& +UserInfoVC::raw_credential() const +{ + return raw; +} + +const UserInfoClaims& UserInfoVC::subject() const { return parsed_cred->credential_subject; diff --git a/lib/hpke/test/userinfo_vc.cpp b/lib/hpke/test/userinfo_vc.cpp index cdde3322..f083f573 100644 --- a/lib/hpke/test/userinfo_vc.cpp +++ b/lib/hpke/test/userinfo_vc.cpp @@ -1,5 +1,6 @@ #include #include +#include #include namespace opt = MLS_NAMESPACE::tls::opt; @@ -127,6 +128,100 @@ TEST_CASE("UserInfoVC Parsing and Validation") CHECK(vc.key_id() == opt::get(issuer_jwk.key_id)); CHECK(vc.not_before().time_since_epoch() == known_not_before); CHECK(vc.not_after().time_since_epoch() == known_not_after); - CHECK(vc.subject() == known_subject); CHECK(vc.public_key() == known_subject_jwk); + + const auto& subject = vc.subject(); + CHECK(subject.sub.value_or("") == known_subject.at("sub")); + + CHECK(vc.subject().name.value_or("") == known_subject.at("name")); + CHECK(vc.subject().given_name.value_or("") == known_subject.at("given_name")); + CHECK(vc.subject().family_name.value_or("") == + known_subject.at("family_name")); + CHECK(vc.subject().preferred_username.value_or("") == + known_subject.at("preferred_username")); + CHECK(vc.subject().email.value_or("") == known_subject.at("email")); + CHECK(vc.subject().picture.value_or("") == known_subject.at("picture")); +} + +TEST_CASE("UserInfoClaims Field Parsing") +{ + nlohmann::json credentialSubject = { + { "test", "test" }, + { "sub", "sub" }, + { "name", "name" }, + { "given_name", "given_name" }, + { "family_name", "family_name" }, + { "middle_name", "middle_name" }, + { "nickname", "nickname" }, + { "preferred_username", "preferred_username" }, + { "profile", "profile" }, + { "picture", "picture" }, + { "website", "website" }, + { "email", "email" }, + { "email_verified", true }, + { "gender", "gender" }, + { "birthdate", "birthdate" }, + { "zoneinfo", "zoneinfo" }, + { "locale", "locale" }, + { "phone_number", "phone_number" }, + { "phone_number_verified", true }, + { "address", + { { "formatted", "formatted" }, + { "street_address", "street_address" }, + { "locality", "locality" }, + { "region", "region" }, + { "postal_code", "postal_code" }, + { "country", "country" } } }, + { "updated_at", 42 } + }; + + const auto userinfo_claims = UserInfoClaims::from_json(credentialSubject); + + CHECK(userinfo_claims.sub == credentialSubject.at("sub")); + CHECK(userinfo_claims.name == credentialSubject.at("name")); + CHECK(userinfo_claims.given_name == credentialSubject.at("given_name")); + CHECK(userinfo_claims.family_name == credentialSubject.at("family_name")); + CHECK(userinfo_claims.middle_name == credentialSubject.at("middle_name")); + CHECK(userinfo_claims.nickname == credentialSubject.at("nickname")); + CHECK(userinfo_claims.preferred_username == + credentialSubject.at("preferred_username")); + CHECK(userinfo_claims.profile == credentialSubject.at("profile")); + CHECK(userinfo_claims.picture == credentialSubject.at("picture")); + CHECK(userinfo_claims.website == credentialSubject.at("website")); + CHECK(userinfo_claims.email == credentialSubject.at("email")); + CHECK(userinfo_claims.email_verified == + credentialSubject.at("email_verified")); + CHECK(userinfo_claims.gender == credentialSubject.at("gender")); + CHECK(userinfo_claims.birthdate == credentialSubject.at("birthdate")); + CHECK(userinfo_claims.zoneinfo == credentialSubject.at("zoneinfo")); + CHECK(userinfo_claims.locale == credentialSubject.at("locale")); + CHECK(userinfo_claims.phone_number == credentialSubject.at("phone_number")); + CHECK(userinfo_claims.phone_number_verified == + credentialSubject.at("phone_number_verified")); + CHECK(userinfo_claims.updated_at == credentialSubject.at("updated_at")); + + auto address = userinfo_claims.address.value_or(UserInfoClaimsAddress()); + CHECK(address.formatted == credentialSubject.at("address").at("formatted")); + CHECK(address.street_address == + credentialSubject.at("address").at("street_address")); + CHECK(address.locality == credentialSubject.at("address").at("locality")); + CHECK(address.region == credentialSubject.at("address").at("region")); + CHECK(address.postal_code == + credentialSubject.at("address").at("postal_code")); + CHECK(address.country == credentialSubject.at("address").at("country")); +} + +TEST_CASE("UserInfoClaims Edge Cases") +{ + CHECK_THROWS_WITH( + UserInfoClaims::from_json({ { "updated_at", "42" } }), + "[json.exception.type_error.302] type must be number, but is string"); + + CHECK_THROWS_WITH( + UserInfoClaims::from_json({ { "name", true } }), + "[json.exception.type_error.302] type must be string, but is boolean"); + + CHECK_THROWS_WITH( + UserInfoClaims::from_json({ { "email_verified", "true" } }), + "[json.exception.type_error.302] type must be boolean, but is string"); }