/** * 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.util; import org.candlepin.model.CertificateSerial; import org.candlepin.model.CertificateSerialCurator; import org.candlepin.pki.PKIReader; import org.candlepin.pki.PKIUtility; import org.candlepin.pki.X509CRLEntryWrapper; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.persist.Transactional; import org.apache.commons.codec.binary.Base64InputStream; import org.apache.commons.codec.binary.Base64OutputStream; import org.apache.commons.io.IOUtils; import org.bouncycastle.asn1.x509.CRLReason; import org.bouncycastle.crypto.CryptoException; import org.bouncycastle.jce.provider.X509CRLEntryObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.PrivateKey; import java.security.cert.X509CRL; import java.security.interfaces.RSAPrivateKey; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Pattern; /** * CrlFileUtil */ @Singleton public class CrlFileUtil { private static final Logger log = LoggerFactory.getLogger(CrlFileUtil.class); private static final Pattern CRL_HEADER_PATTERN = Pattern.compile("^(-+)BEGIN (.+)\\1$"); private static final Pattern CRL_FOOTER_PATTERN = Pattern.compile("^(-+)END (.+)\\1$"); private static final Pattern WHITESPACE = Pattern.compile("^\\s.*$"); private final PKIReader pkiReader; private final PKIUtility pkiUtility; private CertificateSerialCurator certificateSerialCurator; private final ReadWriteLock lock = new ReentrantReadWriteLock(true); @Inject public CrlFileUtil(PKIReader pkiReader, PKIUtility pkiUtility, CertificateSerialCurator curator) { this.pkiReader = pkiReader; this.pkiUtility = pkiUtility; this.certificateSerialCurator = curator; } /** * Initializes a new CRL at the specified location * * @param file * The file to initialize * * @throws IOException * If an IO error occurs while initializing the CRL file */ public void initializeCRLFile(File file, Collection<BigInteger> revoke) throws IOException { FileOutputStream output = null; List<X509CRLEntryWrapper> entries = new LinkedList<X509CRLEntryWrapper>(); for (BigInteger serial : revoke) { entries.add(new X509CRLEntryWrapper(serial, new Date())); } X509CRL crl = this.pkiUtility.createX509CRL(entries, BigInteger.ONE); try { output = new FileOutputStream(file); this.pkiUtility.writePemEncoded(crl, output); } finally { IOUtils.closeQuietly(output); } } /** * Opens the specified CRL file for streaming. If the CRL file contains a header in the form of * "/-+BEGIN .+-+\n/i", the header and matching footer will be automatically truncated. * * Note: The stream returned by this method must be closed when the calling function is done * with it. * * @param file * The file to open as a CRL file. * * @throws IOException * If an IO error occurs while open the CRL file * * @return * an input stream for the specified CRL file */ public File stripCRLFile(File file) throws IOException { File tempFile = File.createTempFile("candlepin_crl_", ".pem"); BufferedReader r = new BufferedReader(new FileReader(file)); BufferedWriter w = new BufferedWriter(new FileWriter(tempFile)); String line = null; try { do { line = r.readLine(); } while (line != null && (WHITESPACE.matcher(line).matches() || CRL_HEADER_PATTERN.matcher(line).matches()) ); if (line == null) { throw new IOException("No data left in file " + file); } do { w.write(line); line = r.readLine(); } while (line != null && !CRL_FOOTER_PATTERN.matcher(line).matches()); return tempFile; } finally { w.close(); r.close(); } } /** * Updates the specified CRL file by adding or removing entries. If both lists are either null * or empty, the CRL file will not be modified by this method. If the file does not exist or * appears to be empty, it will be initialized before processing the lists. * * @param file * The CRL file to update * * @param revoke * A collection of serials to revoke (add) * * @param unrevoke * A collection of serials to unrevoke (remove) * * @throws IOException * if an IO error occurs while updating the CRL file */ public void updateCRLFile(File file, final Collection<BigInteger> revoke, final Collection<BigInteger> unrevoke) throws IOException { if (!file.exists() || file.length() == 0) { this.initializeCRLFile(file, revoke); return; } File strippedFile = stripCRLFile(file); InputStream input = null; InputStream reaper = null; BufferedOutputStream output = null; OutputStream filter = null; OutputStream encoder = null; try { // Impl note: // Due to the way the X509CRLStreamWriter works (and the DER format in general), we have // to make two passes through the file. input = new Base64InputStream(new FileInputStream(strippedFile)); reaper = new Base64InputStream(new FileInputStream(strippedFile)); // Note: This will break if we ever stop using RSA keys PrivateKey key = this.pkiReader.getCaKey(); X509CRLStreamWriter writer = new X509CRLStreamWriter( input, (RSAPrivateKey) key, this.pkiReader.getCACert()); // Add new entries if (revoke != null) { Date now = new Date(); for (BigInteger serial : revoke) { writer.add(serial, now, CRLReason.privilegeWithdrawn); } } // Unfortunately, we need to do the prescan before checking if we have changes queued, // or we could miss cases where we have entries to remove, but nothing to add. if (unrevoke != null && !unrevoke.isEmpty()) { writer.preScan(reaper, new CRLEntryValidator() { public boolean shouldDelete(X509CRLEntryObject entry) { return unrevoke.contains(entry.getSerialNumber()); } }); } else { writer.preScan(reaper); } // Verify we actually have work to do now if (writer.hasChangesQueued()) { output = new BufferedOutputStream(new FileOutputStream(file)); filter = new FilterOutputStream(output) { private boolean needsLineBreak = true; public void write(int b) throws IOException { this.needsLineBreak = (b != (byte) '\n'); super.write(b); } public void write(byte[] buffer) throws IOException { this.needsLineBreak = (buffer[buffer.length - 1] != (byte) '\n'); super.write(buffer); } public void write(byte[] buffer, int off, int len) throws IOException { this.needsLineBreak = (buffer[off + len - 1] != (byte) '\n'); super.write(buffer, off, len); } public void close() throws IOException { if (this.needsLineBreak) { super.write((int) '\n'); this.needsLineBreak = false; } // Impl note: // We're intentionally not propagating the call here. } }; encoder = new Base64OutputStream(filter, true, 76, new byte[] { (byte) '\n' }); output.write("-----BEGIN X509 CRL-----\n".getBytes()); writer.lock(); writer.write(encoder); encoder.close(); filter.close(); output.write("-----END X509 CRL-----\n".getBytes()); output.close(); } } catch (GeneralSecurityException e) { // This should never actually happen log.error("Unexpected security error occurred while retrieving CA key", e); } catch (CryptoException e) { // Something went horribly wrong with the stream writer log.error("Unexpected error occurred while writing new CRL file", e); } finally { for (Closeable stream : Arrays.asList(encoder, output, reaper, input)) { if (stream != null) { try { stream.close(); } catch (IOException e) { log.error("Unexpected exception occurred while closing stream: {}", stream, e); } } } if (!strippedFile.delete()) { log.error("Unable to delete temporary CRL file: {}", strippedFile); } } } @Transactional public boolean syncCRLWithDB(File file) throws IOException { List<BigInteger> revoke = new LinkedList<BigInteger>(); List<CertificateSerial> serials = this.certificateSerialCurator .retrieveTobeCollectedSerials() .list(); for (CertificateSerial serial : serials) { revoke.add(serial.getSerial()); serial.setCollected(true); } List<BigInteger> unrevoke = new LinkedList<BigInteger>(); for (CertificateSerial serial : this.certificateSerialCurator.getExpiredSerials()) { unrevoke.add(serial.getSerial()); } if (revoke.size() > 0 || unrevoke.size() > 0) { this.updateCRLFile(file, revoke, unrevoke); // Store the state of the newly-revoked serials as "collected" this.certificateSerialCurator.saveOrUpdateAll(serials, true, true); } return true; } }