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