Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -68,6 +69,7 @@
import org.carapaceproxy.core.RuntimeServerConfiguration;
import org.carapaceproxy.server.certificates.DynamicCertificateState;
import org.carapaceproxy.server.certificates.DynamicCertificatesManager;
import org.carapaceproxy.server.config.AcmeProviderConfiguration;
import org.carapaceproxy.server.config.ConfigurationChangeInProgressException;
import org.carapaceproxy.server.config.ConfigurationNotValidException;
import org.carapaceproxy.server.config.SSLCertificateConfiguration;
Expand Down Expand Up @@ -96,14 +98,23 @@ public static final class CertificatesResponse {

private final Collection<CertificateBean> certificates;
private final String localStorePath;
private final Collection<String> acmeProviders;

public CertificatesResponse(final Collection<CertificateBean> certificates, final HttpProxyServer server) {
this.certificates = certificates;
this.localStorePath = server.getCurrentConfiguration().getLocalCertificatesStorePath();
this.acmeProviders = availableProviders(server.getCurrentConfiguration());
}

}

private static Collection<String> availableProviders(final RuntimeServerConfiguration conf) {
final var providers = new ArrayList<String>();
providers.add(AcmeProviderConfiguration.DEFAULT_PROVIDER_NAME);
conf.getAcmeProviders().keySet().stream().sorted().forEach(providers::add);
return providers;
}

@Data
public static final class CertificateBean {

Expand All @@ -119,6 +130,7 @@ public static final class CertificateBean {
private String serialNumber;
private int attemptsCount;
private String message;
private String provider;

public CertificateBean(
final String id,
Expand Down Expand Up @@ -207,6 +219,7 @@ private static void fillCertificateBean(
}
if (certificate.isAcme()) {
bean.setDaysBeforeRenewal(certificate.getDaysBeforeRenewal() + "");
bean.setProvider(certificate.getProvider());
}
bean.setStatus(certificateStateToString(state));
} catch (GeneralSecurityException | IOException ex) {
Expand All @@ -232,6 +245,7 @@ public static final class CertificateForm {
private Set<String> subjectAltNames;
private String type;
private int daysBeforeRenewal = DEFAULT_DAYS_BEFORE_RENEWAL;
private String provider = AcmeProviderConfiguration.DEFAULT_PROVIDER_NAME;
}

@POST
Expand All @@ -254,15 +268,21 @@ public Response createCertificate(CertificateForm form) {
if (form.daysBeforeRenewal < 0) {
return FormValidationResponse.fieldInvalid("daysBeforeRenewal");
}
final var server = (HttpProxyServer) context.getAttribute("server");
final var provider = normalizeProvider(form.provider);
if (!isKnownProvider(provider, server)) {
return FormValidationResponse.fieldInvalid("provider");
}
if (findCertificateById(form.domain) != null) {
return FormValidationResponse.fieldConflict("domain");
}

final var cert = new CertificateData(form.domain, null, WAITING);
cert.setSubjectAltNames(form.subjectAltNames);
cert.setDaysBeforeRenewal(form.daysBeforeRenewal);
cert.setProvider(provider);
try {
((HttpProxyServer) context.getAttribute("server")).updateDynamicCertificateForDomain(cert);
server.updateDynamicCertificateForDomain(cert);
} catch (Exception e) {
return FormValidationResponse.error(e);
}
Expand Down Expand Up @@ -302,6 +322,15 @@ public Response downloadCertificateById(@PathParam("certId") final String certId
.build();
}

private static String normalizeProvider(final String provider) {
return provider == null || provider.isBlank() ? AcmeProviderConfiguration.DEFAULT_PROVIDER_NAME : provider;
}

private static boolean isKnownProvider(final String provider, final HttpProxyServer server) {
return AcmeProviderConfiguration.DEFAULT_PROVIDER_NAME.equals(provider)
|| server.getCurrentConfiguration().getAcmeProviders().containsKey(provider);
}

private CertificateBean findCertificateById(final String certId) {
HttpProxyServer server = (HttpProxyServer) context.getAttribute("server");
SSLCertificateConfiguration certificate = server.getCurrentConfiguration().getCertificates().get(certId);
Expand Down Expand Up @@ -329,8 +358,10 @@ public Response uploadCertificate(
@QueryParam("subjectaltnames") final List<String> subjectAltNames,
@QueryParam("type") @DefaultValue("manual") final String type,
@QueryParam("daysbeforerenewal") final Integer daysbeforerenewal,
@QueryParam("provider") final String provider,
final InputStream uploadedInputStream) throws Exception {

final var server = (HttpProxyServer) context.getAttribute("server");
try (InputStream input = uploadedInputStream) {
// Certificate type (manual | acme)
CertificateMode certType = stringToCertificateMode(type);
Expand All @@ -354,6 +385,14 @@ public Response uploadCertificate(
}
}

final var acmeProvider = normalizeProvider(provider);
if (provider != null && !provider.isBlank() && !CertificateMode.ACME.equals(certType)) {
return Response.status(422).entity("ERROR: param 'provider' available for type 'acme' only").build();
}
if (!isKnownProvider(acmeProvider, server)) {
return Response.status(422).entity("ERROR: unknown ACME provider '" + acmeProvider + "'").build();
}

String encodedData = "";
DynamicCertificateState state = WAITING;
if (data != null && data.length > 0) {
Expand All @@ -367,8 +406,9 @@ public Response uploadCertificate(
cert.setManual(MANUAL.equals(certType));
cert.setSubjectAltNames(Set.copyOf(subjectAltNames));
cert.setDaysBeforeRenewal(daysbeforerenewal != null ? daysbeforerenewal : DEFAULT_DAYS_BEFORE_RENEWAL);
cert.setProvider(acmeProvider);

((HttpProxyServer) context.getAttribute("server")).updateDynamicCertificateForDomain(cert);
server.updateDynamicCertificateForDomain(cert);

return Response.status(200).entity("SUCCESS: Certificate saved").build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.carapaceproxy.server.certificates.DynamicCertificateState;
import org.carapaceproxy.server.config.AcmeProviderConfiguration;
import org.shredzone.acme4j.toolbox.JSON;

/**
Expand All @@ -57,6 +58,8 @@ public class CertificateData {
@EqualsAndHashCode.Exclude
private Map<String, JSON> pendingChallengesData;
private boolean manual;
// not stored in db: re-set from the certificate configuration, like manual and daysBeforeRenewal
private String provider = AcmeProviderConfiguration.DEFAULT_PROVIDER_NAME;
private int daysBeforeRenewal;
private Date expiringDate;
private String serialNumber; // hex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,22 @@ default void close() {
default void commitConfiguration(ConfigurationStore newConfigurationStore) {
}

KeyPair loadAcmeUserKeyPair();
/**
* Load the ACME account key pair for the given provider.
*
* @param providerName the name of the ACME provider the account belongs to
* @return the key pair, or null if none was saved yet
*/
KeyPair loadAcmeUserKeyPair(String providerName);

boolean saveAcmeUserKey(KeyPair pair);
/**
* Save the ACME account key pair for the given provider, without overwriting an existing one.
*
* @param pair the key pair to save
* @param providerName the name of the ACME provider the account belongs to
* @return true if the key pair was saved, false if one already exists (e.g. saved concurrently by another peer)
*/
boolean saveAcmeUserKey(KeyPair pair, String providerName);

KeyPair loadKeyPairForDomain(String domain);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import java.util.stream.Collectors;
import org.apache.bookkeeper.stats.StatsLogger;
import org.carapaceproxy.server.certificates.DynamicCertificateState;
import org.carapaceproxy.server.config.AcmeProviderConfiguration;
import org.carapaceproxy.utils.StringUtils;
import org.shredzone.acme4j.toolbox.JSON;
import org.slf4j.Logger;
Expand Down Expand Up @@ -329,29 +330,59 @@ public void commitConfiguration(ConfigurationStore newConfigurationStore) {
}

@Override
public KeyPair loadAcmeUserKeyPair() {
public KeyPair loadAcmeUserKeyPair(String providerName) {
try {
return loadKeyPair(ACME_USER_KEY);
return loadKeyPair(acmeUserKeyName(providerName));
} catch (Exception err) {
LOG.error("Error while performing KeyPair loading for ACME user.", err);
LOG.error("Error while performing KeyPair loading for ACME user of provider {}.", providerName, err);
throw new ConfigurationStoreException(err);
}
}

@Override
public boolean saveAcmeUserKey(KeyPair pair) {
public boolean saveAcmeUserKey(KeyPair pair, String providerName) {
try {
return saveKeyPair(pair, ACME_USER_KEY, false);
return saveKeyPair(pair, acmeUserKeyName(providerName), false);
} catch (Exception err) {
LOG.error("Error while performing KeyPar saving for ACME user.", err);
LOG.error("Error while performing KeyPair saving for ACME user of provider {}.", providerName, err);
throw new ConfigurationStoreException(err);
}
}

/**
* The account key of the built-in provider keeps the legacy {@link #ACME_USER_KEY} name,
* so existing Let's Encrypt accounts survive the upgrade; other providers get a dedicated key.
* <p>
* Renaming a provider intentionally registers a fresh ACME account under the new name;
* the row of the old one stays around, unused but harmless.
*
* @param providerName the name of the ACME provider
* @return the primary key of the provider account key pair in the keypairs table
*/
private static String acmeUserKeyName(String providerName) {
return AcmeProviderConfiguration.DEFAULT_PROVIDER_NAME.equals(providerName)
? ACME_USER_KEY
: ACME_USER_KEY + "_" + providerName;
}

/**
* Whether the primary key belongs to a provider account key pair, hence off-limits for domain key pairs.
* <p>
* The whole {@code _acmeuserkey} prefix is reserved: a domain literally named like that
* would be silently skipped by the domain lookups.
* Safe assumption, as hostnames cannot start with {@code _} and the suffix is a validated provider name.
*
* @param pk a primary key of the key pairs table
* @return true if it is an {@link #acmeUserKeyName(String) account key name}
*/
private static boolean isAcmeUserKey(String pk) {
return pk.equals(ACME_USER_KEY) || pk.startsWith(ACME_USER_KEY + "_");
}

@Override
public KeyPair loadKeyPairForDomain(String domain) {
try {
if (domain.equals(ACME_USER_KEY)) {
if (isAcmeUserKey(domain)) {
return null;
}
return loadKeyPair(domain);
Expand All @@ -364,11 +395,11 @@ public KeyPair loadKeyPairForDomain(String domain) {
@Override
public boolean saveKeyPairForDomain(KeyPair pair, String domain, boolean update) {
try {
if (!domain.equals(ACME_USER_KEY)) {
if (!isAcmeUserKey(domain)) {
return saveKeyPair(pair, domain, update);
}
} catch (Exception err) {
LOG.error("Error while performing KeyPar saving for domain {}.", domain, err);
LOG.error("Error while performing KeyPair saving for domain {}.", domain, err);
throw new ConfigurationStoreException(err);
}
return false;
Expand Down Expand Up @@ -414,7 +445,7 @@ private boolean saveKeyPair(KeyPair pair, String pk, boolean update) {

@Override
public CertificateData loadCertificateForDomain(String domain) {
if (domain.equals(ACME_USER_KEY)) {
if (isAcmeUserKey(domain)) {
return null;
}
try (Connection con = datasource.getConnection()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class PropertiesConfigurationStore implements ConfigurationStore {
private final ConcurrentHashMap<String, CertificateData> certificates = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, KeyPair> domainsKeyPair = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, String> acmeChallengeTokens = new ConcurrentHashMap<>();
private KeyPair acmeUserKey;
private final ConcurrentHashMap<String, KeyPair> acmeUserKeys = new ConcurrentHashMap<>();

public PropertiesConfigurationStore(Properties properties) {
this.properties = properties;
Expand All @@ -69,17 +69,13 @@ public void forEach(String prefix, BiConsumer<String, String> consumer) {
}

@Override
public KeyPair loadAcmeUserKeyPair() {
return acmeUserKey;
public KeyPair loadAcmeUserKeyPair(String providerName) {
return acmeUserKeys.get(providerName);
}

@Override
public boolean saveAcmeUserKey(KeyPair pair) {
if (acmeUserKey == null) {
acmeUserKey = pair;
return true;
}
return false;
public boolean saveAcmeUserKey(KeyPair pair, String providerName) {
return acmeUserKeys.putIfAbsent(providerName, pair) == null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
import org.carapaceproxy.server.cache.ContentsCache;
import org.carapaceproxy.server.certificates.DynamicCertificatesManager;
import org.carapaceproxy.server.certificates.ocsp.OcspStaplingManager;
import org.carapaceproxy.server.config.AcmeProviderConfiguration;
import org.carapaceproxy.server.config.BackendConfiguration;
import org.carapaceproxy.server.config.ConfigurationChangeInProgressException;
import org.carapaceproxy.server.config.ConfigurationNotValidException;
Expand Down Expand Up @@ -687,8 +688,14 @@ private void performCertificateUpdate(Properties props, String key, CertificateD
props.setProperty(key.replace("hostname", "mode"), cert.isManual() ? "manual" : "acme");
if (cert.isManual()) {
props.remove(key.replace("hostname", "daysbeforerenewal")); // type changed from acme to manual
props.remove(key.replace("hostname", "provider"));
} else {
props.setProperty(key.replace("hostname", "daysbeforerenewal"), cert.getDaysBeforeRenewal() + "");
if (AcmeProviderConfiguration.DEFAULT_PROVIDER_NAME.equals(cert.getProvider())) {
props.remove(key.replace("hostname", "provider")); // default provider is implicit
} else {
props.setProperty(key.replace("hostname", "provider"), cert.getProvider());
}
}
if (cert.getSubjectAltNames() != null && !cert.getSubjectAltNames().isEmpty()) {
props.setProperty(
Expand All @@ -707,6 +714,9 @@ private void performCertificateCreate(Properties props, CertificateData cert) {
props.setProperty(prefix + "mode", cert.isManual() ? "manual" : "acme");
if (!cert.isManual()) {
props.setProperty(prefix + "daysbeforerenewal", cert.getDaysBeforeRenewal() + "");
if (!AcmeProviderConfiguration.DEFAULT_PROVIDER_NAME.equals(cert.getProvider())) {
props.setProperty(prefix + "provider", cert.getProvider());
}
}
if (cert.getSubjectAltNames() != null && !cert.getSubjectAltNames().isEmpty()) {
props.setProperty(prefix + "san", String.join(",", cert.getSubjectAltNames()));
Expand Down
Loading