001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2018 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.unmodifiableMap;
017import static java.util.Objects.requireNonNull;
018import static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;
019
020import java.io.Serial;
021import java.io.Serializable;
022import java.net.InetAddress;
023import java.net.UnknownHostException;
024import java.util.Map;
025import java.util.TreeMap;
026
027import org.shredzone.acme4j.exception.AcmeProtocolException;
028import org.shredzone.acme4j.toolbox.JSON;
029
030/**
031 * Represents an identifier.
032 * <p>
033 * The ACME protocol only defines the DNS identifier, which identifies a domain name.
034 * acme4j also supports IP identifiers.
035 * <p>
036 * CAs, and other acme4j modules, may define further, proprietary identifier types.
037 *
038 * @since 2.3
039 */
040public class Identifier implements Serializable {
041    @Serial
042    private static final long serialVersionUID = -7777851842076362412L;
043
044    /**
045     * Type constant for DNS identifiers.
046     */
047    public static final String TYPE_DNS = "dns";
048
049    /**
050     * Type constant for IP identifiers.
051     *
052     * @see <a href="https://tools.ietf.org/html/rfc8738">RFC 8738</a>
053     */
054    public static final String TYPE_IP = "ip";
055
056    static final String KEY_TYPE = "type";
057    static final String KEY_VALUE = "value";
058    static final String KEY_ANCESTOR_DOMAIN = "ancestorDomain";
059    static final String KEY_SUBDOMAIN_AUTH_ALLOWED = "subdomainAuthAllowed";
060
061    private final Map<String, Object> content = new TreeMap<>();
062
063    /**
064     * Creates a new {@link Identifier}.
065     * <p>
066     * This is a generic constructor for identifiers. Refer to the documentation of your
067     * CA to find out about the accepted identifier types and values.
068     * <p>
069     * Note that for DNS identifiers, no ASCII encoding of unicode domain takes place
070     * here. Use {@link #dns(String)} instead.
071     *
072     * @param type
073     *            Identifier type
074     * @param value
075     *            Identifier value
076     */
077    public Identifier(String type, String value) {
078        content.put(KEY_TYPE, requireNonNull(type, KEY_TYPE));
079        content.put(KEY_VALUE, requireNonNull(value, KEY_VALUE));
080    }
081
082    /**
083     * Creates a new {@link Identifier} from the given {@link JSON} structure.
084     *
085     * @param json
086     *            {@link JSON} containing the identifier data
087     */
088    public Identifier(JSON json) {
089        if (!json.contains(KEY_TYPE)) {
090            throw new AcmeProtocolException("Required key " + KEY_TYPE + " is missing");
091        }
092        if (!json.contains(KEY_VALUE)) {
093            throw new AcmeProtocolException("Required key " + KEY_VALUE + " is missing");
094        }
095        content.putAll(json.toMap());
096    }
097
098    /**
099     * Makes a copy of the given Identifier.
100     */
101    private Identifier(Identifier identifier) {
102        content.putAll(identifier.content);
103    }
104
105    /**
106     * Creates a new DNS identifier for the given domain name.
107     *
108     * @param domain
109     *            Domain name. Unicode domains are automatically ASCII encoded.
110     * @return New {@link Identifier}
111     */
112    public static Identifier dns(String domain) {
113        return new Identifier(TYPE_DNS, toAce(domain));
114    }
115
116    /**
117     * Creates a new IP identifier for the given {@link InetAddress}.
118     *
119     * @param ip
120     *            {@link InetAddress}
121     * @return New {@link Identifier}
122     */
123    public static Identifier ip(InetAddress ip) {
124        return new Identifier(TYPE_IP, ip.getHostAddress());
125    }
126
127    /**
128     * Creates a new IP identifier for the given {@link InetAddress}.
129     *
130     * @param ip
131     *            IP address as {@link String}
132     * @return New {@link Identifier}
133     * @since 2.7
134     */
135    public static Identifier ip(String ip) {
136        try {
137            return ip(InetAddress.getByName(ip));
138        } catch (UnknownHostException ex) {
139            throw new IllegalArgumentException("Bad IP: " + ip, ex);
140        }
141    }
142
143    /**
144     * Sets an ancestor domain, as required in RFC-9444.
145     *
146     * @param domain
147     *         The ancestor domain to be set. Unicode domains are automatically ASCII
148     *         encoded.
149     * @return An {@link Identifier} that contains the ancestor domain.
150     * @since 3.3.0
151     */
152    public Identifier withAncestorDomain(String domain) {
153        expectType(TYPE_DNS);
154
155        var result = new Identifier(this);
156        result.content.put(KEY_ANCESTOR_DOMAIN, toAce(domain));
157        return result;
158    }
159
160    /**
161     * Gives the permission to authorize subdomains of this domain, as required in
162     * RFC-9444.
163     *
164     * @return An {@link Identifier} that allows subdomain auths.
165     * @since 3.3.0
166     */
167    public Identifier allowSubdomainAuth() {
168        expectType(TYPE_DNS);
169
170        var result = new Identifier(this);
171        result.content.put(KEY_SUBDOMAIN_AUTH_ALLOWED, true);
172        return result;
173    }
174
175    /**
176     * Returns the identifier type.
177     */
178    public String getType() {
179        return content.get(KEY_TYPE).toString();
180    }
181
182    /**
183     * Returns the identifier value.
184     */
185    public String getValue() {
186        return content.get(KEY_VALUE).toString();
187    }
188
189    /**
190     * Returns the domain name if this is a DNS identifier.
191     *
192     * @return Domain name. Unicode domains are ASCII encoded.
193     * @throws AcmeProtocolException
194     *             if this is not a DNS identifier.
195     */
196    public String getDomain() {
197        expectType(TYPE_DNS);
198        return getValue();
199    }
200
201    /**
202     * Returns the IP address if this is an IP identifier.
203     *
204     * @return {@link InetAddress}
205     * @throws AcmeProtocolException
206     *             if this is not a DNS identifier.
207     */
208    public InetAddress getIP() {
209        expectType(TYPE_IP);
210        try {
211            return InetAddress.getByName(getValue());
212        } catch (UnknownHostException ex) {
213            throw new AcmeProtocolException("bad ip identifier value", ex);
214        }
215    }
216
217    /**
218     * Returns the identifier as JSON map.
219     */
220    public Map<String, Object> toMap() {
221        return unmodifiableMap(content);
222    }
223
224    /**
225     * Makes sure this identifier is of the given type.
226     *
227     * @param type
228     *         Expected type
229     * @throws AcmeProtocolException
230     *         if this identifier is of a different type
231     */
232    private void expectType(String type) {
233        if (!type.equals(getType())) {
234            throw new AcmeProtocolException("expected '" + type + "' identifier, but found '" + getType() + "'");
235        }
236    }
237
238    @Override
239    public String toString() {
240        if (content.size() == 2) {
241            return getType() + '=' + getValue();
242        }
243        return content.toString();
244    }
245
246    @Override
247    public boolean equals(Object obj) {
248        if (!(obj instanceof Identifier i)) {
249            return false;
250        }
251        return content.equals(i.content);
252    }
253
254    @Override
255    public int hashCode() {
256        return content.hashCode();
257    }
258
259    @Override
260    protected final void finalize() {
261        // CT_CONSTRUCTOR_THROW: Prevents finalizer attack
262    }
263
264}