1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package social.grain
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test
import social.grain.data.api.derToJose
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.Signature
import java.security.spec.ECGenParameterSpec
/**
* ES256 signatures are the one place a silent encoding bug would cost a
* developer a day: JCA emits ASN.1 DER, JWS wants raw r||s, and a wrong answer
* shows up only as an opaque 401 from the PDS.
*/
class DerToJoseTest {
@Test
fun `converts a real ECDSA signature to 64 raw bytes`() {
val keyPair = KeyPairGenerator.getInstance("EC").apply {
initialize(ECGenParameterSpec("secp256r1"))
}.generateKeyPair()
// Many signatures, because whether r or s needs a leading zero byte (or
// a padding byte stripped) depends on the random nonce.
repeat(200) { index ->
val der = Signature.getInstance("SHA256withECDSA").run {
initSign(keyPair.private)
update("payload-$index".toByteArray())
sign()
}
val jose = derToJose(der)
assertEquals("Signature $index should be 64 bytes", 64, jose.size)
}
}
@Test
fun `preserves r and s values across the conversion`() {
val keyPair = KeyPairGenerator.getInstance("EC").apply {
initialize(ECGenParameterSpec("secp256r1"))
}.generateKeyPair()
repeat(50) { index ->
val der = Signature.getInstance("SHA256withECDSA").run {
initSign(keyPair.private)
update("check-$index".toByteArray())
sign()
}
val jose = derToJose(der)
val (expectedR, expectedS) = parseDerIntegers(der)
assertEquals(expectedR, BigInteger(1, jose.copyOfRange(0, 32)))
assertEquals(expectedS, BigInteger(1, jose.copyOfRange(32, 64)))
}
}
@Test
fun `left-pads short components rather than shifting them`() {
// A hand-built DER whose r is only one byte long: the raw form must be
// 31 zero bytes then 0x07, not 0x07 followed by garbage.
val der = byteArrayOf(
0x30, 0x08,
0x02, 0x01, 0x07,
0x02, 0x03, 0x01, 0x02, 0x03,
)
val jose = derToJose(der)
assertEquals(64, jose.size)
assertArrayEquals(ByteArray(31), jose.copyOfRange(0, 31))
assertEquals(0x07.toByte(), jose[31])
assertEquals(BigInteger("010203", 16), BigInteger(1, jose.copyOfRange(32, 64)))
}
/** Minimal DER reader, independent of the implementation under test. */
private fun parseDerIntegers(der: ByteArray): Pair<BigInteger, BigInteger> {
var offset = 2
if (der[1].toInt() and 0xFF > 0x80) offset += der[1].toInt() and 0x7F
val rLen = der[offset + 1].toInt()
val r = BigInteger(der.copyOfRange(offset + 2, offset + 2 + rLen))
val sStart = offset + 2 + rLen
val sLen = der[sStart + 1].toInt()
val s = BigInteger(der.copyOfRange(sStart + 2, sStart + 2 + sLen))
return r to s
}
}