Skip to content

Commit

Permalink
fix: bitstring for revocation registry (#188)
Browse files Browse the repository at this point in the history
Signed-off-by: Cristian G <[email protected]>
  • Loading branch information
cristianIOHK committed Aug 27, 2024
1 parent 919f2d9 commit 6a0fd8b
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fun ByteArray.padStart(length: Int, padValue: Byte): ByteArray {
fun ByteArray.gunzip(): ByteArray {
val byteArrayInputStream = ByteArrayInputStream(this)
val gzipInputStream = GZIPInputStream(byteArrayInputStream)
val decompressedBytes = gzipInputStream.readBytes()
val decompressedBytes = gzipInputStream.readAllBytes()

return decompressedBytes
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ import org.hyperledger.identus.walletsdk.pollux.models.PresentationSubmission
import org.hyperledger.identus.walletsdk.pollux.models.SDJWTCredential
import org.hyperledger.identus.walletsdk.pollux.models.VerificationKeyType
import org.hyperledger.identus.walletsdk.pollux.models.W3CCredential
import org.hyperledger.identus.walletsdk.pollux.utils.Bitstring
import org.hyperledger.identus.walletsdk.pollux.utils.BitString

/**
* Class representing the implementation of the Pollux interface.
Expand Down Expand Up @@ -440,8 +440,7 @@ open class PolluxImpl(
val revocationRegistryJson = fetchRevocationRegistry(credentialStatus)
validateProof(revocationRegistryJson)
return checkEncodedListRevoked(revocationRegistryJson, credentialStatus.statusListIndex)
}
return false
} ?: return false
}

private suspend fun validateProof(revocationRegistryJson: String): Boolean {
Expand Down Expand Up @@ -555,20 +554,25 @@ open class PolluxImpl(
if (credentialSubject.containsKey("encodedList")) {
val encodedList = credentialSubject["encodedList"]?.jsonPrimitive?.content
if (encodedList != null) {
val decodedBytes = Base64.getUrlDecoder().decode(encodedList)
val decompressedBytes = decodedBytes.gunzip()
val bitString = Bitstring(decompressedBytes)

if (statusListIndex > decompressedBytes.size) {
throw PolluxError.StatusListOutOfBoundIndex()
}
return bitString.get(statusListIndex)
return verifyStatusListIndexForEncodedList(encodedList, statusListIndex)
}
}
}
return false
}

fun verifyStatusListIndexForEncodedList(encodedList: String, statusListIndex: Int): Boolean {
val decodedBytes = Base64.getUrlDecoder().decode(encodedList)
val decompressedBytes =
decodedBytes.gunzip()
val bitString = BitString(decompressedBytes)

if (statusListIndex > decompressedBytes.size) {
throw PolluxError.StatusListOutOfBoundIndex()
}
return bitString.isRevoked(statusListIndex)
}

suspend fun fetchRevocationRegistry(credentialStatus: JWTVerifiableCredential.CredentialStatus): String {
val result = api.request(
HttpMethod.Get.value,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.hyperledger.identus.walletsdk.pollux.utils

import java.util.BitSet

/**
* The `BitString` class represents a sequence of bits using a from a provided `ByteArray`.
* It reverses the bits from left to right to follow the revocation list specs of left to right indexing.
*
* @property byteArray The byte array to be
* @property size The number of bits in the sequence.
*/
class BitString(byteArray: ByteArray) {
val bitSet: BitSet
val size: Int

init {
bitSet = BitSet.valueOf(
byteArray.map { value -> reverseBits(value.toInt()).toByte() }.toByteArray()
)
size = byteArray.size * 8
}

/**
* Checks if the bit at the specified index is set (i.e., if the bit is "revoked").
*
* @param index The index of the bit to check.
* @return `true` if the bit at the specified index is set, `false` otherwise.
* @throws Exception if the index is greater than or equal to the size of the bit sequence.
*/
fun isRevoked(index: Int): Boolean {
if (index >= size) {
throw Exception("BitString error: index cannot be bigger than the size.")
}
return bitSet.get(index)
}

// companion object {
/**
* Reverses the bits in an 8-bit integer.
*
* @param b The integer whose bits need to be reversed.
* @return The integer with its bits reversed.
*/
private fun reverseBits(b: Int): Int {
var result = 0
for (i in 0 until 8) {
val bit = (b shr i) and 1
result = (result shl 1) or bit
}
return result
}
// }
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -1045,18 +1045,15 @@ class PolluxImplTest {
}

@Test
fun testEncodedListUnGzip_whenNotRevoked_thenReturnFalse() = runTest {
val httpResponse = correctHttpResponseFetchRevocationRegistry()

pollux = PolluxImpl(apollo, castorMock, api)

assertFalse(pollux.checkEncodedListRevoked(httpResponse.jsonString, 3))
}

@Test
fun testEncodedListUnGzip_whenRevoked_thenReturnFalse() = runTest {
val revocationRegistryJson = """
{
fun testIsCredentialRevoked_whenCorrectJson_thenValidationCorrect() = runTest {
val response = """{
"proof": {
"type": "EcdsaSecp256k1Signature2019",
"proofPurpose": "assertionMethod",
"verificationMethod": "data:application/json;base64,eyJAY29udGV4dCI6WyJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3YxIl0sInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia2V5X29wcyI6WyJ2ZXJpZnkiXSwia3R5IjoiRUMiLCJ4IjoiQ1hJRmwyUjE4YW1lTEQteWtTT0dLUW9DQlZiRk01b3Vsa2MydklySnRTND0iLCJ5IjoiRDJRWU5pNi1BOXoxbHhwUmpLYm9jS1NUdk5BSXNOVnNsQmpsemVnWXlVQT0ifX0=",
"created": "2024-07-25T22:49:59.091957Z",
"jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFUzI1NksifQ..FJLUBsZhGB1o_G1UwsVaoL-8agvcpoelJtAr2GlNOOqCSOd-WNEj5-FOgv0m0QcdKMokl2TxibJMg3Y-MJq4-A"
},
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/vc/status-list/2021/v1"
Expand All @@ -1065,27 +1062,96 @@ class PolluxImplTest {
"VerifiableCredential",
"StatusList2021Credential"
],
"issuer": "did:prism:f3a762b1f9741f82791fbdafd0ea2bbc7b8720fcc4fba66f9fac8f896fac35d9",
"id": "http://192.168.68.113:8000/cloud-agent/credential-status/6c67b6c0-1bd9-47a1-85ee-f88edaa5a894",
"issuanceDate": 1718845277,
"id": "http://localhost:8085/credential-status/01def9a2-2bcb-4bb3-8a36-6834066431d0",
"issuer": "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a",
"issuanceDate": 1721947798,
"credentialSubject": {
"type": "StatusList2021",
"statusPurpose": "Revocation",
"encodedList": "H4sIAAAAAAAA_-3BIQEAAAACIJP_JzvDAjQAAAAAAAAAAAAAAAAAAADA2wCGK9BQAEAAAA=="
},
"proof": {
"type": "EcdsaSecp256k1Signature2019",
"proofPurpose": "assertionMethod",
"verificationMethod": "data:application/json;base64,eyJAY29udGV4dCI6WyJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3YxIl0sInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia2V5X29wcyI6WyJ2ZXJpZnkiXSwia3R5IjoiRUMiLCJ4IjoiQU5xNHdCZS1QdlY4UXEzN214THl0RnVxTFJlbG51X251SVMyMEhoZTZsYmsiLCJ5IjoiQU9nWkZmQnBmWlZuNWVuLTFBVEV2YnNyOHExMVpUWTZodEJtTC1xbXFRd3cifX0=",
"created": "2024-06-20T01:01:18.063982543Z",
"jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFUzI1NksifQ..c9_mSbKooKiER7GBMj6bty8367VbIj3pRCXF_5vsbhjhe7cA4SoNtUn-DNzlwzIM5n-VMptmuWf33PEXtklEQA"
"encodedList": "H4sIAAAAAAAA_-3BIQEAAAACIKf6f4UzLEADAAAAAAAAAAAAAAAAAAAAvA3PduITAEAAAA=="
}
}
"""
}"""
val httpResponse = HttpResponse(status = HttpStatusCode.OK.value, response)

apollo = ApolloImpl()
val api = spy(
ApiImpl(
httpClient {
install(ContentNegotiation) {
json(
Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
}
)
}
}
)
)

pollux = PolluxImpl(apollo, castorMock, api)

doReturn(httpResponse)
.`when`(api).request(
HttpMethod.Get.value,
"http://10.91.100.126:8000/prism-agent/credential-status/514e8528-4b38-477a-b0e4-324bbe220464",
emptyArray(),
arrayOf(KeyValue(HttpHeaders.ContentType, Typ.Encrypted.typ)),
null
)

val credential = JWTCredential.fromJwtString(
"eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206ZTAyZTgwOTlkNTAzNTEzNDVjNWRkODMxYTllOTExMmIzOTRhODVkMDA2NGEyZWI1OTQyOTA4MDczNGExNTliNjpDcmtCQ3JZQkVqb0tCbUYxZEdndE1SQUVTaTRLQ1hObFkzQXlOVFpyTVJJaEF1Vlljb3JmV25MMGZZdEE1dmdKSzRfLW9iM2JVRGMtdzJVT0hkTzNRRXZxRWpzS0IybHpjM1ZsTFRFUUFrb3VDZ2x6WldOd01qVTJhekVTSVFMQ3U5Tm50cXVwQmotME5DZE1BNzV6UmVCZXlhQ0pPMWFHWWVQNEJNUUhWQkk3Q2dkdFlYTjBaWEl3RUFGS0xnb0pjMlZqY0RJMU5tc3hFaUVET1dndlF4NnZSdTZ3VWI0RlljSnVhRUNqOUJqUE1KdlJwOEx3TTYxaEVUNCIsInN1YiI6ImRpZDpwcmlzbTpiZDgxZmY1NDQzNDJjMTAwNDZkZmE0YmEyOTVkNWIzNmU0Y2ZlNWE3ZWIxMjBlMTBlZTVjMjQ4NzAwNjUxMDA5OkNvVUJDb0lCRWpzS0IyMWhjM1JsY2pBUUFVb3VDZ2x6WldOd01qVTJhekVTSVFQdjVQNXl5Z3Jad2FKbFl6bDU5bTJIQURLVFhVTFBzUmUwa2dlRUh2dExnQkpEQ2c5aGRYUm9aVzUwYVdOaGRHbHZiakFRQkVvdUNnbHpaV053TWpVMmF6RVNJUVB2NVA1eXlnclp3YUpsWXpsNTltMkhBREtUWFVMUHNSZTBrZ2VFSHZ0TGdBIiwibmJmIjoxNzE1MDAwNjc0LCJleHAiOjE3MTg2MDA2NzQsInZjIjp7ImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImVtYWlsQWRkcmVzcyI6ImNyaXN0aWFuLmNhc3Ryb0Bpb2hrLmlvIiwiaWQiOiJkaWQ6cHJpc206YmQ4MWZmNTQ0MzQyYzEwMDQ2ZGZhNGJhMjk1ZDViMzZlNGNmZTVhN2ViMTIwZTEwZWU1YzI0ODcwMDY1MTAwOTpDb1VCQ29JQkVqc0tCMjFoYzNSbGNqQVFBVW91Q2dselpXTndNalUyYXpFU0lRUHY1UDV5eWdyWndhSmxZemw1OW0ySEFES1RYVUxQc1JlMGtnZUVIdnRMZ0JKRENnOWhkWFJvWlc1MGFXTmhkR2x2YmpBUUJFb3VDZ2x6WldOd01qVTJhekVTSVFQdjVQNXl5Z3Jad2FKbFl6bDU5bTJIQURLVFhVTFBzUmUwa2dlRUh2dExnQSJ9LCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sIkBjb250ZXh0IjpbImh0dHBzOlwvXC93d3cudzMub3JnXC8yMDE4XC9jcmVkZW50aWFsc1wvdjEiXSwiY3JlZGVudGlhbFN0YXR1cyI6eyJzdGF0dXNQdXJwb3NlIjoiUmV2b2NhdGlvbiIsInN0YXR1c0xpc3RJbmRleCI6MjUsImlkIjoiaHR0cDpcL1wvMTAuOTEuMTAwLjEyNjo4MDAwXC9wcmlzbS1hZ2VudFwvY3JlZGVudGlhbC1zdGF0dXNcLzUxNGU4NTI4LTRiMzgtNDc3YS1iMGU0LTMyNGJiZTIyMDQ2NCMyNSIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwOlwvXC8xMC45MS4xMDAuMTI2OjgwMDBcL3ByaXNtLWFnZW50XC9jcmVkZW50aWFsLXN0YXR1c1wvNTE0ZTg1MjgtNGIzOC00NzdhLWIwZTQtMzI0YmJlMjIwNDY0In19fQ.5OmmL5tdcRKugiHVt01PJUhp9r22zgMPPALUOB41g_1_BPHE3ezqJ2QhSmScU_EOGYcKksHftdrvfoND65nSjw"
)
val vc = JWTVerifiableCredential(
context = credential.verifiableCredential!!.context,
type = credential.verifiableCredential!!.type,
credentialSchema = credential.verifiableCredential!!.credentialSchema,
credentialSubject = credential.verifiableCredential!!.credentialSubject,
credentialStatus = Json.decodeFromString("""{"statusPurpose":"Revocation","statusListIndex":2,"id":"http://10.91.100.126:8000/prism-agent/credential-status/514e8528-4b38-477a-b0e4-324bbe220464#25","type":"StatusList2021Entry","statusListCredential":"http://10.91.100.126:8000/prism-agent/credential-status/514e8528-4b38-477a-b0e4-324bbe220464"}"""),
refreshService = credential.verifiableCredential!!.refreshService,
evidence = credential.verifiableCredential!!.evidence,
termsOfUse = credential.verifiableCredential!!.termsOfUse
)

credential.verifiableCredential = vc
val isRevoked = pollux.isCredentialRevoked(credential)
assertTrue(isRevoked)

val vc1 = JWTVerifiableCredential(
context = credential.verifiableCredential!!.context,
type = credential.verifiableCredential!!.type,
credentialSchema = credential.verifiableCredential!!.credentialSchema,
credentialSubject = credential.verifiableCredential!!.credentialSubject,
credentialStatus = Json.decodeFromString("""{"statusPurpose":"Revocation","statusListIndex":3,"id":"http://10.91.100.126:8000/prism-agent/credential-status/514e8528-4b38-477a-b0e4-324bbe220464#25","type":"StatusList2021Entry","statusListCredential":"http://10.91.100.126:8000/prism-agent/credential-status/514e8528-4b38-477a-b0e4-324bbe220464"}"""),
refreshService = credential.verifiableCredential!!.refreshService,
evidence = credential.verifiableCredential!!.evidence,
termsOfUse = credential.verifiableCredential!!.termsOfUse
)

credential.verifiableCredential = vc1
assertFalse(pollux.isCredentialRevoked(credential))
}

@Test
fun testEncodedListUnGzip_whenNotRevoked_thenReturnFalse() = runTest {
val httpResponse = correctHttpResponseFetchRevocationRegistry()

pollux = PolluxImpl(apollo, castorMock, api)

assertFalse(pollux.checkEncodedListRevoked(httpResponse.jsonString, 3))
}

@Test
fun testEncodedListUnGzip_whenFirstThreeRevoked_thenProvedOk() = runTest {
val encodedList = "H4sIAAAAAAAA_-3BIQEAAAACIKf6f4UzLEADAAAAAAAAAAAAAAAAAAAAvA3PduITAEAAAA=="

pollux = PolluxImpl(apollo, castorMock, api)

assertTrue(pollux.checkEncodedListRevoked(revocationRegistryJson, 1))
assertTrue(pollux.verifyStatusListIndexForEncodedList(encodedList, 1))
assertTrue(pollux.verifyStatusListIndexForEncodedList(encodedList, 2))
assertFalse(pollux.verifyStatusListIndexForEncodedList(encodedList, 3))
}

private suspend fun createVerificationTestCase(testCaseOptions: VerificationTestCase): Triple<String, String, String> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.hyperledger.identus.walletsdk.pollux.utils

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.util.*

class BitStringTest {

@Test
fun `test BitString with some 0s`() {
val byteArray = byteArrayOf(0.toByte())
val bitString = BitString(byteArray)

for (i in 0 until bitString.size) {
assertFalse("Index $i should not be revoked", bitString.isRevoked(i))
}
}

@Test
fun `test BitString with some 1s`() {
val byteArray = byteArrayOf(128.toByte(), 3.toByte())
val bitString = BitString(byteArray)

assertTrue("Index 0 should be revoked", bitString.isRevoked(0))
assertTrue("Index 8 should be revoked", bitString.isRevoked(14))
assertTrue("Index 8 should be revoked", bitString.isRevoked(15))
for (i in 1 until 14) {
assertFalse("Index $i should not be revoked", bitString.isRevoked(i))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ class Sdk {

(pluto as PlutoImpl).start(context)
agent.start()
agent.startFetchingMessages()
try {
agent.startFetchingMessages()
} catch (_: Exception) {}

agentStatusStream.postValue(EdgeAgent.State.RUNNING)
}
Expand Down

0 comments on commit 6a0fd8b

Please sign in to comment.