[java-idp-plugin-vci] 01/01: Signed credential issuer metadata, common signer used for signing, kid resolving to use common signing
Codeberg
noreply at shibboleth.net
Thu Sep 3 12:13:33 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch dev/SignedConf
in repository java-idp-plugin-vci.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/93eaf02fb0e79c767069d73b947c30f322c162d4
commit 93eaf02fb0e79c767069d73b947c30f322c162d4
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Thu Sep 3 15:13:15 2026 +0300
Signed credential issuer metadata, common signer used for signing, kid resolving to use common signing
---
.../plugin/oauth/security/DidJwkSupport.java | 112 +++++++++++
.../profile/impl/SignStatusListToken.java | 73 ++-----
.../openidvci/config/OpenIDVCIConfiguration.java | 33 ++--
...efaultOpenIDVCIIssuerMetadataConfiguration.java | 59 ++++++
...ignatureSigningConfigurationLookupFunction.java | 58 ++++++
.../CredentialIssuerMetadataSuccessResponse.java | 1 +
...edCredentialIssuerMetadataSuccessResponse.java} | 24 +--
.../FormOutboundIssuerMetadataResponseMessage.java | 180 ++++++++++++++++-
.../openidvci/profile/impl/SignJWTCredential.java | 95 ++-------
.../vci/issuer-metadata/issuer-metadata-beans.xml | 25 ++-
.../vci/issuer-metadata/issuer-metadata-flow.xml | 6 +-
.../idp/service/relying-party/postconfig.xml | 23 +--
.../openidvci/conf/openid-vci-credentials.xml | 13 ++
.../plugin/openidvci/conf/openid-vci.properties | 6 +
.../shibboleth/plugin/openidvci/module.properties | 4 +-
.../plugin/oauth/security/DidJwkSupportTest.java | 93 +++++++++
...mOutboundIssuerMetadataResponseMessageTest.java | 212 +++++++++++++++++++++
17 files changed, 827 insertions(+), 190 deletions(-)
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/oauth/security/DidJwkSupport.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/oauth/security/DidJwkSupport.java
new file mode 100644
index 0000000..29de09e
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/oauth/security/DidJwkSupport.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.oauth.security;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.crypto.SecretKey;
+
+import org.opensaml.security.credential.Credential;
+
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+
+/**
+ * Support for naming the signing key of a token by the did:jwk it is issued
+ * under.
+ *
+ */
+public final class DidJwkSupport {
+
+ /** Prefix of a did:jwk identifier. */
+ @Nonnull
+ private static final String PREFIX = "did:jwk:";
+
+ /** Fragment naming the only verification method of a did:jwk document. */
+ @Nonnull
+ private static final String VERIFICATION_METHOD = "#0";
+
+ /** Constructor. */
+ private DidJwkSupport() {
+ }
+
+ /**
+ * Name the signing key of the parameters by the issuer of the token.
+ *
+ * A verifier resolves the key of a did:jwk issuer by the 'kid' header, which
+ * has to name the verification method of the document rather than the key
+ * itself. An issuer that is no did:jwk leaves the key named as it is.
+ *
+ * @param parameters parameters to name the key of
+ * @param issuer issuer the token names
+ * @return parameters naming the key, or the given ones when there is nothing to
+ * name it by
+ */
+ @Nonnull
+ public static SignatureSigningParameters nameKeyByIssuer(@Nonnull final SignatureSigningParameters parameters,
+ @Nullable final String issuer) {
+
+ final Credential credential = parameters.getSigningCredential();
+ if (issuer == null || !issuer.startsWith(PREFIX) || credential == null) {
+ return parameters;
+ }
+
+ final SignatureSigningParameters named = new SignatureSigningParameters();
+ named.setSignatureAlgorithm(parameters.getSignatureAlgorithm());
+ named.setSigningCredential(nameKey(credential, issuer + VERIFICATION_METHOD));
+ return named;
+ }
+
+ /**
+ * Copy a credential, naming its key.
+ *
+ * @param credential credential to copy
+ * @param keyName name to give the key
+ * @return the copy
+ */
+ @Nonnull
+ private static Credential nameKey(@Nonnull final Credential credential, @Nonnull final String keyName) {
+
+ final BasicJWKCredential copy = new BasicJWKCredential();
+ copy.getKeyNames().add(keyName);
+ copy.setKid(keyName);
+
+ final PublicKey publicKey = credential.getPublicKey();
+ if (publicKey != null) {
+ copy.setPublicKey(publicKey);
+ }
+ final PrivateKey privateKey = credential.getPrivateKey();
+ if (privateKey != null) {
+ copy.setPrivateKey(privateKey);
+ }
+ final SecretKey secretKey = credential.getSecretKey();
+ if (secretKey != null) {
+ copy.setSecretKey(secretKey);
+ }
+ copy.setUsageType(credential.getUsageType());
+ copy.setEntityId(credential.getEntityId());
+ if (credential instanceof final JWKCredential jwk) {
+ copy.setAlgorithm(jwk.getAlgorithm());
+ }
+ return copy;
+ }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/oauth/statuslist/profile/impl/SignStatusListToken.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/oauth/statuslist/profile/impl/SignStatusListToken.java
index ce21ffb..976afb7 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/oauth/statuslist/profile/impl/SignStatusListToken.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/oauth/statuslist/profile/impl/SignStatusListToken.java
@@ -16,46 +16,37 @@
package org.geant.shibboleth.plugin.oauth.statuslist.profile.impl;
-import java.security.interfaces.ECPrivateKey;
-import java.util.List;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.geant.shibboleth.plugin.oauth.security.DidJwkSupport;
import org.geant.shibboleth.plugin.oauth.statuslist.context.StatusListTokenContext;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
-import org.opensaml.security.credential.Credential;
import org.slf4j.Logger;
-import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.JOSEObjectType;
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.JWSHeader;
-import com.nimbusds.jose.crypto.ECDSASigner;
-import com.nimbusds.jose.util.Base64;
-import com.nimbusds.jwt.SignedJWT;
-
import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.impl.JWSTokenSigner;
+import net.shibboleth.oidc.security.jose.SignatureException;
import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.logic.FunctionSupport;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
* Action that signs claims in {@link StatusListTokenContext#getClaims} and
* stores the result to {@link StatusListTokenContext#setSignedToken}.
*
- * Same as SignJWTCredential of the credentials flow, including support only for
- * EC family only. Sets the statuslist+jwt type header required of the token.
+ * Signing is done with the parameters of the status list signing configuration,
+ * which offers EC algorithms only. Sets the statuslist+jwt type header required
+ * of the token.
*/
public class SignStatusListToken extends AbstractProfileAction {
@@ -78,10 +69,6 @@ public class SignStatusListToken extends AbstractProfileAction {
@Nonnull
private Function<ProfileRequestContext, SecurityParametersContext> securityParametersLookupStrategy;
- /** Strategy used to obtain the certificate chain for signing key. */
- @Nonnull
- private Function<SignatureSigningParameters, List<Base64>> certificateChainLookupStrategy;
-
/** Strategy used to locate the status list token context. */
@Nonnull
private Function<ProfileRequestContext, StatusListTokenContext> statusListTokenContextLookupStrategy;
@@ -90,14 +77,6 @@ public class SignStatusListToken extends AbstractProfileAction {
@NonnullAfterInit
private SignatureSigningParameters signatureSigningParameters;
- /** Signing credential. */
- @NonnullAfterInit
- private Credential signingCredential;
-
- /** EC private key for signing. */
- @NonnullAfterInit
- private ECPrivateKey privateKey;
-
/** Constructor. */
public SignStatusListToken() {
final Function<ProfileRequestContext, SecurityParametersContext> splcs = new ChildContextLookup<>(
@@ -109,8 +88,6 @@ public class SignStatusListToken extends AbstractProfileAction {
StatusListTokenContext.class).compose(new OutboundMessageContextLookup());
assert tlcs != null;
statusListTokenContextLookupStrategy = tlcs;
-
- certificateChainLookupStrategy = FunctionSupport.constant(null);
}
/**
@@ -137,18 +114,6 @@ public class SignStatusListToken extends AbstractProfileAction {
"StatusListTokenContext lookup strategy cannot be null");
}
- /**
- * Set the strategy used to obtain the certificate chain for the signing key.
- *
- * @param strategy lookup strategy
- */
- public void setCertificateChainLookupStrategy(
- @Nonnull final Function<SignatureSigningParameters, List<Base64>> strategy) {
- checkSetterPreconditions();
- certificateChainLookupStrategy = Constraint.isNotNull(strategy,
- "CertificateChainLookupStrategy lookup strategy cannot be null");
- }
-
/**
* Get the signing parameters to apply.
*
@@ -185,37 +150,23 @@ public class SignStatusListToken extends AbstractProfileAction {
log.debug("{} no signature signing parameters available", getLogPrefix());
return false;
}
- signingCredential = signatureSigningParameters.getSigningCredential();
- if (signingCredential == null) {
+ if (signatureSigningParameters.getSigningCredential() == null) {
log.debug("{} no signature signing credential available", getLogPrefix());
return false;
}
- if (signingCredential.getPrivateKey() instanceof final ECPrivateKey ecKey) {
- privateKey = ecKey;
- return true;
- }
- log.error("{} No EC private key as signing parameter", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
- return false;
+ return true;
}
/** {@inheritDoc} */
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
try {
- // For now we support only EC family.
- final SignedJWT signedJWT = new SignedJWT(
- new JWSHeader.Builder(new JWSAlgorithm(signatureSigningParameters.getSignatureAlgorithm()))
- .x509CertChain(certificateChainLookupStrategy.apply(signatureSigningParameters))
- .type(new JOSEObjectType(TOKEN_TYPE))
- .keyID(CredentialConversionUtil.resolveKid(signingCredential)).build(),
- ctx.getClaims());
-
- signedJWT.sign(new ECDSASigner(privateKey));
- ctx.setSignedToken(signedJWT.serialize());
+ ctx.setSignedToken(new JWSTokenSigner(
+ DidJwkSupport.nameKeyByIssuer(signatureSigningParameters, ctx.getClaims().getIssuer()))
+ .sign(ctx.getClaims(), TOKEN_TYPE).serialize());
log.debug("{} Signed status list token for {}", getLogPrefix(), ctx.getClaims().getSubject());
- } catch (final JOSEException e) {
+ } catch (final SignatureException e) {
log.error("{} Signing status list token failed", getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/OpenIDVCIConfiguration.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/OpenIDVCIConfiguration.java
index d77411f..4498346 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/OpenIDVCIConfiguration.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/OpenIDVCIConfiguration.java
@@ -115,20 +115,23 @@ public interface OpenIDVCIConfiguration extends ConditionalProfileConfiguration
@Positive
@Nonnull
Duration getCredentialLifetime(@Nullable final ProfileRequestContext profileRequestContext);
-
+
/**
- * Get the {@link SignatureValidationConfiguration} to be used for Credential Proof JWT signature validation.
+ * Get the {@link SignatureValidationConfiguration} to be used for Credential
+ * Proof JWT signature validation.
*
* @param profileRequestContext current profile request context
*
* @return the signature validation configuration to use
*/
- @ConfigurationSetting(name="proofSignatureValidationConfiguration")
- @Nullable SignatureValidationConfiguration getProofSignatureValidationConfiguration(
+ @ConfigurationSetting(name = "proofSignatureValidationConfiguration")
+ @Nullable
+ SignatureValidationConfiguration getProofSignatureValidationConfiguration(
@Nullable final ProfileRequestContext profileRequestContext);
-
+
/**
- * Get the {@link SignatureSigningConfiguration} used for signing issued Credentials.
+ * Get the {@link SignatureSigningConfiguration} used for signing issued
+ * Credentials.
*
* <p>
* Configuration sets signing keys and algorithms deployment allows at all.
@@ -140,19 +143,22 @@ public interface OpenIDVCIConfiguration extends ConditionalProfileConfiguration
*
* @return the signature signing configuration to use
*/
- @ConfigurationSetting(name="credentialSignatureSigningConfiguration")
- @Nullable SignatureSigningConfiguration getCredentialSignatureSigningConfiguration(
+ @ConfigurationSetting(name = "credentialSignatureSigningConfiguration")
+ @Nullable
+ SignatureSigningConfiguration getCredentialSignatureSigningConfiguration(
@Nullable final ProfileRequestContext profileRequestContext);
/**
- * Get the {@link ClaimsValidator} to apply to Proof JWTs being validated by this profile.
+ * Get the {@link ClaimsValidator} to apply to Proof JWTs being validated by
+ * this profile.
*
* @param profileRequestContext current profile request context
*
* @return the validator to use
*/
- @ConfigurationSetting(name="proofClaimsValidator")
- @Nullable ClaimsValidator getProofClaimsValidator(@Nullable final ProfileRequestContext profileRequestContext);
+ @ConfigurationSetting(name = "proofClaimsValidator")
+ @Nullable
+ ClaimsValidator getProofClaimsValidator(@Nullable final ProfileRequestContext profileRequestContext);
/**
* Get the {@link Function} to create nonces to be used with Proof JWTs.
@@ -161,8 +167,9 @@ public interface OpenIDVCIConfiguration extends ConditionalProfileConfiguration
*
* @return the nonce generator to use
*/
- @ConfigurationSetting(name="proofNonceGenerator")
- @Nullable Function<ProfileRequestContext, String> getProofNonceGenerator(
+ @ConfigurationSetting(name = "proofNonceGenerator")
+ @Nullable
+ Function<ProfileRequestContext, String> getProofNonceGenerator(
@Nullable final ProfileRequestContext profileRequestContext);
}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/impl/DefaultOpenIDVCIIssuerMetadataConfiguration.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/impl/DefaultOpenIDVCIIssuerMetadataConfiguration.java
index e429639..7026d7f 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/impl/DefaultOpenIDVCIIssuerMetadataConfiguration.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/impl/DefaultOpenIDVCIIssuerMetadataConfiguration.java
@@ -16,10 +16,19 @@
package org.geant.shibboleth.plugin.openidvci.config.impl;
+import java.util.function.Function;
+
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
import net.shibboleth.idp.profile.config.AbstractInterceptorAwareProfileConfiguration;
+import net.shibboleth.oidc.security.jose.SignatureSigningConfiguration;
+import net.shibboleth.shared.annotation.ConfigurationSetting;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
/**
* Profile configuration for the endpoint publishing Credential Issuer metadata,
@@ -41,9 +50,59 @@ public class DefaultOpenIDVCIIssuerMetadataConfiguration extends AbstractInterce
@NotEmpty
public static final String PROFILE_ID = "http://geant.org/ns/profiles/openid/vci/issuer-metadata";
+ /** Signing of the published metadata. */
+ @Nonnull
+ private Function<ProfileRequestContext, SignatureSigningConfiguration>
+ issuerMetadataSignatureSigningConfigurationLookupStrategy;
+
/** Constructor. */
public DefaultOpenIDVCIIssuerMetadataConfiguration() {
super(PROFILE_ID);
+ issuerMetadataSignatureSigningConfigurationLookupStrategy = FunctionSupport.constant(null);
+ }
+
+ /**
+ * Get the {@link SignatureSigningConfiguration} used for signing the published
+ * metadata.
+ *
+ * <p>
+ * Signed metadata is published only when this offers a signing credential.
+ * </p>
+ *
+ * @param profileRequestContext current profile request context
+ *
+ * @return the signature signing configuration to use
+ */
+ @ConfigurationSetting(name="issuerMetadataSignatureSigningConfiguration")
+ @Nullable
+ public SignatureSigningConfiguration getIssuerMetadataSignatureSigningConfiguration(
+ @Nullable final ProfileRequestContext profileRequestContext) {
+ return issuerMetadataSignatureSigningConfigurationLookupStrategy.apply(profileRequestContext);
+ }
+
+ /**
+ * Set the {@link SignatureSigningConfiguration} to sign the published metadata
+ * with.
+ *
+ * @param configuration configuration to use
+ *
+ */
+ public void setIssuerMetadataSignatureSigningConfiguration(
+ @Nullable final SignatureSigningConfiguration configuration) {
+ issuerMetadataSignatureSigningConfigurationLookupStrategy = FunctionSupport.constant(configuration);
+ }
+
+ /**
+ * Set a lookup strategy for the {@link SignatureSigningConfiguration} to sign
+ * the published metadata with.
+ *
+ * @param strategy lookup strategy
+ *
+ */
+ public void setIssuerMetadataSignatureSigningConfigurationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, SignatureSigningConfiguration> strategy) {
+ issuerMetadataSignatureSigningConfigurationLookupStrategy = Constraint.isNotNull(strategy,
+ "Lookup strategy cannot be null");
}
}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/navigate/IssuerMetadataSignatureSigningConfigurationLookupFunction.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/navigate/IssuerMetadataSignatureSigningConfigurationLookupFunction.java
new file mode 100644
index 0000000..6502feb
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/config/navigate/IssuerMetadataSignatureSigningConfigurationLookupFunction.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.geant.shibboleth.plugin.openidvci.config.navigate;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.geant.shibboleth.plugin.openidvci.config.impl.DefaultOpenIDVCIIssuerMetadataConfiguration;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.security.jose.SignatureSigningConfiguration;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * A function that obtains
+ * {@link DefaultOpenIDVCIIssuerMetadataConfiguration#getIssuerMetadataSignatureSigningConfiguration}
+ * if such a profile is available from a {@link RelyingPartyContext} obtained
+ * via a lookup function, by default a child of the
+ * {@link ProfileRequestContext}.
+ */
+public class IssuerMetadataSignatureSigningConfigurationLookupFunction
+ extends AbstractRelyingPartyLookupFunction<List<SignatureSigningConfiguration>> {
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull
+ public List<SignatureSigningConfiguration> apply(@Nullable final ProfileRequestContext input) {
+ final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+ if (rpc != null) {
+ final ProfileConfiguration pc = rpc.getProfileConfig();
+ if (pc instanceof DefaultOpenIDVCIIssuerMetadataConfiguration impc) {
+ final SignatureSigningConfiguration config = impc.getIssuerMetadataSignatureSigningConfiguration(input);
+ if (config != null) {
+ return CollectionSupport.listOf(config);
+ }
+ }
+ }
+ return CollectionSupport.emptyList();
+ }
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java
index 733320a..e60c6f8 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java
@@ -60,6 +60,7 @@ public class CredentialIssuerMetadataSuccessResponse implements SuccessResponse
public HTTPResponse toHTTPResponse() {
final HTTPResponse httpResponse = new HTTPResponse(HTTPResponse.SC_OK);
httpResponse.setEntityContentType(ContentType.APPLICATION_JSON);
+ httpResponse.setHeader("Vary", "Accept");
httpResponse.setContent(content);
return httpResponse;
}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/SignedCredentialIssuerMetadataSuccessResponse.java
similarity index 68%
copy from openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java
copy to openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/SignedCredentialIssuerMetadataSuccessResponse.java
index 733320a..cb36807 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/SignedCredentialIssuerMetadataSuccessResponse.java
@@ -16,22 +16,20 @@
package org.geant.shibboleth.plugin.openidvci.messaging.impl;
-import java.util.Map;
-
import javax.annotation.Nonnull;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.SuccessResponse;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
/**
- * Response carrying the Credential Issuer metadata, as in <a href=
- * "https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata">
+ * Response carrying the Credential Issuer metadata as signed metadata, as in
+ * <a href=
+ * "https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-signed-metadata">
* OpenID4VCI</a>.
*/
-public class CredentialIssuerMetadataSuccessResponse implements SuccessResponse {
+public class SignedCredentialIssuerMetadataSuccessResponse implements SuccessResponse {
/** Serialized metadata document. */
@Nonnull
@@ -40,13 +38,10 @@ public class CredentialIssuerMetadataSuccessResponse implements SuccessResponse
/**
* Constructor.
*
- * @param metadata members of the metadata document
- *
- * @throws JsonProcessingException if the members cannot be serialized
+ * @param metadata the signed metadata document
*/
- public CredentialIssuerMetadataSuccessResponse(@Nonnull final Map<String, Object> metadata)
- throws JsonProcessingException {
- content = new ObjectMapper().writeValueAsString(metadata);
+ public SignedCredentialIssuerMetadataSuccessResponse(@Nonnull final SignedJWT metadata) {
+ content = metadata.serialize();
}
/** {@inheritDoc} */
@@ -59,7 +54,8 @@ public class CredentialIssuerMetadataSuccessResponse implements SuccessResponse
@Override
public HTTPResponse toHTTPResponse() {
final HTTPResponse httpResponse = new HTTPResponse(HTTPResponse.SC_OK);
- httpResponse.setEntityContentType(ContentType.APPLICATION_JSON);
+ httpResponse.setEntityContentType(ContentType.APPLICATION_JWT);
+ httpResponse.setHeader("Vary", "Accept");
httpResponse.setContent(content);
return httpResponse;
}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundIssuerMetadataResponseMessage.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundIssuerMetadataResponseMessage.java
index 9d2e788..a6c9c3a 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundIssuerMetadataResponseMessage.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundIssuerMetadataResponseMessage.java
@@ -16,19 +16,35 @@
package org.geant.shibboleth.plugin.openidvci.profile.impl;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.function.Function;
+
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialIssuerMetadataSuccessResponse;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.SignedCredentialIssuerMetadataSuccessResponse;
import org.geant.shibboleth.plugin.openidvci.metadata.CredentialIssuerMetadata;
import org.geant.shibboleth.plugin.openidvci.metadata.resolver.CredentialIssuerMetadataResolver;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
import org.slf4j.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import jakarta.servlet.http.HttpServletRequest;
import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.security.impl.JWSTokenSigner;
+import net.shibboleth.oidc.security.jose.SignatureException;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -37,12 +53,26 @@ import net.shibboleth.shared.primitive.LoggerFactory;
import net.shibboleth.shared.resolver.ResolverException;
/**
- * Action forming {@link CredentialIssuerMetadataSuccessResponse} from the
- * document the attached {@link CredentialIssuerMetadataResolver} resolves.
+ * Action forming {@link CredentialIssuerMetadataSuccessResponse}, or
+ * {@link SignedCredentialIssuerMetadataSuccessResponse} when the request names
+ * signed metadata and signing is configured, from the document the attached
+ * {@link CredentialIssuerMetadataResolver} resolves.
*
*/
public class FormOutboundIssuerMetadataResponseMessage extends AbstractProfileAction {
+ /** Media type of signed metadata. */
+ @Nonnull
+ private static final String SIGNED_MEDIA_TYPE = "application/jwt";
+
+ /** Media type of unsigned metadata. */
+ @Nonnull
+ private static final String UNSIGNED_MEDIA_TYPE = "application/json";
+
+ /** Type of the signed metadata document. */
+ @Nonnull
+ private static final String SIGNED_DOCUMENT_TYPE = "openidvci-issuer-metadata+jwt";
+
/** Class logger. */
@Nonnull
private final Logger log = LoggerFactory.getLogger(FormOutboundIssuerMetadataResponseMessage.class);
@@ -51,10 +81,28 @@ public class FormOutboundIssuerMetadataResponseMessage extends AbstractProfileAc
@NonnullAfterInit
private CredentialIssuerMetadataResolver metadataResolver;
+ /**
+ * Strategy used to locate the {@link SecurityParametersContext} to sign with.
+ */
+ @Nonnull
+ private Function<ProfileRequestContext, SecurityParametersContext> securityParametersLookupStrategy;
+
+ /** Lifetime of signed metadata. */
+ @Nullable
+ private Duration signedMetadataLifetime;
+
/** Metadata to publish. */
@NonnullBeforeExec
private CredentialIssuerMetadata metadata;
+ /** Constructor. */
+ public FormOutboundIssuerMetadataResponseMessage() {
+ final Function<ProfileRequestContext, SecurityParametersContext> splcs = new ChildContextLookup<>(
+ SecurityParametersContext.class).compose(new OutboundMessageContextLookup());
+ assert splcs != null;
+ securityParametersLookupStrategy = splcs;
+ }
+
/**
* Set the resolver of the metadata that is being published.
*
@@ -65,6 +113,29 @@ public class FormOutboundIssuerMetadataResponseMessage extends AbstractProfileAc
metadataResolver = Constraint.isNotNull(resolver, "The metadata resolver cannot be null");
}
+ /**
+ * Set the strategy used to locate the {@link SecurityParametersContext} to sign
+ * with.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setSecurityParametersLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, SecurityParametersContext> strategy) {
+ checkSetterPreconditions();
+ securityParametersLookupStrategy = Constraint.isNotNull(strategy,
+ "SecurityParametersContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the lifetime of signed metadata.
+ *
+ * @param lifetime lifetime to use, or null to publish it without one
+ */
+ public void setSignedMetadataLifetime(@Nullable final Duration lifetime) {
+ checkSetterPreconditions();
+ signedMetadataLifetime = lifetime;
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -107,6 +178,16 @@ public class FormOutboundIssuerMetadataResponseMessage extends AbstractProfileAc
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (prefersSignedMetadata()) {
+ final SignatureSigningParameters parameters = resolveSigningParameters(profileRequestContext);
+ if (parameters != null) {
+ publishSigned(profileRequestContext, parameters);
+ return;
+ }
+ log.debug("{} Signed metadata was asked for but no signing credential is configured, "
+ + "publishing the document unsigned", getLogPrefix());
+ }
+
try {
profileRequestContext.ensureOutboundMessageContext()
.setMessage(new CredentialIssuerMetadataSuccessResponse(metadata.getMembers()));
@@ -120,6 +201,101 @@ public class FormOutboundIssuerMetadataResponseMessage extends AbstractProfileAc
metadata.getCredentialIssuer(), metadata.getMembers().keySet());
}
+ /**
+ * Publish the document as signed metadata.
+ *
+ * @param profileRequestContext current profile request context
+ * @param parameters parameters to sign with
+ */
+ private void publishSigned(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final SignatureSigningParameters parameters) {
+
+ final Instant now = Instant.now();
+ final JWTClaimsSet.Builder claims = new JWTClaimsSet.Builder();
+ metadata.getMembers().forEach(claims::claim);
+ claims.subject(metadata.getCredentialIssuer().getValue());
+ claims.issueTime(Date.from(now));
+ final Duration lifetime = signedMetadataLifetime;
+ if (lifetime != null) {
+ claims.expirationTime(Date.from(now.plus(lifetime)));
+ }
+
+ final SignedJWT document;
+ try {
+ document = new JWSTokenSigner(parameters).sign(claims.build(), SIGNED_DOCUMENT_TYPE);
+ } catch (final SignatureException e) {
+ log.error("{} Could not sign Credential Issuer metadata", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
+ return;
+ }
+
+ profileRequestContext.ensureOutboundMessageContext()
+ .setMessage(new SignedCredentialIssuerMetadataSuccessResponse(document));
+
+ log.debug("{} Publishing signed Credential Issuer metadata of {}, members {}", getLogPrefix(),
+ metadata.getCredentialIssuer(), metadata.getMembers().keySet());
+ }
+
+ /**
+ * Get whether the request names signed metadata and not the unsigned document.
+ *
+ * Only an exact media type names a representation, a wildcard names neither.
+ *
+ * @return whether to publish signed metadata
+ */
+ private boolean prefersSignedMetadata() {
+
+ final HttpServletRequest request = getHttpServletRequest();
+ if (request == null) {
+ return false;
+ }
+
+ final String accept = request.getHeader("Accept");
+ if (accept == null || accept.isBlank()) {
+ return false;
+ }
+
+ return names(accept, SIGNED_MEDIA_TYPE) && !names(accept, UNSIGNED_MEDIA_TYPE);
+ }
+
+ /**
+ * Get whether an Accept header names a media type.
+ *
+ * @param accept header to read
+ * @param mediaType media type to look for
+ * @return whether the header names the media type
+ */
+ private boolean names(@Nonnull final String accept, @Nonnull final String mediaType) {
+
+ for (final String entry : accept.split(",")) {
+ if (mediaType.equalsIgnoreCase(entry.split(";")[0].trim())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Resolve the parameters to sign the document with.
+ *
+ * @param profileRequestContext current profile request context
+ * @return the parameters, or null when signing is not configured
+ */
+ @Nullable
+ private SignatureSigningParameters resolveSigningParameters(
+ @Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final SecurityParametersContext context = securityParametersLookupStrategy.apply(profileRequestContext);
+ if (context == null) {
+ return null;
+ }
+ final SignatureSigningParameters parameters = context.getSignatureSigningParameters();
+ if (parameters == null || parameters.getSigningCredential() == null) {
+ return null;
+ }
+ return parameters;
+ }
+
/**
* Check the document has a member, building an event when it does not.
*
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java
index cf8bae7..76107a6 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java
@@ -23,8 +23,7 @@ import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
-import java.security.interfaces.ECPrivateKey;
-
+import org.geant.shibboleth.plugin.oauth.security.DidJwkSupport;
import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCICredentialsRequest;
import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
@@ -34,33 +33,24 @@ import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
-import org.opensaml.security.credential.Credential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.impl.JWSTokenSigner;
+import net.shibboleth.oidc.security.jose.SignatureException;
import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.logic.FunctionSupport;
-import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.JOSEObjectType;
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.JWSHeader;
-import com.nimbusds.jose.crypto.ECDSASigner;
-import com.nimbusds.jose.util.Base64;
-import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jwt.JWTClaimsSet;
/**
* Action that signs all jwt verifiable credentials
* {@link CredentialsContext#getJWTCredentials} storing the result to
* {@link CredentialsContext#setSignedCredential}
- *
- * TODO: Use generic JWSTokenSigner and modify it to support the case.
*/
public class SignJWTCredential extends AbstractOIDCResponseAction {
@@ -86,25 +76,12 @@ public class SignJWTCredential extends AbstractOIDCResponseAction {
@NonnullAfterInit
private SignatureSigningParameters signatureSigningParameters;
- /** Signing credential. */
- @NonnullAfterInit
- private Credential signingCredential;
-
- /** EC private key for signing. */
- @NonnullAfterInit
- private ECPrivateKey privateKey;
-
- /** Strategy used to obtain the certificate chain for signing key. */
- @Nonnull
- private Function<SignatureSigningParameters, List<Base64>> certificateChainLookupStrategy;
-
public SignJWTCredential() {
final Function<ProfileRequestContext, SecurityParametersContext> splcs = new ChildContextLookup<>(
SecurityParametersContext.class).compose(new OutboundMessageContextLookup());
assert splcs != null;
securityParametersLookupStrategy = splcs;
issuerLookupStrategy = (Function<ProfileRequestContext, String>) new IssuerLookupFunction();
- certificateChainLookupStrategy = FunctionSupport.constant(null);
}
/**
@@ -130,18 +107,6 @@ public class SignJWTCredential extends AbstractOIDCResponseAction {
issuerLookupStrategy = Constraint.isNotNull(strategy, "IssuerLookupStrategy lookup strategy cannot be null");
}
- /**
- * Set the strategy used to locate the issuer value to use.
- *
- * @param strategy lookup strategy
- */
- public void setCertificateChainLookupStrategy(
- @Nonnull Function<SignatureSigningParameters, List<Base64>> strategy) {
- checkSetterPreconditions();
- certificateChainLookupStrategy = Constraint.isNotNull(strategy,
- "CertificateChainLookupStrategy lookup strategy cannot be null");
- }
-
/**
* Get the signing parameters to apply.
*
@@ -176,18 +141,11 @@ public class SignJWTCredential extends AbstractOIDCResponseAction {
log.debug("{} no signature signing parameters available", getLogPrefix());
return false;
}
- signingCredential = signatureSigningParameters.getSigningCredential();
- if (signingCredential == null) {
+ if (signatureSigningParameters.getSigningCredential() == null) {
log.debug("{} no signature signing credential available", getLogPrefix());
return false;
}
- if (signingCredential.getPrivateKey() instanceof final ECPrivateKey ecKey) {
- privateKey = ecKey;
- return true;
- }
- log.error("{} No EC private key as signing parameter", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
- return false;
+ return true;
}
/** {@inheritDoc} */
@@ -199,34 +157,21 @@ public class SignJWTCredential extends AbstractOIDCResponseAction {
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
return;
}
- try {
- //
- // For now we support only EC family
- final ECDSASigner signer = new ECDSASigner(privateKey);
- final List<String> credentials = new ArrayList<>();
- ctx.getJWTCredentials().forEach(credential -> {
- SignedJWT signedJWT = new SignedJWT(
- new JWSHeader.Builder(new JWSAlgorithm(signatureSigningParameters.getSignatureAlgorithm()))
- .x509CertChain(certificateChainLookupStrategy.apply(signatureSigningParameters))
- .type(new JOSEObjectType(ctx.getCredentialConfiguration().getFormat()))
- .keyID(CredentialConversionUtil.resolveKid(signingCredential)).build(),
- credential);
-
- try {
- signedJWT.sign(signer);
- } catch (JOSEException e) {
- log.error("{} Signing credential failed", getLogPrefix(), e);
- ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_CREDENTIAL);
- return;
- }
- credentials.add(signedJWT.serialize());
- });
- ctx.setSignedCredential(credentials);
- log.info("Signed {} credentials", credentials.size());
- } catch (JOSEException e) {
- log.error("{} Error occurred while parsing objects {}", getLogPrefix(), e);
- ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ final String format = ctx.getCredentialConfiguration().getFormat();
+ final List<String> credentials = new ArrayList<>();
+ for (final JWTClaimsSet credential : ctx.getJWTCredentials()) {
+ try {
+ credentials.add(new JWSTokenSigner(
+ DidJwkSupport.nameKeyByIssuer(signatureSigningParameters, credential.getIssuer()))
+ .sign(credential, format).serialize());
+ } catch (final SignatureException e) {
+ log.error("{} Signing credential failed", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_CREDENTIAL);
+ return;
+ }
}
+ ctx.setSignedCredential(credentials);
+ log.info("Signed {} credentials", credentials.size());
}
}
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/issuer-metadata/issuer-metadata-beans.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/issuer-metadata/issuer-metadata-beans.xml
index 3d5fc83..3d95bc9 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/issuer-metadata/issuer-metadata-beans.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/issuer-metadata/issuer-metadata-beans.xml
@@ -17,10 +17,31 @@
<bean id="InitializeUnverifiedRelyingPartyContext"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeUnverifiedRelyingPartyContext" scope="prototype" />
+ <bean id="PopulateIssuerMetadataSignatureSigningParameters"
+ class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters"
+ scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Outbound">
+ <property name="configurationLookupStrategy">
+ <bean lazy-init="true"
+ class="org.geant.shibboleth.plugin.openidvci.config.navigate.IssuerMetadataSignatureSigningConfigurationLookupFunction" />
+ </property>
+ <!-- Metadata is signed with the issuer's own key, there is no client to have one. -->
+ <property name="signatureSigningParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.BasicSignatureSigningParametersResolver" />
+ </property>
+ <property name="securityParametersContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
+ c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+ </property>
+ </bean>
+
<bean id="FormOutboundMessage"
class="org.geant.shibboleth.plugin.openidvci.profile.impl.FormOutboundIssuerMetadataResponseMessage"
scope="prototype"
- p:metadataResolver-ref="#{'%{openidvci.issuerMetadata.resolver:openidvci.DefaultCredentialIssuerMetadataResolver}'.trim()}" />
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:metadataResolver-ref="#{'%{openidvci.issuerMetadata.resolver:openidvci.DefaultCredentialIssuerMetadataResolver}'.trim()}"
+ p:signedMetadataLifetime="#{'%{openidvci.issuerMetadata.signedLifetime:}'.trim().isEmpty() ? null : T(java.time.Duration).parse('%{openidvci.issuerMetadata.signedLifetime:}'.trim())}" />
<bean id="BuildErrorResponseFromEvent" class="net.shibboleth.oidc.profile.impl.BuildJSONErrorResponseFromEvent"
scope="prototype" p:defaultStatusCode="500" p:defaultCode="server_error"
@@ -32,6 +53,8 @@
<map value-type="com.nimbusds.oauth2.sdk.ErrorObject">
<entry key="#{T(org.opensaml.profile.action.EventIds).IO_ERROR}"
value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+ <entry key="#{T(org.opensaml.profile.action.EventIds).INVALID_SEC_CFG}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
</map>
</property>
</bean>
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/issuer-metadata/issuer-metadata-flow.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/issuer-metadata/issuer-metadata-flow.xml
index 850637d..3840e91 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/issuer-metadata/issuer-metadata-flow.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/issuer-metadata/issuer-metadata-flow.xml
@@ -4,11 +4,6 @@
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
parent="openid/vci/abstract-api">
- <!--
- Publishes Credential Issuer metadata. There is no inbound message to decode and no
- client to authenticate, a wallet reads this before it knows anything about us. So the
- relying party is the unverified one, same as with the status list endpoint.
- -->
<action-state id="InitializeMandatoryContexts">
<evaluate expression="InitializeProfileRequestContext" />
<evaluate expression="PopulateMetricContext" />
@@ -17,6 +12,7 @@
<evaluate expression="InitializeUnverifiedRelyingPartyContext" />
<evaluate expression="SelectRelyingPartyConfiguration" />
<evaluate expression="SelectProfileConfiguration" />
+ <evaluate expression="PopulateIssuerMetadataSignatureSigningParameters" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="BuildResponseMessage" />
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 38b07a9..7d055b4 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -54,7 +54,8 @@
-->
<bean id="OpenID.VCI.IssuerMetadata" lazy-init="true"
class="org.geant.shibboleth.plugin.openidvci.config.impl.DefaultOpenIDVCIIssuerMetadataConfiguration"
- p:securityConfiguration-ref="%{idp.security.oidc.config:shibboleth.oidc.DefaultSecurityConfiguration}" />
+ p:securityConfiguration-ref="%{idp.security.oidc.config:shibboleth.oidc.DefaultSecurityConfiguration}"
+ p:issuerMetadataSignatureSigningConfiguration-ref="openidvci.issuerMetadata.SigningConfiguration" />
<bean id="OAuth.StatusList" lazy-init="true"
class="org.geant.shibboleth.plugin.oauth.statuslist.config.DefaultStatusListConfiguration"
@@ -106,14 +107,6 @@
</property>
</bean>
- <!--
- Signing configuration of issued Credentials. Keys are OP's response signing credentials,
- unless deployer defines list bean "openidvci.SigningCredentials". Then only those are
- used. Own key must be published also, see conf/openid-vci-credentials.xml.
-
- Only EC algorithms are offered, signing actions support nothing else. Used algorithm is
- narrowed still per Credential Configuration by credential_signing_alg_values_supported.
- -->
<bean id="openidvci.SigningConfiguration"
parent="shibboleth.oidc.BasicSignatureSigningConfiguration"
p:signingCredentials="#{getObject('openidvci.SigningCredentials') ?: getObject('shibboleth.oidc.SigningCredentialsFactory')}">
@@ -129,18 +122,12 @@
</property>
</bean>
- <!--
- Signing configuration of Status List Tokens. Kept separate from Credential one, status
- list is general purpose mechanism and does not serve only Verifiable Credentials. Keys
- default still to same ones.
-
- Status List Token names same issuer as Credentials referring to it. While issuer is
- did:jwk, issuer IS the signing key. Own key here means Credential refers to status list
- of some other issuer. Separate the keys only when openidvci.issuer names real issuer.
- -->
<bean id="oauth.statuslist.SigningConfiguration" parent="openidvci.SigningConfiguration"
p:signingCredentials="#{getObject('oauth.statuslist.SigningCredentials') ?: getObject('openidvci.SigningCredentials') ?: getObject('shibboleth.oidc.SigningCredentialsFactory')}" />
+ <bean id="openidvci.issuerMetadata.SigningConfiguration" parent="openidvci.SigningConfiguration"
+ p:signingCredentials="#{getObject('openidvci.issuerMetadata.SigningCredentials')}" />
+
<bean id="openidvci.ProofSignatureValidationConfiguration"
parent="shibboleth.oidc.BasicSignatureValidationConfiguration"
p:signatureTrustEngine-ref="openidvci.TokenKeyTrustEngineForProofJWT"/>
diff --git a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci-credentials.xml b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci-credentials.xml
index 5377ffa..27dc5cd 100644
--- a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci-credentials.xml
+++ b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci-credentials.xml
@@ -42,4 +42,17 @@
</util:list>
-->
+ <!--
+ Signing key of Credential Issuer metadata. Must be EC key, signing action supports
+ nothing else.
+ -->
+ <!--
+ <bean id="openidvci.issuerMetadata.DefaultESSigningCredential" parent="shibboleth.JWKCredential"
+ p:resource="%{idp.signing.openidvci.issuerMetadata.es.key}" />
+
+ <util:list id="openidvci.issuerMetadata.SigningCredentials">
+ <ref bean="openidvci.issuerMetadata.DefaultESSigningCredential" />
+ </util:list>
+ -->
+
</beans>
diff --git a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci.properties b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci.properties
index 6b93c88..baf5c2b 100644
--- a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci.properties
+++ b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci.properties
@@ -13,6 +13,10 @@ idp.signing.openidvci.es.key = %{idp.home}/credentials/openid-vci-signing-es.jwk
# in conf/openid-vci-credentials.xml. Must be EC key.
idp.signing.oauth.statuslist.es.key = %{idp.home}/credentials/oauth-statuslist-signing-es.jwk
+# Signing key of Credential Issuer metadata. Used only if you enable the credential of the
+# same name in conf/openid-vci-credentials.xml. Must be EC key.
+idp.signing.openidvci.issuerMetadata.es.key = %{idp.home}/credentials/openid-vci-issuer-metadata-signing-es.jwk
+
# Credential Configurations. Published in Credential Issuer metadata and used as the
# instruction on how a credential is formed. Every flow reads them from here, through one
# resolver, so the file is read when it changes and not once per request.
@@ -30,6 +34,8 @@ idp.signing.oauth.statuslist.es.key = %{idp.home}/credentials/oauth-statuslist-s
#openidvci.issuerMetadata.resolver.values = openidvci.issuerMetadata.DefaultDynamicValueResolvers
#openidvci.issuerMetadata.minRefreshDelay = PT5M
#openidvci.issuerMetadata.maxRefreshDelay = PT4H
+# Lifetime written as 'exp' of signed metadata. Leave unset to publish it without one.
+#openidvci.issuerMetadata.signedLifetime = PT4H
# Settings for stating this deployment as a Credential Issuer in OpenID Federation. Active
# only when conf/openid-vci-oidfed.xml is imported into conf/global.xml. Set to false to
diff --git a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/module.properties b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/module.properties
index 8608bf1..b616ad0 100644
--- a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/module.properties
+++ b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/module.properties
@@ -29,7 +29,9 @@ function of this plugin and the key each one can be given. \
Wire /.well-known/openid-credential-issuer to /idp/profile/openid/vci/issuer-metadata to publish \
Credential Issuer metadata. Urls in static/openid-credential-issuer.json name your host already, \
it is a Velocity template evaluated against the issuer of the OP. Credential Configurations are not \
-kept in that file, they are published from metadata/verifiable-credentials.json. \
+kept in that file, they are published from metadata/verifiable-credentials.json. Signed metadata \
+is served in addition to the unsigned document, to a wallet asking for application/jwt, only once \
+you enable the metadata signing key in conf/openid-vci-credentials.xml. \
If you run the OpenID Federation plugin, add <import resource="openid-vci-oidfed.xml" /> in \
conf/global.xml to state this deployment as a Credential Issuer in its Entity Configuration. Do \
not import it without that plugin, the IdP will not start.
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/oauth/security/DidJwkSupportTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/oauth/security/DidJwkSupportTest.java
new file mode 100644
index 0000000..70cc2f5
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/oauth/security/DidJwkSupportTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.oauth.security;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+
+import net.shibboleth.oidc.jwa.support.SignatureConstants;
+import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+
+/**
+ * Unit tests for {@link DidJwkSupport}.
+ */
+public class DidJwkSupportTest {
+
+ private static final String DID = "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IngiLCJ5IjoieSJ9";
+
+ private ECKey key;
+
+ private SignatureSigningParameters parameters;
+
+ @BeforeMethod
+ protected void setUp() throws Exception {
+ key = new ECKeyGenerator(Curve.P_256).keyID("signingvci").generate();
+
+ final BasicJWKCredential credential = new BasicJWKCredential();
+ credential.setPublicKey(key.toECPublicKey());
+ credential.setPrivateKey(key.toECPrivateKey());
+ credential.setKid(key.getKeyID());
+
+ parameters = new SignatureSigningParameters();
+ parameters.setSigningCredential(credential);
+ parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_ES_256);
+ }
+
+ @Test
+ public void testNamedByDidJwk() throws Exception {
+ final SignatureSigningParameters named = DidJwkSupport.nameKeyByIssuer(parameters, DID);
+
+ Assert.assertEquals(CredentialConversionUtil.resolveKid(named.getSigningCredential()), DID + "#0");
+ Assert.assertEquals(named.getSignatureAlgorithm(), SignatureConstants.ALGO_ID_SIGNATURE_ES_256);
+ Assert.assertEquals(named.getSigningCredential().getPublicKey(), key.toECPublicKey());
+ Assert.assertEquals(named.getSigningCredential().getPrivateKey(), key.toECPrivateKey());
+ }
+
+ /** The key of the given parameters is left as it is. */
+ @Test
+ public void testGivenParametersUntouched() throws Exception {
+ DidJwkSupport.nameKeyByIssuer(parameters, DID);
+
+ Assert.assertEquals(CredentialConversionUtil.resolveKid(parameters.getSigningCredential()), "signingvci");
+ }
+
+ @Test(dataProvider = "notDidJwk")
+ public void testIssuerIsNoDidJwk(final String issuer) throws Exception {
+ Assert.assertSame(DidJwkSupport.nameKeyByIssuer(parameters, issuer), parameters);
+ }
+
+ @DataProvider(name = "notDidJwk")
+ public Object[][] notDidJwk() {
+ return new Object[][] { { null }, { "" }, { "https://example.org" }, { "did:web:example.org" }, };
+ }
+
+ @Test
+ public void testNoSigningCredential() throws Exception {
+ final SignatureSigningParameters empty = new SignatureSigningParameters();
+
+ Assert.assertSame(DidJwkSupport.nameKeyByIssuer(empty, DID), empty);
+ }
+
+}
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundIssuerMetadataResponseMessageTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundIssuerMetadataResponseMessageTest.java
index 0557811..25eab5d 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundIssuerMetadataResponseMessageTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundIssuerMetadataResponseMessageTest.java
@@ -16,11 +16,18 @@
package org.geant.shibboleth.plugin.openidvci.profile.impl;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialIssuerMetadataSuccessResponse;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.SignedCredentialIssuerMetadataSuccessResponse;
import org.geant.shibboleth.plugin.openidvci.metadata.CredentialIssuerMetadata;
import org.geant.shibboleth.plugin.openidvci.metadata.resolver.CredentialIssuerMetadataResolver;
import org.opensaml.messaging.context.MessageContext;
@@ -28,15 +35,30 @@ import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jose.crypto.ECDSAVerifier;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import jakarta.servlet.http.HttpServletRequest;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.oidc.jwa.support.SignatureConstants;
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
+import net.shibboleth.shared.primitive.NonnullSupplier;
import net.shibboleth.shared.resolver.ResolverException;
+import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.webflow.execution.RequestContext;
/**
@@ -48,11 +70,17 @@ public class FormOutboundIssuerMetadataResponseMessageTest {
private RequestContext requestCtx;
+ private MockHttpServletRequest httpRequest;
+
+ private ECKey signingKey;
+
@BeforeMethod
protected void setUp() throws Exception {
requestCtx = new RequestContextBuilder().buildRequestContext();
profileRequestCtx = new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
profileRequestCtx.setOutboundMessageContext(new MessageContext());
+ httpRequest = new MockHttpServletRequest();
+ signingKey = new ECKeyGenerator(Curve.P_256).keyID("metadata-signing").generate();
}
private Map<String, Object> members() {
@@ -67,12 +95,45 @@ public class FormOutboundIssuerMetadataResponseMessageTest {
private FormOutboundIssuerMetadataResponseMessage actionOf(final CredentialIssuerMetadata metadata)
throws Exception {
+ return actionOf(metadata, null);
+ }
+
+ private FormOutboundIssuerMetadataResponseMessage actionOf(final CredentialIssuerMetadata metadata,
+ final Duration lifetime) throws Exception {
final FormOutboundIssuerMetadataResponseMessage action = new FormOutboundIssuerMetadataResponseMessage();
action.setMetadataResolver(new StubResolver(metadata, false));
+ action.setHttpServletRequestSupplier(new NonnullSupplier<>() {
+ public HttpServletRequest get() {
+ return httpRequest;
+ }
+ });
+ action.setSignedMetadataLifetime(lifetime);
action.initialize();
return action;
}
+ private void configureSigning(final String algorithm, final PrivateKey key) throws Exception {
+ final BasicJWKCredential credential = new BasicJWKCredential();
+ credential.setPublicKey(signingKey.toPublicKey());
+ credential.setPrivateKey(key);
+ credential.setKid(signingKey.getKeyID());
+ credential.getKeyNames().add(signingKey.getKeyID());
+
+ final SignatureSigningParameters parameters = new SignatureSigningParameters();
+ parameters.setSigningCredential(credential);
+ parameters.setSignatureAlgorithm(algorithm);
+
+ profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(SecurityParametersContext.class)
+ .setSignatureSigningParameters(parameters);
+ }
+
+ private SignedJWT published() throws Exception {
+ final Object message = profileRequestCtx.ensureOutboundMessageContext().getMessage();
+ Assert.assertTrue(message instanceof SignedCredentialIssuerMetadataSuccessResponse);
+ return SignedJWT.parse(
+ ((SignedCredentialIssuerMetadataSuccessResponse) message).toHTTPResponse().getContent());
+ }
+
@Test
public void testSuccess() throws Exception {
final FormOutboundIssuerMetadataResponseMessage action = actionOf(CredentialIssuerMetadata.parse(members()));
@@ -122,6 +183,157 @@ public class FormOutboundIssuerMetadataResponseMessageTest {
EventIds.IO_ERROR);
}
+ @Test
+ public void testUnsignedCarriesVaryAccept() throws Exception {
+ final FormOutboundIssuerMetadataResponseMessage action = actionOf(CredentialIssuerMetadata.parse(members()));
+
+ ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+
+ final Object message = profileRequestCtx.ensureOutboundMessageContext().getMessage();
+ Assert.assertEquals(
+ ((CredentialIssuerMetadataSuccessResponse) message).toHTTPResponse().getHeaderMap().get("Vary"),
+ List.of("Accept"));
+ }
+
+ @Test(dataProvider = "unsignedAccept")
+ public void testPublishedUnsigned(final String accept) throws Exception {
+ if (accept != null) {
+ httpRequest.addHeader("Accept", accept);
+ }
+ configureSigning(SignatureConstants.ALGO_ID_SIGNATURE_ES_256, signingKey.toPrivateKey());
+
+ ActionTestingSupport.assertProceedEvent(
+ actionOf(CredentialIssuerMetadata.parse(members())).execute(requestCtx));
+
+ Assert.assertTrue(profileRequestCtx.ensureOutboundMessageContext()
+ .getMessage() instanceof CredentialIssuerMetadataSuccessResponse);
+ }
+
+ @DataProvider(name = "unsignedAccept")
+ public Object[][] unsignedAccept() {
+ return new Object[][] {
+ {null},
+ {""},
+ {"*/*"},
+ {"application/*"},
+ {"application/json"},
+ {"application/json, application/jwt"},
+ {"text/html"},
+ };
+ }
+
+ @Test(dataProvider = "signedAccept")
+ public void testPublishedSigned(final String accept) throws Exception {
+ httpRequest.addHeader("Accept", accept);
+ configureSigning(SignatureConstants.ALGO_ID_SIGNATURE_ES_256, signingKey.toPrivateKey());
+
+ ActionTestingSupport.assertProceedEvent(
+ actionOf(CredentialIssuerMetadata.parse(members())).execute(requestCtx));
+
+ Assert.assertTrue(profileRequestCtx.ensureOutboundMessageContext()
+ .getMessage() instanceof SignedCredentialIssuerMetadataSuccessResponse);
+ }
+
+ @DataProvider(name = "signedAccept")
+ public Object[][] signedAccept() {
+ return new Object[][] {
+ {"application/jwt"},
+ {"APPLICATION/JWT"},
+ {"application/jwt, text/html"},
+ {"application/jwt;q=0.9"},
+ {" application/jwt ; charset=utf-8 "},
+ };
+ }
+
+ @Test
+ public void testSignedRequestedWithoutSigningConfigured() throws Exception {
+ httpRequest.addHeader("Accept", "application/jwt");
+
+ ActionTestingSupport.assertProceedEvent(
+ actionOf(CredentialIssuerMetadata.parse(members())).execute(requestCtx));
+
+ Assert.assertTrue(profileRequestCtx.ensureOutboundMessageContext()
+ .getMessage() instanceof CredentialIssuerMetadataSuccessResponse);
+ }
+
+ @Test
+ public void testSignedDocument() throws Exception {
+ httpRequest.addHeader("Accept", "application/jwt");
+ configureSigning(SignatureConstants.ALGO_ID_SIGNATURE_ES_256, signingKey.toPrivateKey());
+
+ final Instant before = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ ActionTestingSupport.assertProceedEvent(
+ actionOf(CredentialIssuerMetadata.parse(members())).execute(requestCtx));
+ final SignedJWT document = published();
+
+ Assert.assertEquals(document.getHeader().getType().toString(), "openidvci-issuer-metadata+jwt");
+ Assert.assertEquals(document.getHeader().getAlgorithm().getName(),
+ SignatureConstants.ALGO_ID_SIGNATURE_ES_256);
+ Assert.assertEquals(document.getHeader().getKeyID(), signingKey.getKeyID());
+ Assert.assertNull(document.getHeader().getX509CertChain());
+
+ Assert.assertTrue(document.verify(new ECDSAVerifier(signingKey.toECPublicKey())));
+
+ Assert.assertEquals(document.getJWTClaimsSet().getSubject(), "https://example.org");
+ Assert.assertFalse(document.getJWTClaimsSet().getIssueTime().before(Date.from(before)));
+ Assert.assertNull(document.getJWTClaimsSet().getExpirationTime());
+ Assert.assertNull(document.getJWTClaimsSet().getIssuer());
+
+ for (final Map.Entry<String, Object> member : members().entrySet()) {
+ Assert.assertEquals(document.getJWTClaimsSet().getClaim(member.getKey()), member.getValue(),
+ "Member " + member.getKey());
+ }
+ }
+
+ @Test
+ public void testSignedDocumentLifetime() throws Exception {
+ httpRequest.addHeader("Accept", "application/jwt");
+ configureSigning(SignatureConstants.ALGO_ID_SIGNATURE_ES_256, signingKey.toPrivateKey());
+
+ ActionTestingSupport.assertProceedEvent(
+ actionOf(CredentialIssuerMetadata.parse(members()), Duration.ofHours(4)).execute(requestCtx));
+ final SignedJWT document = published();
+
+ Assert.assertEquals(
+ document.getJWTClaimsSet().getExpirationTime().getTime()
+ - document.getJWTClaimsSet().getIssueTime().getTime(),
+ Duration.ofHours(4).toMillis());
+ }
+
+ @Test
+ public void testSignedResponseIsJwt() throws Exception {
+ httpRequest.addHeader("Accept", "application/jwt");
+ configureSigning(SignatureConstants.ALGO_ID_SIGNATURE_ES_256, signingKey.toPrivateKey());
+
+ ActionTestingSupport.assertProceedEvent(
+ actionOf(CredentialIssuerMetadata.parse(members())).execute(requestCtx));
+
+ final HTTPResponse response = ((SignedCredentialIssuerMetadataSuccessResponse) profileRequestCtx
+ .ensureOutboundMessageContext().getMessage()).toHTTPResponse();
+ Assert.assertEquals(response.getStatusCode(), 200);
+ Assert.assertEquals(response.getEntityContentType().getType(), ContentType.APPLICATION_JWT.getType());
+ Assert.assertEquals(response.getHeaderMap().get("Vary"), List.of("Accept"));
+ }
+
+ @Test
+ public void testSigningKeyIsNotEC() throws Exception {
+ httpRequest.addHeader("Accept", "application/jwt");
+ configureSigning(SignatureConstants.ALGO_ID_SIGNATURE_ES_256,
+ KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate());
+
+ ActionTestingSupport.assertEvent(actionOf(CredentialIssuerMetadata.parse(members())).execute(requestCtx),
+ EventIds.INVALID_SEC_CFG);
+ }
+
+ @Test
+ public void testSigningConfigurationOffersNoAlgorithm() throws Exception {
+ httpRequest.addHeader("Accept", "application/jwt");
+ configureSigning(null, signingKey.toPrivateKey());
+
+ ActionTestingSupport.assertEvent(actionOf(CredentialIssuerMetadata.parse(members())).execute(requestCtx),
+ EventIds.INVALID_SEC_CFG);
+ }
+
private static class StubResolver implements CredentialIssuerMetadataResolver {
private final CredentialIssuerMetadata metadata;
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list