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