001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2016 Richard "Shred" Körber 005 * http://acme4j.shredzone.org 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 013 */ 014package org.shredzone.acme4j; 015 016import java.net.HttpURLConnection; 017import java.net.MalformedURLException; 018import java.net.URL; 019import java.security.cert.CertificateEncodingException; 020import java.security.cert.X509Certificate; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.List; 024 025import org.shredzone.acme4j.connector.Connection; 026import org.shredzone.acme4j.connector.Resource; 027import org.shredzone.acme4j.exception.AcmeException; 028import org.shredzone.acme4j.exception.AcmeProtocolException; 029import org.shredzone.acme4j.exception.AcmeRetryAfterException; 030import org.shredzone.acme4j.toolbox.JSONBuilder; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034/** 035 * Represents a certificate and its certificate chain. 036 */ 037public class Certificate extends AcmeResource { 038 private static final long serialVersionUID = 7381527770159084201L; 039 private static final Logger LOG = LoggerFactory.getLogger(Certificate.class); 040 private static final int MAX_CHAIN_LENGTH = 10; 041 042 private URL chainCertUrl; 043 private X509Certificate cert = null; 044 private X509Certificate[] chain = null; 045 046 protected Certificate(Session session, URL certUrl) { 047 super(session); 048 setLocation(certUrl); 049 } 050 051 protected Certificate(Session session, URL certUrl, URL chainUrl, X509Certificate cert) { 052 super(session); 053 setLocation(certUrl); 054 this.chainCertUrl = chainUrl; 055 this.cert = cert; 056 } 057 058 /** 059 * Creates a new instance of {@link Certificate} and binds it to the {@link Session}. 060 * 061 * @param session 062 * {@link Session} to be used 063 * @param location 064 * Location of the Certificate 065 * @return {@link Certificate} bound to the session and location 066 */ 067 public static Certificate bind(Session session, URL location) { 068 return new Certificate(session, location); 069 } 070 071 /** 072 * Returns the URL of the certificate chain. {@code null} if not known or not 073 * available. 074 */ 075 public URL getChainLocation() { 076 return chainCertUrl; 077 } 078 079 /** 080 * Downloads the certificate. The result is only the end-entity certificate, without 081 * the issuer chain. 082 * <p> 083 * The result is cached. 084 * 085 * @return {@link X509Certificate} that was downloaded 086 * @throws AcmeRetryAfterException 087 * the certificate is still being created, and the server returned an 088 * estimated date when it will be ready for download. You should wait for 089 * the date given in {@link AcmeRetryAfterException#getRetryAfter()} 090 * before trying again. 091 * @see #downloadFullChain() 092 */ 093 public X509Certificate download() throws AcmeException { 094 if (cert == null) { 095 LOG.debug("download"); 096 try (Connection conn = getSession().provider().connect()) { 097 conn.sendRequest(getLocation(), getSession()); 098 conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED); 099 conn.handleRetryAfter("certificate is not available for download yet"); 100 101 chainCertUrl = conn.getLink("up"); 102 cert = conn.readCertificate(); 103 } 104 } 105 return cert; 106 } 107 108 /** 109 * Downloads the issuer chain. The result does not contain the end-entity certificate. 110 * <p> 111 * The result is cached. 112 * 113 * @return Chain of {@link X509Certificate}s 114 * @throws AcmeRetryAfterException 115 * the certificate is still being created, and the server returned an 116 * estimated date when it will be ready for download. You should wait for 117 * the date given in {@link AcmeRetryAfterException#getRetryAfter()} 118 * before trying again. 119 */ 120 public X509Certificate[] downloadChain() throws AcmeException { 121 if (chain == null) { 122 if (chainCertUrl == null) { 123 download(); 124 } 125 126 if (chainCertUrl == null) { 127 throw new AcmeProtocolException("No certificate chain provided"); 128 } 129 130 LOG.debug("downloadChain"); 131 132 List<X509Certificate> certChain = new ArrayList<>(); 133 URL link = chainCertUrl; 134 while (link != null && certChain.size() < MAX_CHAIN_LENGTH) { 135 try (Connection conn = getSession().provider().connect()) { 136 conn.sendRequest(link, getSession()); 137 conn.accept(HttpURLConnection.HTTP_OK); 138 139 certChain.add(conn.readCertificate()); 140 link = conn.getLink("up"); 141 } 142 } 143 if (link != null) { 144 throw new AcmeProtocolException("Recursion limit reached (" + MAX_CHAIN_LENGTH 145 + "). Didn't get " + link); 146 } 147 148 chain = certChain.toArray(new X509Certificate[certChain.size()]); 149 } 150 return Arrays.copyOf(chain, chain.length); 151 } 152 153 /** 154 * Downloads the certificate and the corresponding issuer chain. The issued 155 * certificate is always on index 0, followed by the CA certificates, with the root CA 156 * being at the last index. 157 * <p> 158 * The result is cached. 159 * 160 * @return Chain of {@link X509Certificate}s 161 * @throws AcmeRetryAfterException 162 * the certificate is still being created, and the server returned an 163 * estimated date when it will be ready for download. You should wait for 164 * the date given in {@link AcmeRetryAfterException#getRetryAfter()} 165 * before trying again. 166 * @since 1.1 167 */ 168 public X509Certificate[] downloadFullChain() throws AcmeException { 169 downloadChain(); 170 171 X509Certificate[] result = new X509Certificate[chain.length + 1]; 172 result[0] = cert; 173 System.arraycopy(chain, 0, result, 1, chain.length); 174 return result; 175 } 176 177 /** 178 * Revokes this certificate. 179 */ 180 public void revoke() throws AcmeException { 181 revoke(null); 182 } 183 184 /** 185 * Revokes this certificate. 186 * 187 * @param reason 188 * {@link RevocationReason} stating the reason of the revocation that is 189 * used when generating OCSP responses and CRLs. {@code null} to give no 190 * reason. 191 */ 192 public void revoke(RevocationReason reason) throws AcmeException { 193 LOG.debug("revoke"); 194 URL resUrl = getSession().resourceUrl(Resource.REVOKE_CERT); 195 if (resUrl == null) { 196 throw new AcmeProtocolException("CA does not support certificate revocation"); 197 } 198 199 if (cert == null) { 200 download(); 201 } 202 203 try (Connection conn = getSession().provider().connect()) { 204 JSONBuilder claims = new JSONBuilder(); 205 claims.putResource(Resource.REVOKE_CERT); 206 claims.putBase64("certificate", cert.getEncoded()); 207 if (reason != null) { 208 claims.put("reason", reason.getReasonCode()); 209 } 210 211 conn.sendSignedRequest(resUrl, claims, getSession()); 212 conn.accept(HttpURLConnection.HTTP_OK); 213 } catch (CertificateEncodingException ex) { 214 throw new AcmeProtocolException("Invalid certificate", ex); 215 } 216 } 217 218 /** 219 * Revoke a certificate. This call is meant to be used for revoking certificates if 220 * the account's key pair was lost. 221 * 222 * @param session 223 * {@link Session} to be used. Here you can also generate a session by 224 * using the key pair that was used for signing the CSR. 225 * @param cert 226 * {@link X509Certificate} to be revoked 227 * @param reason 228 * {@link RevocationReason} stating the reason of the revocation that is 229 * used when generating OCSP responses and CRLs. {@code null} to give no 230 * reason. 231 */ 232 public static void revoke(Session session, X509Certificate cert, 233 RevocationReason reason) throws AcmeException { 234 try { 235 URL dummyUrl = new URL("http://"); 236 new Certificate(session, dummyUrl, null, cert).revoke(reason); 237 } catch (MalformedURLException ex) { 238 throw new InternalError(ex); 239 } 240 } 241 242}