|
28 | 28 | import java.io.InputStream;
|
29 | 29 | import java.nio.ByteBuffer;
|
30 | 30 | import java.nio.ByteOrder;
|
| 31 | +import java.util.ArrayList; |
31 | 32 | import java.util.Arrays;
|
32 |
| -import lombok.AllArgsConstructor; |
| 33 | +import java.util.List; |
| 34 | +import java.util.Optional; |
33 | 35 | import lombok.NonNull;
|
| 36 | +import lombok.ToString; |
| 37 | +import lombok.Value; |
34 | 38 |
|
35 | 39 | public class BinaryUtil {
|
36 | 40 |
|
@@ -229,10 +233,48 @@ public static byte[] encodeDerLength(final int length) {
|
229 | 233 | }
|
230 | 234 | }
|
231 | 235 |
|
232 |
| - @AllArgsConstructor |
| 236 | + @ToString |
| 237 | + public enum DerTagClass { |
| 238 | + UNIVERSAL, |
| 239 | + APPLICATION, |
| 240 | + CONTEXT_SPECIFIC, |
| 241 | + PRIVATE; |
| 242 | + |
| 243 | + public static DerTagClass parse(byte tag) { |
| 244 | + switch ((tag >> 6) & 0x03) { |
| 245 | + case 0x0: |
| 246 | + return DerTagClass.UNIVERSAL; |
| 247 | + case 0x1: |
| 248 | + return DerTagClass.APPLICATION; |
| 249 | + case 0x2: |
| 250 | + return DerTagClass.CONTEXT_SPECIFIC; |
| 251 | + case 0x3: |
| 252 | + return DerTagClass.PRIVATE; |
| 253 | + default: |
| 254 | + throw new RuntimeException("This should be impossible"); |
| 255 | + } |
| 256 | + } |
| 257 | + } |
| 258 | + |
| 259 | + @Value |
| 260 | + private static class ParseDerAnyResult { |
| 261 | + DerTagClass tagClass; |
| 262 | + boolean constructed; |
| 263 | + byte tagValue; |
| 264 | + byte[] content; |
| 265 | + int nextOffset; |
| 266 | + } |
| 267 | + |
| 268 | + @Value |
233 | 269 | public static class ParseDerResult<T> {
|
234 |
| - public final T result; |
235 |
| - public final int nextOffset; |
| 270 | + /** The parsed value, excluding the tag-and-length header. */ |
| 271 | + public T result; |
| 272 | + |
| 273 | + /** |
| 274 | + * The offset of the first octet past the end of the parsed value. In other words, the offset to |
| 275 | + * continue reading from. |
| 276 | + */ |
| 277 | + public int nextOffset; |
236 | 278 | }
|
237 | 279 |
|
238 | 280 | public static ParseDerResult<Integer> parseDerLength(@NonNull byte[] der, int offset) {
|
@@ -287,46 +329,163 @@ public static ParseDerResult<Integer> parseDerLength(@NonNull byte[] der, int of
|
287 | 329 | }
|
288 | 330 | }
|
289 | 331 |
|
290 |
| - private static ParseDerResult<byte[]> parseDerTagged( |
291 |
| - @NonNull byte[] der, int offset, byte expectTag) { |
| 332 | + private static ParseDerAnyResult parseDerAny(@NonNull byte[] der, int offset) { |
292 | 333 | final int len = der.length - offset;
|
293 | 334 | if (len == 0) {
|
294 | 335 | throw new IllegalArgumentException(
|
295 | 336 | String.format("Empty input at offset %d: 0x%s", offset, BinaryUtil.toHex(der)));
|
296 | 337 | } else {
|
297 | 338 | final byte tag = der[offset];
|
298 |
| - if (tag == expectTag) { |
299 |
| - final ParseDerResult<Integer> contentLen = parseDerLength(der, offset + 1); |
300 |
| - final int contentEnd = contentLen.nextOffset + contentLen.result; |
301 |
| - return new ParseDerResult<>( |
302 |
| - Arrays.copyOfRange(der, contentLen.nextOffset, contentEnd), contentEnd); |
| 339 | + final ParseDerResult<Integer> contentLen = parseDerLength(der, offset + 1); |
| 340 | + final int contentEnd = contentLen.nextOffset + contentLen.result; |
| 341 | + return new ParseDerAnyResult( |
| 342 | + DerTagClass.parse(tag), |
| 343 | + (tag & 0x20) != 0, |
| 344 | + (byte) (tag & 0x1f), |
| 345 | + Arrays.copyOfRange(der, contentLen.nextOffset, contentEnd), |
| 346 | + contentEnd); |
| 347 | + } |
| 348 | + } |
| 349 | + |
| 350 | + /** |
| 351 | + * Parse a DER header with the given tag value, constructed bit and tag class, and return a copy |
| 352 | + * of the value octets. If any of the three criteria do not match, return empty instead. |
| 353 | + * |
| 354 | + * @param der DER source to read from. |
| 355 | + * @param offset The offset in <code>der</code> from which to start reading. |
| 356 | + * @param expectTag The expected tag value, excluding the constructed bit and tag class. This is |
| 357 | + * the 5 least significant bits of the tag octet. |
| 358 | + * @param constructed The expected "constructed" bit. This is bit 6 (the third-most significant |
| 359 | + * bit) of the tag octet. |
| 360 | + * @param expectTagClass The expected tag class. This is the 2 most significant bits of the tag |
| 361 | + * octet. |
| 362 | + * @return A copy of the value octets, if the parsed tag matches <code>expectTag</code>, <code> |
| 363 | + * constructed</code> and <code>expectTagClass</code>, otherwise empty. {@link |
| 364 | + * ParseDerResult#nextOffset} is always returned. |
| 365 | + */ |
| 366 | + public static ParseDerResult<Optional<byte[]>> parseDerTaggedOrSkip( |
| 367 | + @NonNull byte[] der, |
| 368 | + int offset, |
| 369 | + byte expectTag, |
| 370 | + boolean constructed, |
| 371 | + DerTagClass expectTagClass) { |
| 372 | + final ParseDerAnyResult result = parseDerAny(der, offset); |
| 373 | + if (result.tagValue == expectTag |
| 374 | + && result.constructed == constructed |
| 375 | + && result.tagClass == expectTagClass) { |
| 376 | + return new ParseDerResult<>(Optional.of(result.content), result.nextOffset); |
| 377 | + } else { |
| 378 | + return new ParseDerResult<>(Optional.empty(), result.nextOffset); |
| 379 | + } |
| 380 | + } |
| 381 | + |
| 382 | + /** |
| 383 | + * Parse a DER header with the given tag value, constructed bit and tag class, and return a copy |
| 384 | + * of the value octets. If any of the three criteria do not match, throw an {@link |
| 385 | + * IllegalArgumentException}. |
| 386 | + * |
| 387 | + * @param der DER source to read from. |
| 388 | + * @param offset The offset in <code>der</code> from which to start reading. |
| 389 | + * @param expectTag The expected tag value, excluding the constructed bit and tag class. This is |
| 390 | + * the 5 least significant bits of the tag octet. |
| 391 | + * @param constructed The expected "constructed" bit. This is bit 6 (the third-most significant |
| 392 | + * bit) of the tag octet. |
| 393 | + * @param expectTagClass The expected tag class. This is the 2 most significant bits of the tag |
| 394 | + * octet. |
| 395 | + * @return A copy of the value octets, if the parsed tag matches <code>expectTag</code>, <code> |
| 396 | + * constructed</code> and <code>expectTagClass</code>, otherwise empty. {@link |
| 397 | + * ParseDerResult#nextOffset} is always returned. |
| 398 | + */ |
| 399 | + private static ParseDerResult<byte[]> parseDerTagged( |
| 400 | + @NonNull byte[] der, |
| 401 | + int offset, |
| 402 | + byte expectTag, |
| 403 | + boolean constructed, |
| 404 | + DerTagClass expectTagClass) { |
| 405 | + final ParseDerAnyResult result = parseDerAny(der, offset); |
| 406 | + if (result.tagValue == expectTag) { |
| 407 | + if (result.constructed == constructed) { |
| 408 | + if (result.tagClass == expectTagClass) { |
| 409 | + return new ParseDerResult<>(result.content, result.nextOffset); |
| 410 | + } else { |
| 411 | + throw new IllegalArgumentException( |
| 412 | + String.format( |
| 413 | + "Incorrect tag class: expected %s, found %s at offset %d: 0x%s", |
| 414 | + expectTagClass, result.tagClass, offset, BinaryUtil.toHex(der))); |
| 415 | + } |
303 | 416 | } else {
|
304 | 417 | throw new IllegalArgumentException(
|
305 | 418 | String.format(
|
306 |
| - "Incorrect tag: 0x%02x (expected 0x%02x) at offset %d: 0x%s", |
307 |
| - tag, expectTag, offset, BinaryUtil.toHex(der))); |
| 419 | + "Incorrect constructed bit: expected %s, found %s at offset %d: 0x%s", |
| 420 | + constructed, result.constructed, offset, BinaryUtil.toHex(der))); |
308 | 421 | }
|
| 422 | + } else { |
| 423 | + throw new IllegalArgumentException( |
| 424 | + String.format( |
| 425 | + "Incorrect tag: expected 0x%02x, found 0x%02x at offset %d: 0x%s", |
| 426 | + expectTag, result.tagValue, offset, BinaryUtil.toHex(der))); |
309 | 427 | }
|
310 | 428 | }
|
311 | 429 |
|
312 |
| - /** Parse a SEQUENCE and return a copy of the content octets. */ |
313 |
| - public static ParseDerResult<byte[]> parseDerSequence(@NonNull byte[] der, int offset) { |
314 |
| - return parseDerTagged(der, offset, (byte) 0x30); |
| 430 | + /** Function to parse an element of a DER SEQUENCE. */ |
| 431 | + @FunctionalInterface |
| 432 | + public interface ParseDerSequenceElementFunction<T> { |
| 433 | + /** |
| 434 | + * Parse an element of a DER SEQUENCE. |
| 435 | + * |
| 436 | + * @param sequenceDer The content octets of the parent SEQUENCE. This includes ALL elements in |
| 437 | + * the sequence. |
| 438 | + * @param elementOffset The offset into <code>sequenceDer</code> from where to parse the |
| 439 | + * element. |
| 440 | + * @return A {@link ParseDerResult} whose {@link ParseDerResult#result} is the parsed element |
| 441 | + * and {@link ParseDerResult#nextOffset} is the offset of the first octet past the end of |
| 442 | + * the parsed element. |
| 443 | + */ |
| 444 | + ParseDerResult<T> parse(@NonNull byte[] sequenceDer, int elementOffset); |
315 | 445 | }
|
316 | 446 |
|
317 | 447 | /**
|
318 |
| - * Parse an explicitly tagged value of class "context-specific" (bits 8-7 are 0b10), in |
319 |
| - * "constructed" encoding (bit 6 is 1), with a prescribed tag value, and return a copy of the |
320 |
| - * content octets. |
| 448 | + * Parse the elements of a SEQUENCE using the given element parsing function. |
| 449 | + * |
| 450 | + * @param der DER source array to read from |
| 451 | + * @param offset Offset from which to begin reading the first element |
| 452 | + * @param endOffset Offset of the first octet past the end of the sequence |
| 453 | + * @param parseElement Function to use to parse each element in the sequence. |
321 | 454 | */
|
322 |
| - private static ParseDerResult<byte[]> parseDerExplicitlyTaggedContextSpecificConstructed( |
323 |
| - @NonNull byte[] der, int offset, byte tagNumber) { |
324 |
| - if (tagNumber <= 30 && tagNumber >= 0) { |
325 |
| - return parseDerTagged(der, offset, (byte) ((tagNumber & 0x1f) | 0xa0)); |
326 |
| - } else { |
327 |
| - throw new UnsupportedOperationException( |
328 |
| - String.format("Tag number out of range: %d (expected 0 to 30, inclusive)", tagNumber)); |
| 455 | + public static <T> ParseDerResult<List<T>> parseDerSequenceContents( |
| 456 | + @NonNull byte[] der, |
| 457 | + int offset, |
| 458 | + int endOffset, |
| 459 | + @NonNull ParseDerSequenceElementFunction<T> parseElement) { |
| 460 | + List<T> result = new ArrayList<>(); |
| 461 | + int seqOffset = offset; |
| 462 | + while (seqOffset < endOffset) { |
| 463 | + ParseDerResult<T> elementResult = parseElement.parse(der, seqOffset); |
| 464 | + result.add(elementResult.result); |
| 465 | + seqOffset = elementResult.nextOffset; |
329 | 466 | }
|
| 467 | + return new ParseDerResult<>(result, endOffset); |
| 468 | + } |
| 469 | + |
| 470 | + /** |
| 471 | + * Parse a SEQUENCE using the given element parsing function. |
| 472 | + * |
| 473 | + * @param der DER source array to read from |
| 474 | + * @param offset Offset from which to begin reading the SEQUENCE |
| 475 | + * @param parseElement Function to use to parse each element in the sequence. |
| 476 | + */ |
| 477 | + public static <T> ParseDerResult<List<T>> parseDerSequence( |
| 478 | + @NonNull byte[] der, int offset, @NonNull ParseDerSequenceElementFunction<T> parseElement) { |
| 479 | + final ParseDerResult<byte[]> seq = |
| 480 | + parseDerTagged(der, offset, (byte) 0x10, true, DerTagClass.UNIVERSAL); |
| 481 | + final ParseDerResult<List<T>> res = |
| 482 | + parseDerSequenceContents(seq.result, 0, seq.result.length, parseElement); |
| 483 | + return new ParseDerResult<>(res.result, seq.nextOffset); |
| 484 | + } |
| 485 | + |
| 486 | + /** Parse an Octet String. */ |
| 487 | + public static ParseDerResult<byte[]> parseDerOctetString(@NonNull byte[] der, int offset) { |
| 488 | + return parseDerTagged(der, offset, (byte) 0x04, false, DerTagClass.UNIVERSAL); |
330 | 489 | }
|
331 | 490 |
|
332 | 491 | public static byte[] encodeDerObjectId(@NonNull byte[] oid) {
|
|
0 commit comments