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.nio.charset.StandardCharsets;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.Objects;
23  
24  import org.apache.juli.logging.Log;
25  import org.apache.juli.logging.LogFactory;
26  
27  import net.sf.michaelo.tomcat.realm.Sid;
28  
29  /**
30   * A class representing the <a href=
31   * "https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/69e86ccc-85e3-41b9-b514-7d969cd0ed73">{@code KERB_VALIDATION_INFO}</a>
32   * structure from MS-PAC. This implementation only parses the members which are required for the
33   * purpose of this component, everything else is skipped.
34   */
35  public class KerbValidationInfo {
36  
37  	public final static long EXTRA_SIDS_USER_FLAG = 0x00000020L;
38  	public final static long RESOURCE_GROUP_IDS_USER_FLAG = 0x00000200L;
39  
40  	protected final Log logger = LogFactory.getLog(getClass());
41  
42  	private final String effectiveName;
43  	private final String fullName;
44  	private final String logonScript;
45  	private final String profilePath;
46  	private final String homeDirectory;
47  	private final String homeDirectoryDrive;
48  
49  	private final long userId;
50  	private final long primaryGroupId;
51  	private final List<GroupMembership> groupIds;
52  
53  	private final long userFlags;
54  
55  	private final String logonServer;
56  	private final String logonDomainName;
57  	private final Sid logonDomainId;
58  
59  	private final long userAccountControl;
60  
61  	private List<KerbSidAndAttributes> extraSids;
62  
63  	private Sid resourceGroupDomainSid;
64  	private List<GroupMembership> resourceGroupIds;
65  
66  	/**
67  	 * Parses a Kerberos validation info object from a byte array.
68  	 *
69  	 * @param infoBytes
70  	 *            Kerberos validation info structure encoded as bytes
71  	 * @throws NullPointerException
72  	 *             if {@code infoBytes} is null
73  	 * @throws IllegalArgumentException
74  	 *             if {@code infoBytes} is empty
75  	 * @throws IllegalArgumentException
76  	 *             if {@code GroupCount} is not equal to the actually marshaled group count
77  	 * @throws IllegalArgumentException
78  	 *             if {@code SidCount} is not zero, but flag D is not set in {@code UserFlags}
79  	 * @throws IllegalArgumentException
80  	 *             if {@code ExtraSids} is not {@code null}, but flag D is not set in {@code UserFlags}
81  	 * @throws IllegalArgumentException
82  	 *             if {@code SidCount} is not equal to the actually marshaled SID count
83  	 * @throws IllegalArgumentException
84  	 *             if {@code ResourceGroupDomainSid} is not {@code null}, but flag H is not set in
85  	 *             {@code UserFlags}
86  	 * @throws IllegalArgumentException
87  	 *             if {@code ResourceGroupCount} is not zero, but flag H is not set in
88  	 *             {@code UserFlags}
89  	 * @throws IllegalArgumentException
90  	 *             if {@code ResourceGroupIds} is not {@code null}, but flag H is not set in
91  	 *             {@code UserFlags}
92  	 * @throws IllegalArgumentException
93  	 *             if {@code ResourceGroupCount} is not equal to the actually marshaled resource
94  	 *             group count
95  	 * @throws IllegalArgumentException
96  	 *             if any {@code RPC_UNICODE_STRING} is incorrectly NDR-encoded
97  	 */
98  	public KerbValidationInfo(byte[] infoBytes) {
99  		Objects.requireNonNull(infoBytes, "infoBytes cannot be null");
100 		if (infoBytes.length == 0)
101 			throw new IllegalArgumentException("infoBytes cannot be empty");
102 
103 		PacDataBuffer buf = new PacDataBuffer(infoBytes);
104 
105 		// common RPC header
106 		buf.skip(8);
107 		// RPC type marshalling private header
108 		buf.skip(8);
109 		// RPC unique pointer
110 		long uniquePointer = buf.getUnsignedInt();
111 		logPointer("RPC unique", uniquePointer);
112 
113 		// LogonTime
114 		buf.skip(8);
115 		// LogoffTime
116 		buf.skip(8);
117 		// KickOffTime
118 		buf.skip(8);
119 		// PasswordLastSet
120 		buf.skip(8);
121 		// PasswordCanChange
122 		buf.skip(8);
123 		// PasswordMustChange
124 		buf.skip(8);
125 		// EffectiveName
126 		RpcUnicodeString effectiveName = getRpcUnicodeString(buf);
127 		logPointer("effectiveName", effectiveName.getPointer());
128 		// FullName
129 		RpcUnicodeString fullName = getRpcUnicodeString(buf);
130 		logPointer("fullName", fullName.getPointer());
131 		// LogonScript
132 		RpcUnicodeString logonScript = getRpcUnicodeString(buf);
133 		logPointer("logonScript", logonScript.getPointer());
134 		// ProfilePath
135 		RpcUnicodeString profilePath = getRpcUnicodeString(buf);
136 		logPointer("profilePath", profilePath.getPointer());
137 		// HomeDirectory
138 		RpcUnicodeString homeDirectory = getRpcUnicodeString(buf);
139 		logPointer("homeDirectory", homeDirectory.getPointer());
140 		// HomeDirectoryDrive
141 		RpcUnicodeString homeDirectoryDrive = getRpcUnicodeString(buf);
142 		logPointer("homeDirectoryDrive", homeDirectoryDrive.getPointer());
143 		// LogonCount
144 		buf.skip(2);
145 		// BadPasswordCount
146 		buf.skip(2);
147 		// UserId
148 		this.userId = buf.getUnsignedInt();
149 		// PrimaryGroupId
150 		this.primaryGroupId = buf.getUnsignedInt();
151 		// GroupCount
152 		long groupCount = buf.getUnsignedInt();
153 		// GroupIds
154 		long groupIdsPointer = buf.getUnsignedInt();
155 		logPointer("groupIds", groupIdsPointer);
156 		// UserFlags
157 		/* Something isn't right, it appears to be that the bits are in reverse order
158 		 * or the documentation is wrong:
159 		 * - flag H should be at bit 22, but is at bit 9
160 		 * - flag D should be at bit 26, but is at bit 5
161 		 *
162 		 * Samba has the same reversed order: https://github.com/samba-team/samba/blob/9844ac289be3430fd3f72c5e57fa00e012c5d417/librpc/idl/netlogon.idl#L251-L263
163 		 */
164 		this.userFlags = buf.getUnsignedInt();
165 		// UserSessionKey
166 		buf.skip(16);
167 		// LogonServer
168 		RpcUnicodeString logonServer = getRpcUnicodeString(buf);
169 		logPointer("logonServer", logonServer.getPointer());
170 		// LogonDomainName
171 		RpcUnicodeString logonDomainName = getRpcUnicodeString(buf);
172 		logPointer("logonDomainName", logonDomainName.getPointer());
173 		// LogonDomainId
174 		long logonDomainIdPointer = buf.getUnsignedInt();
175 		logPointer("logonDomainId", logonDomainIdPointer);
176 		// Reserved1
177 		buf.skip(8);
178 		// UserAccountControl
179 		/*
180 		 * This is NOT userAccountControl from LDAP, see
181 		 * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/b10cfda1-f24f-441b-8f43-80cb93e786ec
182 		 */
183 		this.userAccountControl = buf.getUnsignedInt();
184 		// SubAuthStatus
185 		buf.skip(4);
186 		// LastSuccessfulILogon
187 		buf.skip(8);
188 		// LastFailedILogon
189 		buf.skip(8);
190 		// FailedILogonCount
191 		buf.skip(4);
192 		// Reserved3
193 		buf.skip(4);
194 		// SidCount
195 		long sidCount = buf.getUnsignedInt();
196 		// ExtraSids
197 		long extraSidsPointer = buf.getUnsignedInt();
198 		logPointer("extraSids", extraSidsPointer);
199 		// ResourceGroupDomainSid
200 		long resourceGroupDomainSidPointer = buf.getUnsignedInt();
201 		logPointer("resourceGroupDomainSid", resourceGroupDomainSidPointer);
202 		// ResourceGroupCount
203 		long resourceGroupCount = buf.getUnsignedInt();
204 		// ResourceGroupIds
205 		long resourceGroupIdsPointer = buf.getUnsignedInt();
206 		logPointer("resourceGroupIds", resourceGroupIdsPointer);
207 
208 		this.effectiveName = getNdrString(buf, effectiveName);
209 		this.fullName = getNdrString(buf, fullName);
210 		this.logonScript = getNdrString(buf, logonScript);
211 		this.profilePath = getNdrString(buf, profilePath);
212 		this.homeDirectory = getNdrString(buf, homeDirectory);
213 		this.homeDirectoryDrive = getNdrString(buf, homeDirectoryDrive);
214 
215 		long actualGroupCount = buf.getUnsignedInt();
216 		if (groupCount != actualGroupCount)
217 			throw new IllegalArgumentException("GroupCount is " + groupCount
218 					+ ", but actual GroupCount is " + actualGroupCount);
219 
220 		this.groupIds = new ArrayList<GroupMembership>();
221 		for (long l = 0L; l < groupCount; l++) {
222 			long relativeId = buf.getUnsignedInt();
223 			long attributes = buf.getUnsignedInt();
224 			this.groupIds.add(new GroupMembership(relativeId, attributes));
225 		}
226 
227 		this.logonServer = getNdrString(buf, logonServer);
228 		this.logonDomainName = getNdrString(buf, logonDomainName);
229 		this.logonDomainId = getRpcSid(buf);
230 
231 		if (sidCount != 0L && (userFlags & EXTRA_SIDS_USER_FLAG) == 0L)
232 			throw new IllegalArgumentException("SidCount is " + sidCount
233 					+ ", but flag D is not set in UserFlags (" + toHexString(userFlags) + ")");
234 
235 		if (extraSidsPointer != 0L && (userFlags & EXTRA_SIDS_USER_FLAG) == 0L)
236 			throw new IllegalArgumentException("ExtraSids is not null ("
237 					+ toHexString(extraSidsPointer)
238 					+ "), but flag D is not set in UserFlags (" + toHexString(userFlags) + ")");
239 
240 		// No need to check for UserFlags because the above tests make sure that flag D is set
241 		if (extraSidsPointer != 0L) {
242 			this.extraSids = new ArrayList<>();
243 			long actualSidCount = buf.getUnsignedInt();
244 			if (sidCount != actualSidCount)
245 				throw new IllegalArgumentException(
246 						"SidCount is " + sidCount + ", but actual SidCount is " + actualSidCount);
247 			long[] sidAttrs = new long[(int) sidCount];
248 			for (long l = 0L; l < sidCount; l++) {
249 				long extraSidPointer = buf.getUnsignedInt();
250 				long attributes = buf.getUnsignedInt();
251 				sidAttrs[(int) l] = attributes;
252 				logPointer("extraSid[" + l + "]", extraSidPointer);
253 			}
254 			for (long l = 0L; l < sidCount; l++) {
255 				Sid extraSid = getRpcSid(buf);
256 				this.extraSids.add(new KerbSidAndAttributes(extraSid, sidAttrs[(int) l]));
257 			}
258 		}
259 
260 		if (resourceGroupDomainSidPointer != 0L && (userFlags & RESOURCE_GROUP_IDS_USER_FLAG) == 0L)
261 			throw new IllegalArgumentException("ResourceGroupDomainSid is not null ("
262 					+ toHexString(resourceGroupDomainSidPointer)
263 					+ "), but flag H is not set in UserFlags (" + toHexString(userFlags) + ")");
264 
265 		if (resourceGroupCount != 0L && (userFlags & RESOURCE_GROUP_IDS_USER_FLAG) == 0L)
266 			throw new IllegalArgumentException("ResourceGroupCount is " + sidCount
267 					+ ", but flag H is not set in UserFlags (" + toHexString(userFlags) + ")");
268 
269 		if (resourceGroupIdsPointer != 0L && (userFlags & RESOURCE_GROUP_IDS_USER_FLAG) == 0L)
270 			throw new IllegalArgumentException("ResourceGroupIds is not null ("
271 					+ toHexString(resourceGroupIdsPointer)
272 					+ "), but flag H is not set in UserFlags (" + toHexString(userFlags) + ")");
273 
274 		// No need to check for UserFlags because the above tests make sure that flag H is set
275 		if (resourceGroupDomainSidPointer != 0L) {
276 			this.resourceGroupDomainSid = getRpcSid(buf);
277 
278 			long actualResourceGroupCount = buf.getUnsignedInt();
279 			if (resourceGroupCount != actualResourceGroupCount)
280 				throw new IllegalArgumentException("ResourceGroupCount is " + resourceGroupCount
281 						+ ", but actual ResourceGroupCount is " + actualResourceGroupCount);
282 
283 			// No need to check for UserFlags because the above tests make sure that flag H is set
284 			if (resourceGroupIdsPointer != 0L) {
285 				this.resourceGroupIds = new ArrayList<>();
286 				for (long l = 0L; l < resourceGroupCount; l++) {
287 					long relativeId = buf.getUnsignedInt();
288 					long attributes = buf.getUnsignedInt();
289 					this.resourceGroupIds.add(new GroupMembership(relativeId, attributes));
290 				}
291 			}
292 		}
293 	}
294 
295 	public String getEffectiveName() {
296 		return effectiveName;
297 	}
298 
299 	public String getFullName() {
300 		return fullName;
301 	}
302 
303 	public String getLogonScript() {
304 		return logonScript;
305 	}
306 
307 	public String getProfilePath() {
308 		return profilePath;
309 	}
310 
311 	public String getHomeDirectory() {
312 		return homeDirectory;
313 	}
314 
315 	public String getHomeDirectoryDrive() {
316 		return homeDirectoryDrive;
317 	}
318 
319 	public long getUserId() {
320 		return userId;
321 	}
322 
323 	public long getPrimaryGroupId() {
324 		return primaryGroupId;
325 	}
326 
327 	public List<GroupMembership> getGroupIds() {
328 		return Collections.unmodifiableList(groupIds);
329 	}
330 
331 	public long getUserFlags() {
332 		return userFlags;
333 	}
334 
335 	public String getLogonServer() {
336 		return logonServer;
337 	}
338 
339 	public String getLogonDomainName() {
340 		return logonDomainName;
341 	}
342 
343 	public Sid getLogonDomainId() {
344 		return logonDomainId;
345 	}
346 
347 	public long getUserAccountControl() {
348 		return userAccountControl;
349 	}
350 
351 	public List<KerbSidAndAttributes> getExtraSids() {
352 		return extraSids != null ? Collections.unmodifiableList(extraSids) : extraSids;
353 	}
354 
355 	public Sid getResourceGroupDomainSid() {
356 		return resourceGroupDomainSid;
357 	}
358 
359 	public List<GroupMembership> getResourceGroupIds() {
360 		return resourceGroupIds != null ? Collections.unmodifiableList(resourceGroupIds)
361 				: resourceGroupIds;
362 	}
363 
364 	private RpcUnicodeString getRpcUnicodeString(PacDataBuffer buf) {
365 		int length = buf.getUnsignedShort();
366 		int maximumLength = buf.getUnsignedShort();
367 		if (maximumLength % 2 == 1)
368 			maximumLength -= 1;
369 		long pointer = buf.getUnsignedInt();
370 
371 		return new RpcUnicodeString(length, maximumLength, pointer);
372 	}
373 
374 	/* See:
375 	 *  - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/ad703c18-564d-4238-a371-8d43cf442f81
376 	 *  - https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03_04_02
377 	 */
378 	private String getNdrString(PacDataBuffer buf, RpcUnicodeString string) {
379 		long maximumCount = buf.getUnsignedInt();
380 		long offset = buf.getUnsignedInt();
381 		long actualCount = buf.getUnsignedInt();
382 
383 		if (offset > maximumCount || actualCount > maximumCount - offset)
384 			throw new IllegalArgumentException(
385 					"Incorrectly NDR-encoded UNICODE_STRING: maximumCount: " + maximumCount
386 							+ ", offset: " + offset + ", actualCount: " + actualCount);
387 
388 		if (maximumCount != string.getMaximumLength() / 2L
389 				|| actualCount != string.getLength() / 2L)
390 			throw new IllegalArgumentException(
391 					"NDR-encoded UNICODE_STRING does not match RPC_UNICODE_STRING: maximumCount: "
392 							+ maximumCount + ", actualCount: " + actualCount + ", maximumLength: "
393 							+ string.getMaximumLength() + ", length: " + string.getLength());
394 
395 		buf.skip(2 * (int) offset);
396 
397 		byte[] dst = new byte[2 * (int) actualCount];
398 		buf.get(dst);
399 
400 		return new String(dst, StandardCharsets.UTF_16LE);
401 	}
402 
403 	private Sid getRpcSid(PacDataBuffer buf) {
404 		long actualSubAuthorityCount = buf.getUnsignedInt();
405 		byte[] sidBytes = new byte[8 + (int) actualSubAuthorityCount * 4];
406 		buf.get(sidBytes);
407 		return new Sid(sidBytes);
408 	}
409 
410 	private void logPointer(String name, long pointer) {
411 		if (logger.isTraceEnabled())
412 			logger.trace(name + " pointer: " + toHexString(pointer));
413 	}
414 
415 	private String toHexString(long l) {
416 		return String.format("0x%08X", l);
417 	}
418 
419 }