Skip to content

Commit

Permalink
Add support for schema URL
Browse files Browse the repository at this point in the history
Signed-off-by: lovesh <[email protected]>
  • Loading branch information
lovesh committed Oct 5, 2023
1 parent 7473a90 commit e8f0b3a
Show file tree
Hide file tree
Showing 12 changed files with 1,483 additions and 779 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@docknetwork/crypto-wasm-ts",
"version": "0.45.0",
"version": "0.46.0",
"description": "Typescript abstractions over Dock's Rust crypto library's WASM wrapper",
"homepage": "https://github.com/docknetwork/crypto-wasm-ts",
"main": "lib/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import { BytearrayWrapper } from '../bytearray-wrapper';
import { IPresentedAttributeBounds, IPresentedAttributeVE } from './presentation-specification';
import { Presentation } from './presentation';
import { getR1CS, ParsedR1CSFile } from '../r1cs/file';
import { convertDateToTimestamp } from '../util';

type Credential = BBSCredential | BBSPlusCredential | PSCredential;

Expand Down
2 changes: 1 addition & 1 deletion src/anonymous-credentials/credential-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export abstract class CredentialBuilder<
> extends CredentialBuilderCommon {
// NOTE: Follows semver and must be updated accordingly when the logic of this class changes or the
// underlying crypto changes.
static VERSION = '0.2.0';
static VERSION = '0.3.0';

_encodedAttributes?: { [key: string]: Uint8Array };
_sig?: Signature;
Expand Down
140 changes: 118 additions & 22 deletions src/anonymous-credentials/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,10 @@ export interface IJsonSchemaProperties {
[key: string]: object;
}

export interface IJsonSchema {
/**
* JSON schema that contains the properties
*/
export interface IEmbeddedJsonSchema {
[META_SCHEMA_STR]: string;
$id?: string;
title?: string;
Expand All @@ -383,6 +386,16 @@ export interface IJsonSchema {
definitions?: { [key: string]: object };
}

/**
* JSON schema that does not contain the properties but its $id property can be used to fetch the properties.
*/
export interface IJsonSchema {
[META_SCHEMA_STR]: string;
$id: string;
title?: string;
type: string;
}

export interface ISchemaParsingOpts {
useDefaults: boolean;
defaultMinimumInteger: number;
Expand All @@ -407,7 +420,7 @@ export type CredVal = string | number | object | CredVal[];
export class CredentialSchema extends Versioned {
// NOTE: Follows semver and must be updated accordingly when the logic of this class changes or the
// underlying crypto changes.
static VERSION = '0.0.3';
static VERSION = '0.1.0';

private static readonly STR_TYPE = 'string';
private static readonly STR_REV_TYPE = 'stringReversible';
Expand Down Expand Up @@ -467,25 +480,28 @@ export class CredentialSchema extends Versioned {
]);

readonly schema: ISchema;
readonly jsonSchema: IJsonSchema;
readonly jsonSchema: IEmbeddedJsonSchema | IJsonSchema;
readonly parsingOptions: ISchemaParsingOpts;
// @ts-ignore
encoder: Encoder;
fullJsonSchema?: IEmbeddedJsonSchema;

/**
* Takes a schema object as per JSON-schema syntax (`IJsonSchema`), validates it and converts it to an internal
* representation (`ISchema`) and stores both as the one with JSON-schema syntax is added to the credential representation.
* @param jsonSchema
* @param jsonSchema - Could be a JSON schema with properties or contain an $id key which is used to fetch them
* @param parsingOpts - Options to parse the schema like whether to use defaults and what defaults to use
* @param addMissingParsingOpts - Whether to update `parsingOpts` for any missing options with default options. Pass false
* when deserializing to get the exact object that was serialized which is necessary when verifying signatures
* @param overrides - Override any properties of the schema
* @param fullJsonSchema - When `jsonSchema` does not contain the properties, this object is expected to contain them.
*/
constructor(
jsonSchema: IJsonSchema,
jsonSchema: IEmbeddedJsonSchema | IJsonSchema,
parsingOpts: Partial<ISchemaParsingOpts> = DefaultSchemaParsingOpts,
addMissingParsingOpts = true,
overrides?: Partial<ISchemaOverrides>
overrides?: Partial<ISchemaOverrides>,
fullJsonSchema?: IEmbeddedJsonSchema
) {
// This functions flattens schema object twice but the repetition can be avoided. Keeping this deliberately for code clarity.
let pOpts;
Expand All @@ -494,7 +510,17 @@ export class CredentialSchema extends Versioned {
} else {
pOpts = { ...parsingOpts };
}
const schema = CredentialSchema.convertToInternalSchemaObj(jsonSchema, pOpts, '', undefined) as ISchema;

let isEmbeddedSchema = CredentialSchema.isEmbeddedJsonSchema(jsonSchema);
if (!isEmbeddedSchema && fullJsonSchema === undefined) {
throw new Error('Either pass an embedded schema or the actual schema');
}
const schema = CredentialSchema.convertToInternalSchemaObj(
isEmbeddedSchema ? jsonSchema : fullJsonSchema,
pOpts,
'',
undefined
) as ISchema;
CredentialSchema.validate(schema);

if (overrides !== undefined && overrides.version !== undefined) {
Expand All @@ -507,6 +533,7 @@ export class CredentialSchema extends Versioned {
// internal representation; trading off memory for CPU time.
this.jsonSchema = jsonSchema;
this.parsingOptions = pOpts;
this.fullJsonSchema = fullJsonSchema;
this.initEncoder();
}

Expand Down Expand Up @@ -672,7 +699,7 @@ export class CredentialSchema extends Versioned {
}
}

static essential(withDefinitions = true): IJsonSchema {
static essential(withDefinitions = true): IEmbeddedJsonSchema {
const s = {
// Currently only assuming support for draft-07 but other might work as well
[META_SCHEMA_STR]: 'http://json-schema.org/draft-07/schema#',
Expand Down Expand Up @@ -724,39 +751,50 @@ export class CredentialSchema extends Versioned {
}

toJSON(): object {
return {
[ID_STR]: this.asEmbeddedJsonSchema(),
const embedded = this.hasEmbeddedJsonSchema();
const j = {
[ID_STR]: CredentialSchema.convertToDataUri(this.jsonSchema),
[TYPE_STR]: SCHEMA_TYPE_STR,
parsingOptions: this.parsingOptions,
version: this._version
};
if (!embedded) {
j['fullJsonSchema'] = CredentialSchema.convertToDataUri(this.fullJsonSchema as IEmbeddedJsonSchema);
}
return j;
}

static fromJSON(j: object): CredentialSchema {
// @ts-ignore
const { id, type, parsingOptions, version } = j;
const { id, type, parsingOptions, version, fullJsonSchema } = j;
if (type !== SCHEMA_TYPE_STR) {
throw new Error(`Schema type was "${type}", expected: "${SCHEMA_TYPE_STR}"`);
}
const jsonSchema = this.extractJsonSchemaFromEmbedded(id);
const jsonSchema = this.convertFromDataUri(id);
let full: IEmbeddedJsonSchema | undefined;
if (fullJsonSchema !== undefined) {
if (CredentialSchema.isEmbeddedJsonSchema(jsonSchema)) {
throw new Error(`Actual schema was provided even when the given jsonSchema was an embedded one`);
}
full = this.convertFromDataUri(fullJsonSchema) as IEmbeddedJsonSchema;
if (!CredentialSchema.isEmbeddedJsonSchema(full)) {
throw new Error(`Expected actual schema to be an embedded one but got ${full}`);
}
}
// Note: `parsingOptions` might still be in an incorrect format which can fail the next call
// Note: Passing `addMissingParsingOpts` as false to recreate the exact same object that was serialized. This is important
// when verifying signatures.
// @ts-ignore
return new CredentialSchema(jsonSchema, parsingOptions, false, { version: version });
}

asEmbeddedJsonSchema(): string {
return CredentialSchema.asEmbeddedJsonSchema(this.jsonSchema);
return new CredentialSchema(jsonSchema, parsingOptions, false, { version: version }, full);
}

static asEmbeddedJsonSchema(jsonSchema: IJsonSchema): string {
static convertToDataUri(jsonSchema: IEmbeddedJsonSchema | IJsonSchema): string {
return `data:application/json;charset=utf-8,${encodeURIComponent(JSON.stringify(jsonSchema))}`;
}

static extractJsonSchemaFromEmbedded(embedded: string): IJsonSchema {
static convertFromDataUri(embedded: string): IEmbeddedJsonSchema | IJsonSchema {
if (!embedded.startsWith('data:')) {
throw new Error(`Embedded schema must be a data URI`);
throw new Error(`Embedded schema must be a data URI but was ${embedded}`);
}

// Strip new lines
Expand Down Expand Up @@ -869,6 +907,25 @@ export class CredentialSchema extends Versioned {
};
}*/

/**
* Same as the constructor of this class but gets the JSON schema from a callback
* @param jsonSchema - The JSON schema that contains the URL to fetch the full JSON schema, i.e. properties
* @param schemaGetter - The callback that takes the `$id` field of `jsonSchema` and returns the full JSON schema.
* @param parsingOpts
* @param addMissingParsingOpts
* @param overrides
*/
static async newSchemaFromExternal(
jsonSchema: IJsonSchema,
schemaGetter: (url: string) => Promise<IEmbeddedJsonSchema>,
parsingOpts: Partial<ISchemaParsingOpts> = DefaultSchemaParsingOpts,
addMissingParsingOpts = true,
overrides?: Partial<ISchemaOverrides>
): Promise<CredentialSchema> {
const fullJsonSchema = await schemaGetter(jsonSchema.$id);
return new CredentialSchema(jsonSchema, parsingOpts, addMissingParsingOpts, overrides, fullJsonSchema);
}

getJsonLdContext(): object {
const terms = new Set<string>();
terms.add(SCHEMA_STR);
Expand Down Expand Up @@ -909,6 +966,26 @@ export class CredentialSchema extends Versioned {
};
}

/**
* Returns true if the JSON schema provided during the object creation was an embedded one.
*/
hasEmbeddedJsonSchema(): boolean {
return this.fullJsonSchema === undefined;
}

/**
* Gets the embedded JSON schema either from the one that was provided or the one that was fetched.
*/
getEmbeddedJsonSchema(): IEmbeddedJsonSchema {
// @ts-ignore
return this.hasEmbeddedJsonSchema() ? this.jsonSchema : this.fullJsonSchema;
}

getJsonSchemaProperties(): object {
// @ts-ignore
return this.getEmbeddedJsonSchema().properties;
}

static getDummyContextValue(term: string): string {
return `dk:${term}`;
}
Expand Down Expand Up @@ -1064,10 +1141,21 @@ export class CredentialSchema extends Versioned {
*/
// @ts-ignore
static generateAppropriateSchema(cred: object, schema: CredentialSchema): CredentialSchema {
const newJsonSchema = JSON.parse(JSON.stringify(schema.jsonSchema));
// This JSON parse and stringify is to make `newJsonSchema` a copy of `schema.jsonSchema` and not a reference
const newJsonSchema = JSON.parse(JSON.stringify(schema.getEmbeddedJsonSchema()));
const props = newJsonSchema.properties;
CredentialSchema.generateFromCredential(cred, props, schema.version);
return new CredentialSchema(newJsonSchema, schema.parsingOptions, false, { version: schema.version });
if (schema.hasEmbeddedJsonSchema()) {
return new CredentialSchema(newJsonSchema, schema.parsingOptions, false, { version: schema.version });
} else {
return new CredentialSchema(
schema.jsonSchema,
schema.parsingOptions,
false,
{ version: schema.version },
newJsonSchema
);
}
}

/**
Expand Down Expand Up @@ -1224,6 +1312,14 @@ export class CredentialSchema extends Versioned {
throw new Error(`Schema should contain a top level key ${fieldName} and its type must be "string"`);
}
}

/**
* Returns true if the given object is an embedded schema, i.e. it has the properties.
* @param obj
*/
static isEmbeddedJsonSchema(obj: IEmbeddedJsonSchema | IJsonSchema): boolean {
return obj['properties'] !== undefined;
}
}

export function getTransformedMinMax(name: string, valTyp: ValueTypes, min: number, max: number): [number, number] {
Expand Down
44 changes: 37 additions & 7 deletions tests/anonymous-credentials/blind-issuance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
BoundCheckSmcParamsUncompressed,
BoundCheckSmcWithKVProverParamsUncompressed,
BoundCheckSmcWithKVVerifierParamsUncompressed,
BoundCheckSmcWithKVSetup
BoundCheckSmcWithKVSetup, DefaultSchemaParsingOpts, META_SCHEMA_STR
} from '../../src';
import {
SignatureParams,
Expand Down Expand Up @@ -133,7 +133,7 @@ function checkBlindedCredJson(blindedCred: BlindedCredential<any>, pk: PublicKey
// Skip the tests if PS signatures are used as blind sigs are not integrated yet
const skipIfPS = isPS() ? describe.skip : describe;

skipIfPS(`${Scheme} Blind issuance of credentials`, () => {
skipIfPS.each([true, false])(`${Scheme} Blind issuance of credentials with withSchemaRef=%s`, (withSchemaRef) => {
let sk1: SecretKey, pk1: PublicKey;
let sk2: SecretKey, pk2: PublicKey;
let sk3: SecretKey, pk3: PublicKey;
Expand Down Expand Up @@ -168,6 +168,12 @@ skipIfPS(`${Scheme} Blind issuance of credentials`, () => {
let boundCheckSmcKVProverParams: BoundCheckSmcWithKVProverParamsUncompressed;
let boundCheckSmcKVVerifierParams: BoundCheckSmcWithKVVerifierParamsUncompressed;

const nonEmbeddedSchema = {
$id: 'https://example.com?hash=abc123ff',
[META_SCHEMA_STR]: 'http://json-schema.org/draft-07/schema#',
type: 'object',
};

function setupBoundCheck() {
if (boundCheckProvingKey === undefined) {
[boundCheckProvingKey, boundCheckVerifyingKey] = getBoundCheckSnarkKeys(loadSnarkSetupFromFiles);
Expand Down Expand Up @@ -234,7 +240,12 @@ skipIfPS(`${Scheme} Blind issuance of credentials`, () => {
sk1 = keypair1.sk;
pk1 = keypair1.pk;

schema1 = new CredentialSchema(getExampleSchema(10));
if (withSchemaRef) {
schema1 = new CredentialSchema(nonEmbeddedSchema, DefaultSchemaParsingOpts, true, undefined, getExampleSchema(10))
} else {
schema1 = new CredentialSchema(getExampleSchema(10));
}

const accumKeypair1 = PositiveAccumulator.generateKeypair(
dockAccumulatorParams(),
stringToBytes('secret-seed-for-accum')
Expand All @@ -257,13 +268,21 @@ skipIfPS(`${Scheme} Blind issuance of credentials`, () => {
sk2 = keypair2.sk;
pk2 = keypair2.pk;

schema2 = new CredentialSchema(getExampleSchema(9));
if (withSchemaRef) {
schema2 = new CredentialSchema(nonEmbeddedSchema, DefaultSchemaParsingOpts, true, undefined, getExampleSchema(9))
} else {
schema2 = new CredentialSchema(getExampleSchema(9));
}

const keypair3 = KeyPair.generate(params);
sk3 = keypair3.sk;
pk3 = keypair3.pk;

schema3 = new CredentialSchema(getExampleSchema(7));
if (withSchemaRef) {
schema3 = new CredentialSchema(nonEmbeddedSchema, DefaultSchemaParsingOpts, true, undefined, getExampleSchema(7))
} else {
schema3 = new CredentialSchema(getExampleSchema(7));
}
});

it('should be able to request a credential when some attributes are blinded', async () => {
Expand Down Expand Up @@ -674,7 +693,12 @@ skipIfPS(`${Scheme} Blind issuance of credentials`, () => {
const ck = SaverChunkedCommitmentKey.generate(stringToBytes('a new nonce'));
const commKey = ck.decompress();

const schema = new CredentialSchema(getExampleSchema(8));
let schema;
if (withSchemaRef) {
schema = new CredentialSchema(nonEmbeddedSchema, DefaultSchemaParsingOpts, true, undefined, getExampleSchema(8))
} else {
schema = new CredentialSchema(getExampleSchema(8));
}
const blindedSubject = {
sensitive: {
phone: '810-1234567',
Expand Down Expand Up @@ -836,7 +860,13 @@ skipIfPS(`${Scheme} Blind issuance of credentials`, () => {
const provingKeyLtPub = prk.decompress();
const verifyingKeyLtPub = prk.getVerifyingKeyUncompressed();

const schema = new CredentialSchema(getExampleSchema(12));
let schema;
if (withSchemaRef) {
schema = new CredentialSchema(nonEmbeddedSchema, DefaultSchemaParsingOpts, true, undefined, getExampleSchema(12))
} else {
schema = new CredentialSchema(getExampleSchema(12));
}

const blindedSubject = {
education: {
score1: 55,
Expand Down
Loading

0 comments on commit e8f0b3a

Please sign in to comment.