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