| /* |
| * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package sun.security.testlibrary; |
| |
| import java.io.*; |
| import java.net.*; |
| import java.security.*; |
| import java.security.cert.CRLReason; |
| import java.security.cert.X509Certificate; |
| import java.security.cert.Extension; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.Signature; |
| import java.util.*; |
| import java.util.concurrent.*; |
| import java.text.SimpleDateFormat; |
| import java.math.BigInteger; |
| |
| import sun.security.x509.*; |
| import sun.security.x509.PKIXExtensions; |
| import sun.security.provider.certpath.ResponderId; |
| import sun.security.provider.certpath.CertId; |
| import sun.security.provider.certpath.OCSPResponse; |
| import sun.security.provider.certpath.OCSPResponse.ResponseStatus; |
| import sun.security.util.Debug; |
| import sun.security.util.DerInputStream; |
| import sun.security.util.DerOutputStream; |
| import sun.security.util.DerValue; |
| import sun.security.util.ObjectIdentifier; |
| |
| |
| /** |
| * This is a simple OCSP server designed to listen and respond to incoming |
| * requests. |
| */ |
| public class SimpleOCSPServer { |
| private final Debug debug = Debug.getInstance("oserv"); |
| private static final ObjectIdentifier OCSP_BASIC_RESPONSE_OID = |
| ObjectIdentifier.newInternal( |
| new int[] { 1, 3, 6, 1, 5, 5, 7, 48, 1, 1}); |
| private static final SimpleDateFormat utcDateFmt = |
| new SimpleDateFormat("MMM dd yyyy, HH:mm:ss z"); |
| |
| static final int FREE_PORT = 0; |
| |
| // CertStatus values |
| public static enum CertStatus { |
| CERT_STATUS_GOOD, |
| CERT_STATUS_REVOKED, |
| CERT_STATUS_UNKNOWN, |
| } |
| |
| // Fields used for the networking portion of the responder |
| private ServerSocket servSocket; |
| private InetAddress listenAddress; |
| private int listenPort; |
| |
| // Keystore information (certs, keys, etc.) |
| private KeyStore keystore; |
| private X509Certificate issuerCert; |
| private X509Certificate signerCert; |
| private PrivateKey signerKey; |
| |
| // Fields used for the operational portions of the server |
| private boolean logEnabled = false; |
| private ExecutorService threadPool; |
| private volatile boolean started = false; |
| private volatile boolean serverReady = false; |
| private volatile boolean receivedShutdown = false; |
| private volatile boolean acceptConnections = true; |
| private volatile long delayMsec = 0; |
| |
| // Fields used in the generation of responses |
| private long nextUpdateInterval = -1; |
| private Date nextUpdate = null; |
| private ResponderId respId; |
| private AlgorithmId sigAlgId; |
| private Map<CertId, CertStatusInfo> statusDb = |
| Collections.synchronizedMap(new HashMap<>()); |
| |
| /** |
| * Construct a SimpleOCSPServer using keystore, password, and alias |
| * parameters. |
| * |
| * @param ks the keystore to be used |
| * @param password the password to access key material in the keystore |
| * @param issuerAlias the alias of the issuer certificate |
| * @param signerAlias the alias of the signer certificate and key. A |
| * value of {@code null} means that the {@code issuerAlias} will be used |
| * to look up the signer key. |
| * |
| * @throws GeneralSecurityException if there are problems accessing the |
| * keystore or finding objects within the keystore. |
| * @throws IOException if a {@code ResponderId} cannot be generated from |
| * the signer certificate. |
| */ |
| public SimpleOCSPServer(KeyStore ks, String password, String issuerAlias, |
| String signerAlias) throws GeneralSecurityException, IOException { |
| this(null, FREE_PORT, ks, password, issuerAlias, signerAlias); |
| } |
| |
| /** |
| * Construct a SimpleOCSPServer using specific network parameters, |
| * keystore, password, and alias. |
| * |
| * @param addr the address to bind the server to. A value of {@code null} |
| * means the server will bind to all interfaces. |
| * @param port the port to listen on. A value of {@code 0} will mean that |
| * the server will randomly pick an open ephemeral port to bind to. |
| * @param ks the keystore to be used |
| * @param password the password to access key material in the keystore |
| * @param issuerAlias the alias of the issuer certificate |
| * @param signerAlias the alias of the signer certificate and key. A |
| * value of {@code null} means that the {@code issuerAlias} will be used |
| * to look up the signer key. |
| * |
| * @throws GeneralSecurityException if there are problems accessing the |
| * keystore or finding objects within the keystore. |
| * @throws IOException if a {@code ResponderId} cannot be generated from |
| * the signer certificate. |
| */ |
| public SimpleOCSPServer(InetAddress addr, int port, KeyStore ks, |
| String password, String issuerAlias, String signerAlias) |
| throws GeneralSecurityException, IOException { |
| Objects.requireNonNull(ks, "Null keystore provided"); |
| Objects.requireNonNull(issuerAlias, "Null issuerName provided"); |
| |
| utcDateFmt.setTimeZone(TimeZone.getTimeZone("GMT")); |
| |
| keystore = ks; |
| issuerCert = (X509Certificate)ks.getCertificate(issuerAlias); |
| if (issuerCert == null) { |
| throw new IllegalArgumentException("Certificate for alias " + |
| issuerAlias + " not found"); |
| } |
| |
| if (signerAlias != null) { |
| signerCert = (X509Certificate)ks.getCertificate(signerAlias); |
| if (signerCert == null) { |
| throw new IllegalArgumentException("Certificate for alias " + |
| signerAlias + " not found"); |
| } |
| signerKey = (PrivateKey)ks.getKey(signerAlias, |
| password.toCharArray()); |
| if (signerKey == null) { |
| throw new IllegalArgumentException("PrivateKey for alias " + |
| signerAlias + " not found"); |
| } |
| } else { |
| signerCert = issuerCert; |
| signerKey = (PrivateKey)ks.getKey(issuerAlias, |
| password.toCharArray()); |
| if (signerKey == null) { |
| throw new IllegalArgumentException("PrivateKey for alias " + |
| issuerAlias + " not found"); |
| } |
| } |
| |
| sigAlgId = AlgorithmId.get("Sha256withRSA"); |
| respId = new ResponderId(signerCert.getSubjectX500Principal()); |
| listenAddress = addr; |
| listenPort = port; |
| } |
| |
| /** |
| * Start the server. The server will bind to the specified network |
| * address and begin listening for incoming connections. |
| * |
| * @throws IOException if any number of things go wonky. |
| */ |
| public synchronized void start() throws IOException { |
| // You cannot start the server twice. |
| if (started) { |
| log("Server has already been started"); |
| return; |
| } else { |
| started = true; |
| } |
| |
| // Create and start the thread pool |
| threadPool = Executors.newFixedThreadPool(32, new ThreadFactory() { |
| @Override |
| public Thread newThread(Runnable r) { |
| Thread t = Executors.defaultThreadFactory().newThread(r); |
| t.setDaemon(true); |
| return t; |
| } |
| }); |
| |
| threadPool.submit(new Runnable() { |
| @Override |
| public void run() { |
| try (ServerSocket sSock = new ServerSocket()) { |
| servSocket = sSock; |
| servSocket.setReuseAddress(true); |
| servSocket.setSoTimeout(500); |
| servSocket.bind(new InetSocketAddress(listenAddress, |
| listenPort), 128); |
| log("Listening on " + servSocket.getLocalSocketAddress()); |
| |
| // Singal ready |
| serverReady = true; |
| |
| // Update the listenPort with the new port number. If |
| // the server is restarted, it will bind to the same |
| // port rather than picking a new one. |
| listenPort = servSocket.getLocalPort(); |
| |
| // Main dispatch loop |
| while (!receivedShutdown) { |
| try { |
| Socket newConnection = servSocket.accept(); |
| if (!acceptConnections) { |
| try { |
| log("Reject connection"); |
| newConnection.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| continue; |
| } |
| threadPool.submit(new OcspHandler(newConnection)); |
| } catch (SocketTimeoutException timeout) { |
| // Nothing to do here. If receivedShutdown |
| // has changed to true then the loop will |
| // exit on its own. |
| } catch (IOException ioe) { |
| // Something bad happened, log and force a shutdown |
| log("Unexpected Exception: " + ioe); |
| stop(); |
| } |
| } |
| |
| log("Shutting down..."); |
| threadPool.shutdown(); |
| } catch (IOException ioe) { |
| err(ioe); |
| } finally { |
| // Reset state variables so the server can be restarted |
| receivedShutdown = false; |
| started = false; |
| serverReady = false; |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Make the OCSP server reject incoming connections. |
| */ |
| public synchronized void rejectConnections() { |
| log("Reject OCSP connections"); |
| acceptConnections = false; |
| } |
| |
| /** |
| * Make the OCSP server accept incoming connections. |
| */ |
| public synchronized void acceptConnections() { |
| log("Accept OCSP connections"); |
| acceptConnections = true; |
| } |
| |
| |
| /** |
| * Stop the OCSP server. |
| */ |
| public synchronized void stop() { |
| if (started) { |
| receivedShutdown = true; |
| log("Received shutdown notification"); |
| } |
| } |
| |
| /** |
| * Print {@code SimpleOCSPServer} operating parameters. |
| * |
| * @return the {@code SimpleOCSPServer} operating parameters in |
| * {@code String} form. |
| */ |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("OCSP Server:\n"); |
| sb.append("----------------------------------------------\n"); |
| sb.append("issuer: ").append(issuerCert.getSubjectX500Principal()). |
| append("\n"); |
| sb.append("signer: ").append(signerCert.getSubjectX500Principal()). |
| append("\n"); |
| sb.append("ResponderId: ").append(respId).append("\n"); |
| sb.append("----------------------------------------------"); |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Helpful debug routine to hex dump byte arrays. |
| * |
| * @param data the array of bytes to dump to stdout. |
| * |
| * @return the hexdump of the byte array |
| */ |
| private static String dumpHexBytes(byte[] data) { |
| return dumpHexBytes(data, 16, "\n", " "); |
| } |
| |
| /** |
| * |
| * @param data the array of bytes to dump to stdout. |
| * @param itemsPerLine the number of bytes to display per line |
| * if the {@code lineDelim} character is blank then all bytes will be |
| * printed on a single line. |
| * @param lineDelim the delimiter between lines |
| * @param itemDelim the delimiter between bytes |
| * |
| * @return The hexdump of the byte array |
| */ |
| private static String dumpHexBytes(byte[] data, int itemsPerLine, |
| String lineDelim, String itemDelim) { |
| StringBuilder sb = new StringBuilder(); |
| if (data != null) { |
| for (int i = 0; i < data.length; i++) { |
| if (i % itemsPerLine == 0 && i != 0) { |
| sb.append(lineDelim); |
| } |
| sb.append(String.format("%02X", data[i])).append(itemDelim); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Enable or disable the logging feature. |
| * |
| * @param enable {@code true} to enable logging, {@code false} to |
| * disable it. The setting must be activated before the server calls |
| * its start method. Any calls after that have no effect. |
| */ |
| public void enableLog(boolean enable) { |
| if (!started) { |
| logEnabled = enable; |
| } |
| } |
| |
| /** |
| * Sets the nextUpdate interval. Intervals will be calculated relative |
| * to the server startup time. When first set, the nextUpdate date is |
| * calculated based on the current time plus the interval. After that, |
| * calls to getNextUpdate() will return this date if it is still |
| * later than current time. If not, the Date will be updated to the |
| * next interval that is later than current time. This value must be set |
| * before the server has had its start method called. Calls made after |
| * the server has been started have no effect. |
| * |
| * @param interval the recurring time interval in seconds used to |
| * calculate nextUpdate times. A value less than or equal to 0 will |
| * disable the nextUpdate feature. |
| */ |
| public synchronized void setNextUpdateInterval(long interval) { |
| if (!started) { |
| if (interval <= 0) { |
| nextUpdateInterval = -1; |
| nextUpdate = null; |
| log("nexUpdate support has been disabled"); |
| } else { |
| nextUpdateInterval = interval * 1000; |
| nextUpdate = new Date(System.currentTimeMillis() + |
| nextUpdateInterval); |
| log("nextUpdate set to " + nextUpdate); |
| } |
| } |
| } |
| |
| /** |
| * Return the nextUpdate {@code Date} object for this server. If the |
| * nextUpdate date has already passed, set a new nextUpdate based on |
| * the nextUpdate interval and return that date. |
| * |
| * @return a {@code Date} object set to the nextUpdate field for OCSP |
| * responses. |
| */ |
| private synchronized Date getNextUpdate() { |
| if (nextUpdate != null && nextUpdate.before(new Date())) { |
| long nuEpochTime = nextUpdate.getTime(); |
| long currentTime = System.currentTimeMillis(); |
| |
| // Keep adding nextUpdate intervals until you reach a date |
| // that is later than current time. |
| while (currentTime >= nuEpochTime) { |
| nuEpochTime += nextUpdateInterval; |
| } |
| |
| // Set the nextUpdate for future threads |
| nextUpdate = new Date(nuEpochTime); |
| log("nextUpdate updated to new value: " + nextUpdate); |
| } |
| return nextUpdate; |
| } |
| |
| /** |
| * Add entries into the responder's status database. |
| * |
| * @param newEntries a map of {@code CertStatusInfo} objects, keyed on |
| * their serial number (as a {@code BigInteger}). All serial numbers |
| * are assumed to have come from this responder's issuer certificate. |
| * |
| * @throws IOException if a CertId cannot be generated. |
| */ |
| public void updateStatusDb(Map<BigInteger, CertStatusInfo> newEntries) |
| throws IOException { |
| if (newEntries != null) { |
| for (BigInteger serial : newEntries.keySet()) { |
| CertStatusInfo info = newEntries.get(serial); |
| if (info != null) { |
| CertId cid = new CertId(issuerCert, |
| new SerialNumber(serial)); |
| statusDb.put(cid, info); |
| log("Added entry for serial " + serial + "(" + |
| info.getType() + ")"); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Check the status database for revocation information one one or more |
| * certificates. |
| * |
| * @param reqList the list of {@code LocalSingleRequest} objects taken |
| * from the incoming OCSP request. |
| * |
| * @return a {@code Map} of {@code CertStatusInfo} objects keyed by their |
| * {@code CertId} values, for each single request passed in. Those |
| * CertIds not found in the statusDb will have returned List members with |
| * a status of UNKNOWN. |
| */ |
| private Map<CertId, CertStatusInfo> checkStatusDb( |
| List<LocalOcspRequest.LocalSingleRequest> reqList) { |
| // TODO figure out what, if anything to do with request extensions |
| Map<CertId, CertStatusInfo> returnMap = new HashMap<>(); |
| |
| for (LocalOcspRequest.LocalSingleRequest req : reqList) { |
| CertId cid = req.getCertId(); |
| CertStatusInfo info = statusDb.get(cid); |
| if (info != null) { |
| log("Status for SN " + cid.getSerialNumber() + ": " + |
| info.getType()); |
| returnMap.put(cid, info); |
| } else { |
| log("Status for SN " + cid.getSerialNumber() + |
| " not found, using CERT_STATUS_UNKNOWN"); |
| returnMap.put(cid, |
| new CertStatusInfo(CertStatus.CERT_STATUS_UNKNOWN)); |
| } |
| } |
| |
| return Collections.unmodifiableMap(returnMap); |
| } |
| |
| /** |
| * Set the digital signature algorithm used to sign OCSP responses. |
| * |
| * @param algName The algorithm name |
| * |
| * @throws NoSuchAlgorithmException if the algorithm name is invalid. |
| */ |
| public void setSignatureAlgorithm(String algName) |
| throws NoSuchAlgorithmException { |
| if (!started) { |
| sigAlgId = AlgorithmId.get(algName); |
| } |
| } |
| |
| /** |
| * Get the port the OCSP server is running on. |
| * |
| * @return the port that the OCSP server is running on, or -1 if the |
| * server has not yet been bound to a port. |
| */ |
| public int getPort() { |
| if (serverReady) { |
| InetSocketAddress inetSock = |
| (InetSocketAddress)servSocket.getLocalSocketAddress(); |
| return inetSock.getPort(); |
| } else { |
| return -1; |
| } |
| } |
| |
| /** |
| * Use to check if OCSP server is ready to accept connection. |
| * |
| * @return true if server ready, false otherwise |
| */ |
| public boolean isServerReady() { |
| return serverReady; |
| } |
| |
| /** |
| * Set a delay between the reception of the request and production of |
| * the response. |
| * |
| * @param delayMillis the number of milliseconds to wait before acting |
| * on the incoming request. |
| */ |
| public void setDelay(long delayMillis) { |
| delayMsec = delayMillis > 0 ? delayMillis : 0; |
| if (delayMsec > 0) { |
| log("OCSP latency set to " + delayMsec + " milliseconds."); |
| } else { |
| log("OCSP latency disabled"); |
| } |
| } |
| |
| /** |
| * Log a message to stdout. |
| * |
| * @param message the message to log |
| */ |
| private synchronized void log(String message) { |
| if (logEnabled || debug != null) { |
| System.out.println("[" + Thread.currentThread().getName() + "]: " + |
| message); |
| } |
| } |
| |
| /** |
| * Log an error message on the stderr stream. |
| * |
| * @param message the message to log |
| */ |
| private static synchronized void err(String message) { |
| System.out.println("[" + Thread.currentThread().getName() + "]: " + |
| message); |
| } |
| |
| /** |
| * Log exception information on the stderr stream. |
| * |
| * @param exc the exception to dump information about |
| */ |
| private static synchronized void err(Throwable exc) { |
| System.out.print("[" + Thread.currentThread().getName() + |
| "]: Exception: "); |
| exc.printStackTrace(System.out); |
| } |
| |
| /** |
| * The {@code CertStatusInfo} class defines an object used to return |
| * information from the internal status database. The data in this |
| * object may be used to construct OCSP responses. |
| */ |
| public static class CertStatusInfo { |
| private CertStatus certStatusType; |
| private CRLReason reason; |
| private Date revocationTime; |
| |
| /** |
| * Create a Certificate status object by providing the status only. |
| * If the status is {@code REVOKED} then current time is assumed |
| * for the revocation time. |
| * |
| * @param statType the status for this entry. |
| */ |
| public CertStatusInfo(CertStatus statType) { |
| this(statType, null, null); |
| } |
| |
| /** |
| * Create a CertStatusInfo providing both type and revocation date |
| * (if applicable). |
| * |
| * @param statType the status for this entry. |
| * @param revDate if applicable, the date that revocation took place. |
| * A value of {@code null} indicates that current time should be used. |
| * If the value of {@code statType} is not {@code CERT_STATUS_REVOKED}, |
| * then the {@code revDate} parameter is ignored. |
| */ |
| public CertStatusInfo(CertStatus statType, Date revDate) { |
| this(statType, revDate, null); |
| } |
| |
| /** |
| * Create a CertStatusInfo providing type, revocation date |
| * (if applicable) and revocation reason. |
| * |
| * @param statType the status for this entry. |
| * @param revDate if applicable, the date that revocation took place. |
| * A value of {@code null} indicates that current time should be used. |
| * If the value of {@code statType} is not {@code CERT_STATUS_REVOKED}, |
| * then the {@code revDate} parameter is ignored. |
| * @param revReason the reason the certificate was revoked. A value of |
| * {@code null} means that no reason was provided. |
| */ |
| public CertStatusInfo(CertStatus statType, Date revDate, |
| CRLReason revReason) { |
| Objects.requireNonNull(statType, "Cert Status must be non-null"); |
| certStatusType = statType; |
| switch (statType) { |
| case CERT_STATUS_GOOD: |
| case CERT_STATUS_UNKNOWN: |
| revocationTime = null; |
| break; |
| case CERT_STATUS_REVOKED: |
| revocationTime = revDate != null ? (Date)revDate.clone() : |
| new Date(); |
| break; |
| default: |
| throw new IllegalArgumentException("Unknown status type: " + |
| statType); |
| } |
| } |
| |
| /** |
| * Get the cert status type |
| * |
| * @return the status applied to this object (e.g. |
| * {@code CERT_STATUS_GOOD}, {@code CERT_STATUS_UNKNOWN}, etc.) |
| */ |
| public CertStatus getType() { |
| return certStatusType; |
| } |
| |
| /** |
| * Get the revocation time (if applicable). |
| * |
| * @return the revocation time as a {@code Date} object, or |
| * {@code null} if not applicable (i.e. if the certificate hasn't been |
| * revoked). |
| */ |
| public Date getRevocationTime() { |
| return (revocationTime != null ? (Date)revocationTime.clone() : |
| null); |
| } |
| |
| /** |
| * Get the revocation reason. |
| * |
| * @return the revocation reason, or {@code null} if one was not |
| * provided. |
| */ |
| public CRLReason getRevocationReason() { |
| return reason; |
| } |
| } |
| |
| /** |
| * Runnable task that handles incoming OCSP Requests and returns |
| * responses. |
| */ |
| private class OcspHandler implements Runnable { |
| private final Socket sock; |
| InetSocketAddress peerSockAddr; |
| |
| /** |
| * Construct an {@code OcspHandler}. |
| * |
| * @param incomingSocket the socket the server created on accept() |
| */ |
| private OcspHandler(Socket incomingSocket) { |
| sock = incomingSocket; |
| } |
| |
| /** |
| * Run the OCSP Request parser and construct a response to be sent |
| * back to the client. |
| */ |
| @Override |
| public void run() { |
| // If we have implemented a delay to simulate network latency |
| // wait out the delay here before any other processing. |
| try { |
| if (delayMsec > 0) { |
| Thread.sleep(delayMsec); |
| } |
| } catch (InterruptedException ie) { |
| // Just log the interrupted sleep |
| log("Delay of " + delayMsec + " milliseconds was interrupted"); |
| } |
| |
| try (Socket ocspSocket = sock; |
| InputStream in = ocspSocket.getInputStream(); |
| OutputStream out = ocspSocket.getOutputStream()) { |
| peerSockAddr = |
| (InetSocketAddress)ocspSocket.getRemoteSocketAddress(); |
| log("Received incoming connection from " + peerSockAddr); |
| String[] headerTokens = readLine(in).split(" "); |
| LocalOcspRequest ocspReq = null; |
| LocalOcspResponse ocspResp = null; |
| ResponseStatus respStat = ResponseStatus.INTERNAL_ERROR; |
| try { |
| if (headerTokens[0] != null) { |
| switch (headerTokens[0]) { |
| case "POST": |
| ocspReq = parseHttpOcspPost(in); |
| break; |
| case "GET": |
| // req = parseHttpOcspGet(in); |
| // TODO implement the GET parsing |
| throw new IOException("GET method unsupported"); |
| default: |
| respStat = ResponseStatus.MALFORMED_REQUEST; |
| throw new IOException("Not a GET or POST"); |
| } |
| } else { |
| respStat = ResponseStatus.MALFORMED_REQUEST; |
| throw new IOException("Unable to get HTTP method"); |
| } |
| |
| if (ocspReq != null) { |
| log(ocspReq.toString()); |
| // Get responses for all CertIds in the request |
| Map<CertId, CertStatusInfo> statusMap = |
| checkStatusDb(ocspReq.getRequests()); |
| if (statusMap.isEmpty()) { |
| respStat = ResponseStatus.UNAUTHORIZED; |
| } else { |
| ocspResp = new LocalOcspResponse( |
| ResponseStatus.SUCCESSFUL, statusMap, |
| ocspReq.getExtensions()); |
| } |
| } else { |
| respStat = ResponseStatus.MALFORMED_REQUEST; |
| throw new IOException("Found null request"); |
| } |
| } catch (IOException | RuntimeException exc) { |
| err(exc); |
| } |
| if (ocspResp == null) { |
| ocspResp = new LocalOcspResponse(respStat); |
| } |
| sendResponse(out, ocspResp); |
| } catch (IOException | CertificateException exc) { |
| err(exc); |
| } |
| } |
| |
| /** |
| * Send an OCSP response on an {@code OutputStream}. |
| * |
| * @param out the {@code OutputStream} on which to send the response. |
| * @param resp the OCSP response to send. |
| * |
| * @throws IOException if an encoding error occurs. |
| */ |
| public void sendResponse(OutputStream out, LocalOcspResponse resp) |
| throws IOException { |
| StringBuilder sb = new StringBuilder(); |
| |
| byte[] respBytes; |
| try { |
| respBytes = resp.getBytes(); |
| } catch (RuntimeException re) { |
| err(re); |
| return; |
| } |
| |
| sb.append("HTTP/1.0 200 OK\r\n"); |
| sb.append("Content-Type: application/ocsp-response\r\n"); |
| sb.append("Content-Length: ").append(respBytes.length); |
| sb.append("\r\n\r\n"); |
| |
| out.write(sb.toString().getBytes("UTF-8")); |
| out.write(respBytes); |
| log(resp.toString()); |
| } |
| |
| /** |
| * Parse the incoming HTTP POST of an OCSP Request. |
| * |
| * @param inStream the input stream from the socket bound to this |
| * {@code OcspHandler}. |
| * |
| * @return the OCSP Request as a {@code LocalOcspRequest} |
| * |
| * @throws IOException if there are network related issues or problems |
| * occur during parsing of the OCSP request. |
| * @throws CertificateException if one or more of the certificates in |
| * the OCSP request cannot be read/parsed. |
| */ |
| private LocalOcspRequest parseHttpOcspPost(InputStream inStream) |
| throws IOException, CertificateException { |
| boolean endOfHeader = false; |
| boolean properContentType = false; |
| int length = -1; |
| |
| while (!endOfHeader) { |
| String[] lineTokens = readLine(inStream).split(" "); |
| if (lineTokens[0].isEmpty()) { |
| endOfHeader = true; |
| } else if (lineTokens[0].equalsIgnoreCase("Content-Type:")) { |
| if (lineTokens[1] == null || |
| !lineTokens[1].equals( |
| "application/ocsp-request")) { |
| log("Unknown Content-Type: " + |
| (lineTokens[1] != null ? |
| lineTokens[1] : "<NULL>")); |
| return null; |
| } else { |
| properContentType = true; |
| log("Content-Type = " + lineTokens[1]); |
| } |
| } else if (lineTokens[0].equalsIgnoreCase("Content-Length:")) { |
| if (lineTokens[1] != null) { |
| length = Integer.parseInt(lineTokens[1]); |
| log("Content-Length = " + length); |
| } |
| } |
| } |
| |
| // Okay, make sure we got what we needed from the header, then |
| // read the remaining OCSP Request bytes |
| if (properContentType && length >= 0) { |
| byte[] ocspBytes = new byte[length]; |
| inStream.read(ocspBytes); |
| return new LocalOcspRequest(ocspBytes); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Read a line of text that is CRLF-delimited. |
| * |
| * @param is the {@code InputStream} tied to the socket |
| * for this {@code OcspHandler} |
| * |
| * @return a {@code String} consisting of the line of text |
| * read from the stream with the CRLF stripped. |
| * |
| * @throws IOException if any I/O error occurs. |
| */ |
| private String readLine(InputStream is) throws IOException { |
| PushbackInputStream pbis = new PushbackInputStream(is); |
| ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
| boolean done = false; |
| while (!done) { |
| byte b = (byte)pbis.read(); |
| if (b == '\r') { |
| byte bNext = (byte)pbis.read(); |
| if (bNext == '\n' || bNext == -1) { |
| done = true; |
| } else { |
| pbis.unread(bNext); |
| bos.write(b); |
| } |
| } else if (b == -1) { |
| done = true; |
| } else { |
| bos.write(b); |
| } |
| } |
| |
| return new String(bos.toByteArray(), "UTF-8"); |
| } |
| } |
| |
| |
| /** |
| * Simple nested class to handle OCSP requests without making |
| * changes to sun.security.provider.certpath.OCSPRequest |
| */ |
| public class LocalOcspRequest { |
| |
| private byte[] nonce; |
| private byte[] signature = null; |
| private AlgorithmId algId = null; |
| private int version = 0; |
| private GeneralName requestorName = null; |
| private Map<String, Extension> extensions = Collections.emptyMap(); |
| private final List<LocalSingleRequest> requestList = new ArrayList<>(); |
| private final List<X509Certificate> certificates = new ArrayList<>(); |
| |
| /** |
| * Construct a {@code LocalOcspRequest} from its DER encoding. |
| * |
| * @param requestBytes the DER-encoded bytes |
| * |
| * @throws IOException if decoding errors occur |
| * @throws CertificateException if certificates are found in the |
| * OCSP request and they do not parse correctly. |
| */ |
| private LocalOcspRequest(byte[] requestBytes) throws IOException, |
| CertificateException { |
| Objects.requireNonNull(requestBytes, "Received null input"); |
| |
| DerInputStream dis = new DerInputStream(requestBytes); |
| |
| // Parse the top-level structure, it should have no more than |
| // two elements. |
| DerValue[] topStructs = dis.getSequence(2); |
| for (DerValue dv : topStructs) { |
| if (dv.tag == DerValue.tag_Sequence) { |
| parseTbsRequest(dv); |
| } else if (dv.isContextSpecific((byte)0)) { |
| parseSignature(dv); |
| } else { |
| throw new IOException("Unknown tag at top level: " + |
| dv.tag); |
| } |
| } |
| } |
| |
| /** |
| * Parse the signature block from an OCSP request |
| * |
| * @param sigSequence a {@code DerValue} containing the signature |
| * block at the outer sequence datum. |
| * |
| * @throws IOException if any non-certificate-based parsing errors occur |
| * @throws CertificateException if certificates are found in the |
| * OCSP request and they do not parse correctly. |
| */ |
| private void parseSignature(DerValue sigSequence) |
| throws IOException, CertificateException { |
| DerValue[] sigItems = sigSequence.data.getSequence(3); |
| if (sigItems.length != 3) { |
| throw new IOException("Invalid number of signature items: " + |
| "expected 3, got " + sigItems.length); |
| } |
| |
| algId = AlgorithmId.parse(sigItems[0]); |
| signature = sigItems[1].getBitString(); |
| |
| if (sigItems[2].isContextSpecific((byte)0)) { |
| DerValue[] certDerItems = sigItems[2].data.getSequence(4); |
| int i = 0; |
| for (DerValue dv : certDerItems) { |
| X509Certificate xc = new X509CertImpl(dv); |
| certificates.add(xc); |
| } |
| } else { |
| throw new IOException("Invalid tag in signature block: " + |
| sigItems[2].tag); |
| } |
| } |
| |
| /** |
| * Parse the to-be-signed request data |
| * |
| * @param tbsReqSeq a {@code DerValue} object containing the to-be- |
| * signed OCSP request at the outermost SEQUENCE tag. |
| * @throws IOException if any parsing errors occur |
| */ |
| private void parseTbsRequest(DerValue tbsReqSeq) throws IOException { |
| while (tbsReqSeq.data.available() > 0) { |
| DerValue dv = tbsReqSeq.data.getDerValue(); |
| if (dv.isContextSpecific((byte)0)) { |
| // The version was explicitly called out |
| version = dv.data.getInteger(); |
| } else if (dv.isContextSpecific((byte)1)) { |
| // A GeneralName was provided |
| requestorName = new GeneralName(dv.data.getDerValue()); |
| } else if (dv.isContextSpecific((byte)2)) { |
| // Parse the extensions |
| DerValue[] extItems = dv.data.getSequence(2); |
| extensions = parseExtensions(extItems); |
| } else if (dv.tag == DerValue.tag_Sequence) { |
| while (dv.data.available() > 0) { |
| requestList.add(new LocalSingleRequest(dv.data)); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Parse a SEQUENCE of extensions. This routine is used both |
| * at the overall request level and down at the singleRequest layer. |
| * |
| * @param extDerItems an array of {@code DerValue} items, each one |
| * consisting of a DER-encoded extension. |
| * |
| * @return a {@code Map} of zero or more extensions, |
| * keyed by its object identifier in {@code String} form. |
| * |
| * @throws IOException if any parsing errors occur. |
| */ |
| private Map<String, Extension> parseExtensions(DerValue[] extDerItems) |
| throws IOException { |
| Map<String, Extension> extMap = new HashMap<>(); |
| |
| if (extDerItems != null && extDerItems.length != 0) { |
| for (DerValue extDerVal : extDerItems) { |
| sun.security.x509.Extension ext = |
| new sun.security.x509.Extension(extDerVal); |
| extMap.put(ext.getId(), ext); |
| } |
| } |
| |
| return extMap; |
| } |
| |
| /** |
| * Return the list of single request objects in this OCSP request. |
| * |
| * @return an unmodifiable {@code List} of zero or more requests. |
| */ |
| private List<LocalSingleRequest> getRequests() { |
| return Collections.unmodifiableList(requestList); |
| } |
| |
| /** |
| * Return the list of X.509 Certificates in this OCSP request. |
| * |
| * @return an unmodifiable {@code List} of zero or more |
| * {@cpde X509Certificate} objects. |
| */ |
| private List<X509Certificate> getCertificates() { |
| return Collections.unmodifiableList(certificates); |
| } |
| |
| /** |
| * Return the map of OCSP request extensions. |
| * |
| * @return an unmodifiable {@code Map} of zero or more |
| * {@code Extension} objects, keyed by their object identifiers |
| * in {@code String} form. |
| */ |
| private Map<String, Extension> getExtensions() { |
| return Collections.unmodifiableMap(extensions); |
| } |
| |
| /** |
| * Display the {@code LocalOcspRequest} in human readable form. |
| * |
| * @return a {@code String} representation of the |
| * {@code LocalOcspRequest} |
| */ |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| |
| sb.append(String.format("OCSP Request: Version %d (0x%X)", |
| version + 1, version)).append("\n"); |
| if (requestorName != null) { |
| sb.append("Requestor Name: ").append(requestorName). |
| append("\n"); |
| } |
| |
| int requestCtr = 0; |
| for (LocalSingleRequest lsr : requestList) { |
| sb.append("Request [").append(requestCtr++).append("]\n"); |
| sb.append(lsr).append("\n"); |
| } |
| if (!extensions.isEmpty()) { |
| sb.append("Extensions (").append(extensions.size()). |
| append(")\n"); |
| for (Extension ext : extensions.values()) { |
| sb.append("\t").append(ext).append("\n"); |
| } |
| } |
| if (signature != null) { |
| sb.append("Signature: ").append(algId).append("\n"); |
| sb.append(dumpHexBytes(signature)).append("\n"); |
| int certCtr = 0; |
| for (X509Certificate cert : certificates) { |
| sb.append("Certificate [").append(certCtr++).append("]"). |
| append("\n"); |
| sb.append("\tSubject: "); |
| sb.append(cert.getSubjectX500Principal()).append("\n"); |
| sb.append("\tIssuer: "); |
| sb.append(cert.getIssuerX500Principal()).append("\n"); |
| sb.append("\tSerial: ").append(cert.getSerialNumber()); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Inner class designed to handle the decoding/representation of |
| * single requests within a {@code LocalOcspRequest} object. |
| */ |
| public class LocalSingleRequest { |
| private final CertId cid; |
| private Map<String, Extension> extensions = Collections.emptyMap(); |
| |
| private LocalSingleRequest(DerInputStream dis) |
| throws IOException { |
| DerValue[] srItems = dis.getSequence(2); |
| |
| // There should be 1, possibly 2 DerValue items |
| if (srItems.length == 1 || srItems.length == 2) { |
| // The first parsable item should be the mandatory CertId |
| cid = new CertId(srItems[0].data); |
| if (srItems.length == 2) { |
| if (srItems[1].isContextSpecific((byte)0)) { |
| DerValue[] extDerItems = srItems[1].data.getSequence(2); |
| extensions = parseExtensions(extDerItems); |
| } else { |
| throw new IOException("Illegal tag in Request " + |
| "extensions: " + srItems[1].tag); |
| } |
| } |
| } else { |
| throw new IOException("Invalid number of items in " + |
| "Request (" + srItems.length + ")"); |
| } |
| } |
| |
| /** |
| * Get the {@code CertId} for this single request. |
| * |
| * @return the {@code CertId} for this single request. |
| */ |
| private CertId getCertId() { |
| return cid; |
| } |
| |
| /** |
| * Return the map of single request extensions. |
| * |
| * @return an unmodifiable {@code Map} of zero or more |
| * {@code Extension} objects, keyed by their object identifiers |
| * in {@code String} form. |
| */ |
| private Map<String, Extension> getExtensions() { |
| return Collections.unmodifiableMap(extensions); |
| } |
| |
| /** |
| * Display the {@code LocalSingleRequest} in human readable form. |
| * |
| * @return a {@code String} representation of the |
| * {@code LocalSingleRequest} |
| */ |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("CertId, Algorithm = "); |
| sb.append(cid.getHashAlgorithm()).append("\n"); |
| sb.append("\tIssuer Name Hash: "); |
| sb.append(dumpHexBytes(cid.getIssuerNameHash(), 256, "", "")); |
| sb.append("\n"); |
| sb.append("\tIssuer Key Hash: "); |
| sb.append(dumpHexBytes(cid.getIssuerKeyHash(), 256, "", "")); |
| sb.append("\n"); |
| sb.append("\tSerial Number: ").append(cid.getSerialNumber()); |
| if (!extensions.isEmpty()) { |
| sb.append("Extensions (").append(extensions.size()). |
| append(")\n"); |
| for (Extension ext : extensions.values()) { |
| sb.append("\t").append(ext).append("\n"); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| } |
| } |
| |
| /** |
| * Simple nested class to handle OCSP requests without making |
| * changes to sun.security.provider.certpath.OCSPResponse |
| */ |
| public class LocalOcspResponse { |
| private final int version = 0; |
| private final OCSPResponse.ResponseStatus responseStatus; |
| private final Map<CertId, CertStatusInfo> respItemMap; |
| private final Date producedAtDate; |
| private final List<LocalSingleResponse> singleResponseList = |
| new ArrayList<>(); |
| private final Map<String, Extension> responseExtensions; |
| private byte[] signature; |
| private final List<X509Certificate> certificates; |
| private final byte[] encodedResponse; |
| |
| /** |
| * Constructor for the generation of non-successful responses |
| * |
| * @param respStat the OCSP response status. |
| * |
| * @throws IOException if an error happens during encoding |
| * @throws NullPointerException if {@code respStat} is {@code null} |
| * or {@code respStat} is successful. |
| */ |
| public LocalOcspResponse(OCSPResponse.ResponseStatus respStat) |
| throws IOException { |
| this(respStat, null, null); |
| } |
| |
| /** |
| * Construct a response from a list of certificate |
| * status objects and extensions. |
| * |
| * @param respStat the status of the entire response |
| * @param itemMap a {@code Map} of {@code CertId} objects and their |
| * respective revocation statuses from the server's response DB. |
| * @param reqExtensions a {@code Map} of request extensions |
| * |
| * @throws IOException if an error happens during encoding |
| * @throws NullPointerException if {@code respStat} is {@code null} |
| * or {@code respStat} is successful, and a {@code null} {@code itemMap} |
| * has been provided. |
| */ |
| public LocalOcspResponse(OCSPResponse.ResponseStatus respStat, |
| Map<CertId, CertStatusInfo> itemMap, |
| Map<String, Extension> reqExtensions) throws IOException { |
| responseStatus = Objects.requireNonNull(respStat, |
| "Illegal null response status"); |
| if (responseStatus == ResponseStatus.SUCCESSFUL) { |
| respItemMap = Objects.requireNonNull(itemMap, |
| "SUCCESSFUL responses must have a response map"); |
| producedAtDate = new Date(); |
| |
| // Turn the answerd from the response DB query into a list |
| // of single responses. |
| for (CertId id : itemMap.keySet()) { |
| singleResponseList.add( |
| new LocalSingleResponse(id, itemMap.get(id))); |
| } |
| |
| responseExtensions = setResponseExtensions(reqExtensions); |
| certificates = new ArrayList<>(); |
| if (signerCert != issuerCert) { |
| certificates.add(signerCert); |
| } |
| certificates.add(issuerCert); |
| } else { |
| respItemMap = null; |
| producedAtDate = null; |
| responseExtensions = null; |
| certificates = null; |
| } |
| encodedResponse = this.getBytes(); |
| } |
| |
| /** |
| * Set the response extensions based on the request extensions |
| * that were received. Right now, this is limited to the |
| * OCSP nonce extension. |
| * |
| * @param reqExts a {@code Map} of zero or more request extensions |
| * |
| * @return a {@code Map} of zero or more response extensions, keyed |
| * by the extension object identifier in {@code String} form. |
| */ |
| private Map<String, Extension> setResponseExtensions( |
| Map<String, Extension> reqExts) { |
| Map<String, Extension> respExts = new HashMap<>(); |
| String ocspNonceStr = PKIXExtensions.OCSPNonce_Id.toString(); |
| |
| if (reqExts != null) { |
| for (String id : reqExts.keySet()) { |
| if (id.equals(ocspNonceStr)) { |
| // We found a nonce, add it into the response extensions |
| Extension ext = reqExts.get(id); |
| if (ext != null) { |
| respExts.put(id, ext); |
| log("Added OCSP Nonce to response"); |
| } else { |
| log("Error: Found nonce entry, but found null " + |
| "value. Skipping"); |
| } |
| } |
| } |
| } |
| |
| return respExts; |
| } |
| |
| /** |
| * Get the DER-encoded response bytes for this response |
| * |
| * @return a byte array containing the DER-encoded bytes for |
| * the response |
| * |
| * @throws IOException if any encoding errors occur |
| */ |
| private byte[] getBytes() throws IOException { |
| DerOutputStream outerSeq = new DerOutputStream(); |
| DerOutputStream responseStream = new DerOutputStream(); |
| responseStream.putEnumerated(responseStatus.ordinal()); |
| if (responseStatus == ResponseStatus.SUCCESSFUL && |
| respItemMap != null) { |
| encodeResponseBytes(responseStream); |
| } |
| |
| // Commit the outermost sequence bytes |
| outerSeq.write(DerValue.tag_Sequence, responseStream); |
| return outerSeq.toByteArray(); |
| } |
| |
| private void encodeResponseBytes(DerOutputStream responseStream) |
| throws IOException { |
| DerOutputStream explicitZero = new DerOutputStream(); |
| DerOutputStream respItemStream = new DerOutputStream(); |
| |
| respItemStream.putOID(OCSP_BASIC_RESPONSE_OID); |
| |
| byte[] basicOcspBytes = encodeBasicOcspResponse(); |
| respItemStream.putOctetString(basicOcspBytes); |
| explicitZero.write(DerValue.tag_Sequence, respItemStream); |
| responseStream.write(DerValue.createTag(DerValue.TAG_CONTEXT, |
| true, (byte)0), explicitZero); |
| } |
| |
| private byte[] encodeBasicOcspResponse() throws IOException { |
| DerOutputStream outerSeq = new DerOutputStream(); |
| DerOutputStream basicORItemStream = new DerOutputStream(); |
| |
| // Encode the tbsResponse |
| byte[] tbsResponseBytes = encodeTbsResponse(); |
| basicORItemStream.write(tbsResponseBytes); |
| |
| try { |
| sigAlgId.derEncode(basicORItemStream); |
| |
| // Create the signature |
| Signature sig = Signature.getInstance(sigAlgId.getName()); |
| sig.initSign(signerKey); |
| sig.update(tbsResponseBytes); |
| signature = sig.sign(); |
| basicORItemStream.putBitString(signature); |
| } catch (GeneralSecurityException exc) { |
| err(exc); |
| throw new IOException(exc); |
| } |
| |
| // Add certificates |
| try { |
| DerOutputStream certStream = new DerOutputStream(); |
| ArrayList<DerValue> certList = new ArrayList<>(); |
| if (signerCert != issuerCert) { |
| certList.add(new DerValue(signerCert.getEncoded())); |
| } |
| certList.add(new DerValue(issuerCert.getEncoded())); |
| DerValue[] dvals = new DerValue[certList.size()]; |
| certStream.putSequence(certList.toArray(dvals)); |
| basicORItemStream.write(DerValue.createTag(DerValue.TAG_CONTEXT, |
| true, (byte)0), certStream); |
| } catch (CertificateEncodingException cex) { |
| err(cex); |
| throw new IOException(cex); |
| } |
| |
| // Commit the outermost sequence bytes |
| outerSeq.write(DerValue.tag_Sequence, basicORItemStream); |
| return outerSeq.toByteArray(); |
| } |
| |
| private byte[] encodeTbsResponse() throws IOException { |
| DerOutputStream outerSeq = new DerOutputStream(); |
| DerOutputStream tbsStream = new DerOutputStream(); |
| |
| // Note: We're not going explicitly assert the version |
| tbsStream.write(respId.getEncoded()); |
| tbsStream.putGeneralizedTime(producedAtDate); |
| |
| // Sequence of responses |
| encodeSingleResponses(tbsStream); |
| |
| // TODO: add response extension support |
| encodeExtensions(tbsStream); |
| |
| outerSeq.write(DerValue.tag_Sequence, tbsStream); |
| return outerSeq.toByteArray(); |
| } |
| |
| private void encodeSingleResponses(DerOutputStream tbsStream) |
| throws IOException { |
| DerValue[] srDerVals = new DerValue[singleResponseList.size()]; |
| int srDvCtr = 0; |
| |
| for (LocalSingleResponse lsr : singleResponseList) { |
| srDerVals[srDvCtr++] = new DerValue(lsr.getBytes()); |
| } |
| |
| tbsStream.putSequence(srDerVals); |
| } |
| |
| private void encodeExtensions(DerOutputStream tbsStream) |
| throws IOException { |
| DerOutputStream extSequence = new DerOutputStream(); |
| DerOutputStream extItems = new DerOutputStream(); |
| |
| for (Extension ext : responseExtensions.values()) { |
| ext.encode(extItems); |
| } |
| extSequence.write(DerValue.tag_Sequence, extItems); |
| tbsStream.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, |
| (byte)1), extSequence); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| |
| sb.append("OCSP Response: ").append(responseStatus).append("\n"); |
| if (responseStatus == ResponseStatus.SUCCESSFUL) { |
| sb.append("Response Type: "). |
| append(OCSP_BASIC_RESPONSE_OID.toString()).append("\n"); |
| sb.append(String.format("Version: %d (0x%X)", version + 1, |
| version)).append("\n"); |
| sb.append("Responder Id: ").append(respId.toString()). |
| append("\n"); |
| sb.append("Produced At: "). |
| append(utcDateFmt.format(producedAtDate)).append("\n"); |
| |
| int srCtr = 0; |
| for (LocalSingleResponse lsr : singleResponseList) { |
| sb.append("SingleResponse [").append(srCtr++).append("]\n"); |
| sb.append(lsr); |
| } |
| |
| if (!responseExtensions.isEmpty()) { |
| sb.append("Extensions (").append(responseExtensions.size()). |
| append(")\n"); |
| for (Extension ext : responseExtensions.values()) { |
| sb.append("\t").append(ext).append("\n"); |
| } |
| } else { |
| sb.append("\n"); |
| } |
| |
| if (signature != null) { |
| sb.append("Signature: ").append(sigAlgId).append("\n"); |
| sb.append(dumpHexBytes(signature)).append("\n"); |
| int certCtr = 0; |
| for (X509Certificate cert : certificates) { |
| sb.append("Certificate [").append(certCtr++).append("]"). |
| append("\n"); |
| sb.append("\tSubject: "); |
| sb.append(cert.getSubjectX500Principal()).append("\n"); |
| sb.append("\tIssuer: "); |
| sb.append(cert.getIssuerX500Principal()).append("\n"); |
| sb.append("\tSerial: ").append(cert.getSerialNumber()); |
| sb.append("\n"); |
| } |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| private class LocalSingleResponse { |
| private final CertId certId; |
| private final CertStatusInfo csInfo; |
| private final Date thisUpdate; |
| private final Date lsrNextUpdate; |
| private final Map<String, Extension> singleExtensions; |
| |
| public LocalSingleResponse(CertId cid, CertStatusInfo info) { |
| certId = Objects.requireNonNull(cid, "CertId must be non-null"); |
| csInfo = Objects.requireNonNull(info, |
| "CertStatusInfo must be non-null"); |
| |
| // For now, we'll keep things simple and make the thisUpdate |
| // field the same as the producedAt date. |
| thisUpdate = producedAtDate; |
| lsrNextUpdate = getNextUpdate(); |
| |
| // TODO Add extensions support |
| singleExtensions = Collections.emptyMap(); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("Certificate Status: ").append(csInfo.getType()); |
| sb.append("\n"); |
| if (csInfo.getType() == CertStatus.CERT_STATUS_REVOKED) { |
| sb.append("Revocation Time: "); |
| sb.append(utcDateFmt.format(csInfo.getRevocationTime())); |
| sb.append("\n"); |
| if (csInfo.getRevocationReason() != null) { |
| sb.append("Revocation Reason: "); |
| sb.append(csInfo.getRevocationReason()).append("\n"); |
| } |
| } |
| |
| sb.append("CertId, Algorithm = "); |
| sb.append(certId.getHashAlgorithm()).append("\n"); |
| sb.append("\tIssuer Name Hash: "); |
| sb.append(dumpHexBytes(certId.getIssuerNameHash(), 256, "", "")); |
| sb.append("\n"); |
| sb.append("\tIssuer Key Hash: "); |
| sb.append(dumpHexBytes(certId.getIssuerKeyHash(), 256, "", "")); |
| sb.append("\n"); |
| sb.append("\tSerial Number: ").append(certId.getSerialNumber()); |
| sb.append("\n"); |
| sb.append("This Update: "); |
| sb.append(utcDateFmt.format(thisUpdate)).append("\n"); |
| if (lsrNextUpdate != null) { |
| sb.append("Next Update: "); |
| sb.append(utcDateFmt.format(lsrNextUpdate)).append("\n"); |
| } |
| |
| if (!singleExtensions.isEmpty()) { |
| sb.append("Extensions (").append(singleExtensions.size()). |
| append(")\n"); |
| for (Extension ext : singleExtensions.values()) { |
| sb.append("\t").append(ext).append("\n"); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| public byte[] getBytes() throws IOException { |
| byte[] nullData = { }; |
| DerOutputStream responseSeq = new DerOutputStream(); |
| DerOutputStream srStream = new DerOutputStream(); |
| |
| // Encode the CertId |
| certId.encode(srStream); |
| |
| // Next, encode the CertStatus field |
| CertStatus csiType = csInfo.getType(); |
| switch (csiType) { |
| case CERT_STATUS_GOOD: |
| srStream.write(DerValue.createTag(DerValue.TAG_CONTEXT, |
| false, (byte)0), nullData); |
| break; |
| case CERT_STATUS_REVOKED: |
| DerOutputStream revInfo = new DerOutputStream(); |
| revInfo.putGeneralizedTime(csInfo.getRevocationTime()); |
| CRLReason revReason = csInfo.getRevocationReason(); |
| if (revReason != null) { |
| byte[] revDer = new byte[3]; |
| revDer[0] = DerValue.tag_Enumerated; |
| revDer[1] = 1; |
| revDer[2] = (byte)revReason.ordinal(); |
| revInfo.write(DerValue.createTag( |
| DerValue.TAG_CONTEXT, true, (byte)0), |
| revDer); |
| } |
| srStream.write(DerValue.createTag( |
| DerValue.TAG_CONTEXT, true, (byte)1), |
| revInfo); |
| break; |
| case CERT_STATUS_UNKNOWN: |
| srStream.write(DerValue.createTag(DerValue.TAG_CONTEXT, |
| false, (byte)2), nullData); |
| break; |
| default: |
| throw new IOException("Unknown CertStatus: " + csiType); |
| } |
| |
| // Add the necessary dates |
| srStream.putGeneralizedTime(thisUpdate); |
| if (lsrNextUpdate != null) { |
| DerOutputStream nuStream = new DerOutputStream(); |
| nuStream.putGeneralizedTime(lsrNextUpdate); |
| srStream.write(DerValue.createTag(DerValue.TAG_CONTEXT, |
| true, (byte)0), nuStream); |
| } |
| |
| // TODO add singleResponse Extension support |
| |
| // Add the single response to the response output stream |
| responseSeq.write(DerValue.tag_Sequence, srStream); |
| return responseSeq.toByteArray(); |
| } |
| } |
| } |
| } |