Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: fetch attestation as part of verifyCredential #708

Merged
merged 15 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion packages/core/src/__integrationtests__/Attestation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => {
await expect(
Credential.verifySignature(presentation)
).resolves.not.toThrow()
await Credential.verifyPresentation(presentation)
Credential.verifyWellFormed(presentation, { ctype: driversLicenseCType })

const attestation = Attestation.fromCredentialAndDid(
presentation,
Expand All @@ -183,6 +183,10 @@ describe('When there is an attester, claimer and ctype drivers license', () => {
expect(storedAttestation).not.toBeNull()
expect(storedAttestation?.revoked).toBe(false)

await expect(
Credential.verifyPresentation(presentation)
).resolves.toMatchObject({ attester: attester.uri, revoked: false })

// Claim the deposit back by submitting the reclaimDeposit extrinsic with the deposit payer's account.
const reclaimTx = api.tx.attestation.reclaimDeposit(attestation.claimHash)
await submitTx(reclaimTx, tokenHolder)
Expand All @@ -191,6 +195,10 @@ describe('When there is an attester, claimer and ctype drivers license', () => {
expect(
(await api.query.attestation.attestations(attestation.claimHash)).isNone
).toBe(true)

await expect(
Credential.verifyPresentation(presentation)
).rejects.toThrowErrorMatchingInlineSnapshot(`"Attestation not found"`)
}, 60_000)

it('should not be possible to attest a claim without enough tokens', async () => {
Expand Down Expand Up @@ -399,6 +407,10 @@ describe('When there is an attester, claimer and ctype drivers license', () => {
)
expect(storedAttestationAfter).not.toBeNull()
expect(storedAttestationAfter?.revoked).toBe(true)

await expect(
Credential.verifyCredential(credential)
).resolves.toMatchObject({ attester: attester.uri, revoked: true })
}, 40_000)

it('should be possible for the deposit payer to remove an attestation', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/__integrationtests__/Delegation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ describe('and attestation rights have been delegated', () => {
await expect(
Credential.verifySignature(presentation)
).resolves.not.toThrow()
await Credential.verifyPresentation(presentation)
Credential.verifyWellFormed(presentation, { ctype: driversLicenseCType })

const attestation = Attestation.fromCredentialAndDid(
credential,
Expand Down
20 changes: 15 additions & 5 deletions packages/core/src/attestation/Attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,22 @@ export function verifyAgainstCredential(
attestation: IAttestation,
credential: ICredential
): void {
if (
credential.claim.cTypeHash !== attestation.cTypeHash ||
credential.rootHash !== attestation.claimHash
) {
const credentialMismatch =
credential.claim.cTypeHash !== attestation.cTypeHash
const ctypeMismatch = credential.rootHash !== attestation.claimHash
const delegationMismatch =
credential.delegationId !== attestation.delegationId
if (credentialMismatch || ctypeMismatch || delegationMismatch) {
throw new SDKErrors.CredentialUnverifiableError(
'Attestation does not match credential'
`Some attributes of the on-chain attestation diverge from the credential: ${[
'cTypeHash',
'delegationId',
'claimHash',
]
.filter(
(_, i) => [ctypeMismatch, delegationMismatch, credentialMismatch][i]
)
.join(', ')}`
)
}
Credential.verifyDataIntegrity(credential)
Expand Down
205 changes: 182 additions & 23 deletions packages/core/src/credential/Credential.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ import type {
import { Crypto, SDKErrors, UUID } from '@kiltprotocol/utils'
import * as Did from '@kiltprotocol/did'
import {
ApiMocks,
createLocalDemoFullDidFromKeypair,
KeyTool,
makeSigningKeyTool,
} from '@kiltprotocol/testing'
import { ConfigService } from '@kiltprotocol/config'
import { randomAsHex } from '@polkadot/util-crypto'
import * as Attestation from '../attestation'
import * as Claim from '../claim'
import * as CType from '../ctype'
import * as Credential from './Credential'
import { init } from '../kilt'

const testCType = CType.fromProperties('Credential', {
a: { type: 'string' },
Expand All @@ -62,6 +66,20 @@ function buildCredential(
return credential
}

beforeAll(async () => {
const api = ApiMocks.createAugmentedApi()
api.query.attestation = {
attestations: jest.fn().mockResolvedValue(
ApiMocks.mockChainQueryReturn('attestation', 'attestations', {
revoked: false,
attester: '4s5d7QHWSX9xx4DLafDtnTHK87n5e9G3UoKRrCDQ2gnrzYmZ',
ctypeHash: CType.idToHash(testCType.$id),
} as any)
),
} as any
await init({ api })
})

describe('Credential', () => {
const identityAlice =
'did:kilt:4nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS'
Expand All @@ -85,9 +103,11 @@ describe('Credential', () => {
)
// check proof on complete data
expect(() => Credential.verifyDataIntegrity(credential)).not.toThrow()
await Credential.verifyCredential(credential, {
ctype: testCType,
})
await expect(
Credential.verifyCredential(credential, {
ctype: testCType,
})
).resolves.toMatchObject({ revoked: false, attester: identityBob })

// just deleting a field will result in a wrong proof
delete credential.claimNonceMap[Object.keys(credential.claimNonceMap)[0]]
Expand Down Expand Up @@ -287,11 +307,19 @@ describe('Credential', () => {
[]
)
expect(() =>
Credential.verifyAgainstCType(builtCredential, testCType)
Credential.verifyWellFormed(builtCredential, { ctype: testCType })
).not.toThrow()
builtCredential.claim.contents.name = 123
const builtCredentialWrong = buildCredential(
identityBob,
{
a: 'a',
b: 'b',
c: 1,
},
[]
)
expect(() =>
Credential.verifyAgainstCType(builtCredential, testCType)
Credential.verifyWellFormed(builtCredentialWrong, { ctype: testCType })
).toThrow()
})

Expand All @@ -304,6 +332,103 @@ describe('Credential', () => {
Credential.fromClaim(claimA2).rootHash
)
})

it('re-checks attestation status', async () => {
const api = ConfigService.get('api')
const credential = buildCredential(
identityBob,
{
a: 'a',
b: 'b',
c: 'c',
},
[]
)

const { attester, revoked } = await Credential.verifyAttested(credential)
expect(revoked).toBe(false)
await expect(
Credential.refreshRevocationStatus({ ...credential, revoked, attester })
).resolves.toMatchObject({ revoked, attester })

jest.mocked(api.query.attestation.attestations).mockResolvedValueOnce(
ApiMocks.mockChainQueryReturn('attestation', 'attestations', {
revoked: true,
attester: Did.toChain(attester),
ctypeHash: credential.claim.cTypeHash,
} as any) as any
)
await expect(
Credential.refreshRevocationStatus({ ...credential, revoked, attester })
).resolves.toMatchObject({ revoked: true, attester })

await expect(
Credential.refreshRevocationStatus(credential as any)
).rejects.toThrowErrorMatchingInlineSnapshot(
`"This function expects a VerifiedCredential with properties \`revoked\` (boolean) and \`attester\` (string)"`
)

jest
.mocked(api.query.attestation.attestations)
.mockResolvedValueOnce(
ApiMocks.mockChainQueryReturn('attestation', 'attestations') as any
)
await expect(
Credential.refreshRevocationStatus({ ...credential, revoked, attester })
).rejects.toThrowErrorMatchingInlineSnapshot(`"Attestation not found"`)

jest.mocked(api.query.attestation.attestations).mockResolvedValueOnce(
ApiMocks.mockChainQueryReturn(
'attestation',
'attestations',
ApiMocks.mockChainQueryReturn('attestation', 'attestations', {
revoked: false,
attester: Did.toChain(identityAlice),
ctypeHash: credential.claim.cTypeHash,
} as any) as any
) as any
)
await expect(
Credential.refreshRevocationStatus({ ...credential, revoked, attester })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Attester has changed since first verification"`
)

jest.mocked(api.query.attestation.attestations).mockResolvedValueOnce(
ApiMocks.mockChainQueryReturn(
'attestation',
'attestations',
ApiMocks.mockChainQueryReturn('attestation', 'attestations', {
revoked: true,
attester: Did.toChain(attester),
ctypeHash: randomAsHex(),
} as any) as any
) as any
)
await expect(
Credential.refreshRevocationStatus({ ...credential, revoked, attester })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Some attributes of the on-chain attestation diverge from the credential: claimHash"`
)

jest.mocked(api.query.attestation.attestations).mockResolvedValueOnce(
ApiMocks.mockChainQueryReturn(
'attestation',
'attestations',
ApiMocks.mockChainQueryReturn('attestation', 'attestations', {
revoked: true,
attester: Did.toChain(attester),
ctypeHash: credential.claim.cTypeHash,
authorizationId: { Delegation: randomAsHex() },
} as any) as any
) as any
)
await expect(
Credential.refreshRevocationStatus({ ...credential, revoked, attester })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Some attributes of the on-chain attestation diverge from the credential: delegationId"`
)
})
})

describe('Presentations', () => {
Expand Down Expand Up @@ -378,6 +503,16 @@ describe('Presentations', () => {
[],
keyAlice.getSignCallback(identityAlice)
)

jest
.mocked(ConfigService.get('api').query.attestation.attestations)
.mockResolvedValue(
ApiMocks.mockChainQueryReturn('attestation', 'attestations', {
revoked: false,
attester: Did.toChain(identityBob.uri),
ctypeHash: CType.idToHash(testCType.$id),
} as any) as any
)
})

it('verify credentials signed by a full DID', async () => {
Expand All @@ -395,9 +530,11 @@ describe('Presentations', () => {

// check proof on complete data
expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow()
await Credential.verifyPresentation(presentation, {
didResolveKey,
})
await expect(
Credential.verifyPresentation(presentation, {
didResolveKey,
})
).resolves.toMatchObject({ revoked: false, attester: identityBob.uri })
})
it('verify credentials signed by a light DID', async () => {
const { getSignCallback, authentication } = makeSigningKeyTool('ed25519')
Expand All @@ -419,9 +556,11 @@ describe('Presentations', () => {

// check proof on complete data
expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow()
await Credential.verifyPresentation(presentation, {
didResolveKey,
})
await expect(
Credential.verifyPresentation(presentation, {
didResolveKey,
})
).resolves.toMatchObject({ revoked: false, attester: identityBob.uri })
})

it('throws if signature is missing on credential presentation', async () => {
Expand Down Expand Up @@ -658,6 +797,16 @@ describe('create presentation', () => {
migratedClaimerFullDid.uri
)
)

jest
.mocked(ConfigService.get('api').query.attestation.attestations)
.mockResolvedValue(
ApiMocks.mockChainQueryReturn('attestation', 'attestations', {
revoked: false,
attester: Did.toChain(attester.uri),
ctypeHash: CType.idToHash(ctype.$id),
} as any) as any
)
})

it('should create presentation and exclude specific attributes using a full DID', async () => {
Expand All @@ -670,9 +819,11 @@ describe('create presentation', () => {
),
challenge,
})
await Credential.verifyPresentation(presentation, {
didResolveKey,
})
await expect(
Credential.verifyPresentation(presentation, {
didResolveKey,
})
).resolves.toMatchObject({ revoked: false, attester: attester.uri })
expect(presentation.claimerSignature?.challenge).toEqual(challenge)
})
it('should create presentation and exclude specific attributes using a light DID', async () => {
Expand All @@ -697,9 +848,11 @@ describe('create presentation', () => {
),
challenge,
})
await Credential.verifyPresentation(presentation, {
didResolveKey,
})
await expect(
Credential.verifyPresentation(presentation, {
didResolveKey,
})
).resolves.toMatchObject({ revoked: false, attester: attester.uri })
expect(presentation.claimerSignature?.challenge).toEqual(challenge)
})
it('should create presentation and exclude specific attributes using a migrated DID', async () => {
Expand All @@ -726,9 +879,11 @@ describe('create presentation', () => {
),
challenge,
})
await Credential.verifyPresentation(presentation, {
didResolveKey,
})
await expect(
Credential.verifyPresentation(presentation, {
didResolveKey,
})
).resolves.toMatchObject({ revoked: false, attester: attester.uri })
expect(presentation.claimerSignature?.challenge).toEqual(challenge)
})

Expand Down Expand Up @@ -795,9 +950,13 @@ describe('create presentation', () => {
})

it('should verify the credential claims structure against the ctype', () => {
expect(() => Credential.verifyAgainstCType(credential, ctype)).not.toThrow()
expect(() =>
CType.verifyClaimAgainstSchema(credential.claim.contents, ctype)
).not.toThrow()
credential.claim.contents.name = 123

expect(() => Credential.verifyAgainstCType(credential, ctype)).toThrow()
expect(() =>
CType.verifyClaimAgainstSchema(credential.claim.contents, ctype)
).toThrow()
})
})
Loading