View Javadoc
1   /*
2    * Copyright 2024–2026 Michael Osipov
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.michaelo.tomcat.pac;
17  
18  import java.io.BufferedReader;
19  import java.io.IOException;
20  import java.io.InputStreamReader;
21  import java.io.UncheckedIOException;
22  import java.nio.charset.StandardCharsets;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.nio.file.Paths;
26  import java.security.SignatureException;
27  import java.util.ArrayList;
28  import java.util.Base64;
29  import java.util.List;
30  import java.util.Scanner;
31  import java.util.concurrent.atomic.AtomicInteger;
32  import java.util.stream.Stream;
33  
34  import javax.security.auth.Subject;
35  import javax.security.auth.kerberos.KerberosKey;
36  import javax.security.auth.kerberos.KerberosPrincipal;
37  import javax.security.auth.kerberos.KeyTab;
38  import javax.security.auth.login.LoginContext;
39  import javax.security.auth.login.LoginException;
40  
41  import com.sun.security.jgss.AuthorizationDataEntry;
42  
43  import net.sf.michaelo.tomcat.authenticator.SpnegoAuthenticator;
44  import net.sf.michaelo.tomcat.pac.asn1.AdIfRelevantAsn1Parser;
45  import net.sf.michaelo.tomcat.realm.Krb5AuthzDataDumpingActiveDirectoryRealm;
46  import net.sf.michaelo.tomcat.realm.Sid;
47  
48  /**
49   * A Kerberos {@code AuthorizationData} dump printer produced by {@link Krb5AuthzDataDumpingActiveDirectoryRealm}.
50   *
51   * <p>This class can be called via its main method, it supports the following optional parameters:
52   *
53   * <ul>
54   *   <li>output format {@code --format} {@code listing} (default) or {@code sql},
55   *   <li>verify the PAC server signature with the {@link PrivateSunPacSignatureVerifier} and a login context
56   *       {@code --verify-signature} <code>{loginEntryName}</code>, The configuration of the login entry must be
57   *       identical to the one from {@link SpnegoAuthenticator#getLoginEntryName()}
58   * </ul>
59   *
60   * and the following positional parameters:
61   *
62   * <ul>
63   *   <li>dump file/directory {@code path...}: either a file or a directory containing dumps.
64   * </ul>
65   *
66   * <p>The {@code sql} format output can be used to import the data into a SQLite database for later analysis.
67   */
68  public class Krb5AuthzDataDumpPrinter {
69  
70  	private static final AtomicInteger PAC_ID_GENERATOR = new AtomicInteger();
71  
72  	private static void dumpFile(Path file, String format, KerberosKey[] keys) throws IOException, SignatureException {
73  		System.err.printf("Processing file '%s'%n", file);
74  		List<AuthorizationDataEntry> adEntries = new ArrayList<>();
75  		try (Scanner scanner = new Scanner(file)) {
76  			while (scanner.hasNext()) {
77  				int type = scanner.nextInt();
78  				byte[] data = Base64.getDecoder().decode(scanner.next());
79  				adEntries.add(new AuthorizationDataEntry(type, data));
80  			}
81  		}
82  
83  		for (AuthorizationDataEntry adEntry : adEntries) {
84  			int type = adEntry.getType();
85  			byte[] data = adEntry.getData();
86  			switch (type) {
87  				case AdIfRelevantAsn1Parser.AD_IF_RELEVANT:
88  					List<AuthorizationDataEntry> adIfRelevantEntries = AdIfRelevantAsn1Parser.parse(data);
89  					for (AuthorizationDataEntry adIfRelevantEntry : adIfRelevantEntries) {
90  						int adIfRelevantType = adIfRelevantEntry.getType();
91  						byte[] adIfRelevantTypeData = adIfRelevantEntry.getData();
92  						switch (adIfRelevantType) {
93  							case AdIfRelevantAsn1Parser.AD_WIN2K_PAC:
94  								int pacId = PAC_ID_GENERATOR.incrementAndGet();
95  
96  								Pac pac = new Pac(adIfRelevantTypeData, new PrivateSunPacSignatureVerifier());
97  
98  								if (keys != null) {
99  									if (format.equals("listing")) System.out.print("Verifying PAC server signature...");
100 									pac.verifySignature(keys);
101 									if (format.equals("listing")) System.out.println("PASSED");
102 								}
103 
104 								KerbValidationInfo kerbValidationInfo = pac.getKerbValidationInfo();
105 								String effectiveName = kerbValidationInfo.getEffectiveName();
106 								String fullName = kerbValidationInfo.getFullName();
107 								String logonScript = kerbValidationInfo.getLogonScript();
108 								String profilePath = kerbValidationInfo.getProfilePath();
109 								String homeDirectory = kerbValidationInfo.getHomeDirectory();
110 								String homeDirectoryDrive = kerbValidationInfo.getHomeDirectoryDrive();
111 								long userId = kerbValidationInfo.getUserId();
112 								long primaryGroupId = kerbValidationInfo.getPrimaryGroupId();
113 								List<GroupMembership> groupIds = kerbValidationInfo.getGroupIds();
114 								long userFlags = kerbValidationInfo.getUserFlags();
115 								String logonServer = kerbValidationInfo.getLogonServer();
116 								String logonDomainName = kerbValidationInfo.getLogonDomainName();
117 								Sid logonDomainId = kerbValidationInfo.getLogonDomainId();
118 								long userAccountControl = kerbValidationInfo.getUserAccountControl();
119 								List<KerbSidAndAttributes> extraSids = kerbValidationInfo.getExtraSids();
120 								Sid resourceGroupDomainSid = kerbValidationInfo.getResourceGroupDomainSid();
121 								List<GroupMembership> resourceGroupIds = kerbValidationInfo.getResourceGroupIds();
122 								switch (format) {
123 									case "listing":
124 										System.out.println("KerbValidationInfo:");
125 										System.out.println("  effectiveName: " + effectiveName);
126 										System.out.println("  fullName: " + fullName);
127 										System.out.println("  logonScript: " + logonScript);
128 										System.out.println("  profilePath: " + profilePath);
129 										System.out.println("  homeDirectory: " + homeDirectory);
130 										System.out.println("  homeDirectoryDrive: " + homeDirectoryDrive);
131 										System.out.println("  userId: " + userId);
132 										System.out.println("  primaryGroupId: " + primaryGroupId);
133 										System.out.println("  groupIds (" + groupIds.size() + "):");
134 										for (GroupMembership groupId : groupIds) {
135 											System.out.println("    - " + groupId + " ("
136 													+ logonDomainId.append(groupId.getRelativeId()) + ")");
137 										}
138 										System.out.printf("  userFlags: 0x%08X%n", userFlags);
139 										System.out.println("  logonServer: " + logonServer);
140 										System.out.println("  logonDomainName: " + logonDomainName);
141 										System.out.println("  logonDomainId: " + logonDomainId);
142 										System.out.printf("  userAccountControl: 0x%08X%n", userAccountControl);
143 										if (extraSids != null) {
144 											System.out.println("  extraSids (" + extraSids.size() + "):");
145 											for (KerbSidAndAttributes extraSid : extraSids) {
146 												System.out.println("    - " + extraSid);
147 											}
148 										}
149 										if (resourceGroupDomainSid != null) {
150 											System.out.println("  resourceGroupDomainSid: " + resourceGroupDomainSid);
151 											System.out.println("  resourceGroupIds (" + resourceGroupIds.size() + "):");
152 											for (GroupMembership resourceGroupId : resourceGroupIds) {
153 												System.out.println("    - " + resourceGroupId + " ("
154 														+ resourceGroupDomainSid.append(resourceGroupId.getRelativeId())
155 														+ ")");
156 											}
157 										}
158 										break;
159 									case "sql":
160 										System.out.printf(
161 												"insert into kerb_validation_info(pacId, effectiveName, fullName, logonScript, profilePath, homeDirectory, homeDirectoryDrive, userId, primaryGroupId, userFlags, logonServer, logonDomainName, logonDomainId, userAccountControl, resourceGroupDomainSid)"
162 														+ " values(%d, '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, %d, '%s', '%s', '%s', %d, %s);%n",
163 												pacId,
164 												effectiveName,
165 												fullName,
166 												logonScript,
167 												profilePath,
168 												homeDirectory,
169 												homeDirectoryDrive,
170 												userId,
171 												primaryGroupId,
172 												userFlags,
173 												logonServer,
174 												logonDomainName,
175 												logonDomainId,
176 												userAccountControl,
177 												nullSafe(resourceGroupDomainSid));
178 										for (GroupMembership groupId : groupIds) {
179 											System.out.printf(
180 													"insert into group_ids(pacId, relativeId, attributes) values(%d, %d, %d);%n",
181 													pacId, groupId.getRelativeId(), groupId.getAttributes());
182 										}
183 										if (extraSids != null) {
184 											for (KerbSidAndAttributes extraSid : extraSids) {
185 												System.out.printf(
186 														"insert into extra_sids(pacId, sid, attributes) values(%d, '%s', %d);%n",
187 														pacId, extraSid.getSid(), extraSid.getAttributes());
188 											}
189 										}
190 										if (resourceGroupDomainSid != null) {
191 											for (GroupMembership resourceGroupId : resourceGroupIds) {
192 												System.out.printf(
193 														"insert into resource_group_ids(pacId, relativeId, attributes) values(%d, %d, %d);%n",
194 														pacId,
195 														resourceGroupId.getRelativeId(),
196 														resourceGroupId.getAttributes());
197 											}
198 										}
199 										break;
200 								}
201 								UpnDnsInfo upnDnsInfo = pac.getUpnDnsInfo();
202 								if (upnDnsInfo != null) {
203 									String upn = upnDnsInfo.getUpn();
204 									String dnsDomainName = upnDnsInfo.getDnsDomainName();
205 									long flags = upnDnsInfo.getFlags();
206 									String samName = upnDnsInfo.getSamName();
207 									Sid sid = upnDnsInfo.getSid();
208 									switch (format) {
209 										case "listing":
210 											System.out.println("UpnDnsInfo:");
211 											System.out.println("  upn: " + upn);
212 											System.out.println("  dnsDomainName: " + dnsDomainName);
213 											System.out.printf("  flags: 0x%08X%n", flags);
214 											if (samName != null) {
215 												System.out.println("  samName: " + samName);
216 												System.out.println("  sid: " + sid);
217 											}
218 											break;
219 										case "sql":
220 											System.out.printf(
221 													"insert into upn_dns_info(pacId, upn, dnsDomainName, flags, samName, sid)"
222 															+ " values(%d, '%s', '%s', %d, %s, %s);%n",
223 													pacId, upn, dnsDomainName, flags, nullSafe(samName), nullSafe(sid));
224 											break;
225 									}
226 								}
227 								PacClientInfo pacClientInfo = pac.getPacClientInfo();
228 								switch (format) {
229 									case "listing":
230 										System.out.println("PacClientInfo:");
231 										System.out.println("  name: " + pacClientInfo.getName());
232 										break;
233 								}
234 								PacSignatureData serverSignature = pac.getServerSignature();
235 								PacSignatureData kdcSignature = pac.getKdcSignature();
236 								switch (format) {
237 									case "listing":
238 										System.out.println("ServerSignature:");
239 										System.out.println("  type: " + serverSignature.getType());
240 										System.out.println("  signature: "
241 												+ Base64.getEncoder().encodeToString(serverSignature.getSignature()));
242 										System.out.println("KdcSignature:");
243 										System.out.println("  type: " + kdcSignature.getType());
244 										System.out.println("  signature: "
245 												+ Base64.getEncoder().encodeToString(kdcSignature.getSignature()));
246 										break;
247 								}
248 								break;
249 							default:
250 								System.err.println(
251 										"Ignoring unsupported authorization data (AD-IF-RELEVANT) entry type "
252 												+ adIfRelevantType + " with data "
253 												+ Base64.getEncoder().encodeToString(adIfRelevantTypeData));
254 								break;
255 						}
256 					}
257 					break;
258 				default:
259 					System.err.println("Ignoring unsupported authorization data entry type " + type + " with data "
260 							+ Base64.getEncoder().encodeToString(data));
261 					break;
262 			}
263 		}
264 	}
265 
266 	private static String nullSafe(Object obj) {
267 		return obj != null ? "'" + obj + "'" : "NULL";
268 	}
269 
270 	public static void main(String[] args) throws IOException, SignatureException {
271 		if (args.length == 0) {
272 			System.err.println("No arguments provided");
273 			System.exit(1);
274 		}
275 
276 		int positionalArgs = 0;
277 		String formatValue = "listing";
278 		String verifySignatureValue = null;
279 		boolean breakLoop = false;
280 		while (positionalArgs < args.length && !breakLoop) {
281 			switch (args[positionalArgs]) {
282 				case "--format":
283 					positionalArgs++;
284 					if (positionalArgs > args.length - 1)
285 						throw new IllegalArgumentException("Missing option value for '--format'");
286 					formatValue = args[positionalArgs++];
287 					break;
288 				case "--verify-signature":
289 					positionalArgs++;
290 					if (positionalArgs > args.length - 1)
291 						throw new IllegalArgumentException("Missing option value for '--verify-signature'");
292 					verifySignatureValue = args[positionalArgs++];
293 					break;
294 				case "--":
295 					positionalArgs++;
296 					breakLoop = true;
297 					break;
298 				default:
299 					breakLoop = true;
300 					break;
301 			}
302 		}
303 
304 		if (!formatValue.equals("listing") && !formatValue.equals("sql"))
305 			throw new IllegalArgumentException("Unsupported format value: " + formatValue);
306 
307 		KerberosKey[] keysValue = null;
308 		if (verifySignatureValue != null) {
309 			String loginEntryName = verifySignatureValue;
310 			LoginContext lc = null;
311 			try {
312 				lc = new LoginContext(loginEntryName);
313 				lc.login();
314 				Subject subject = lc.getSubject();
315 				KerberosPrincipal principal = subject.getPrincipals(KerberosPrincipal.class)
316 						.iterator()
317 						.next();
318 				KeyTab keyTab =
319 						subject.getPrivateCredentials(KeyTab.class).iterator().next();
320 				keysValue = keyTab.getKeys(principal);
321 			} catch (LoginException | SecurityException e) {
322 				throw new IllegalStateException(
323 						"Failed to load Kerberos keys for login entry '" + loginEntryName + "'", e);
324 			} finally {
325 				if (lc != null) {
326 					try {
327 						lc.logout();
328 					} catch (LoginException e) {
329 						; // Ignore
330 					}
331 				}
332 			}
333 		}
334 		final KerberosKey[] keys = keysValue;
335 
336 		final String format = formatValue;
337 		if (format.equals("sql")) {
338 			System.out.println("BEGIN TRANSACTION;");
339 			try (BufferedReader r = new BufferedReader(new InputStreamReader(
340 					Krb5AuthzDataDumpPrinter.class.getResourceAsStream("/net/sf/michaelo/tomcat/pac/create-tables.sql"),
341 					StandardCharsets.UTF_8))) {
342 				r.lines().forEach(line -> System.out.println(line));
343 			}
344 		}
345 
346 		for (int i = positionalArgs; i < args.length; i++) {
347 			Path path = Paths.get(args[i]);
348 			if (Files.notExists(path)) {
349 				System.err.printf("Ignoring non-existing path '%s'%n", path);
350 				continue;
351 			}
352 			if (Files.isRegularFile(path)) {
353 				dumpFile(path, format, keys);
354 			} else if (Files.isDirectory(path)) {
355 				try (Stream<Path> stream = Files.walk(path)) {
356 					stream.filter(Files::isRegularFile).forEach(file -> {
357 						try {
358 							dumpFile(file, format, keys);
359 						} catch (IOException e) {
360 							throw new UncheckedIOException(e);
361 						} catch (SignatureException e) {
362 							throw new RuntimeException(e);
363 						}
364 					});
365 				}
366 			} else {
367 				System.err.printf("Ignoring unsupported path '%s'%n", path);
368 				continue;
369 			}
370 		}
371 
372 		if (format.equals("sql")) {
373 			System.out.println("COMMIT;");
374 		}
375 	}
376 }