~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-android

1 collaborator
0
1
main
85 lines · 3.1 KB Raw History
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
    }
}