-
Notifications
You must be signed in to change notification settings - Fork 402
/
X509CertUtil.java
359 lines (312 loc) · 11.5 KB
/
X509CertUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
/*******************************************************************************
* Copyright (c) 2022 Sierra Wireless and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.html.
*
* Contributors:
* Sierra Wireless - initial API and implementation
*******************************************************************************/
package org.eclipse.leshan.core.util;
import java.net.InetAddress;
import java.security.Principal;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.security.auth.x500.X500Principal;
/**
* X.509 Certificate Utilities for accessing certificate details.
*/
public class X509CertUtil {
/**
* <a href="https://tools.ietf.org/html/rfc3280#section-4.2.1.7">rfc3280#section-4.2.1.7</a> - GeneralName
*/
public enum GeneralName {
/**
* otherName [0] OtherName
*/
OTHER_NAME(0),
/**
* rfc822Name [1] IA5String
*/
RFC822_NAME(1),
/**
* dNSName [2] IA5String
*/
DNS_NAME(2),
/**
* x400Address [3] ORAddress
*/
X400_ADDRESS(3),
/**
* directoryName [4] Name
*/
DIRECTORY_NAME(4),
/**
* ediPartyName [5] EDIPartyName
*/
EDI_PARTY_NAME(5),
/**
* uniformResourceIdentifier [6] IA5String
*/
UNIFORM_RESOURCE_IDENTIFIER(6),
/**
* iPAddress [7] OCTET STRING
*/
IP_ADDRESS(7),
/**
* registeredID [8] OBJECT IDENTIFIER
*/
REGISTERED_ID(8);
/**
* The code value.
*/
public final int value;
/**
* Instantiates a new code with the specified code value.
*
* @param value the integer value of the code
*/
private GeneralName(final int value) {
this.value = value;
}
/**
* Converts the specified integer value to a general name code.
*
* @param value the integer value
* @return the general name code
* @throws IllegalArgumentException if the integer value does not represent a valid general name code.
*/
public static GeneralName valueOf(final int value) {
switch (value) {
case 0:
return OTHER_NAME;
case 1:
return RFC822_NAME;
case 2:
return DNS_NAME;
case 3:
return X400_ADDRESS;
case 4:
return DIRECTORY_NAME;
case 5:
return EDI_PARTY_NAME;
case 6:
return UNIFORM_RESOURCE_IDENTIFIER;
case 7:
return IP_ADDRESS;
case 8:
return REGISTERED_ID;
default:
throw new IllegalArgumentException(String.format("Unknown GeneralName class code: %d", value));
}
}
}
/**
* Helper for checking if given character is HEX character.
*
* @param ch Char to test
* @return true if HEX char, false otherwise.
*/
private static boolean isHex(char ch) {
return (ch >= '0' && ch <= '9' || ch >= 'A' && ch <= 'F' || ch >= 'a' && ch <= 'f');
}
private static Pattern extactCNPattern = Pattern.compile("CN=(.*?)(,|$)");
/**
* Extract "common name" from "distinguished name".
*
* @param dn The distinguished name.
* @return The extracted common name.
* @throws IllegalStateException if no CN is contained in DN.
*/
public static String extractCN(String dn) {
// Extract common name
Matcher endpointMatcher = extactCNPattern.matcher(dn);
if (endpointMatcher.find()) {
return endpointMatcher.group(1);
} else {
throw new IllegalStateException(
"Unable to extract sender identity : can not get common name in certificate");
}
}
/**
* Parses <a href="https://tools.ietf.org/html/rfc2253#section-3">RFC 2253 name string</a>.
* <p>
* Extracts field keys and their values in form as they are given in name string.
*
* @param name RFC 2253 name string
* @return Map with field keys and their values
*/
public static Map<String, String> parseRfc2253Name(String name) {
Map<String, String> map = new HashMap<String, String>();
// Parse RFC 2253 string
boolean inValue = false;
StringBuilder key = new StringBuilder();
StringBuilder value = new StringBuilder();
for (int i = 0; i < name.length(); i++) {
char ch = name.charAt(i);
if (!inValue) {
if (ch == '=') {
inValue = true;
if (key.length() == 0)
throw new IllegalArgumentException("Key in RFC 2253 name cannot be empty");
} else {
key.append(ch);
}
} else {
if (ch == '\\') {
char nextCh = name.charAt(i + 1);
// Check for HEX encoding
if (isHex(nextCh) && isHex(name.charAt(i + 2))) {
int val = Integer.parseInt(name.substring(i + 1, i + 2), 16);
value.append((char) val);
i += 2;
} else {
value.append(nextCh);
i++;
}
} else if (ch == ',' || ch == '+') {
inValue = false;
map.put(key.toString(), value.toString());
key = new StringBuilder();
value = new StringBuilder();
} else {
value.append(ch);
}
}
}
if (key.length() > 0) {
map.put(key.toString(), value.toString());
}
return map;
}
/**
* Extracts field from given principal's name.
* <p>
* Notes: Can parse most name strings but HEX encoded DER values are returned back in DER HEX form. Notes: Only
* understands X500Principal.
*
* @param principal Source principal.
* @param field Field key as defined in RFC 2253.
* @return null or value as string.
*/
public static String getPrincipalField(Principal principal, String field) {
if (principal instanceof X500Principal) {
X500Principal x500Principal = (X500Principal) principal;
// Extra practical OID that Java implementation hides for "not being standard" even thou
// list in RFC is only example OIDs list (see JDK class AVAKeyword). These extra OID's are
// actually defined in Sun packages but we should not access them as they are not standard
// Java classes.
HashMap<String, String> extraOids = new HashMap<String, String>();
extraOids.put("2.5.4.5", "SERIALNUMBER");
String dn = x500Principal.getName(X500Principal.RFC2253, extraOids);
Map<String, String> fields = parseRfc2253Name(dn);
if (fields.containsKey(field)) {
return fields.get(field);
}
}
return null;
}
/**
* DNS name matcher
* <p>
* Supports matching with wildcard DNS names.
*
* @param matcher Matcher pattern
* @param target Target DNS name to check
* @return true if matches, false otherwise
*/
private static boolean dnsNameMatch(String matcher, String target) {
if (matcher.startsWith("*.")) {
// Simple filtering out
if (!target.endsWith(matcher.substring(1)))
return false;
// Wildcards only work in one sub level so no extra dots
String host = target.substring(0, target.length() - (matcher.length() - 1));
return host.indexOf('.') == -1;
}
return matcher.equals(target);
}
/**
* Match DNS name against X.509 Certificate's Subjects
*
* @param certificate Target X.509 certificate
* @param dnsName DNS name to match
* @return True if match, false otherwise
*/
public static boolean matchSubjectDnsName(X509Certificate certificate, String dnsName) {
try {
// First one need to check SANs if they are present
Collection<List<?>> sans = certificate.getSubjectAlternativeNames();
if (sans != null) {
for (List<?> san : sans) {
int generalName = (Integer) san.get(0);
if (generalName == GeneralName.DNS_NAME.value) {
String value = (String) san.get(1);
if (dnsNameMatch(value, dnsName)) {
return true;
}
}
}
// Strict Subject Alternative Name mode:
// - Do not allow fallback to Subject DN matching
return false;
}
// If subject alternative names are not present fallback to old ways at looking in Subject DN
String cn = getPrincipalField(certificate.getSubjectX500Principal(), "CN");
if (dnsNameMatch(cn, dnsName)) {
return true;
}
} catch (CertificateParsingException e) {
// Ignore exception and just return no match
}
return false;
}
/**
* Match IP address against X.509 Certificate's Subjects
*
* @param certificate Target X.509 certificate
* @param address IP address to match
* @return True if match, false otherwise
*/
public static boolean matchSubjectInetAddress(X509Certificate certificate, InetAddress address) {
try {
String hostAddress = address.getHostAddress();
// First one need to check SANs if they are present
Collection<List<?>> sans = certificate.getSubjectAlternativeNames();
if (sans != null) {
for (List<?> san : sans) {
int generalName = (Integer) san.get(0);
if (generalName == GeneralName.IP_ADDRESS.value) {
String value = (String) san.get(1);
if (hostAddress.equals(value)) {
return true;
}
}
}
// Strict Subject Alternative Name mode:
// - Do not allow fallback to Subject DN matching
return false;
}
// If subject alternative names are not present fallback to old ways at looking in Subject DN
String cn = getPrincipalField(certificate.getSubjectX500Principal(), "CN");
if (hostAddress.equals(cn)) {
return true;
}
} catch (CertificateParsingException e) {
// Ignore exception and just return no match
}
return false;
}
}