diff --git a/config/module_oidc.php.dist b/config/module_oidc.php.dist index d35d7104..037920d7 100644 --- a/config/module_oidc.php.dist +++ b/config/module_oidc.php.dist @@ -22,6 +22,7 @@ declare(strict_types=1); */ use SimpleSAML\Module\oidc\ModuleConfig; +use SimpleSAML\OpenID\Codebooks\AtContextsEnum; use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum; use SimpleSAML\OpenID\Codebooks\CredentialTypesEnum; @@ -29,7 +30,7 @@ use SimpleSAML\OpenID\Codebooks\LanguageTagsEnum; $config = [ /** - * (optional) Issuer (OP) identifier which will be used as an issuer (iss) + * (optional) Issuer (OP) identifier that will be used as an issuer (iss) * claim in tokens. If not set, it will fall back to the current HTTP * scheme, host and port number if no standard port is used. * Description of the issuer from OIDC Core specification: "Verifiable @@ -55,16 +56,16 @@ $config = [ * example, for key-rollover scenarios. Just add those entries later in * the list, so they can be published on the OP JWKS discovery endpoint. * - * The format is array of associative arrays, where each array value + * The format is an array of associative arrays, where each array value * consists of the following properties (keys): * - ModuleConfig::KEY_ALGORITHM - \SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum case * representing the algorithm. * - ModuleConfig::KEY_PRIVATE_KEY_FILENAME - the name of the file - * containing private key inPEM format, which is available in SSP `cert` + * containing a private key in PEM format, which is available in SSP `cert` * folder. * - ModuleConfig::KEY_PUBLIC_KEY_FILENAME - the name of the file containing - * corresponding public key in PEM format, which is available in SSP `cert` - * folder. + * the corresponding public key in PEM format, which is available in + * the SSP `cert` folder. * - ModuleConfig::KEY_PRIVATE_KEY_PASSWORD - private key password, if * needed. * - ModuleConfig::KEY_KEY_ID - Optional string representing key identifier. @@ -286,7 +287,7 @@ $config = [ /** * If this OP supports ACRs, indicate which usable auth source supports * which ACRs. Order of ACRs is important, more important ones being first. - * Syntax: array (array with an auth source as a key and + * Syntax: array (array with an auth source as a key and * value being array of ACR values as strings) */ @@ -339,7 +340,7 @@ $config = [ * Source and destination will have entity IDs corresponding to the OP * issuer ID and Client ID respectively. * - ['Source']['entityid'] - contains OpenId Provider issuer ID - * - ['Destination']['entityid'] - contains Relying Party (OIDC Client) ID + * - ['Destination']['entityid'] - contains Relying Party (OIDC Client) ID. * In addition to that, the following OIDC related data will be available * in the state array: * - ['Oidc']['OpenIdProviderMetadata'] - contains information otherwise @@ -966,6 +967,101 @@ $config = [ // REQUIRED ClaimsEnum::Vct->value => 'ResearchAndScholarshipCredentialDcSdJwt', ], + + 'ResearchAndScholarshipCredentialVcSdJwt' => [ + ClaimsEnum::Format->value => CredentialFormatIdentifiersEnum::VcSdJwt->value, + ClaimsEnum::Scope->value => 'ResearchAndScholarshipCredentialVcSdJwt', + ClaimsEnum::Display->value => [ + [ + ClaimsEnum::Name->value => 'ResearchAndScholarshipCredentialVcSdJwt', + ClaimsEnum::Locale->value => 'en-US', + ClaimsEnum::Description->value => 'Research and Scholarship Credential', + ], + ], + ClaimsEnum::Claims->value => [ + [ + ClaimsEnum::Path->value => [ClaimsEnum::Credential_Subject->value, 'eduPersonPrincipalName'], + ClaimsEnum::Mandatory->value => true, + ClaimsEnum::Display->value => [ + [ + ClaimsEnum::Name->value => 'Principal Name', + ClaimsEnum::Locale->value => LanguageTagsEnum::EnUs->value, + ], + ], + ], + [ + ClaimsEnum::Path->value => [ClaimsEnum::Credential_Subject->value, 'eduPersonTargetedID'], + ClaimsEnum::Mandatory->value => false, + ClaimsEnum::Display->value => [ + [ + ClaimsEnum::Name->value => 'Targeted ID', + ClaimsEnum::Locale->value => LanguageTagsEnum::EnUs->value, + ], + ], + ], + [ + ClaimsEnum::Path->value => [ClaimsEnum::Credential_Subject->value, 'displayName'], + ClaimsEnum::Mandatory->value => false, + ClaimsEnum::Display->value => [ + [ + ClaimsEnum::Name->value => 'Display Name', + ClaimsEnum::Locale->value => LanguageTagsEnum::EnUs->value, + ], + ], + ], + [ + ClaimsEnum::Path->value => [ClaimsEnum::Credential_Subject->value, 'givenName'], + ClaimsEnum::Mandatory->value => false, + ClaimsEnum::Display->value => [ + [ + ClaimsEnum::Name->value => 'Given Name', + ClaimsEnum::Locale->value => LanguageTagsEnum::EnUs->value, + ], + ], + ], + [ + ClaimsEnum::Path->value => [ClaimsEnum::Credential_Subject->value, 'sn'], + ClaimsEnum::Display->value => [ + [ + ClaimsEnum::Name->value => 'Last Name', + ClaimsEnum::Locale->value => LanguageTagsEnum::EnUs->value, + ], + ], + ], + [ + ClaimsEnum::Path->value => [ClaimsEnum::Credential_Subject->value, 'mail'], + ClaimsEnum::Display->value => [ + [ + ClaimsEnum::Name->value => 'Email Address', + ClaimsEnum::Locale->value => LanguageTagsEnum::EnUs->value, + ], + ], + ], + [ + ClaimsEnum::Path->value => [ClaimsEnum::Credential_Subject->value, 'eduPersonScopedAffiliation'], + ClaimsEnum::Display->value => [ + [ + ClaimsEnum::Name->value => 'Scoped Affiliation', + ClaimsEnum::Locale->value => LanguageTagsEnum::EnUs->value, + ], + ], + ], + ], + + /** + * VCDM 2.0 context is REQUIRED for 'vc+sd-jwt' format. + */ + ClaimsEnum::AtContext->value => [ + AtContextsEnum::W3OrgNsCredentialsV2->value, + ], + + // REQUIRED + /** @see https://www.w3.org/TR/vc-data-model-2.0/#types */ + ClaimsEnum::Type->value => [ + CredentialTypesEnum::VerifiableCredential->value, + 'ResearchAndScholarshipCredentialVcSdJwt', + ], + ], ], /** @@ -999,6 +1095,15 @@ $config = [ ['mail' => ['mail']], ['eduPersonScopedAffiliation' => ['eduPersonScopedAffiliation']], ], + 'ResearchAndScholarshipCredentialVcSdJwt' => [ + ['eduPersonPrincipalName' => [ClaimsEnum::Credential_Subject->value, 'eduPersonPrincipalName']], + ['eduPersonTargetedID' => [ClaimsEnum::Credential_Subject->value, 'eduPersonTargetedID']], + ['displayName' => [ClaimsEnum::Credential_Subject->value, 'displayName']], + ['givenName' => [ClaimsEnum::Credential_Subject->value, 'givenName']], + ['sn' => [ClaimsEnum::Credential_Subject->value, 'sn']], + ['mail' => [ClaimsEnum::Credential_Subject->value, 'mail']], + ['eduPersonScopedAffiliation' => [ClaimsEnum::Credential_Subject->value, 'eduPersonScopedAffiliation']], + ], ], /** diff --git a/src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php b/src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php index e17e2019..b61ccb32 100644 --- a/src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php +++ b/src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php @@ -23,7 +23,6 @@ use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum; use SimpleSAML\OpenID\Codebooks\CredentialTypesEnum; use SimpleSAML\OpenID\Codebooks\HttpMethodsEnum; -use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; use SimpleSAML\OpenID\Did; use SimpleSAML\OpenID\Exceptions\OpenId4VciProofException; use SimpleSAML\OpenID\Exceptions\OpenIdException; @@ -612,6 +611,10 @@ public function credential(Request $request): Response continue; } + if ($credentialFormatId === CredentialFormatIdentifiersEnum::VcSdJwt->value) { + array_unshift($credentialClaimPath, ClaimsEnum::Credential_Subject->value); + } + /** @psalm-suppress ArgumentTypeCoercion */ $disclosure = $this->verifiableCredentials->disclosureFactory()->build( value: $attributeValue, @@ -688,7 +691,7 @@ public function credential(Request $request): Response ); } - if (in_array($credentialFormatId, self::SD_JWT_FORMAT_IDS, true)) { + if ($credentialFormatId === CredentialFormatIdentifiersEnum::DcSdJwt->value) { $sdJwtPayload = [ ClaimsEnum::Iss->value => $issuerDid, ClaimsEnum::Iat->value => $issuedAt->getTimestamp(), @@ -698,9 +701,9 @@ public function credential(Request $request): Response ClaimsEnum::Vct->value => $resolvedCredentialIdentifier, ]; - if ($proof instanceof OpenId4VciProof) { + if ($proof instanceof OpenId4VciProof && is_string($proofKeyId = $proof->getKeyId())) { $sdJwtPayload[ClaimsEnum::Cnf->value] = [ - ClaimsEnum::Kid->value => $proof->getKeyId(), + ClaimsEnum::Kid->value => $proofKeyId, ]; } @@ -712,7 +715,43 @@ public function credential(Request $request): Response ClaimsEnum::Kid->value => $issuerDid . '#0', ], disclosureBag: $disclosureBag, - jwtTypesEnum: JwtTypesEnum::VcSdJwt, + ); + } + + if ($credentialFormatId === CredentialFormatIdentifiersEnum::VcSdJwt->value) { + $sdJwtPayload = [ + ClaimsEnum::AtContext->value => [ + AtContextsEnum::W3OrgNsCredentialsV2->value, + ], + ClaimsEnum::Id->value => $vcId, + ClaimsEnum::Type->value => [ + CredentialTypesEnum::VerifiableCredential->value, + $resolvedCredentialIdentifier, + ], + ClaimsEnum::Issuer->value => $issuerDid, + ClaimsEnum::ValidFrom->value => $issuedAt->format(\DateTimeInterface::RFC3339), + ClaimsEnum::Credential_Subject->value => + $credentialSubject[ClaimsEnum::Credential_Subject->value] ?? [], + ClaimsEnum::Iss->value => $issuerDid, + ClaimsEnum::Iat->value => $issuedAt->getTimestamp(), + ClaimsEnum::Nbf->value => $issuedAt->getTimestamp(), + ClaimsEnum::Sub->value => $sub, + ClaimsEnum::Jti->value => $vcId, + ]; + + if ($proof instanceof OpenId4VciProof && is_string($proofKeyId = $proof->getKeyId())) { + $sdJwtPayload[ClaimsEnum::Cnf->value] = [ + ClaimsEnum::Kid->value => $proofKeyId, + ]; + } + + $verifiableCredential = $this->verifiableCredentials->vcSdJwtFactory()->fromData( + $signingKey, + $signatureAlgorithm, + $sdJwtPayload, + [ + ClaimsEnum::Kid->value => $issuerDid . '#0', + ], ); }