/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.service.impl;
import org.candlepin.common.config.Configuration;
import org.candlepin.config.ConfigProperties;
import org.candlepin.model.CertificateSerial;
import org.candlepin.model.CertificateSerialCurator;
import org.candlepin.model.Consumer;
import org.candlepin.model.Entitlement;
import org.candlepin.model.EntitlementCertificate;
import org.candlepin.model.EntitlementCertificateCurator;
import org.candlepin.model.EntitlementCurator;
import org.candlepin.model.Environment;
import org.candlepin.model.EnvironmentContent;
import org.candlepin.model.KeyPairCurator;
import org.candlepin.model.Pool;
import org.candlepin.model.Product;
import org.candlepin.model.ProductContent;
import org.candlepin.model.ProductCurator;
import org.candlepin.pki.PKIUtility;
import org.candlepin.pki.X509ByteExtensionWrapper;
import org.candlepin.pki.X509ExtensionWrapper;
import org.candlepin.service.BaseEntitlementCertServiceAdapter;
import org.candlepin.util.CertificateSizeException;
import org.candlepin.util.OIDUtil;
import org.candlepin.util.Util;
import org.candlepin.util.X509ExtensionUtil;
import org.candlepin.util.X509Util;
import org.candlepin.util.X509V3ExtensionUtil;
import com.google.common.collect.Collections2;
import com.google.inject.Inject;
import org.apache.commons.lang.BooleanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnap.commons.i18n.I18n;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* DefaultEntitlementCertServiceAdapter
*/
public class DefaultEntitlementCertServiceAdapter extends BaseEntitlementCertServiceAdapter {
private PKIUtility pki;
private X509ExtensionUtil extensionUtil;
private X509V3ExtensionUtil v3extensionUtil;
private KeyPairCurator keyPairCurator;
private CertificateSerialCurator serialCurator;
private EntitlementCurator entCurator;
private I18n i18n;
private Configuration config;
private ProductCurator productCurator;
private static Logger log =
LoggerFactory.getLogger(DefaultEntitlementCertServiceAdapter.class);
@Inject
public DefaultEntitlementCertServiceAdapter(PKIUtility pki,
X509ExtensionUtil extensionUtil,
X509V3ExtensionUtil v3extensionUtil,
EntitlementCertificateCurator entCertCurator,
KeyPairCurator keyPairCurator,
CertificateSerialCurator serialCurator,
EntitlementCurator entCurator, I18n i18n,
Configuration config,
ProductCurator productCurator) {
this.pki = pki;
this.extensionUtil = extensionUtil;
this.v3extensionUtil = v3extensionUtil;
this.entCertCurator = entCertCurator;
this.keyPairCurator = keyPairCurator;
this.serialCurator = serialCurator;
this.entCurator = entCurator;
this.i18n = i18n;
this.config = config;
this.productCurator = productCurator;
}
// NOTE: we use entitlement here, but it version does not...
// NOTE: we can get consumer from entitlement.getConsumer()
@Override
public EntitlementCertificate generateEntitlementCert(Entitlement entitlement, Product product)
throws GeneralSecurityException, IOException {
return generateSingleCert(entitlement, product);
}
private EntitlementCertificate generateSingleCert(Entitlement entitlement, Product product)
throws GeneralSecurityException, IOException {
Map<String, Entitlement> entitlements = new HashMap<String, Entitlement>();
entitlements.put(entitlement.getPool().getId(), entitlement);
Map<String, Product> products = new HashMap<String, Product>();
products.put(entitlement.getPool().getId(), product);
Map<String, EntitlementCertificate> result = generateEntitlementCerts(entitlement.getConsumer(),
entitlements, products);
return result.get(entitlement.getPool().getId());
}
@Override
public Map<String, EntitlementCertificate> generateEntitlementCerts(Consumer consumer,
Map<String, Entitlement> entitlements, Map<String, Product> products)
throws GeneralSecurityException, IOException {
return doEntitlementCertGeneration(consumer, entitlements, products);
}
private Set<Product> getDerivedProductsForDistributor(Entitlement ent) {
Set<Product> derivedProducts = new HashSet<Product>();
boolean derived = ent.getPool().hasAttribute(Pool.Attributes.DERIVED_POOL);
if (!derived && ent.getConsumer().isManifestDistributor() &&
ent.getPool().getDerivedProduct() != null) {
derivedProducts.add(ent.getPool().getDerivedProduct());
derivedProducts
.addAll(productCurator.getPoolDerivedProvidedProductsCached(ent.getPool()));
}
return derivedProducts;
}
// TODO: productModels not used by V1 certificates. This whole v1/v3 split needs
// a re-org. Passing them here because it eliminates a substantial performance hit
// recalculating this for the entitlement body in v3 certs.
public X509Certificate createX509Certificate(Entitlement ent, Product product, Set<Product> products,
List<org.candlepin.model.dto.Product> productModels, BigInteger serialNumber, KeyPair keyPair,
boolean useContentPrefix)
throws GeneralSecurityException, IOException {
// oidutil is busted at the moment, so do this manually
Set<X509ExtensionWrapper> extensions;
Set<X509ByteExtensionWrapper> byteExtensions = new LinkedHashSet<X509ByteExtensionWrapper>();
products.add(product);
Map<String, EnvironmentContent> promotedContent = getPromotedContent(ent);
String contentPrefix = getContentPrefix(ent, useContentPrefix);
if (shouldGenerateV3(ent)) {
extensions = prepareV3Extensions(ent, contentPrefix, promotedContent);
byteExtensions = prepareV3ByteExtensions(product, productModels,
ent, contentPrefix, promotedContent);
}
else {
extensions = prepareV1Extensions(products, ent, contentPrefix,
promotedContent);
}
setupEntitlementEndDate(ent);
Calendar cal = Calendar.getInstance();
cal.add(Calendar.HOUR, -1);
Date startDate = ent.getStartDate().before(cal.getTime()) ? ent.getStartDate() : cal.getTime();
X509Certificate x509Cert = this.pki.createX509Certificate(
createDN(ent), extensions, byteExtensions, startDate,
ent.getEndDate(), keyPair, serialNumber, null);
return x509Cert;
}
/**
* Modify the entitlements end date
* @param ent
*/
private void setupEntitlementEndDate(Entitlement ent) {
Pool pool = ent.getPool();
Consumer consumer = ent.getConsumer();
Date startDate = new Date();
if (consumer.getCreated() != null) {
startDate = consumer.getCreated();
}
boolean isUnmappedGuestPool = BooleanUtils.toBoolean(
pool.getAttributeValue(Pool.Attributes.UNMAPPED_GUESTS_ONLY));
if (isUnmappedGuestPool) {
Date sevenDaysFromRegistration = new Date(startDate.getTime() + 7L * 24L * 60L * 60L * 1000L);
log.info("Setting 7 day expiration for unmapped guest pool entilement: {}",
sevenDaysFromRegistration);
ent.setEndDateOverride(sevenDaysFromRegistration);
entCurator.merge(ent);
}
}
private boolean shouldGenerateV3(Entitlement entitlement) {
Consumer consumer = entitlement.getConsumer();
return consumer != null && consumer.isCertV3Capable();
}
/**
* @param ent
* @param useContentPrefix
* @return
* @throws IOException
*/
private String getContentPrefix(Entitlement ent, boolean useContentPrefix)
throws IOException {
String contentPrefix = null;
if (useContentPrefix) {
contentPrefix = ent.getOwner().getContentPrefix();
Environment env = ent.getConsumer().getEnvironment();
if (contentPrefix != null && !contentPrefix.equals("")) {
if (env != null) {
contentPrefix = contentPrefix.replaceAll("\\$env", env.getName());
}
contentPrefix = this.cleanUpPrefix(contentPrefix);
}
}
return contentPrefix;
}
/**
* @param ent
* @return
*/
private Map<String, EnvironmentContent> getPromotedContent(Entitlement ent) {
// Build a set of all content IDs promoted to the consumer's environment so
// we can determine if anything needs to be skipped:
Map<String, EnvironmentContent> promotedContent =
new HashMap<String, EnvironmentContent>();
if (ent.getConsumer().getEnvironment() != null) {
log.debug("Consumer has environment, checking for promoted content in: " +
ent.getConsumer().getEnvironment());
for (EnvironmentContent envContent :
ent.getConsumer().getEnvironment().getEnvironmentContent()) {
log.debug(" promoted content: " + envContent.getContent().getId());
promotedContent.put(envContent.getContent().getId(), envContent);
}
}
return promotedContent;
}
public Set<X509ExtensionWrapper> prepareV1Extensions(Set<Product> products,
Entitlement ent, String contentPrefix,
Map<String, EnvironmentContent> promotedContent) {
Set<X509ExtensionWrapper> result = new LinkedHashSet<X509ExtensionWrapper>();
Set<String> entitledProductIds = entCurator.listEntitledProductIds(
ent.getConsumer(), ent.getPool().getStartDate(),
ent.getPool().getEndDate());
int contentCounter = 0;
boolean enableEnvironmentFiltering = config.getBoolean(ConfigProperties.ENV_CONTENT_FILTERING);
Product skuProd = ent.getPool().getProduct();
for (Product prod : Collections2.filter(products, X509Util.PROD_FILTER_PREDICATE)) {
log.debug("Adding X509 extensions for product: {}", prod);
result.addAll(extensionUtil.productExtensions(prod));
Set<ProductContent> filteredContent = extensionUtil.filterProductContent(
prod, ent, entCurator, promotedContent, enableEnvironmentFiltering, entitledProductIds);
filteredContent = extensionUtil.filterContentByContentArch(filteredContent,
ent.getConsumer(), prod);
// Keep track of the number of content sets that are being added.
contentCounter += filteredContent.size();
log.debug("Adding X509 extensions for content: {}", filteredContent);
result.addAll(extensionUtil.contentExtensions(filteredContent,
contentPrefix, promotedContent, ent.getConsumer(), skuProd));
}
// For V1 certificates we're going to error out if we exceed a limit which is
// likely going to generate a certificate too large for the CDN, and return an
// informative error message to the user.
if (contentCounter > X509ExtensionUtil.V1_CONTENT_LIMIT) {
String cause = i18n.tr("Too many content sets for certificate {0}. A newer " +
"client may be available to address this problem. " +
"See knowledge database https://access.redhat.com/knowledge/node/129003 for more " +
"information.", ent.getPool().getProductName());
throw new CertificateSizeException(cause);
}
result.addAll(extensionUtil.subscriptionExtensions(ent));
result.addAll(extensionUtil.entitlementExtensions(ent));
result.addAll(extensionUtil.consumerExtensions(ent.getConsumer()));
if (log.isDebugEnabled()) {
for (X509ExtensionWrapper eWrapper : result) {
log.debug("Extension {} with value {}", eWrapper.getOid(), eWrapper.getValue());
}
}
return result;
}
public Set<X509ExtensionWrapper> prepareV3Extensions(Entitlement ent,
String contentPrefix, Map<String, EnvironmentContent> promotedContent) {
Set<X509ExtensionWrapper> result = v3extensionUtil.getExtensions(ent,
contentPrefix, promotedContent);
X509ExtensionWrapper typeExtension = new X509ExtensionWrapper(OIDUtil.REDHAT_OID + "." +
OIDUtil.TOPLEVEL_NAMESPACES.get(OIDUtil.ENTITLEMENT_TYPE_KEY), false, "Basic");
result.add(typeExtension);
return result;
}
public Set<X509ByteExtensionWrapper> prepareV3ByteExtensions(Product sku,
List<org.candlepin.model.dto.Product> productModels, Entitlement ent, String contentPrefix,
Map<String, EnvironmentContent> promotedContent) throws IOException {
Set<X509ByteExtensionWrapper> result = v3extensionUtil.getByteExtensions(sku,
productModels, ent, contentPrefix, promotedContent);
return result;
}
// Encode the entire prefix in case any part of it is not
// URL friendly. Any $ is put back in order to preseve
// the ability to pass $env to the client
public String cleanUpPrefix(String contentPrefix) throws IOException {
StringBuffer output = new StringBuffer("/");
for (String part : contentPrefix.split("/")) {
if (!part.equals("")) {
output.append(URLEncoder.encode(part, "UTF-8"));
output.append("/");
}
}
return output.toString().replace("%24", "$");
}
/**
* @param entitlements a map of entitlements indexed by pool ids to generate
* the certs of
* @param productMap a map of respective products indexed by pool id
* @throws IOException
* @throws GeneralSecurityException
* @return entitlementCerts the respective entitlement certs indexed by pool
* id
*/
private Map<String, EntitlementCertificate> doEntitlementCertGeneration(Consumer consumer,
Map<String, Entitlement> entitlements, Map<String, Product> productMap)
throws GeneralSecurityException, IOException {
log.info("Generating entitlement cert for entitlements");
KeyPair keyPair = keyPairCurator.getConsumerKeyPair(consumer);
byte[] pemEncodedKeyPair = pki.getPemEncoded(keyPair.getPrivate());
Map<String, CertificateSerial> serialMap = new HashMap<String, CertificateSerial>();
for (Entry<String, Entitlement> entry : entitlements.entrySet()) {
// No need to persist the cert serial here as the IDs are generated on object creation.
serialMap.put(entry.getKey(), new CertificateSerial(entry.getValue().getEndDate()));
}
Map<String, EntitlementCertificate> entitlementCerts = new HashMap<String, EntitlementCertificate>();
for (Entry<String, Entitlement> entry : entitlements.entrySet()) {
Entitlement entitlement = entry.getValue();
CertificateSerial serial = serialMap.get(entry.getKey());
Product product = productMap.get(entry.getKey());
log.info("Generating entitlement cert for entitlement: {}", entitlement);
Set<Product> products = new HashSet<Product>(
productCurator.getPoolProvidedProductsCached(entitlement.getPool()));
// If creating a certificate for a distributor, we need
// to add any derived products as well so that their content
// is available in the upstream certificate.
products.addAll(getDerivedProductsForDistributor(entitlement));
products.add(product);
Map<String, EnvironmentContent> promotedContent = getPromotedContent(entitlement);
String contentPrefix = getContentPrefix(entitlement, true);
log.info("Creating X509 cert for product: {}", product);
log.debug("Provided products: {}", products);
List<org.candlepin.model.dto.Product> productModels = v3extensionUtil.createProducts(product,
products, contentPrefix, promotedContent, entitlement.getConsumer(), entitlement);
X509Certificate x509Cert = createX509Certificate(entitlement, product, products, productModels,
BigInteger.valueOf(serial.getId()), keyPair, true);
log.info("Getting PEM encoded cert.");
String pem = new String(this.pki.getPemEncoded(x509Cert));
if (shouldGenerateV3(entitlement)) {
log.debug("Generating v3 entitlement data");
byte[] payloadBytes = v3extensionUtil.createEntitlementDataPayload(product, productModels,
entitlement, contentPrefix, promotedContent);
String payload = "-----BEGIN ENTITLEMENT DATA-----\n";
payload += Util.toBase64(payloadBytes);
payload += "-----END ENTITLEMENT DATA-----\n";
byte[] bytes = pki.getSHA256WithRSAHash(new ByteArrayInputStream(payloadBytes));
String signature = "-----BEGIN RSA SIGNATURE-----\n";
signature += Util.toBase64(bytes);
signature += "-----END RSA SIGNATURE-----\n";
pem += payload + signature;
}
// Build a skeleton cert as part of the entitlement processing.
EntitlementCertificate cert = new EntitlementCertificate();
cert.setKeyAsBytes(pemEncodedKeyPair);
cert.setCert(pem);
cert.setEntitlement(entitlement);
if (log.isDebugEnabled()) {
log.debug("Generated cert serial number: {}", serial.getId());
log.debug("Key: {}", cert.getKey());
log.debug("Cert: {}", cert.getCert());
}
entitlementCerts.put(entry.getKey(), cert);
}
// Serials need to be saved before the certs.
log.info("Persisting new certificate serials");
serialCurator.saveOrUpdateAll(serialMap);
// Now that the serials have been saved, update the newly created
// certs with their serials and add them to the entitlements.
for (Entry<String, Entitlement> entry : entitlements.entrySet()) {
CertificateSerial nextSerial = serialMap.get(entry.getKey());
if (nextSerial == null) {
// This should never happen, but checking to be safe.
throw new RuntimeException(
"Certificate serial not found for entitlement during cert generation.");
}
EntitlementCertificate nextCert = entitlementCerts.get(entry.getKey());
if (nextCert == null) {
// This should never happen, but checking to be safe.
throw new RuntimeException(
"Entitlement certificate not found for entitlement during cert generation");
}
nextCert.setSerial(nextSerial);
entry.getValue().getCertificates().add(nextCert);
}
log.info("Persisting certs.");
entCertCurator.saveOrUpdateAll(entitlementCerts.values(), false, false);
return entitlementCerts;
}
private String createDN(Entitlement ent) {
StringBuilder sb = new StringBuilder("CN=");
sb.append(ent.getId());
sb.append(", O=");
sb.append(ent.getOwner().getKey());
return sb.toString();
}
public List<Long> listEntitlementSerialIds(Consumer consumer) {
return serialCurator.listEntitlementSerialIds(consumer);
}
}