Skip to content

Commit

Permalink
OSGi support for Java HTTP Client
Browse files Browse the repository at this point in the history
This closes #3276
  • Loading branch information
kwin committed Mar 2, 2024
1 parent 3d72570 commit d211b53
Show file tree
Hide file tree
Showing 12 changed files with 1,013 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ set ACL for anonymous
allow jcr:read on /conf restriction(rep:glob,/*/settings/redirects)
end

# user to read keystores of users as well as global truststore
create service user acs-commons-osgi-key-store-factory with path system/acs-commons
set ACL for acs-commons-osgi-key-store-factory
allow jcr:read on /home/users
allow jcr:read on /etc/truststore
end

create service user acs-commons-automatic-package-replicator-service with path system/acs-commons
create path /etc/acs-commons/automatic-package-replication(sling:OrderedFolder)
set ACL for acs-commons-automatic-package-replicator-service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ user.mapping=[ \
"com.adobe.acs.acs-aem-commons-bundle:workflowpackagemanager-service\=[acs-commons-workflowpackagemanager-service]", \
"com.adobe.acs.acs-aem-commons-bundle:redirect-manager\=[acs-commons-manage-redirects-service]", \
"com.adobe.acs.acs-aem-commons-bundle:marketo-conf\=[acs-commons-marketo-conf-service]", \
"com.adobe.acs.acs-aem-commons-bundle:package-garbage-collection\=[acs-commons-package-garbage-collection-service]" \
"com.adobe.acs.acs-aem-commons-bundle:package-garbage-collection\=[acs-commons-package-garbage-collection-service]", \
"com.adobe.acs.acs-aem-commons-bundle:key-store-factory\=[acs-commons-osgi-key-store-factory]"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* ACS AEM Commons
*
* Copyright (C) 2024 Konrad Windszus
*
* 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 com.adobe.acs.commons.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.util.function.Consumer;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Encapsulates a single Java {@link HttpClient}. Its lifetime and basic configuration is managed via OSGi (Config Admin and Declarative
* Services).
* @since 2.2.0 (Bundle Version 6.5.0)
* @see HttpClientFactory HttpClientFactory, for a similar service for the Apache Http Client
*/
public interface OsgiManagedJavaHttpClient {

/** Returns the configured HTTP client.
*
* @return the HTTP client
*/
@NotNull HttpClient getClient();

/**
* Similar to {@link #getClient()} but customizes the underlying {@link HttpClient.Builder} which is used to create the singleton HTTP
* client
*
* @param builderCustomizer a {@link Consumer} taking the {@link HttpClient.Builder} initialized with the configured basic options.
*
* @throws IllegalStateException in case {@link #getClient()} has been called already
*/
@NotNull HttpClient getClient(@Nullable Consumer<HttpClient.Builder> builderCustomizer);

/** Creates a new configured HTTP request.
*
* @param uri the URI to target
* @return the new request
*/
@NotNull HttpRequest createRequest(@NotNull URI uri);

/** Creates a new configured HTTP request.
*
* @param uri the URI to target
* @return the new request
*/
@NotNull HttpRequest createRequest(@NotNull URI uri, @Nullable Consumer<HttpRequest.Builder> builderCustomizer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* ACS AEM Commons
*
* Copyright (C) 2024 Konrad Windszus
*
* 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 com.adobe.acs.commons.http.impl;

import java.io.IOException;
import java.security.KeyStore;
import java.util.Map;

import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.net.ssl.X509TrustManager;

import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.SlingIOException;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.serviceusermapping.ServiceUserMapped;
import org.jetbrains.annotations.NotNull;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import com.adobe.granite.crypto.CryptoException;
import com.adobe.granite.crypto.CryptoSupport;
import com.adobe.granite.keystore.KeyStoreService;

@Component(service=AemKeyStoreFactory.class)
public class AemKeyStoreFactory {

private static final String SUB_SERVICE_NAME = "key-store-factory";

private static final Map<String, Object> SERVICE_USER = Map.of(ResourceResolverFactory.SUBSERVICE,
SUB_SERVICE_NAME);

/** Defer starting the service until service user mapping is available. */
@Reference(target = "(|(" + ServiceUserMapped.SUBSERVICENAME + "=" + SUB_SERVICE_NAME + ")(!("
+ ServiceUserMapped.SUBSERVICENAME + "=*)))")
private ServiceUserMapped serviceUserMapped;

private final ResourceResolverFactory resolverFactory;
private final KeyStoreService keyStoreService;
private final CryptoSupport cryptoSupport;

@Activate()
public AemKeyStoreFactory(@Reference ResourceResolverFactory resolverFactory,
@Reference KeyStoreService keyStoreService,
@Reference CryptoSupport cryptoSupport) {
this.resolverFactory = resolverFactory;
this.keyStoreService = keyStoreService;
this.cryptoSupport = cryptoSupport;
}

/** @return the global AEM trust store
* @throws LoginException
* @see <a href=
* "https://experienceleague.adobe.com/docs/experience-manager-learn/foundation/security/call-internal-apis-having-private-certificate.html?lang=en">Call
* internal APIs having private certificates</a> */
public @NotNull X509TrustManager getTrustManager() throws LoginException {
try (final var serviceResolver = getKeyStoreResourceResolver()) {
return (X509TrustManager) keyStoreService.getTrustManager(serviceResolver);
}
}

/** @return the global AEM trust store
* @throws LoginException
* @see <a href=
* "https://experienceleague.adobe.com/docs/experience-manager-learn/foundation/security/call-internal-apis-having-private-certificate.html?lang=en">Call
* internal APIs having private certificates</a> */
public @NotNull KeyStore getTrustStore() throws LoginException {
try (final var serviceResolver = getKeyStoreResourceResolver()) {
var aemTrustStore = keyStoreService.getTrustStore(serviceResolver);
return aemTrustStore;
}
}

public @NotNull KeyStore getKeyStore(@NotNull final String userId) throws LoginException {
try (final var serviceResolver = getKeyStoreResourceResolver()) {
// using the password set for the user Id's keystore to decrypt the entry
return keyStoreService.getKeyStore(serviceResolver, userId);
}
}

public @NotNull char[] getKeyStorePassword(@NotNull final String userId) throws LoginException {
try (final var serviceResolver = getKeyStoreResourceResolver()) {
User user = retrieveUser(serviceResolver, userId);
String path = getKeyStorePathForUser(user, "store.p12");
return extractStorePassword(serviceResolver, path, cryptoSupport);
}
}

private @NotNull ResourceResolver getKeyStoreResourceResolver() throws LoginException {
return this.resolverFactory.getServiceResourceResolver(SERVICE_USER);
}

// the following methods are extracted from com.adobe.granite.keystore.internal.KeyStoreServiceImpl, because there is no public method
// for retrieving the keystore's password
private static User retrieveUser(ResourceResolver resolver, String userId)
throws IllegalArgumentException, SlingIOException {
UserManager userManager = (UserManager) resolver.adaptTo(UserManager.class);
if (userManager != null) {
Authorizable authorizable;
try {
authorizable = userManager.getAuthorizable(userId);
} catch (RepositoryException var6) {
throw new SlingIOException(new IOException(var6));
}

if (authorizable != null && !authorizable.isGroup()) {
User user = (User) authorizable;
return user;
} else {
throw new IllegalArgumentException("The provided userId does not identify an existing user.");
}
} else {
throw new IllegalArgumentException("Cannot obtain a UserManager for the given resource resolver.");
}
}

private static String getKeyStorePathForUser(User user, String keyStoreFileName) throws SlingIOException {
String userHome;
try {
userHome = user.getPath();
} catch (RepositoryException var4) {
throw new SlingIOException(new IOException(var4));
}
return userHome + "/" + "keystore" + "/" + keyStoreFileName;
}

private static char[] extractStorePassword(ResourceResolver resolver, String storePath, CryptoSupport cryptoSupport)
throws SecurityException {
Resource storeResource = resolver.getResource(storePath);
if (storeResource != null) {
Node storeParentNode = (Node) storeResource.getParent().adaptTo(Node.class);

try {
Property passwordProperty = storeParentNode.getProperty("keystorePassword");
if (passwordProperty != null) {
return cryptoSupport.unprotect(passwordProperty.getString()).toCharArray();
} else {
throw new SecurityException(
"Missing 'keystorePassword' property on " + ResourceUtil.getParent(storePath));
}
} catch (RepositoryException var6) {
throw new SecurityException(var6);
} catch (CryptoException var7) {
throw new SecurityException(var7);
}
} else {
return null;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* ACS AEM Commons
*
* Copyright (C) 2024 Konrad Windszus
*
* 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 com.adobe.acs.commons.http.impl;

import java.net.Socket;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.X509KeyManager;

import org.jetbrains.annotations.NotNull;

public class KeyManagerUtils {

private KeyManagerUtils() {
// no supposed to be instantiated
}

static @NotNull X509KeyManager createSingleClientSideCertificateKeyManager(@NotNull KeyStore keyStore, @NotNull char[] password, @NotNull String clientCertAlias) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
return new FixClientAliasX509KeyManagerWrapper(clientCertAlias, createKeyManager(keyStore, password));
}

private static @NotNull X509KeyManager createKeyManager(@NotNull KeyStore keyStore, @NotNull char[] password)
throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException {
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, password);
return (X509KeyManager) Arrays.stream(kmf.getKeyManagers()).filter(X509KeyManager.class::isInstance).findFirst().orElseThrow(() -> new IllegalStateException("The KeyManagerFactory does not expose a X509KeyManager"));
}

private static final class FixClientAliasX509KeyManagerWrapper implements X509KeyManager {
private final String clientAlias;
private final X509KeyManager delegate;

FixClientAliasX509KeyManagerWrapper(String clientAlias, X509KeyManager delegate) {
this.clientAlias = clientAlias;
this.delegate = delegate;
}

@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return clientAlias;
}

@Override
public X509Certificate[] getCertificateChain(String alias) {
return delegate.getCertificateChain(alias);
}

@Override
public String[] getClientAliases(String s, Principal[] principals) {
return delegate.getClientAliases(s, principals);
}

@Override
public String[] getServerAliases(String s, Principal[] principals) {
return delegate.getServerAliases(s, principals);
}

@Override
public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
return delegate.chooseServerAlias(s, principals, socket);
}

@Override
public PrivateKey getPrivateKey(String s) {
return delegate.getPrivateKey(s);
}
}

}
Loading

0 comments on commit d211b53

Please sign in to comment.