[java-oidc-common] branch main updated: JCOMOIDC-6 Move common crypto/security code from the OIDC plugin
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Nov 27 17:23:53 UTC 2020
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch main
in repository java-oidc-common.
View the commit online:
http://git.shibboleth.net/view/?p=java-oidc-common.git;a=commit;h=b2b5aeb887c6e77780a4a6d13b412c229f7b7c36
The following commit(s) were added to refs/heads/main by this push:
new b2b5aeb JCOMOIDC-6 Move common crypto/security code from the OIDC plugin
b2b5aeb is described below
commit b2b5aeb887c6e77780a4a6d13b412c229f7b7c36
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Nov 27 19:22:52 2020 +0200
JCOMOIDC-6 Move common crypto/security code from the OIDC plugin
https://issues.shibboleth.net/jira/browse/JCOMOIDC-6
Initial import of remote JSON Web Key (JWK) cache and utilities from java-idp-oidc.
---
oidc-common-crypto-api/pom.xml | 11 ++
.../net/shibboleth/oidc/jwk/RemoteJwkSetCache.java | 180 +++++++++++++++++++++
.../java/net/shibboleth/oidc/jwk/package-info.java | 19 +++
.../oidc/jwk/support/RemoteJwkUtils.java | 107 ++++++++++++
.../shibboleth/oidc/jwk/support/package-info.java | 19 +++
.../shibboleth/oidc/jwk/RemoteJwkSetCacheTest.java | 139 ++++++++++++++++
6 files changed, 475 insertions(+)
diff --git a/oidc-common-crypto-api/pom.xml b/oidc-common-crypto-api/pom.xml
index f63b6c0..f5d6779 100644
--- a/oidc-common-crypto-api/pom.xml
+++ b/oidc-common-crypto-api/pom.xml
@@ -23,10 +23,21 @@
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-storage-api</artifactId>
+ </dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
</dependency>
+
+ <!-- Test Dependencies -->
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-storage-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/RemoteJwkSetCache.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/RemoteJwkSetCache.java
new file mode 100644
index 0000000..2982a06
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/RemoteJwkSetCache.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.oidc.jwk;
+
+import java.io.IOException;
+import java.net.URI;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.http.client.HttpClient;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.storage.StorageCapabilities;
+import org.opensaml.storage.StorageCapabilitiesEx;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.jwk.support.RemoteJwkUtils;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Stores fetched remote key set values for a desired period of time.
+ */
+public class RemoteJwkSetCache extends AbstractIdentifiableInitializableComponent {
+
+ /** The context name in the {@link StorageService}. */
+ public static final String CONTEXT_NAME = "oidcRemoteJwkSetContents";
+
+ /** Logger. */
+ private final Logger log = LoggerFactory.getLogger(RemoteJwkSetCache.class);
+
+ /** Backing storage for the remote JWK set contents. */
+ private StorageService storage;
+
+ /** The {@link HttpClient} to use. */
+ @NonnullAfterInit private HttpClient httpClient;
+
+ /** HTTP client security parameters. */
+ @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+
+ /**
+ * Get the backing store for the remote JWK set contents.
+ *
+ * @return the backing store.
+ */
+ @NonnullAfterInit
+ public StorageService getStorage() {
+ return storage;
+ }
+
+ /**
+ * Set the backing store for the remote JWK set contents.
+ *
+ * @param storageService backing store to use
+ */
+ public void setStorage(@Nonnull final StorageService storageService) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ storage = Constraint.isNotNull(storageService, "StorageService cannot be null");
+ final StorageCapabilities caps = storage.getCapabilities();
+ if (caps instanceof StorageCapabilitiesEx) {
+ Constraint.isTrue(((StorageCapabilitiesEx) caps).isServerSide(), "StorageService cannot be client-side");
+ }
+ }
+
+ /**
+ * Set the {@link HttpClient} to use.
+ *
+ * @param client client to use
+ */
+ public void setHttpClient(@Nonnull final HttpClient client) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ httpClient = Constraint.isNotNull(client, "HttpClient cannot be null");
+ }
+
+ /**
+ * Set the optional client security parameters.
+ *
+ * @param params the new client security parameters
+ */
+ public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ httpClientSecurityParameters = params;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void doInitialize() throws ComponentInitializationException {
+ if (storage == null) {
+ throw new ComponentInitializationException("StorageService cannot be null");
+ }
+ if (httpClient == null) {
+ throw new ComponentInitializationException("HttpClient cannot be null");
+ }
+ }
+
+ /**
+ * Returns remote JWK set if found from the cache, otherwise fetches and stores it.
+ *
+ * @param uri value to check
+ * @param expires time for disposal of value from cache
+ *
+ * @return JWK set, null if not found from the cache and cannot be fetched.
+ */
+ public JWKSet fetch(@Nonnull final URI uri, @Nonnull final Instant expires) {
+ return fetch(CONTEXT_NAME, uri, expires);
+ }
+
+ /**
+ * Returns remote JWK set if found from the cache, otherwise fetches and stores it.
+ *
+ * @param context a context label to subdivide the cache
+ * @param uri value to check
+ * @param expires time (in milliseconds since beginning of epoch) for disposal of value from cache
+ *
+ * @return JWK set, null if not found from the cache and cannot be fetched.
+ */
+ @Nullable public JWKSet fetch(@Nonnull @NotEmpty final String context, @Nonnull final URI uri,
+ @Nonnull final Instant expires) {
+ final String key = uri.toString();
+
+ final StorageCapabilities caps = storage.getCapabilities();
+ if (context.length() > caps.getContextSize()) {
+ log.error("context {} too long for StorageService (limit {})", context, caps.getContextSize());
+ return null;
+ }
+
+ try {
+ final StorageRecord<?> entry = storage.read(context, key);
+ if (entry == null) {
+ log.debug("Value '{}' was not in the cache, fetching it", key);
+ final JWKSet remoteJwkSet = RemoteJwkUtils.fetchRemoteJwkSet("RemoteJwkSetCache", uri, httpClient,
+ httpClientSecurityParameters);
+ if (remoteJwkSet != null && remoteJwkSet.getKeys() != null && !remoteJwkSet.getKeys().isEmpty()) {
+ storage.create(context, key, remoteJwkSet.toString(), expires.toEpochMilli());
+ return remoteJwkSet;
+ } else {
+ log.warn("Could not find any remote keys from {}", key);
+ }
+ } else {
+ final JWKSet cachedSet = JWKSet.parse(entry.getValue());
+ log.debug("Cached value found and will be returned, expires at {}", entry.getExpiration());
+ return cachedSet;
+ }
+ } catch (final IOException | java.text.ParseException e) {
+ log.error("Exception reading/writing to storage service", e);
+ }
+
+ return null;
+ }
+}
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/package-info.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/package-info.java
new file mode 100644
index 0000000..0f35dc3
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.
+ */
+
+/** Classes related to JSON Web Key (JWK) handling. */
+package net.shibboleth.oidc.jwk;
\ No newline at end of file
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/RemoteJwkUtils.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/RemoteJwkUtils.java
new file mode 100644
index 0000000..6ab08cd
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/RemoteJwkUtils.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.oidc.jwk.support;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.ParseException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.util.EntityUtils;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.util.JSONObjectUtils;
+
+import net.minidev.json.JSONObject;
+
+/**
+ * Generic utility methods related to remote JWK sets.
+ */
+public final class RemoteJwkUtils {
+
+ /**
+ * Constructor.
+ */
+ private RemoteJwkUtils() {
+ // prevented
+ }
+
+ /**
+ * Fetches the JWK set from the given URI using the given client and security parameters.
+ *
+ * @param logPrefix log prefix
+ * @param uri the endpoint for the JWK set.
+ * @param httpClient HTTP client
+ * @param httpClientSecurityParameters security parameters
+ *
+ * @return The JWK set fetched from the endpoint, or null if it couldn't be fetched.
+ */
+ public static JWKSet fetchRemoteJwkSet(final String logPrefix, final URI uri, final HttpClient httpClient,
+ final HttpClientSecurityParameters httpClientSecurityParameters) {
+ final Logger log = LoggerFactory.getLogger(RemoteJwkUtils.class);
+ final HttpResponse response;
+ try {
+ final HttpUriRequest get = RequestBuilder.get().setUri(uri).build();
+ final HttpClientContext clientContext = HttpClientContext.create();
+ HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, true);
+ HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, get);
+ response = httpClient.execute(get, clientContext);
+ HttpClientSecuritySupport.checkTLSCredentialEvaluated(clientContext, get.getURI().getScheme());
+ } catch (final IOException e) {
+ log.error("{} Could not get the JWK contents from {}", logPrefix, uri, e);
+ return null;
+ }
+ if (response == null) {
+ log.error("{} Could not get the JWK contents from {}", logPrefix, uri);
+ return null;
+ }
+ final String output;
+ try {
+ output = EntityUtils.toString(response.getEntity(), "UTF-8");
+ } catch (final ParseException | IOException e) {
+ log.error("{} Could not parse the JWK contents from {}", logPrefix, uri);
+ return null;
+ } finally {
+ EntityUtils.consumeQuietly(response.getEntity());
+ }
+ log.trace("{} Fetched the following response body: {}", logPrefix, output);
+ final JWKSet jwkSet;
+ try {
+ final JSONObject json = JSONObjectUtils.parse(output);
+ // The following check is needed to avoid NPE from Nimbus if keys claim not found
+ if (JSONObjectUtils.getJSONArray(json, "keys") == null) {
+ log.error("{} Could not find 'keys' array from the JSON from {}", logPrefix, uri);
+ return null;
+ }
+ jwkSet = JWKSet.parse(json);
+ } catch (final java.text.ParseException e) {
+ log.error("{} Could not parse the contents from {}", logPrefix, uri, e);
+ return null;
+ }
+ return jwkSet;
+ }
+
+}
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/package-info.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/package-info.java
new file mode 100644
index 0000000..f8d8e44
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.
+ */
+
+/** Utility classes related to JSON Web Key (JWK) handling. */
+package net.shibboleth.oidc.jwk.support;
\ No newline at end of file
diff --git a/oidc-common-crypto-api/src/test/java/net/shibboleth/oidc/jwk/RemoteJwkSetCacheTest.java b/oidc-common-crypto-api/src/test/java/net/shibboleth/oidc/jwk/RemoteJwkSetCacheTest.java
new file mode 100644
index 0000000..e50acfd
--- /dev/null
+++ b/oidc-common-crypto-api/src/test/java/net/shibboleth/oidc/jwk/RemoteJwkSetCacheTest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.oidc.jwk;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.protocol.HttpContext;
+import org.mockito.Mockito;
+import org.opensaml.core.config.InitializationException;
+import org.opensaml.storage.StorageService;
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link RemoteJwkSetCache}.
+ */
+public class RemoteJwkSetCacheTest {
+
+ RemoteJwkSetCache jwkSetCache;
+ StorageService storageService;
+ HttpClient httpClient;
+
+
+ @BeforeMethod
+ public void setup() throws InitializationException, ComponentInitializationException {
+ jwkSetCache = new RemoteJwkSetCache();
+ storageService = buildStorageService();
+ }
+
+ protected StorageService buildStorageService() throws ComponentInitializationException {
+ MemoryStorageService storageService = new MemoryStorageService();
+ storageService.setId("mockId");
+ storageService.initialize();
+ return storageService;
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void testNoHttpClient() throws ComponentInitializationException {
+ jwkSetCache.setStorage(storageService);
+ jwkSetCache.initialize();
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void testNoStorageService() throws ComponentInitializationException {
+ jwkSetCache.setHttpClient(HttpClientBuilder.create().build());
+ jwkSetCache.initialize();
+ }
+
+ @Test
+ public void testSuccessNoSecurity() throws ClientProtocolException, IOException, ComponentInitializationException,
+ URISyntaxException, InterruptedException {
+ jwkSetCache.setStorage(storageService);
+ jwkSetCache.setHttpClient(createMockHttpClient(validJwkSet()));
+ jwkSetCache.setHttpClientSecurityParameters(null);
+ jwkSetCache.initialize();
+ final String uri = "http://example.org";
+ System.out.println(new URI(uri).toString());
+ JWKSet jwkSet = jwkSetCache.fetch(new URI(uri), Instant.now().plusSeconds(5));
+ Assert.assertNotNull(jwkSet);
+ Assert.assertNotNull(jwkSetCache.getStorage().read(RemoteJwkSetCache.CONTEXT_NAME, uri));
+ jwkSet = jwkSetCache.fetch(new URI(uri), Instant.now().plusSeconds(5));
+ Assert.assertNotNull(jwkSet);
+ Thread.sleep(5001);
+ Assert.assertNull(storageService.read(RemoteJwkSetCache.CONTEXT_NAME, uri));
+ }
+
+ @Test
+ public void testInvalidJwk() throws ClientProtocolException, IOException, ComponentInitializationException,
+ URISyntaxException {
+ jwkSetCache.setStorage(storageService);
+ jwkSetCache.setHttpClient(createMockHttpClient("not_jwk_set"));
+ jwkSetCache.initialize();
+ final JWKSet jwkSet = jwkSetCache.fetch(new URI("http://example.org"), Instant.now().plusSeconds(5));
+ Assert.assertNull(jwkSet);
+ }
+
+ protected HttpClient createMockHttpClient(String output) throws ClientProtocolException, IOException {
+ final HttpClient httpClient = Mockito.mock(HttpClient.class);
+ final HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
+ Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(output));
+ Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(),
+ (HttpContext) Mockito.any())).thenReturn(httpResponse);
+ return httpClient;
+ }
+
+ protected String validJwkSet() {
+ return "{\n" +
+ " \"keys\": [\n" +
+ " {\n" +
+ " \"kid\": \"08d3245c62f86b6362afcbbffe1d069826dd1dc1\",\n" +
+ " \"e\": \"AQAB\",\n" +
+ " \"kty\": \"RSA\",\n" +
+ " \"alg\": \"RS256\",\n" +
+ " \"n\": \"mSLCSG1hK28xrzcSfgbvRinkIRjecBlwsQggynHppHiiT6I80waivIqTJBSFYyVuRCAHXi6apSsL5FUWKd42GOhVUayIyzvuz1CqTuh5a9ACXaJjEVLUFO39QfXxWrxhpSJCTN9aMkdtoV1QJqfAd3IF9MYwfojsoEn3d5XX5TX4RxqZ9-HGbgSLsRuAzFIg9NxxfTYhbECBskhhR4RIcam-1T52FafmK2LMiuIEDPiVg6LvAqWi8gdMRd8WhiP_ZIRJTCH4C0NFKmw1PZyKadVxvwg97vwPTF8qkFdwJ_kjQAMmq77PxankluAkfWjFqbD4JepO4HH3aJvU8Sl_Ow\",\n" +
+ " \"use\": \"sig\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"alg\": \"RS256\",\n" +
+ " \"n\": \"uS9Iep_r83oLpfnMXLnB5a8IVUP7ZRreM1rxNWYnaqEQr1NfRisyIi4cYG7KbWiuLCmRQOD7ybhpdHCcN9ty5evz4irWT5hIa98Jr3a2BISTskBbPmBgUR3_TuQ_fvxeQYCCETJUcho5gXK-yeDWJwcD2iwqpVzIZHz8BBe5AYFUlJMzwgzYMe9aqoOEWVv__Gd7Z_kaz5pa0lOsWUUPNFmeW4e4rtNvosx7ItyyyghIyG2KX-0phOgbfzG6Ub6qA9upBYK9KBtjcoe1ciV-Yn_3HaS5PlugYTo1zYnng1mW7UP5A_QT_HgDqD1clcz0WIEL6usVMRay87ECEmOhrw\",\n" +
+ " \"use\": \"sig\",\n" +
+ " \"kid\": \"b15a2b8f7a6b3f6bc08bc1c56a88410e146d01fd\",\n" +
+ " \"e\": \"AQAB\",\n" +
+ " \"kty\": \"RSA\"\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list