Skip to content

Commit

Permalink
fix: bitstring improvement and test coverage (#270)
Browse files Browse the repository at this point in the history
Signed-off-by: Francisco Javier Ribo Labrador <elribonazo@gmail.com>
  • Loading branch information
elribonazo authored Aug 23, 2024
1 parent 8a1ed3f commit dce65b5
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 45 deletions.
101 changes: 66 additions & 35 deletions src/pollux/utils/Bitstring.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,87 @@
import { base64url } from 'multiformats/bases/base64';
import { gzip, ungzip } from 'pako';

export class Bitstring {
bits: Uint8Array;
length: number;
leftToRightIndexing: boolean;

constructor({
buffer,
leftToRightIndexing
}: {
buffer: Uint8Array;
public bits: Uint8Array;
public length: number;
private leftToRightIndexing: boolean;

constructor(options: {
length?: number;
buffer?: Uint8Array;
leftToRightIndexing?: boolean;
}) {
if (!buffer) {
littleEndianBits?: boolean;
} = {}) {
const { length, buffer, leftToRightIndexing, littleEndianBits } = options;

if (length && buffer) {
throw new Error('Only one of "length" or "buffer" must be given.');
}
this.bits = new Uint8Array(buffer.buffer);
this.length = buffer.length * 8;
this.leftToRightIndexing = leftToRightIndexing ?? false;

if (littleEndianBits !== undefined) {
if (leftToRightIndexing !== undefined) {
throw new Error(
'Using both "littleEndianBits" and "leftToRightIndexing" is not allowed.'
);
}
this.leftToRightIndexing = littleEndianBits;
} else {
this.leftToRightIndexing = leftToRightIndexing ?? true;
}

if (length) {
this.bits = new Uint8Array(Math.ceil(length / 8));
this.length = length;
} else if (buffer) {
this.bits = new Uint8Array(buffer.buffer);
this.length = buffer.length * 8;
} else {
throw new Error('Either "length" or "buffer" must be provided.');
}
}

set(position: number, on: boolean): void {
const { length, leftToRightIndexing } = this;
const { index, bit } = _parsePosition(position, length, leftToRightIndexing);
const { index, bit } = this.parsePosition(position);
if (on) {
this.bits[index] |= bit;
} else {
this.bits[index] &= 0xff ^ bit;
this.bits[index] &= 0xFF ^ bit;
}
}

get(position: number): boolean {
const { length, leftToRightIndexing } = this;
const { index, bit } = _parsePosition(position, length, leftToRightIndexing);
const { index, bit } = this.parsePosition(position);
return !!(this.bits[index] & bit);
}

async encodeBits(): Promise<string> {
return base64url.baseEncode(gzip(this.bits));
}

static async decodeBits(encoded: string): Promise<Uint8Array> {
return ungzip(base64url.baseDecode(encoded));
}

async compressBits(): Promise<Uint8Array> {
return gzip(this.bits);
}

static async uncompressBits(compressed: Uint8Array): Promise<Uint8Array> {
return ungzip(compressed);
}

private parsePosition(position: number): { index: number; bit: number } {
if (position < 0 || position >= this.length) {
throw new Error(
`Position "${position}" is out of range "0-${this.length - 1}".`
);
}

}
const index = Math.floor(position / 8);
const rem = position % 8;
const shift = this.leftToRightIndexing ? 7 - rem : rem;
const bit = 1 << shift;

function _parsePosition(
position: number,
length: number,
leftToRightIndexing: boolean
): { index: number; bit: number } {
if (position >= length) {
throw new Error(
`Position "${position}" is out of range "0-${length - 1}".`
);
return { index, bit };
}
const index = Math.floor(position / 8);
const rem = position % 8;
const shift = leftToRightIndexing ? 7 - rem : rem;
const bit = 1 << shift;
return { index, bit };
}
}
67 changes: 67 additions & 0 deletions tests/pollux/Bitstring.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect } from 'chai';
import { Bitstring } from '../../src/pollux/utils/Bitstring';

describe('Bitstring', () => {
describe('constructor', () => {
it('should create a Bitstring with all indexes false for [0, 0]', () => {
const buffer = new Uint8Array([0, 0]);
const bitstring = new Bitstring({ buffer });
for (let i = 0; i < 16; i++) {
expect(bitstring.get(i)).to.be.false;
}
});

it('should create a Bitstring with indexes 0, 14, 15 true for [128,3]', () => {
const buffer = Uint8Array.from([128, 3]);
const bitstring = new Bitstring({ buffer });
const validIndexes = [0, 14, 15];
for (let i = 0; i < 16; i++) {
const bitstringIndex = bitstring.get(i);
if (validIndexes.includes(i)) {
console.log("Index should be true ", i)
expect(bitstringIndex).to.be.true;
} else {
console.log("Index should be false ", i)
expect(bitstringIndex).to.be.false;
}
}
});

it('should create a Bitstring with indexes 6 true for [2,0]', () => {
const buffer = Uint8Array.from([2, 0]);
const bitstring = new Bitstring({ buffer });
const validIndexes = [6];
for (let i = 0; i < 16; i++) {
const bitstringIndex = bitstring.get(i);
if (validIndexes.includes(i)) {
console.log("Index should be true ", i)
expect(bitstringIndex).to.be.true;
} else {
console.log("Index should be false ", i)
expect(bitstringIndex).to.be.false;
}
}
});

it('should set a bit to true on default instance', async () => {
const buffer = Uint8Array.from([0b00000000]);
const bitstring = new Bitstring({ buffer });
bitstring.set(4, true);
expect(bitstring.get(0)).to.equal(false);
expect(bitstring.get(1)).to.equal(false);
expect(bitstring.get(2)).to.equal(false);
expect(bitstring.get(3)).to.equal(false);
expect(bitstring.get(4)).to.equal(true);
expect(bitstring.get(5)).to.equal(false);
expect(bitstring.get(6)).to.equal(false);
expect(bitstring.get(7)).to.equal(false);
expect(bitstring.bits).to.have.length(1);
expect(bitstring.length).to.equal(8);
// left-to-right bit order
expect(bitstring.bits[0]).to.equal(0b00001000);
});

});


});
33 changes: 23 additions & 10 deletions tests/pollux/Pollux.revocation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import chaiAsPromised from "chai-as-promised";
import * as sinon from "sinon";
import SinonChai from "sinon-chai";

import { CredentialStatusType, Curve, JWTVerifiableCredentialProperties, KeyProperties, KeyTypes, RevocationType } from "../../src/domain";
import { Curve, JWTVerifiableCredentialProperties, KeyTypes } from "../../src/domain";
import { JWTCredential } from "../../src/pollux/models/JWTVerifiableCredential";
import type Castor from "../../src/castor/Castor";
import type Apollo from "../../src/apollo/Apollo";
import type Pollux from "../../src/pollux/Pollux";
import { base64, base64url } from "multiformats/bases/base64";
import { base64 } from "multiformats/bases/base64";
import { VerificationKeyType } from "../../src/castor/types";
import * as Fixtures from "../fixtures";
import { SDJWTCredential } from "../../src/pollux/models/SDJWTVerifiableCredential";
Expand Down Expand Up @@ -49,9 +49,9 @@ describe("Pollux", () => {
"proof": {
"type": "EcdsaSecp256k1Signature2019",
"proofPurpose": "assertionMethod",
"verificationMethod": "data:application/json;base64,eyJAY29udGV4dCI6WyJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3YxIl0sInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia2V5X29wcyI6WyJ2ZXJpZnkiXSwia3R5IjoiRUMiLCJ4IjoiVFlCZ21sM1RpUWRSX1lRRDFoSXVOTzhiUnluU0otcmxQcWFVd3JXa3EtRT0iLCJ5IjoiVjBnVFlBM0xhbFd3Q3hPZHlqb2ZoR2JkYVFEd3EwQXdCblNodFJLXzNYZz0ifX0=",
"created": "2024-06-14T10:56:59.948091Z",
"jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFUzI1NksifQ..Q1mj3jMf5DWK83E55r6vNUPpsYYgclgwYoNFBSYBzA5x6fI_2cPHJsXECnQlG1XMj2ifldngpJXegTpwe3Fgwg"
"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",
Expand All @@ -61,27 +61,40 @@ describe("Pollux", () => {
"VerifiableCredential",
"StatusList2021Credential"
],
"id": "http://localhost:8085/credential-status/575092c2-7eb0-40ae-8f41-3b499f45f3dc",
"id": "http://localhost:8085/credential-status/01def9a2-2bcb-4bb3-8a36-6834066431d0",
"issuer": "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a",
"issuanceDate": 1717714047,
"issuanceDate": 1721947798,
"credentialSubject": {
"type": "StatusList2021",
"statusPurpose": "Revocation",
//Credential index [0] has a value of 2
"encodedList": "H4sIAAAAAAAA_-3BMQ0AAAACIGf_0MbwARoAAAAAAAAAAAAAAAAAAADgbbmHB0sAQAAA"
"encodedList": "H4sIAAAAAAAA_-3BIQEAAAACIKf6f4UzLEADAAAAAAAAAAAAAAAAAAAAvA3PduITAEAAAA=="
}
}
);

const credential = JWTCredential.fromJWS(revocableJWTCredential);
const credential2 = JWTCredential.fromJWS(revocableJWTCredential);
const credential3 = JWTCredential.fromJWS(revocableJWTCredential);

//Workaround to hardcode the revocation index
const vc = credential.properties.get(JWTVerifiableCredentialProperties.vc);
vc.credentialStatus.statusListIndex = 1;
credential.properties.set(JWTVerifiableCredentialProperties.vc, vc);

const vc2 = credential2.properties.get(JWTVerifiableCredentialProperties.vc);
vc2.credentialStatus.statusListIndex = 2;
credential2.properties.set(JWTVerifiableCredentialProperties.vc, vc2);

const vc3 = credential3.properties.get(JWTVerifiableCredentialProperties.vc);
vc3.credentialStatus.statusListIndex = 3;
credential3.properties.set(JWTVerifiableCredentialProperties.vc, vc3);

const revoked = await pollux.isCredentialRevoked(credential)
const revoked2 = await pollux.isCredentialRevoked(credential2)
const revoked3 = await pollux.isCredentialRevoked(credential3)

expect(revoked).to.eq(true)
expect(revoked2).to.eq(true)
expect(revoked3).to.eq(false)

})

Expand Down

0 comments on commit dce65b5

Please sign in to comment.