001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016-2017 ForgeRock AS.
015 */
016
017package org.forgerock.json.jose.utils;
018
019import java.nio.BufferOverflowException;
020import java.nio.ByteBuffer;
021
022import org.forgerock.util.Reject;
023
024/**
025 * Utility methods for reading and writing DER-encoded values. This is just the absolute minimum needed to decode and
026 * encode ECDSA signatures to ES256 format.
027 */
028public final class DerUtils {
029    /**
030     * DER tag for integer values.
031     */
032    public static final byte INTEGER_TAG = 0x02;
033    /**
034     * DER tag for sequence values.
035     */
036    public static final byte SEQUENCE_TAG = 0x30;
037
038    private DerUtils() {
039        // Utility class
040    }
041
042    /**
043     * Reads an unsigned integer value into the given byte array. The output will be in big-endian format and aligned
044     * to take up exactly {@code length} bytes (leaving untouched any unused leading bytes).
045     *
046     * @param input the input DER-encoded byte buffer.
047     * @param output the output byte array.
048     * @param offset the offset into the byte array to start writing the integer value.
049     * @param length the maximum length of the byte value (excluding any leading sign byte).
050     * @throws BufferOverflowException if the integer does not fit in the given output buffer slice.
051     */
052    public static void readUnsignedInteger(ByteBuffer input, byte[] output, int offset, int length) {
053        Reject.ifFalse(input.get() == INTEGER_TAG, "Not an integer");
054        int len = readLength(input);
055        if (len > length) {
056            // See if it fits if we assume the first byte is a redundant zero sign byte
057            if (--len > length || input.get() != 0) {
058                throw new BufferOverflowException();
059            }
060        }
061        input.get(output, offset + (length - len), len);
062    }
063
064    /**
065     * Writes an integer value in DER format to the given buffer.
066     *
067     * @param buffer the buffer to write the value to
068     * @param data the integer value (in big-endian format) to write
069     */
070    public static void writeInteger(final ByteBuffer buffer, final byte[] data) {
071        buffer.put(INTEGER_TAG);
072        // according to DER specification, the data cannot start with a leading 0
073        int startIndex = data.length - 1;
074        for (int index = 0; index < data.length; index++) {
075            if (data[index] != 0) {
076                startIndex = index;
077                break;
078            }
079        }
080        final int length = data.length - startIndex;
081        writeLength(buffer, length);
082        buffer.put(data, startIndex, length);
083    }
084
085    /**
086     * Reads a DER-encoded length field from the given byte buffer.
087     *
088     * @param buffer the buffer to read a length field from.
089     * @return the length field.
090     */
091    public static int readLength(final ByteBuffer buffer) {
092        int b = buffer.get();
093        // See http://luca.ntop.org/Teaching/Appunti/asn1.html. If the high-bit of the first byte is 0 then this byte
094        // is the length. Otherwise the first byte (masking off the high bit) gives the number of following bytes
095        // that encode the length (in big-endian unsigned integer format).
096        if ((b & 0x80) == 0) {
097            return b & 0xFF;
098        } else {
099            int numBytes = b & 0x7F;
100            Reject.ifFalse(numBytes > 0 && numBytes < 4, "Unsupported DER length field");
101
102            int len = 0;
103            for (int i = 0; i < numBytes; ++i) {
104                len = (len << 8) + (buffer.get() & 0xFF);
105            }
106            return len;
107        }
108    }
109
110    /**
111     * Writes a length field to the output.
112     * If the length is 127 or less, the byte is the length.
113     * If the length is 128 or greater, the first byte is a combination of 0x80 to indicate the length is defined and
114     * the number of bytes to specify that length. See DER specification for more information.
115     *
116     * @param output the output buffer.
117     * @param length the length to write.
118     */
119    public static void writeLength(final ByteBuffer output, final int length) {
120        if ((length & 0x0000007F) == length) {
121            output.put((byte) length);
122        } else if ((length & 0x000000FF) == length) {
123            output.put((byte) 0x81);
124            output.put((byte) length);
125        } else if ((length & 0x0000FFFF) == length) {
126            output.put((byte) 0x82);
127            output.put((byte) (length >> 8));
128            output.put((byte) length);
129        } else if ((length & 0x00FFFFFF) == length) {
130            output.put((byte) 0x83);
131            output.put((byte) (length >> 16));
132            output.put((byte) (length >> 8));
133            output.put((byte) length);
134        } else {
135            output.put((byte) 0x84);
136            output.put((byte) (length >> 24));
137            output.put((byte) (length >> 16));
138            output.put((byte) (length >> 8));
139            output.put((byte) length);
140        }
141    }
142}