/** * 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); } }