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.math.BigInteger;
19  import java.security.Key;
20  import java.security.SignatureException;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Base64;
24  import java.util.List;
25  import java.util.Objects;
26  
27  import org.apache.juli.logging.Log;
28  import org.apache.juli.logging.LogFactory;
29  
30  /**
31   * A class representing the <a href=
32   * "https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/166d8064-c863-41e1-9c23-edaaa5f36962">{@code PAC
33   * Data}</a> structure from MS-PAC. This implementation only parses the embedded structures which are required for the
34   * purpose of this component, everything else is skipped.
35   *
36   * <p><strong>Important:</strong> It is imperative to pass a suitable signature verifier implementation and the long
37   * term Kerberos keys for the principal from the keytab which were used to establish the security context. The simplest
38   * implementation is the {@link PrivateSunPacSignatureVerifier} which uses private Sun classes to perform the
39   * calculation.
40   */
41  public class Pac {
42  
43  	private static final BigInteger EIGHT = BigInteger.valueOf(8L);
44  
45  	private static final long KERB_VALIDATION_INFO = 0x00000001L;
46  	private static final long PAC_CLIENT_INFO = 0x0000000AL;
47  	private static final long UPN_DNS_INFO = 0x0000000CL;
48  	private static final long SERVER_SIGNATURE = 0x00000006L;
49  	private static final long KDC_SIGNATURE = 0x00000007L;
50  
51  	protected final Log logger = LogFactory.getLog(getClass());
52  
53  	private KerbValidationInfo kerbValidationInfo;
54  	private UpnDnsInfo upnDnsInfo;
55  	private PacClientInfo pacClientInfo;
56  	private PacSignatureData serverSignature;
57  	private PacSignatureData kdcSignature;
58  
59  	private final PacSignatureVerifier signatureVerifier;
60  	private final byte[] zeroedPacData;
61  
62  	/**
63  	 * Parses a PAC data object from a byte array.
64  	 *
65  	 * @param pacDataBytes PAC data structure encoded as bytes
66  	 * @param signatureVerifier a signature verifier implementation
67  	 * @throws NullPointerException if {@code infoBytes} is null
68  	 * @throws IllegalArgumentException if {@code infoBytes} is empty
69  	 * @throws NullPointerException if {@code signatureVerifier} is null
70  	 * @throws IllegalArgumentException if PAC version is not 0
71  	 * @throws IllegalArgumentException if an embedded {@code PAC_INFO_BUFFER} structure offset is not a multiple of 8
72  	 * @throws IllegalArgumentException if any embedded structure is invalid
73  	 * @throws IllegalArgumentException if any of the required embedded structures ({@code KERB_VALIDATION_INFO},
74  	 *     {@code PAC_CLIENT_INFO}, {@code PAC_SIGNATURE_DATA} (Server Signature), {@code PAC_SIGNATURE_DATA} (KDC
75  	 *     Signature)) is not present
76  	 */
77  	public Pac(byte[] pacDataBytes, PacSignatureVerifier signatureVerifier) {
78  		Objects.requireNonNull(pacDataBytes, "pacDataBytes cannot be null");
79  		if (pacDataBytes.length == 0) throw new IllegalArgumentException("pacDataBytes cannot be empty");
80  
81  		PacDataBuffer buf = new PacDataBuffer(pacDataBytes);
82  		this.signatureVerifier = Objects.requireNonNull(signatureVerifier, "signatureVerifier cannot be null");
83  
84  		// Read PACTYPE structure
85  		if (logger.isTraceEnabled()) logger.trace("Parsing PACTYPE structure...");
86  		// cBuffers
87  		long buffers = buf.getUnsignedInt();
88  		// Version
89  		long version = buf.getUnsignedInt();
90  		if (version != 0L) throw new IllegalArgumentException("PAC must have version 0, but has " + version);
91  
92  		if (logger.isTraceEnabled())
93  			logger.trace("PAC has version " + version + " and contains " + buffers + " buffers");
94  
95  		// Read PAC_INFO_BUFFER structures
96  		if (logger.isTraceEnabled()) logger.trace("Parsing " + buffers + " PAC_INFO_BUFFER structures...");
97  		List<PacInfoBuffer> pacInfoBuffers = new ArrayList<>();
98  		for (long l = 0L; l < buffers; l++) {
99  			// ulType
100 			long type = buf.getUnsignedInt();
101 			// cbBufferSize
102 			long bufferSize = buf.getUnsignedInt();
103 			// Offset
104 			BigInteger offset = buf.getUnsignedLong();
105 			if (!offset.mod(EIGHT).equals(BigInteger.ZERO))
106 				throw new IllegalArgumentException("PAC_INFO_BUFFER offset must be multiple of 8, but is " + offset);
107 			int pos = buf.position();
108 			buf.position(offset.intValue());
109 			byte[] data = new byte[(int) bufferSize];
110 			buf.get(data);
111 			buf.position(pos);
112 			if (logger.isTraceEnabled())
113 				logger.trace("PAC_INFO_BUFFER describes type " + String.format("0x%08X", type)
114 						+ " with size " + bufferSize + " and offset " + offset + " containing data "
115 						+ Base64.getEncoder().encodeToString(data));
116 
117 			pacInfoBuffers.add(new PacInfoBuffer(type, bufferSize, offset, data));
118 		}
119 
120 		zeroedPacData = Arrays.copyOf(pacDataBytes, pacDataBytes.length);
121 
122 		for (PacInfoBuffer pacInfoBuffer : pacInfoBuffers) {
123 			long type = pacInfoBuffer.getType();
124 			byte[] data = pacInfoBuffer.getData();
125 			if (type == KERB_VALIDATION_INFO) {
126 				if (kerbValidationInfo != null) {
127 					if (logger.isTraceEnabled()) logger.trace("Ignoring additional KERB_VALIDATION_INFO structure");
128 				} else {
129 					if (logger.isTraceEnabled()) logger.trace("Parsing KERB_VALIDATION_INFO structure...");
130 					kerbValidationInfo = new KerbValidationInfo(data);
131 				}
132 			} else if (type == UPN_DNS_INFO) {
133 				if (upnDnsInfo != null) {
134 					if (logger.isTraceEnabled()) logger.trace("Ignoring additional UPN_DNS_INFO structure");
135 				} else {
136 					if (logger.isTraceEnabled()) logger.trace("Parsing UPN_DNS_INFO structure...");
137 					upnDnsInfo = new UpnDnsInfo(data);
138 				}
139 			} else if (type == PAC_CLIENT_INFO) {
140 				if (pacClientInfo != null) {
141 					if (logger.isTraceEnabled()) logger.trace("Ignoring additional PAC_CLIENT_INFO structure");
142 				} else {
143 					if (logger.isTraceEnabled()) logger.trace("Parsing PAC_CLIENT_INFO structure...");
144 					pacClientInfo = new PacClientInfo(data);
145 				}
146 			} else if (type == SERVER_SIGNATURE) {
147 				if (serverSignature != null) {
148 					if (logger.isTraceEnabled())
149 						logger.trace("Ignoring additional PAC_SIGNATURE_DATA (Server Signature) structure");
150 				} else {
151 					if (logger.isTraceEnabled())
152 						logger.trace("Parsing PAC_SIGNATURE_DATA (Server Signature) structure...");
153 					serverSignature = new PacSignatureData(data);
154 					int from = pacInfoBuffer.getOffset().intValue() + 4; // sizeof(SignatureType)
155 					int to = from + serverSignature.getType().getSize();
156 					Arrays.fill(zeroedPacData, from, to, (byte) 0);
157 				}
158 			} else if (type == KDC_SIGNATURE) {
159 				if (kdcSignature != null) {
160 					if (logger.isTraceEnabled())
161 						logger.trace("Ignoring additional PAC_SIGNATURE_DATA (KDC Signature) structure");
162 				} else {
163 					if (logger.isTraceEnabled())
164 						logger.trace("Parsing PAC_SIGNATURE_DATA (KDC Signature) structure...");
165 					kdcSignature = new PacSignatureData(data);
166 					int from = pacInfoBuffer.getOffset().intValue() + 4; // sizeof(SignatureType)
167 					int to = from + kdcSignature.getType().getSize();
168 					Arrays.fill(zeroedPacData, from, to, (byte) 0);
169 				}
170 			} else {
171 				if (logger.isTraceEnabled())
172 					logger.trace("Ignoring unsupported structure type " + String.format("0x%08X", type) + " with data "
173 							+ Base64.getEncoder().encodeToString(data));
174 			}
175 		}
176 
177 		if (kerbValidationInfo == null)
178 			throw new IllegalArgumentException("PAC does not contain required KERB_VALIDATION_INFO structure");
179 
180 		if (pacClientInfo == null)
181 			throw new IllegalArgumentException("PAC does not contain required PAC_CLIENT_INFO structure");
182 
183 		if (serverSignature == null)
184 			throw new IllegalArgumentException(
185 					"PAC does not contain required PAC_SIGNATURE_DATA (Server Signature) structure");
186 
187 		if (kdcSignature == null)
188 			throw new IllegalArgumentException(
189 					"PAC does not contain required PAC_SIGNATURE_DATA (KDC Signature) structure");
190 	}
191 
192 	public KerbValidationInfo getKerbValidationInfo() {
193 		return kerbValidationInfo;
194 	}
195 
196 	public UpnDnsInfo getUpnDnsInfo() {
197 		return upnDnsInfo;
198 	}
199 
200 	public PacClientInfo getPacClientInfo() {
201 		return pacClientInfo;
202 	}
203 
204 	public PacSignatureData getServerSignature() {
205 		return serverSignature;
206 	}
207 
208 	public PacSignatureData getKdcSignature() {
209 		return kdcSignature;
210 	}
211 
212 	/**
213 	 * Verifies the server signature of this PAC data structure with zeroed server and KDC signature values with the
214 	 * supplied long term Kerberos keys.
215 	 *
216 	 * @param keys an array of long term Kerberos keys for the principal from the keytab which was used to establish the
217 	 *     security context
218 	 * @throws SignatureException if the signature validation fails with all supplied keys
219 	 * @see PacSignatureVerifier
220 	 */
221 	public void verifySignature(Key[] keys) throws SignatureException {
222 		signatureVerifier.verify(serverSignature, zeroedPacData, keys);
223 	}
224 }