All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.shredzone.acme4j.Certificate Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/*
 * acme4j - Java ACME client
 *
 * Copyright (C) 2016 Richard "Shred" Körber
 *   http://acme4j.shredzone.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * This program 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.
 */
package org.shredzone.acme4j;

import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toUnmodifiableList;
import static org.shredzone.acme4j.toolbox.AcmeUtils.base64UrlEncode;
import static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;

import java.io.IOException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyPair;
import java.security.Principal;
import java.security.Security;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
import org.shredzone.acme4j.exception.AcmeNotSupportedException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents an issued certificate and its certificate chain.
 * 

* A certificate is immutable once it is issued. For renewal, a new certificate must be * ordered. */ public class Certificate extends AcmeResource { private static final long serialVersionUID = 7381527770159084201L; private static final Logger LOG = LoggerFactory.getLogger(Certificate.class); private @Nullable List certChain; private @Nullable Collection alternates; private transient @Nullable RenewalInfo renewalInfo = null; private transient @Nullable List alternateCerts = null; protected Certificate(Login login, URL certUrl) { super(login, certUrl); } /** * Downloads the certificate chain. *

* The certificate is downloaded lazily by the other methods. Usually there is no need * to invoke this method, unless the download is to be enforced. If the certificate * has been downloaded already, nothing will happen. * * @throws AcmeException * if the certificate could not be downloaded */ public void download() throws AcmeException { if (certChain == null) { LOG.debug("download"); try (var conn = getSession().connect()) { conn.sendCertificateRequest(getLocation(), getLogin()); alternates = conn.getLinks("alternate"); certChain = conn.readCertificates(); } } } /** * Returns the created certificate. * * @return The created end-entity {@link X509Certificate} without issuer chain. */ public X509Certificate getCertificate() { lazyDownload(); return requireNonNull(certChain).get(0); } /** * Returns the created certificate and issuer chain. * * @return The created end-entity {@link X509Certificate} and issuer chain. The first * certificate is always the end-entity certificate, followed by the * intermediate certificates required to build a path to a trusted root. */ public List getCertificateChain() { lazyDownload(); return unmodifiableList(requireNonNull(certChain)); } /** * Returns URLs to alternate certificate chains. * * @return Alternate certificate chains, or empty if there are none. */ public List getAlternates() { lazyDownload(); return requireNonNull(alternates).stream().collect(toUnmodifiableList()); } /** * Returns alternate certificate chains, if available. * * @return Alternate certificate chains, or empty if there are none. * @since 2.11 */ public List getAlternateCertificates() { if (alternateCerts == null) { var login = getLogin(); alternateCerts = getAlternates().stream() .map(login::bindCertificate) .collect(toList()); } return unmodifiableList(alternateCerts); } /** * Checks if this certificate was issued by the given issuer name. * * @param issuer * Issuer name to check against, case-sensitive * @return {@code true} if this issuer name was found in the certificate chain as * issuer, {@code false} otherwise. * @since 3.0.0 */ public boolean isIssuedBy(String issuer) { var issuerCn = "CN=" + issuer; return getCertificateChain().stream() .map(X509Certificate::getIssuerX500Principal) .map(Principal::getName) .anyMatch(issuerCn::equals); } /** * Finds a {@link Certificate} that was issued by the given issuer name. * * @param issuer * Issuer name to check against, case-sensitive * @return Certificate that was issued by that issuer, or {@code empty} if there was * none. The returned {@link Certificate} may be this instance, or one of the * {@link #getAlternateCertificates()} instances. If multiple certificates are issued * by that issuer, the first one that was found is returned. * @since 3.0.0 */ public Optional findCertificate(String issuer) { if (isIssuedBy(issuer)) { return Optional.of(this); } return getAlternateCertificates().stream() .filter(c -> c.isIssuedBy(issuer)) .findFirst(); } /** * Writes the certificate to the given writer. It is written in PEM format, with the * end-entity cert coming first, followed by the intermediate certificates. * * @param out * {@link Writer} to write to. The writer is not closed after use. */ public void writeCertificate(Writer out) throws IOException { try { for (var cert : getCertificateChain()) { AcmeUtils.writeToPem(cert.getEncoded(), AcmeUtils.PemLabel.CERTIFICATE, out); } } catch (CertificateEncodingException ex) { throw new IOException("Encoding error", ex); } } /** * Returns this certificate's CertID according to RFC 6960. *

* This method requires the {@link org.bouncycastle.jce.provider.BouncyCastleProvider} * security provider. * * @see RFC 6960 * @since 3.0.0 * @deprecated Is not needed in the ACME context anymore and will thus be removed in * a later version. */ @Deprecated public String getCertID() { var certChain = getCertificateChain(); if (certChain.size() < 2) { throw new AcmeProtocolException("Certificate has no issuer"); } try { var builder = new JcaDigestCalculatorProviderBuilder(); if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) { builder.setProvider(BouncyCastleProvider.PROVIDER_NAME); } var digestCalc = builder.build().get(new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256)); var issuerHolder = new X509CertificateHolder(certChain.get(1).getEncoded()); var certId = new CertificateID(digestCalc, issuerHolder, certChain.get(0).getSerialNumber()); return base64UrlEncode(certId.toASN1Primitive().getEncoded()); } catch (Exception ex) { throw new AcmeProtocolException("Could not compute Certificate ID", ex); } } /** * Returns the location of the certificate's RenewalInfo. Empty if the CA does not * provide this information. * * @draft This method is currently based on an RFC draft. It may be changed or * removed without notice to reflect future changes to the draft. SemVer rules * do not apply here. * @since 3.0.0 */ public Optional getRenewalInfoLocation() { try { return getSession().resourceUrlOptional(Resource.RENEWAL_INFO) .map(baseUrl -> { try { var url = baseUrl.toExternalForm(); if (!url.endsWith("/")) { url += '/'; } url += getRenewalUniqueIdentifier(getCertificate()); return new URL(url); } catch (MalformedURLException ex) { throw new AcmeProtocolException("Invalid RenewalInfo URL", ex); } }); } catch (AcmeException ex) { throw new AcmeLazyLoadingException(this, ex); } } /** * Returns {@code true} if the CA provides renewal information. * * @draft This method is currently based on an RFC draft. It may be changed or * removed without notice to reflect future changes to the draft. SemVer rules * do not apply here. * @since 3.0.0 */ public boolean hasRenewalInfo() { return getRenewalInfoLocation().isPresent(); } /** * Reads the RenewalInfo for this certificate. * * @draft This method is currently based on an RFC draft. It may be changed or * removed without notice to reflect future changes to the draft. SemVer rules * do not apply here. * @return The {@link RenewalInfo} of this certificate. * @throws AcmeNotSupportedException if the CA does not support renewal information. * @since 3.0.0 */ @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended public RenewalInfo getRenewalInfo() { if (renewalInfo == null) { renewalInfo = getRenewalInfoLocation() .map(getLogin()::bindRenewalInfo) .orElseThrow(() -> new AcmeNotSupportedException("renewal-info")); } return renewalInfo; } /** * Revokes this certificate. */ public void revoke() throws AcmeException { revoke(null); } /** * Revokes this certificate. * * @param reason * {@link RevocationReason} stating the reason of the revocation that is * used when generating OCSP responses and CRLs. {@code null} to give no * reason. * @see #revoke(Login, X509Certificate, RevocationReason) * @see #revoke(Session, KeyPair, X509Certificate, RevocationReason) */ public void revoke(@Nullable RevocationReason reason) throws AcmeException { revoke(getLogin(), getCertificate(), reason); } /** * Revoke a certificate. *

* Use this method if the certificate's location is unknown, so you cannot regenerate * a {@link Certificate} instance. This method requires a {@link Login} to your * account and the issued certificate. * * @param login * {@link Login} to the account * @param cert * The {@link X509Certificate} to be revoked * @param reason * {@link RevocationReason} stating the reason of the revocation that is used * when generating OCSP responses and CRLs. {@code null} to give no reason. * @see #revoke(Session, KeyPair, X509Certificate, RevocationReason) * @since 2.6 */ public static void revoke(Login login, X509Certificate cert, @Nullable RevocationReason reason) throws AcmeException { LOG.debug("revoke"); var session = login.getSession(); var resUrl = session.resourceUrl(Resource.REVOKE_CERT); try (var conn = session.connect()) { var claims = new JSONBuilder(); claims.putBase64("certificate", cert.getEncoded()); if (reason != null) { claims.put("reason", reason.getReasonCode()); } conn.sendSignedRequest(resUrl, claims, login); } catch (CertificateEncodingException ex) { throw new AcmeProtocolException("Invalid certificate", ex); } } /** * Revoke a certificate. *

* Use this method if the key pair of your account was lost (so you are unable to * login into your account), but you still have the key pair of the affected domain * and the issued certificate. * * @param session * {@link Session} connected to the ACME server * @param domainKeyPair * Key pair the CSR was signed with * @param cert * The {@link X509Certificate} to be revoked * @param reason * {@link RevocationReason} stating the reason of the revocation that is used * when generating OCSP responses and CRLs. {@code null} to give no reason. * @see #revoke(Login, X509Certificate, RevocationReason) */ public static void revoke(Session session, KeyPair domainKeyPair, X509Certificate cert, @Nullable RevocationReason reason) throws AcmeException { LOG.debug("revoke using the domain key pair"); var resUrl = session.resourceUrl(Resource.REVOKE_CERT); try (var conn = session.connect()) { var claims = new JSONBuilder(); claims.putBase64("certificate", cert.getEncoded()); if (reason != null) { claims.put("reason", reason.getReasonCode()); } conn.sendSignedRequest(resUrl, claims, session, domainKeyPair); } catch (CertificateEncodingException ex) { throw new AcmeProtocolException("Invalid certificate", ex); } } /** * Lazily downloads the certificate. Throws a runtime {@link AcmeLazyLoadingException} * if the download failed. */ private void lazyDownload() { try { download(); } catch (AcmeException ex) { throw new AcmeLazyLoadingException(this, ex); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy