Skip to content

Commit

Permalink
Merge pull request #261 from Concordium/remove-date-from-attribute
Browse files Browse the repository at this point in the history
Remove Date from AttributeType and add TimestampAttribute
  • Loading branch information
shjortConcordium authored Sep 4, 2023
2 parents f92c14a + 2f0306d commit c89ac23
Show file tree
Hide file tree
Showing 13 changed files with 207 additions and 104 deletions.
7 changes: 6 additions & 1 deletion packages/common/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Changelog

## Unreleased
## 9.3.0

### Added

- `sendRawAccountTransaction` to the gRPC Client.

### Changed

- Stopped using `replaceDateWithTimeStampAttribute` and `reviveDateFromTimeStampAttribute` for serializing and parsing verifiable presentation.
- AttributeType no longer contains `Date`, but now instead has `TimestampAttribute`. The statement builders have their types extended to keep allowing for both `Date` and `TimestampAttribute`.

## 9.2.1

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@concordium/common-sdk",
"version": "9.2.1",
"version": "9.3.0",
"license": "Apache-2.0",
"engines": {
"node": ">=14.16.0"
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/types/VerifiablePresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ export class VerifiablePresentation {
return JSONBigInt({
alwaysParseAsBig: true,
useNativeBigInt: true,
}).stringify(this, replaceDateWithTimeStampAttribute);
}).stringify(this);
}

static fromString(json: string): VerifiablePresentation {
// We allow all numbers to be parsed as bigints to avoid lossy conversion of attribute values. The structure does not contain any other numbers.
const parsed: VerifiablePresentation = JSONBigInt({
alwaysParseAsBig: true,
useNativeBigInt: true,
}).parse(json, reviveDateFromTimeStampAttribute);
}).parse(json);
return new VerifiablePresentation(
parsed.presentationContext,
parsed.proof,
Expand Down
48 changes: 43 additions & 5 deletions packages/common/src/web3IdHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as wasm from '@concordium/rust-bindings';
import { stringify } from 'json-bigint';
import { ContractAddress, CryptographicParameters } from './types';
import { replaceDateWithTimeStampAttribute } from './types/VerifiablePresentation';
import { AttributeType } from './web3ProofTypes';
import {
AttributeType,
StatementAttributeType,
TimestampAttribute,
} from './web3ProofTypes';

export type VerifyWeb3IdCredentialSignatureInput = {
globalContext: CryptographicParameters;
Expand All @@ -21,9 +24,7 @@ export function verifyWeb3IdCredentialSignature(
input: VerifyWeb3IdCredentialSignatureInput
): boolean {
// Use json-bigint stringify to ensure we can handle bigints
return wasm.verifyWeb3IdCredentialSignature(
stringify(input, replaceDateWithTimeStampAttribute)
);
return wasm.verifyWeb3IdCredentialSignature(stringify(input));
}

/**
Expand Down Expand Up @@ -51,3 +52,40 @@ export function isStringAttributeInRange(
const upCmp = compareStringAttributes(value, upper);
return upCmp < 0;
}

/**
* Converts a timestamp attribute to a Date.
* @param attribute the timestamp attribute
* @returns a Date representing the timestamp
*/
export function timestampToDate(attribute: TimestampAttribute): Date {
return new Date(Date.parse(attribute.timestamp));
}

/**
* Converts a Date to a timestamp attribute.
* @param value the date to convert to an attribute
* @returns the timestamp attribute for the provided date
*/
export function dateToTimestampAttribute(value: Date): TimestampAttribute {
return {
type: 'date-time',
timestamp: value.toISOString(),
};
}

/**
* Converts a statement attribute to an attribute. Statement attributes allow
* for Date which are mapped into timestamp attributes. All other attribute
* types are mapped one-to-one.
* @param statementAttribute the statement attribute to map
* @returns the mapped attribute type
*/
export function statementAttributeTypeToAttributeType(
statementAttribute: StatementAttributeType
): AttributeType {
if (statementAttribute instanceof Date) {
return dateToTimestampAttribute(statementAttribute);
}
return statementAttribute;
}
16 changes: 15 additions & 1 deletion packages/common/src/web3ProofTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@ import {
} from './commonProofTypes';
import { ContractAddress, CryptographicParameters } from './types';

export type AttributeType = string | bigint | Date;
export type TimestampAttribute = {
type: 'date-time';
timestamp: string;
};
export type AttributeType = string | bigint | TimestampAttribute;
export type StatementAttributeType = AttributeType | Date;

export function isTimestampAttribute(
attribute: AttributeType
): attribute is TimestampAttribute {
return (
(attribute as TimestampAttribute).type === 'date-time' &&
typeof (attribute as TimestampAttribute).timestamp === 'string'
);
}

export type AccountCommitmentInput = {
type: 'account';
Expand Down
75 changes: 43 additions & 32 deletions packages/common/src/web3Proofs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
CredentialStatement,
CredentialSubject,
AttributeType,
isTimestampAttribute,
StatementAttributeType,
} from './web3ProofTypes';
import { getPastDate } from './idProofs';
import {
Expand All @@ -38,13 +40,12 @@ import {
} from './commonProofTypes';
import { ConcordiumHdWallet } from './HdWallet';
import { stringify } from 'json-bigint';
import {
replaceDateWithTimeStampAttribute,
VerifiablePresentation,
} from './types/VerifiablePresentation';
import { VerifiablePresentation } from './types/VerifiablePresentation';
import {
compareStringAttributes,
isStringAttributeInRange,
statementAttributeTypeToAttributeType,
timestampToDate,
} from './web3IdHelpers';

export const MAX_STRING_BYTE_LENGTH = 31;
Expand Down Expand Up @@ -95,7 +96,9 @@ const throwSetError = (
);
};

function isTimeStampAttribute(properties?: CredentialSchemaProperty) {
function isTimestampAttributeSchemaProperty(
properties?: CredentialSchemaProperty
) {
return (
properties &&
properties.type === 'object' &&
Expand All @@ -121,7 +124,10 @@ function isValidTimestampAttribute(attributeValue: Date) {
}

function validateTimestampAttribute(value: AttributeType) {
return value instanceof Date && isValidTimestampAttribute(value);
return (
isTimestampAttribute(value) &&
isValidTimestampAttribute(timestampToDate(value))
);
}

function validateStringAttribute(value: AttributeType) {
Expand Down Expand Up @@ -170,7 +176,7 @@ function verifyRangeStatement(
}
};

if (isTimeStampAttribute(properties)) {
if (isTimestampAttributeSchemaProperty(properties)) {
checkRange(
'timestamp',
validateTimestampAttribute,
Expand All @@ -197,9 +203,11 @@ function verifyRangeStatement(
// The assertions are safe, because we already validated that lower/upper has the correct types.
if (
(properties?.type === 'integer' && statement.upper < statement.lower) ||
(isTimeStampAttribute(properties) &&
(statement.upper as Date).getTime() <
(statement.lower as Date).getTime()) ||
(isTimestampAttributeSchemaProperty(properties) &&
isTimestampAttribute(statement.lower) &&
isTimestampAttribute(statement.upper) &&
timestampToDate(statement.upper).getTime() <
timestampToDate(statement.lower).getTime()) ||
(properties?.type === 'string' &&
compareStringAttributes(
statement.lower as string,
Expand Down Expand Up @@ -243,7 +251,7 @@ function verifySetStatement(
}
};

if (isTimeStampAttribute(properties)) {
if (isTimestampAttributeSchemaProperty(properties)) {
checkSet(
'date-time',
validateTimestampAttribute,
Expand Down Expand Up @@ -381,7 +389,7 @@ export class AtomicStatementBuilder implements InternalBuilder {
}

/**
* If checkConstraints is true, this checks whether the given statement may be added to the statement being built.
* This checks whether the given statement may be added to the statement being built.
* If the statement breaks any rules, this will throw an error.
*/
private check(statement: AtomicStatementV2) {
Expand All @@ -403,14 +411,14 @@ export class AtomicStatementBuilder implements InternalBuilder {
*/
addRange(
attribute: string,
lower: AttributeType,
upper: AttributeType
lower: StatementAttributeType,
upper: StatementAttributeType
): this {
const statement: AtomicStatementV2 = {
type: StatementTypes.AttributeInRange,
attributeTag: attribute,
lower,
upper,
lower: statementAttributeTypeToAttributeType(lower),
upper: statementAttributeTypeToAttributeType(upper),
};
this.check(statement);
this.statements.push(statement);
Expand All @@ -423,11 +431,11 @@ export class AtomicStatementBuilder implements InternalBuilder {
* @param set: the set of values that the attribute must be included in.
* @returns the updated builder
*/
addMembership(attribute: string, set: AttributeType[]): this {
addMembership(attribute: string, set: StatementAttributeType[]): this {
const statement: AtomicStatementV2 = {
type: StatementTypes.AttributeInSet,
attributeTag: attribute,
set,
set: set.map(statementAttributeTypeToAttributeType),
};
this.check(statement);
this.statements.push(statement);
Expand All @@ -440,11 +448,11 @@ export class AtomicStatementBuilder implements InternalBuilder {
* @param set: the set of values that the attribute must be included in.
* @returns the updated builder
*/
addNonMembership(attribute: string, set: AttributeType[]): this {
addNonMembership(attribute: string, set: StatementAttributeType[]): this {
const statement: AtomicStatementV2 = {
type: StatementTypes.AttributeNotInSet,
attributeTag: attribute,
set,
set: set.map(statementAttributeTypeToAttributeType),
};
this.check(statement);
this.statements.push(statement);
Expand Down Expand Up @@ -546,7 +554,7 @@ export class AccountStatementBuild extends AtomicStatementBuilder {
}
}

type InternalBuilder = StatementBuilder<AttributeType, string>;
type InternalBuilder = StatementBuilder<StatementAttributeType, string>;
export class Web3StatementBuilder {
private statements: CredentialStatements = [];

Expand Down Expand Up @@ -601,9 +609,7 @@ export function getVerifiablePresentation(
try {
const s: VerifiablePresentation = VerifiablePresentation.fromString(
// Use json-bigint stringify to ensure we can handle bigints
wasm.createWeb3IdProof(
stringify(input, replaceDateWithTimeStampAttribute)
)
wasm.createWeb3IdProof(stringify(input))
);
return s;
} catch (e) {
Expand Down Expand Up @@ -754,13 +760,14 @@ function isInRange(
return lower <= value && upper > value;
}
if (
value instanceof Date &&
lower instanceof Date &&
upper instanceof Date
isTimestampAttribute(value) &&
isTimestampAttribute(lower) &&
isTimestampAttribute(upper)
) {
return (
lower.getTime() <= value.getTime() &&
upper.getTime() > value.getTime()
timestampToDate(lower).getTime() <=
timestampToDate(value).getTime() &&
timestampToDate(upper).getTime() > timestampToDate(value).getTime()
);
}
// Mismatch in types.
Expand All @@ -774,10 +781,14 @@ function isInSet(value: AttributeType, set: AttributeType[]) {
if (typeof value === 'string' || typeof value === 'bigint') {
return set.includes(value);
}
if (value instanceof Date) {
if (isTimestampAttribute(value)) {
return set
.map((date) => (date instanceof Date ? date.getTime() : undefined))
.includes(value.getTime());
.map((timestamp) =>
isTimestampAttribute(timestamp)
? timestampToDate(timestamp).getTime()
: undefined
)
.includes(timestampToDate(value).getTime());
}
return false;
}
Expand Down
41 changes: 39 additions & 2 deletions packages/common/test/web3IdHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import {
AttributeType,
StatementAttributeType,
TimestampAttribute,
statementAttributeTypeToAttributeType,
} from '../src';
import {
compareStringAttributes,
isStringAttributeInRange,
timestampToDate,
verifyWeb3IdCredentialSignature,
} from '../src/web3IdHelpers';
import fs from 'fs';
Expand Down Expand Up @@ -103,10 +110,13 @@ test('verifyWeb3IdCredentialSignature with timestamps', async () => {
graduationDate:
'5e581a2c4ab96536b5d0918120cae2bb2f92642d4b9df4446890f5c519b2f3ca',
};
const values = {
const values: Record<string, AttributeType> = {
degreeName: 'Bachelor of Science and Arts',
degreeType: 'BachelorDegree',
graduationDate: new Date('2023-08-28T00:00:00.000Z'),
graduationDate: {
type: 'date-time',
timestamp: '2023-08-28T00:00:00.000Z',
},
};

const holder =
Expand Down Expand Up @@ -158,3 +168,30 @@ test('isStringAttributeInRange handles value === lower === upper correctly', ()
expect(isStringAttributeInRange('2', '2', '2')).toBeFalsy();
expect(isStringAttributeInRange('299910', '299910', '299910')).toBeFalsy();
});

test('mapping statement date attribute to timestamp attribute', () => {
const statementAttribute: StatementAttributeType = new Date(0);
expect(statementAttributeTypeToAttributeType(statementAttribute)).toEqual({
type: 'date-time',
timestamp: '1970-01-01T00:00:00.000Z',
});
});

test('mapping timestamp attribute to date', () => {
const timestampAttribute: TimestampAttribute = {
type: 'date-time',
timestamp: '1975-01-01T00:00:00.000Z',
};

expect(timestampToDate(timestampAttribute)).toEqual(new Date(157766400000));
});

test('mapping statement date attribute to timestamp attribute and back again', () => {
const statementAttribute: StatementAttributeType = new Date(50000);
const timestampAttribute =
statementAttributeTypeToAttributeType(statementAttribute);

expect(timestampToDate(timestampAttribute as TimestampAttribute)).toEqual(
statementAttribute
);
});
Loading

0 comments on commit c89ac23

Please sign in to comment.