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}