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}