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