Skip to content

Commit 6dfbe9a

Browse files
committed
Move DER encoding/parsing functions to BinaryUtil
1 parent 1b547a4 commit 6dfbe9a

File tree

4 files changed

+257
-254
lines changed

4 files changed

+257
-254
lines changed

webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java

Lines changed: 7 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@
4141
import java.util.Arrays;
4242
import java.util.HashMap;
4343
import java.util.Map;
44-
import lombok.AllArgsConstructor;
45-
import lombok.NonNull;
4644

4745
final class WebAuthnCodecs {
4846

@@ -197,168 +195,23 @@ private static PublicKey importCoseEcdsaPublicKey(CBORObject cose)
197195
}
198196

199197
final byte[] algId =
200-
encodeDerSequence(
201-
encodeDerObjectId(EC_PUBLIC_KEY_OID.getBytes()), encodeDerObjectId(curveOid));
198+
BinaryUtil.encodeDerSequence(
199+
BinaryUtil.encodeDerObjectId(EC_PUBLIC_KEY_OID.getBytes()),
200+
BinaryUtil.encodeDerObjectId(curveOid));
202201

203202
final byte[] rawKey =
204-
encodeDerBitStringWithZeroUnused(
203+
BinaryUtil.encodeDerBitStringWithZeroUnused(
205204
BinaryUtil.concat(
206205
new byte[] {0x04}, // Raw EC public key with x and y
207206
x,
208207
y));
209208

210-
final byte[] x509Key = encodeDerSequence(algId, rawKey);
209+
final byte[] x509Key = BinaryUtil.encodeDerSequence(algId, rawKey);
211210

212211
KeyFactory kFact = KeyFactory.getInstance("EC");
213212
return kFact.generatePublic(new X509EncodedKeySpec(x509Key));
214213
}
215214

216-
static byte[] encodeDerLength(final int length) {
217-
if (length < 0) {
218-
throw new IllegalArgumentException("Length is negative: " + length);
219-
} else if (length <= 0x7f) {
220-
return new byte[] {(byte) (length & 0xff)};
221-
} else if (length <= 0xff) {
222-
return new byte[] {(byte) (0x80 | 0x01), (byte) (length & 0xff)};
223-
} else if (length <= 0xffff) {
224-
return new byte[] {
225-
(byte) (0x80 | 0x02), (byte) ((length >> 8) & 0xff), (byte) (length & 0xff)
226-
};
227-
} else if (length <= 0xffffff) {
228-
return new byte[] {
229-
(byte) (0x80 | 0x03),
230-
(byte) ((length >> 16) & 0xff),
231-
(byte) ((length >> 8) & 0xff),
232-
(byte) (length & 0xff)
233-
};
234-
} else {
235-
return new byte[] {
236-
(byte) (0x80 | 0x04),
237-
(byte) ((length >> 24) & 0xff),
238-
(byte) ((length >> 16) & 0xff),
239-
(byte) ((length >> 8) & 0xff),
240-
(byte) (length & 0xff)
241-
};
242-
}
243-
}
244-
245-
@AllArgsConstructor
246-
static class ParseDerResult<T> {
247-
final T result;
248-
final int nextOffset;
249-
}
250-
251-
static ParseDerResult<Integer> parseDerLength(@NonNull byte[] der, int offset) {
252-
final int len = der.length - offset;
253-
if (len == 0) {
254-
throw new IllegalArgumentException("Empty input");
255-
} else if ((der[offset] & 0x80) == 0) {
256-
return new ParseDerResult<>(der[offset] & 0xff, offset + 1);
257-
} else {
258-
final int longLen = der[offset] & 0x7f;
259-
if (len >= longLen) {
260-
switch (longLen) {
261-
case 0:
262-
throw new IllegalArgumentException(
263-
String.format(
264-
"Empty length encoding at offset %d: 0x%s", offset, BinaryUtil.toHex(der)));
265-
case 1:
266-
return new ParseDerResult<>(der[offset + 1] & 0xff, offset + 2);
267-
case 2:
268-
return new ParseDerResult<>(
269-
((der[offset + 1] & 0xff) << 8) | (der[offset + 2] & 0xff), offset + 3);
270-
case 3:
271-
return new ParseDerResult<>(
272-
((der[offset + 1] & 0xff) << 16)
273-
| ((der[offset + 2] & 0xff) << 8)
274-
| (der[offset + 3] & 0xff),
275-
offset + 4);
276-
case 4:
277-
if ((der[offset + 1] & 0x80) == 0) {
278-
return new ParseDerResult<>(
279-
((der[offset + 1] & 0xff) << 24)
280-
| ((der[offset + 2] & 0xff) << 16)
281-
| ((der[offset + 3] & 0xff) << 8)
282-
| (der[offset + 4] & 0xff),
283-
offset + 5);
284-
} else {
285-
throw new UnsupportedOperationException(
286-
String.format(
287-
"Length out of range of int: 0x%02x%02x%02x%02x",
288-
der[offset + 1], der[offset + 2], der[offset + 3], der[offset + 4]));
289-
}
290-
default:
291-
throw new UnsupportedOperationException(
292-
String.format("Length is too long for int: %d octets", longLen));
293-
}
294-
} else {
295-
throw new IllegalArgumentException(
296-
String.format(
297-
"Length encoding needs %d octets but only %s remain at index %d: 0x%s",
298-
longLen, len - (offset + 1), offset + 1, BinaryUtil.toHex(der)));
299-
}
300-
}
301-
}
302-
303-
private static ParseDerResult<byte[]> parseDerTagged(
304-
@NonNull byte[] der, int offset, byte expectTag) {
305-
final int len = der.length - offset;
306-
if (len == 0) {
307-
throw new IllegalArgumentException(
308-
String.format("Empty input at offset %d: 0x%s", offset, BinaryUtil.toHex(der)));
309-
} else {
310-
final byte tag = der[offset];
311-
if (tag == expectTag) {
312-
final ParseDerResult<Integer> contentLen = parseDerLength(der, offset + 1);
313-
final int contentEnd = contentLen.nextOffset + contentLen.result;
314-
return new ParseDerResult<>(
315-
Arrays.copyOfRange(der, contentLen.nextOffset, contentEnd), contentEnd);
316-
} else {
317-
throw new IllegalArgumentException(
318-
String.format(
319-
"Incorrect tag: 0x%02x (expected 0x%02x) at offset %d: 0x%s",
320-
tag, expectTag, offset, BinaryUtil.toHex(der)));
321-
}
322-
}
323-
}
324-
325-
/** Parse a SEQUENCE and return a copy of the content octets. */
326-
static ParseDerResult<byte[]> parseDerSequence(@NonNull byte[] der, int offset) {
327-
return parseDerTagged(der, offset, (byte) 0x30);
328-
}
329-
330-
/**
331-
* Parse an explicitly tagged value of class "context-specific" (bits 8-7 are 0b10), in
332-
* "constructed" encoding (bit 6 is 1), with a prescribed tag value, and return a copy of the
333-
* content octets.
334-
*/
335-
static ParseDerResult<byte[]> parseDerExplicitlyTaggedContextSpecificConstructed(
336-
@NonNull byte[] der, int offset, byte tagNumber) {
337-
if (tagNumber <= 30 && tagNumber >= 0) {
338-
return parseDerTagged(der, offset, (byte) ((tagNumber & 0x1f) | 0xa0));
339-
} else {
340-
throw new UnsupportedOperationException(
341-
String.format("Tag number out of range: %d (expected 0 to 30, inclusive)", tagNumber));
342-
}
343-
}
344-
345-
private static byte[] encodeDerObjectId(@NonNull byte[] oid) {
346-
byte[] result = new byte[2 + oid.length];
347-
result[0] = 0x06;
348-
result[1] = (byte) oid.length;
349-
return BinaryUtil.copyInto(oid, result, 2);
350-
}
351-
352-
private static byte[] encodeDerBitStringWithZeroUnused(@NonNull byte[] content) {
353-
return BinaryUtil.concat(
354-
new byte[] {0x03}, encodeDerLength(1 + content.length), new byte[] {0}, content);
355-
}
356-
357-
static byte[] encodeDerSequence(final byte[]... items) {
358-
byte[] content = BinaryUtil.concat(items);
359-
return BinaryUtil.concat(new byte[] {0x30}, encodeDerLength(content.length), content);
360-
}
361-
362215
private static PublicKey importCoseEdDsaPublicKey(CBORObject cose)
363216
throws InvalidKeySpecException, NoSuchAlgorithmException {
364217
final int curveId = cose.get(CBORObject.FromObject(-1)).AsInt32();
@@ -374,7 +227,8 @@ private static PublicKey importCoseEd25519PublicKey(CBORObject cose)
374227
throws InvalidKeySpecException, NoSuchAlgorithmException {
375228
final byte[] rawKey = cose.get(CBORObject.FromObject(-2)).GetByteString();
376229
final byte[] x509Key =
377-
encodeDerSequence(ED25519_ALG_ID.getBytes(), encodeDerBitStringWithZeroUnused(rawKey));
230+
BinaryUtil.encodeDerSequence(
231+
ED25519_ALG_ID.getBytes(), BinaryUtil.encodeDerBitStringWithZeroUnused(rawKey));
378232

379233
KeyFactory kFact = KeyFactory.getInstance("EdDSA");
380234
return kFact.generatePublic(new X509EncodedKeySpec(x509Key));

webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala

Lines changed: 0 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
package com.yubico.webauthn
2626

27-
import com.yubico.internal.util.BinaryUtil
2827
import com.yubico.webauthn.data.ByteArray
2928
import com.yubico.webauthn.test.Util
3029
import org.junit.runner.RunWith
@@ -127,105 +126,5 @@ class WebAuthnCodecsSpec
127126

128127
}
129128

130-
describe("DER parsing and encoding:") {
131-
it("encodeDerLength and parseDerLength are each other's inverse.") {
132-
forAll(
133-
Gen.chooseNum(0, Int.MaxValue),
134-
Arbitrary.arbitrary[Array[Byte]],
135-
) { (len: Int, prefix: Array[Byte]) =>
136-
val encoded = WebAuthnCodecs.encodeDerLength(len)
137-
val decoded = WebAuthnCodecs.parseDerLength(encoded, 0)
138-
val decodedWithPrefix = WebAuthnCodecs.parseDerLength(
139-
BinaryUtil.concat(prefix, encoded),
140-
prefix.length,
141-
)
142-
143-
decoded.result should equal(len)
144-
decoded.nextOffset should equal(encoded.length)
145-
decodedWithPrefix.result should equal(len)
146-
decodedWithPrefix.nextOffset should equal(
147-
prefix.length + encoded.length
148-
)
149-
150-
val recoded = WebAuthnCodecs.encodeDerLength(decoded.result)
151-
recoded should equal(encoded)
152-
}
153-
}
154-
155-
it("parseDerLength tolerates unnecessarily long encodings.") {
156-
WebAuthnCodecs
157-
.parseDerLength(Array(0x81, 0).map(_.toByte), 0)
158-
.result should equal(0)
159-
WebAuthnCodecs
160-
.parseDerLength(Array(0x82, 0, 0).map(_.toByte), 0)
161-
.result should equal(0)
162-
WebAuthnCodecs
163-
.parseDerLength(Array(0x83, 0, 0, 0).map(_.toByte), 0)
164-
.result should equal(0)
165-
WebAuthnCodecs
166-
.parseDerLength(Array(0x84, 0, 0, 0, 0).map(_.toByte), 0)
167-
.result should equal(0)
168-
WebAuthnCodecs
169-
.parseDerLength(Array(0x81, 7).map(_.toByte), 0)
170-
.result should equal(7)
171-
WebAuthnCodecs
172-
.parseDerLength(Array(0x82, 0, 7).map(_.toByte), 0)
173-
.result should equal(7)
174-
WebAuthnCodecs
175-
.parseDerLength(Array(0x83, 0, 0, 7).map(_.toByte), 0)
176-
.result should equal(7)
177-
WebAuthnCodecs
178-
.parseDerLength(Array(0x84, 0, 0, 4, 2).map(_.toByte), 0)
179-
.result should equal(1026)
180-
WebAuthnCodecs
181-
.parseDerLength(Array(0x84, 0, 1, 33, 7).map(_.toByte), 0)
182-
.result should equal(73991)
183-
}
184-
185-
it("encodeDerSequence and parseDerSequenceEnd are (almost) each other's inverse.") {
186-
forAll { (data: Array[Array[Byte]], prefix: Array[Byte]) =>
187-
val encoded = WebAuthnCodecs.encodeDerSequence(data: _*)
188-
val decoded = WebAuthnCodecs.parseDerSequence(encoded, 0)
189-
val encodedWithPrefix = BinaryUtil.concat(prefix, encoded)
190-
val decodedWithPrefix = WebAuthnCodecs.parseDerSequence(
191-
encodedWithPrefix,
192-
prefix.length,
193-
)
194-
195-
val expectedContent: Array[Byte] = BinaryUtil.concat(data: _*)
196-
decoded.result should equal(expectedContent)
197-
decodedWithPrefix.result should equal(expectedContent)
198-
decoded.nextOffset should equal(encoded.length)
199-
decodedWithPrefix.nextOffset should equal(
200-
prefix.length + encoded.length
201-
)
202-
}
203-
}
204-
205-
it("parseDerSequence fails if the first byte is not 0x30.") {
206-
forAll { (tag: Byte, data: Array[Array[Byte]]) =>
207-
whenever(tag != 0x30) {
208-
val encoded = WebAuthnCodecs.encodeDerSequence(data: _*)
209-
an[IllegalArgumentException] shouldBe thrownBy {
210-
WebAuthnCodecs.parseDerSequence(
211-
encoded.updated(0, tag),
212-
0,
213-
)
214-
}
215-
}
216-
}
217-
}
218-
219-
it("parseDerSequence fails on empty input.") {
220-
an[IllegalArgumentException] shouldBe thrownBy {
221-
WebAuthnCodecs.parseDerSequence(Array.empty, 0)
222-
}
223-
forAll { data: Array[Byte] =>
224-
an[IllegalArgumentException] shouldBe thrownBy {
225-
WebAuthnCodecs.parseDerSequence(data, data.length)
226-
}
227-
}
228-
}
229-
}
230129
}
231130
}

0 commit comments

Comments
 (0)