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 static java.util.Collections.unmodifiableList; 017 018import java.io.IOException; 019import java.io.Writer; 020import java.net.URL; 021import java.security.KeyPair; 022import java.security.cert.CertificateEncodingException; 023import java.security.cert.X509Certificate; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.List; 027import java.util.stream.Collectors; 028 029import edu.umd.cs.findbugs.annotations.Nullable; 030import org.shredzone.acme4j.connector.Connection; 031import org.shredzone.acme4j.connector.Resource; 032import org.shredzone.acme4j.exception.AcmeException; 033import org.shredzone.acme4j.exception.AcmeLazyLoadingException; 034import org.shredzone.acme4j.exception.AcmeProtocolException; 035import org.shredzone.acme4j.toolbox.AcmeUtils; 036import org.shredzone.acme4j.toolbox.JSONBuilder; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040/** 041 * Represents a certificate and its certificate chain. 042 * <p> 043 * Note that a certificate is immutable once it is issued. For renewal, a new certificate 044 * must be ordered. 045 */ 046public class Certificate extends AcmeResource { 047 private static final long serialVersionUID = 7381527770159084201L; 048 private static final Logger LOG = LoggerFactory.getLogger(Certificate.class); 049 050 private @Nullable ArrayList<X509Certificate> certChain; 051 private @Nullable ArrayList<URL> alternates; 052 053 protected Certificate(Login login, URL certUrl) { 054 super(login, certUrl); 055 } 056 057 /** 058 * Downloads the certificate chain. 059 * <p> 060 * The certificate is downloaded lazily by the other methods. So usually there is no 061 * need to invoke this method, unless the download is to be enforced. If the 062 * certificate has been downloaded already, nothing will happen. 063 * 064 * @throws AcmeException 065 * if the certificate could not be downloaded 066 */ 067 public void download() throws AcmeException { 068 if (certChain == null) { 069 LOG.debug("download"); 070 try (Connection conn = getSession().connect()) { 071 conn.sendCertificateRequest(getLocation(), getLogin()); 072 alternates = new ArrayList<>(conn.getLinks("alternate")); 073 certChain = new ArrayList<>(conn.readCertificates()); 074 } 075 } 076 } 077 078 /** 079 * Returns the created certificate. 080 * 081 * @return The created end-entity {@link X509Certificate} without issuer chain. 082 */ 083 public X509Certificate getCertificate() { 084 lazyDownload(); 085 return certChain.get(0); 086 } 087 088 /** 089 * Returns the created certificate and issuer chain. 090 * 091 * @return The created end-entity {@link X509Certificate} and issuer chain. The first 092 * certificate is always the end-entity certificate, followed by the 093 * intermediate certificates required to build a path to a trusted root. 094 */ 095 public List<X509Certificate> getCertificateChain() { 096 lazyDownload(); 097 return unmodifiableList(certChain); 098 } 099 100 /** 101 * Returns URLs to alternate certificate chains. 102 * 103 * @return Alternate certificate chains, or empty if there are none. 104 */ 105 public List<URL> getAlternates() { 106 lazyDownload(); 107 if (alternates != null) { 108 return unmodifiableList(alternates); 109 } else { 110 return Collections.emptyList(); 111 } 112 } 113 114 /** 115 * Returns alternate certificate chains, if available. 116 * 117 * @return Alternate certificate chains, or empty if there are none. 118 * @since 2.11 119 */ 120 public List<Certificate> getAlternateCertificates() { 121 Login login = getLogin(); 122 return getAlternates().stream() 123 .map(login::bindCertificate) 124 .collect(Collectors.toList()); 125 } 126 127 /** 128 * Writes the certificate to the given writer. It is written in PEM format, with the 129 * end-entity cert coming first, followed by the intermediate ceritificates. 130 * 131 * @param out 132 * {@link Writer} to write to. The writer is not closed after use. 133 */ 134 public void writeCertificate(Writer out) throws IOException { 135 try { 136 for (X509Certificate cert : getCertificateChain()) { 137 AcmeUtils.writeToPem(cert.getEncoded(), AcmeUtils.PemLabel.CERTIFICATE, out); 138 } 139 } catch (CertificateEncodingException ex) { 140 throw new IOException("Encoding error", ex); 141 } 142 } 143 144 /** 145 * Revokes this certificate. 146 */ 147 public void revoke() throws AcmeException { 148 revoke(null); 149 } 150 151 /** 152 * Revokes this certificate. 153 * 154 * @param reason 155 * {@link RevocationReason} stating the reason of the revocation that is 156 * used when generating OCSP responses and CRLs. {@code null} to give no 157 * reason. 158 */ 159 public void revoke(@Nullable RevocationReason reason) throws AcmeException { 160 revoke(getLogin(), getCertificate(), reason); 161 } 162 163 /** 164 * Revoke a certificate. This call is meant to be used for revoking certificates if 165 * only the account's key pair and the certificate itself is available. 166 * 167 * @param login 168 * {@link Login} to the account 169 * @param cert 170 * The {@link X509Certificate} to be revoked 171 * @param reason 172 * {@link RevocationReason} stating the reason of the revocation that is 173 * used when generating OCSP responses and CRLs. {@code null} to give no 174 * reason. 175 * @since 2.6 176 */ 177 public static void revoke(Login login, X509Certificate cert, @Nullable RevocationReason reason) 178 throws AcmeException { 179 LOG.debug("revoke"); 180 181 Session session = login.getSession(); 182 183 URL resUrl = session.resourceUrl(Resource.REVOKE_CERT); 184 185 try (Connection conn = session.connect()) { 186 JSONBuilder claims = new JSONBuilder(); 187 claims.putBase64("certificate", cert.getEncoded()); 188 if (reason != null) { 189 claims.put("reason", reason.getReasonCode()); 190 } 191 192 conn.sendSignedRequest(resUrl, claims, login); 193 } catch (CertificateEncodingException ex) { 194 throw new AcmeProtocolException("Invalid certificate", ex); 195 } 196 } 197 198 /** 199 * Revoke a certificate. This call is meant to be used for revoking certificates if 200 * the account's key pair was lost. 201 * 202 * @param session 203 * {@link Session} connected to the ACME server 204 * @param domainKeyPair 205 * Key pair the CSR was signed with 206 * @param cert 207 * The {@link X509Certificate} to be revoked 208 * @param reason 209 * {@link RevocationReason} stating the reason of the revocation that is 210 * used when generating OCSP responses and CRLs. {@code null} to give no 211 * reason. 212 */ 213 public static void revoke(Session session, KeyPair domainKeyPair, X509Certificate cert, 214 @Nullable RevocationReason reason) throws AcmeException { 215 LOG.debug("revoke using the domain key pair"); 216 217 URL resUrl = session.resourceUrl(Resource.REVOKE_CERT); 218 219 try (Connection conn = session.connect()) { 220 JSONBuilder claims = new JSONBuilder(); 221 claims.putBase64("certificate", cert.getEncoded()); 222 if (reason != null) { 223 claims.put("reason", reason.getReasonCode()); 224 } 225 226 conn.sendSignedRequest(resUrl, claims, session, domainKeyPair); 227 } catch (CertificateEncodingException ex) { 228 throw new AcmeProtocolException("Invalid certificate", ex); 229 } 230 } 231 232 /** 233 * Lazily downloads the certificate. Throws a runtime {@link AcmeLazyLoadingException} 234 * if the download failed. 235 */ 236 private void lazyDownload() { 237 try { 238 download(); 239 } catch (AcmeException ex) { 240 throw new AcmeLazyLoadingException(this, ex); 241 } 242 } 243 244}