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

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

/*
 * acme4j - Java ACME client
 *
 * Copyright (C) 2015 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 org.shredzone.acme4j.util.AcmeUtils.*;

import java.net.HttpURLConnection;
import java.net.URI;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.connector.ResourceIterator;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents a registration at the ACME server.
 */
public class Registration extends AcmeResource {
    private static final long serialVersionUID = -8177333806740391140L;
    private static final Logger LOG = LoggerFactory.getLogger(Registration.class);

    private static final String KEY_AGREEMENT = "agreement";
    private static final String KEY_AUTHORIZATIONS = "authorizations";
    private static final String KEY_CERTIFICATES = "certificates";
    private static final String KEY_CONTACT = "contact";
    private static final String KEY_STATUS = "status";

    private final List contacts = new ArrayList<>();
    private URI agreement;
    private URI authorizations;
    private URI certificates;
    private Status status;
    private boolean loaded = false;

    protected Registration(Session session, URI location) {
        super(session);
        setLocation(location);
    }

    protected Registration(Session session, URI location, URI agreement) {
        super(session);
        setLocation(location);
        this.agreement = agreement;
    }

    /**
     * Creates a new instance of {@link Registration} and binds it to the {@link Session}.
     *
     * @param session
     *            {@link Session} to be used
     * @param location
     *            Location URI of the registration
     * @return {@link Registration} bound to the session and location
     */
    public static Registration bind(Session session, URI location) {
        return new Registration(session, location);
    }

    /**
     * Returns the URI of the agreement document the user is required to accept.
     */
    public URI getAgreement() {
        if (agreement == null) {
            load();
        }
        return agreement;
    }

    /**
     * List of contact addresses (emails, phone numbers etc).
     */
    public List getContacts() {
        load();
        return Collections.unmodifiableList(contacts);
    }

    /**
     * Returns the current status of the registration.
     */
    public Status getStatus() {
        load();
        return status;
    }

    /**
     * Returns an {@link Iterator} of all {@link Authorization} belonging to this
     * {@link Registration}.
     * 

* Using the iterator will initiate one or more requests to the ACME server. * * @return {@link Iterator} instance that returns {@link Authorization} objects. * {@link Iterator#hasNext()} and {@link Iterator#next()} may throw * {@link AcmeProtocolException} if a batch of authorization URIs could not be * fetched from the server. */ public Iterator getAuthorizations() throws AcmeException { LOG.debug("getAuthorizations"); load(); return new ResourceIterator<>(getSession(), KEY_AUTHORIZATIONS, authorizations, Authorization::bind); } /** * Returns an {@link Iterator} of all {@link Certificate} belonging to this * {@link Registration}. *

* Using the iterator will initiate one or more requests to the ACME server. * * @return {@link Iterator} instance that returns {@link Certificate} objects. * {@link Iterator#hasNext()} and {@link Iterator#next()} may throw * {@link AcmeProtocolException} if a batch of certificate URIs could not be * fetched from the server. */ public Iterator getCertificates() throws AcmeException { LOG.debug("getCertificates"); load(); return new ResourceIterator<>(getSession(), KEY_CERTIFICATES, certificates, Certificate::bind); } /** * Updates the registration to the current account status. */ public void update() throws AcmeException { LOG.debug("update"); try (Connection conn = getSession().provider().connect()) { JSONBuilder claims = new JSONBuilder(); claims.putResource("reg"); conn.sendSignedRequest(getLocation(), claims, getSession()); conn.accept(HttpURLConnection.HTTP_CREATED, HttpURLConnection.HTTP_ACCEPTED); JSON json = conn.readJsonResponse(); unmarshal(json, conn); } } /** * Authorizes a domain. The domain is associated with this registration. *

* IDN domain names will be ACE encoded automatically. * * @param domain * Domain name to be authorized * @return {@link Authorization} object for this domain */ public Authorization authorizeDomain(String domain) throws AcmeException { Objects.requireNonNull(domain, "domain"); if (domain.isEmpty()) { throw new IllegalArgumentException("domain must not be empty"); } LOG.debug("authorizeDomain {}", domain); try (Connection conn = getSession().provider().connect()) { JSONBuilder claims = new JSONBuilder(); claims.putResource(Resource.NEW_AUTHZ); claims.object("identifier") .put("type", "dns") .put("value", toAce(domain)); conn.sendSignedRequest(getSession().resourceUri(Resource.NEW_AUTHZ), claims, getSession()); conn.accept(HttpURLConnection.HTTP_CREATED); JSON json = conn.readJsonResponse(); Authorization auth = new Authorization(getSession(), conn.getLocation()); auth.unmarshalAuthorization(json); return auth; } } /** * Requests a certificate for the given CSR. *

* All domains given in the CSR must be authorized before. * * @param csr * PKCS#10 Certificate Signing Request to be sent to the server * @return The {@link Certificate} */ public Certificate requestCertificate(byte[] csr) throws AcmeException { return requestCertificate(csr, null, null); } /** * Requests a certificate for the given CSR. *

* All domains given in the CSR must be authorized before. * * @param csr * PKCS#10 Certificate Signing Request to be sent to the server * @param notBefore * requested value of the notBefore field in the certificate, {@code null} * for default. May be ignored by the server. * @param notAfter * requested value of the notAfter field in the certificate, {@code null} * for default. May be ignored by the server. * @return The {@link Certificate} */ public Certificate requestCertificate(byte[] csr, Instant notBefore, Instant notAfter) throws AcmeException { Objects.requireNonNull(csr, "csr"); LOG.debug("requestCertificate"); try (Connection conn = getSession().provider().connect()) { JSONBuilder claims = new JSONBuilder(); claims.putResource(Resource.NEW_CERT); claims.putBase64("csr", csr); if (notBefore != null) { claims.put("notBefore", notBefore); } if (notAfter != null) { claims.put("notAfter", notAfter); } conn.sendSignedRequest(getSession().resourceUri(Resource.NEW_CERT), claims, getSession()); int rc = conn.accept(HttpURLConnection.HTTP_CREATED, HttpURLConnection.HTTP_ACCEPTED); X509Certificate cert = null; if (rc == HttpURLConnection.HTTP_CREATED) { try { cert = conn.readCertificate(); } catch (AcmeProtocolException ex) { LOG.warn("Could not parse attached certificate", ex); } } URI chainCertUri = conn.getLink("up"); return new Certificate(getSession(), conn.getLocation(), chainCertUri, cert); } } /** * Changes the {@link KeyPair} associated with the registration. *

* After a successful call, the new key pair is used in the bound {@link Session}, * and the old key pair can be disposed of. * * @param newKeyPair * new {@link KeyPair} to be used for identifying this account */ public void changeKey(KeyPair newKeyPair) throws AcmeException { Objects.requireNonNull(newKeyPair, "newKeyPair"); if (Arrays.equals(getSession().getKeyPair().getPrivate().getEncoded(), newKeyPair.getPrivate().getEncoded())) { throw new IllegalArgumentException("newKeyPair must actually be a new key pair"); } LOG.debug("key-change"); try (Connection conn = getSession().provider().connect()) { URI keyChangeUri = getSession().resourceUri(Resource.KEY_CHANGE); PublicJsonWebKey newKeyJwk = PublicJsonWebKey.Factory.newPublicJwk(newKeyPair.getPublic()); JSONBuilder payloadClaim = new JSONBuilder(); payloadClaim.put("account", getLocation()); payloadClaim.putKey("newKey", newKeyPair.getPublic()); JsonWebSignature innerJws = new JsonWebSignature(); innerJws.setPayload(payloadClaim.toString()); innerJws.getHeaders().setObjectHeaderValue("url", keyChangeUri); innerJws.getHeaders().setJwkHeaderValue("jwk", newKeyJwk); innerJws.setAlgorithmHeaderValue(keyAlgorithm(newKeyJwk)); innerJws.setKey(newKeyPair.getPrivate()); innerJws.sign(); JSONBuilder outerClaim = new JSONBuilder(); outerClaim.putResource(Resource.KEY_CHANGE); // Let's Encrypt needs the resource here outerClaim.put("protected", innerJws.getHeaders().getEncodedHeader()); outerClaim.put("signature", innerJws.getEncodedSignature()); outerClaim.put("payload", innerJws.getEncodedPayload()); conn.sendSignedRequest(keyChangeUri, outerClaim, getSession()); conn.accept(HttpURLConnection.HTTP_OK); getSession().setKeyPair(newKeyPair); } catch (JoseException ex) { throw new AcmeProtocolException("Cannot sign key-change", ex); } } /** * Permanently deactivates an account. Related certificates may still be valid after * account deactivation, and need to be revoked separately if neccessary. *

* A deactivated account cannot be reactivated! */ public void deactivate() throws AcmeException { LOG.debug("deactivate"); try (Connection conn = getSession().provider().connect()) { JSONBuilder claims = new JSONBuilder(); claims.putResource("reg"); claims.put(KEY_STATUS, "deactivated"); conn.sendSignedRequest(getLocation(), claims, getSession()); conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED); } } /** * Lazily updates the object's state when one of the getters is invoked. */ protected void load() { if (!loaded) { try { update(); } catch (AcmeRetryAfterException ex) { // ignore... The object was still updated. LOG.debug("Retry-After", ex); } catch (AcmeException ex) { throw new AcmeProtocolException("Could not load lazily", ex); } } } /** * Sets registration properties according to the given JSON data. * * @param json * JSON data * @param conn * {@link Connection} with headers to be evaluated */ private void unmarshal(JSON json, Connection conn) { if (json.contains(KEY_AGREEMENT)) { this.agreement = json.get(KEY_AGREEMENT).asURI(); } if (json.contains(KEY_CONTACT)) { contacts.clear(); json.get(KEY_CONTACT).asArray().stream() .map(JSON.Value::asURI) .forEach(contacts::add); } this.authorizations = json.get(KEY_AUTHORIZATIONS).asURI(); this.certificates = json.get(KEY_CERTIFICATES).asURI(); if (json.contains(KEY_STATUS)) { this.status = Status.parse(json.get(KEY_STATUS).asString()); } URI location = conn.getLocation(); if (location != null) { setLocation(location); } URI tos = conn.getLink("terms-of-service"); if (tos != null) { this.agreement = tos; } loaded = true; } /** * Modifies the registration data of the account. * * @return {@link EditableRegistration} where the account can be modified */ public EditableRegistration modify() { return new EditableRegistration(); } /** * Editable {@link Registration}. */ public class EditableRegistration { private final List editContacts = new ArrayList<>(); private URI editAgreement; private EditableRegistration() { editContacts.addAll(Registration.this.contacts); editAgreement = Registration.this.agreement; } /** * Returns the list of all contact URIs for modification. Use the {@link List} * methods to modify the contact list. */ public List getContacts() { return editContacts; } /** * Adds a new Contact to the registration. * * @param contact * Contact URI * @return itself */ public EditableRegistration addContact(URI contact) { editContacts.add(contact); return this; } /** * Adds a new Contact to the registration. *

* This is a convenience call for {@link #addContact(URI)}. * * @param contact * Contact URI as string * @return itself */ public EditableRegistration addContact(String contact) { addContact(URI.create(contact)); return this; } /** * Sets a new agreement URI. * * @param agreement * New agreement URI * @return itself */ public EditableRegistration setAgreement(URI agreement) { this.editAgreement = agreement; return this; } /** * Commits the changes and updates the account. */ public void commit() throws AcmeException { LOG.debug("modify/commit"); try (Connection conn = getSession().provider().connect()) { JSONBuilder claims = new JSONBuilder(); claims.putResource("reg"); if (!editContacts.isEmpty()) { claims.put(KEY_CONTACT, editContacts); } if (editAgreement != null) { claims.put(KEY_AGREEMENT, editAgreement); } conn.sendSignedRequest(getLocation(), claims, getSession()); conn.accept(HttpURLConnection.HTTP_ACCEPTED); JSON json = conn.readJsonResponse(); unmarshal(json, conn); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy