diff --git a/packages/core/src/credential/Credential.ts b/packages/core/src/credential/Credential.ts index bf06ba147..bd3d627a4 100644 --- a/packages/core/src/credential/Credential.ts +++ b/packages/core/src/credential/Credential.ts @@ -21,6 +21,8 @@ import { isDidSignature, verifyDidSignature, resolveKey, + signatureToJson, + signatureFromJson, } from '@kiltprotocol/did' import type { DidResolveKey, @@ -34,7 +36,6 @@ import type { SignCallback, } from '@kiltprotocol/types' import { Crypto, DataUtils, SDKErrors } from '@kiltprotocol/utils' -import { u8aToHex } from '@polkadot/util' import * as Claim from '../claim/index.js' import { hashClaimContents } from '../claim/index.js' import { verifyClaimAgainstSchema } from '../ctype/index.js' @@ -233,7 +234,7 @@ export async function verifySignature( ) const signingData = makeSigningData(input, claimerSignature.challenge) await verifyDidSignature({ - signature: claimerSignature, + ...signatureFromJson(claimerSignature), message: signingData, expectedVerificationMethod: 'authentication', didResolveKey, @@ -415,7 +416,7 @@ export async function createPresentation({ excludedClaimProperties ) - const { signature, keyUri } = await signCallback({ + const signature = await signCallback({ data: makeSigningData(presentation, challenge), did: credential.claim.owner, keyRelationship: 'authentication', @@ -423,6 +424,6 @@ export async function createPresentation({ return { ...presentation, - claimerSignature: { signature: u8aToHex(signature), keyUri, challenge }, + claimerSignature: { ...signatureToJson(signature), challenge }, } } diff --git a/packages/core/src/quote/Quote.spec.ts b/packages/core/src/quote/Quote.spec.ts index 592225b60..724fb2d54 100644 --- a/packages/core/src/quote/Quote.spec.ts +++ b/packages/core/src/quote/Quote.spec.ts @@ -9,8 +9,6 @@ * @group unit/quote */ -import { u8aToHex } from '@polkadot/util' - import type { DidDocument, IClaim, @@ -27,7 +25,6 @@ import { Crypto } from '@kiltprotocol/utils' import * as Did from '@kiltprotocol/did' import { createLocalDemoFullDidFromKeypair, - makeDidSignature, makeSigningKeyTool, } from '@kiltprotocol/testing' import * as CType from '../ctype' @@ -142,10 +139,13 @@ describe('Quote', () => { it('tests created quote data against given data', async () => { expect(validQuoteData.attesterDid).toEqual(attesterIdentity.uri) - const signature = await makeDidSignature( - Crypto.hashStr(Crypto.encodeObjectAsStr(validAttesterSignedQuote)), - claimerIdentity.uri, - claimer.getSignCallback(claimerIdentity) + const sign = claimer.getSignCallback(claimerIdentity) + const signature = Did.signatureToJson( + await sign({ + data: Crypto.hash(Crypto.encodeObjectAsStr(validAttesterSignedQuote)), + did: claimerIdentity.uri, + keyRelationship: 'authentication', + }) ) expect(signature).toEqual(quoteBothAgreed.claimerSignature) @@ -166,10 +166,8 @@ describe('Quote', () => { }) ), validAttesterSignedQuote.attesterSignature.signature, - u8aToHex( - Did.getKey(attesterIdentity, attesterKeyId!)?.publicKey || - new Uint8Array() - ) + Did.getKey(attesterIdentity, attesterKeyId!)?.publicKey || + new Uint8Array() ) ).not.toThrow() await Quote.verifyAttesterSignedQuote(validAttesterSignedQuote, { diff --git a/packages/core/src/quote/Quote.ts b/packages/core/src/quote/Quote.ts index 687e3471f..60e0b134c 100644 --- a/packages/core/src/quote/Quote.ts +++ b/packages/core/src/quote/Quote.ts @@ -25,7 +25,12 @@ import type { DidUri, } from '@kiltprotocol/types' import { Crypto, JsonSchema, SDKErrors } from '@kiltprotocol/utils' -import { resolveKey, verifyDidSignature } from '@kiltprotocol/did' +import { + resolveKey, + verifyDidSignature, + signatureToJson, + signatureFromJson, +} from '@kiltprotocol/did' import { QuoteSchema } from './QuoteSchema.js' /** @@ -79,10 +84,7 @@ export async function createAttesterSignedQuote( }) return { ...quoteInput, - attesterSignature: { - keyUri: signature.keyUri, - signature: Crypto.u8aToHex(signature.signature), - }, + attesterSignature: signatureToJson(signature), } } @@ -103,7 +105,7 @@ export async function verifyAttesterSignedQuote( ): Promise { const { attesterSignature, ...basicQuote } = quote await verifyDidSignature({ - signature: attesterSignature, + ...signatureFromJson(attesterSignature), message: Crypto.hashStr(Crypto.encodeObjectAsStr(basicQuote)), expectedVerificationMethod: 'authentication', didResolveKey, @@ -140,13 +142,13 @@ export async function createQuoteAgreement( const { attesterSignature, ...basicQuote } = attesterSignedQuote await verifyDidSignature({ - signature: attesterSignature, + ...signatureFromJson(attesterSignature), message: Crypto.hashStr(Crypto.encodeObjectAsStr(basicQuote)), expectedVerificationMethod: 'authentication', didResolveKey, }) - const { signature, keyUri } = await sign({ + const signature = await sign({ data: Crypto.hash(Crypto.encodeObjectAsStr(attesterSignedQuote)), did: claimerDid, keyRelationship: 'authentication', @@ -155,10 +157,7 @@ export async function createQuoteAgreement( return { ...attesterSignedQuote, rootHash: credentialRootHash, - claimerSignature: { - signature: Crypto.u8aToHex(signature), - keyUri, - }, + claimerSignature: signatureToJson(signature), } } diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index f190a997b..f5bb3a339 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -19,9 +19,14 @@ import { } from '@kiltprotocol/types' import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' import { Crypto } from '@kiltprotocol/utils' -import { makeSigningKeyTool, makeDidSignature } from '@kiltprotocol/testing' +import { makeSigningKeyTool } from '@kiltprotocol/testing' import * as Did from './index.js' -import { verifyDidSignature, isDidSignature } from './Did.signature' +import { + verifyDidSignature, + isDidSignature, + signatureFromJson, + signatureToJson, +} from './Did.signature' import { resolveKey, keyToResolvedKey } from './DidResolver' jest.mock('./DidResolver') @@ -55,50 +60,53 @@ describe('light DID', () => { it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) + const { signature, keyUri } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).resolves.not.toThrow() }) - it('verifies old did signature (with `keyId` property) over string', async () => { + it('deserializes old did signature (with `keyId` property) to new format', async () => { const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) - const oldSignature: any = { - ...signature, - keyId: signature.keyUri, + const { signature, keyUri } = signatureToJson( + await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) + ) + const oldSignature = { + signature, + keyId: keyUri, } - delete oldSignature.keyUri - - // Test the old signature is correctly crafted - expect(oldSignature.signature).toBeDefined() - expect(oldSignature.keyId).toBeDefined() - expect(oldSignature.keyUri).toBeUndefined() - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - expectedVerificationMethod: 'authentication', - }) - ).resolves.not.toThrow() + const deserialized = signatureFromJson(oldSignature) + expect(deserialized.signature).toBeInstanceOf(Uint8Array) + expect(deserialized.keyUri).toStrictEqual(keyUri) + expect(deserialized).not.toHaveProperty('keyId') }) it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const signature = await makeDidSignature( - Crypto.u8aToHex(SIGNED_BYTES), - did.uri, - sign - ) + const { signature, keyUri } = await sign({ + data: SIGNED_BYTES, + did: did.uri, + keyRelationship: 'authentication', + }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).resolves.not.toThrow() @@ -106,11 +114,16 @@ describe('light DID', () => { it('fails if relationship does not match', async () => { const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) + const { signature, keyUri } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) await expect(() => verifyDidSignature({ message: SIGNED_STRING, signature, + keyUri, expectedVerificationMethod: 'assertionMethod', }) ).rejects.toThrow() @@ -118,13 +131,19 @@ describe('light DID', () => { it('fails if key id does not match', async () => { const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) - signature.keyUri += '1a' + // eslint-disable-next-line prefer-const + let { signature, keyUri } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) + keyUri += '1a' jest.mocked(resolveKey).mockRejectedValue(new Error('Key not found')) await expect(() => verifyDidSignature({ message: SIGNED_STRING, signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() @@ -132,11 +151,16 @@ describe('light DID', () => { it('fails if signature does not match', async () => { const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) + const { signature, keyUri } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) await expect(() => verifyDidSignature({ message: SIGNED_STRING.substring(1), signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() @@ -144,13 +168,19 @@ describe('light DID', () => { it('fails if key id malformed', async () => { const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) + // eslint-disable-next-line prefer-const + let { signature, keyUri } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) // @ts-expect-error - signature.keyUri = signature.keyUri.replace('#', '?') + keyUri = keyUri.replace('#', '?') await expect(() => verifyDidSignature({ message: SIGNED_STRING, signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() @@ -159,11 +189,16 @@ describe('light DID', () => { it('does not verify if migrated to Full DID', async () => { jest.mocked(resolveKey).mockRejectedValue(new Error('Migrated')) const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) + const { signature, keyUri } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) await expect(() => verifyDidSignature({ message: SIGNED_STRING, signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() @@ -214,11 +249,16 @@ describe('full DID', () => { it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) + const { signature, keyUri } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).resolves.not.toThrow() @@ -226,15 +266,16 @@ describe('full DID', () => { it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const signature = await makeDidSignature( - Crypto.u8aToHex(SIGNED_BYTES), - did.uri, - sign - ) + const { signature, keyUri } = await sign({ + data: SIGNED_BYTES, + did: did.uri, + keyRelationship: 'authentication', + }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).resolves.not.toThrow() @@ -243,11 +284,16 @@ describe('full DID', () => { it('does not verify if deactivated', async () => { jest.mocked(resolveKey).mockRejectedValue(new Error('Deactivated')) const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) + const { signature, keyUri } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) await expect(() => verifyDidSignature({ message: SIGNED_STRING, signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() @@ -256,11 +302,16 @@ describe('full DID', () => { it('does not verify if not on chain', async () => { jest.mocked(resolveKey).mockRejectedValue(new Error('Not on chain')) const SIGNED_STRING = 'signed string' - const signature = await makeDidSignature(SIGNED_STRING, did.uri, sign) + const { signature, keyUri } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.uri, + keyRelationship: 'authentication', + }) await expect(() => verifyDidSignature({ message: SIGNED_STRING, signature, + keyUri, expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index dce02c54e..df9ed6015 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -5,11 +5,13 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { u8aToHex, isHex } from '@polkadot/util' +import { isHex } from '@polkadot/util' import { DidResolveKey, + DidResourceUri, DidSignature, + SignResponseData, VerificationKeyRelationship, } from '@kiltprotocol/types' import { Crypto, SDKErrors } from '@kiltprotocol/utils' @@ -19,7 +21,8 @@ import { parse, validateUri } from './Did.utils.js' export type DidSignatureVerificationInput = { message: string | Uint8Array - signature: DidSignature + signature: Uint8Array + keyUri: DidResourceUri expectedVerificationMethod?: VerificationKeyRelationship didResolveKey?: DidResolveKey } @@ -54,32 +57,28 @@ function verifyDidSignatureDataStructure( * * @param input Object wrapping all input. * @param input.message The message that was signed. - * @param input.signature An object containing signature and signer key. + * @param input.signature Signature bytes. + * @param input.keyUri DID URI of the key used for signing. * @param input.expectedVerificationMethod Which relationship to the signer DID the key must have. * @param input.didResolveKey Allows specifying a custom DID key resolve. Defaults to the built-in [[resolveKey]]. */ export async function verifyDidSignature({ message, signature, + keyUri, expectedVerificationMethod, didResolveKey = resolveKey, }: DidSignatureVerificationInput): Promise { - verifyDidSignatureDataStructure(signature) - // Add support for old signatures that had the `keyId` instead of the `keyUri` - const inputUri = signature.keyUri || (signature as any).keyId // Verification fails if the signature key URI is not valid - const { fragment } = parse(inputUri) + const { fragment } = parse(keyUri) if (!fragment) throw new SDKErrors.SignatureMalformedError( - `Signature key URI "${inputUri}" invalid` + `Signature key URI "${keyUri}" invalid` ) - const { publicKey } = await didResolveKey( - inputUri, - expectedVerificationMethod - ) + const { publicKey } = await didResolveKey(keyUri, expectedVerificationMethod) - Crypto.verify(message, signature.signature, u8aToHex(publicKey)) + Crypto.verify(message, signature, publicKey) } /** @@ -99,3 +98,33 @@ export function isDidSignature( return false } } + +/** + * Transforms the output of a [[SignCallback]] into the [[DidSignature]] format suitable for json-based data exchange. + * + * @param input Signature data returned from the [[SignCallback]]. + * @param input.signature Signature bytes. + * @param input.keyUri DID URI of the key used for signing. + * @returns A [[DidSignature]] object where signature is hex-encoded. + */ +export function signatureToJson({ + signature, + keyUri, +}: SignResponseData): DidSignature { + return { signature: Crypto.u8aToHex(signature), keyUri } +} + +/** + * Deserializes a [[DidSignature]] for signature verification. + * Handles backwards compatibility to an older version of the interface where the `keyUri` property was called `keyId`. + * + * @param input A [[DidSignature]] object. + * @returns The deserialized DidSignature where the signature is represented as a Uint8Array. + */ +export function signatureFromJson( + input: DidSignature | OldDidSignature +): Pick { + const keyUri = 'keyUri' in input ? input.keyUri : input.keyId + const signature = Crypto.coToUInt8(input.signature) + return { signature, keyUri } +} diff --git a/packages/messaging/src/Message.spec.ts b/packages/messaging/src/Message.spec.ts index 50418c703..6730698f2 100644 --- a/packages/messaging/src/Message.spec.ts +++ b/packages/messaging/src/Message.spec.ts @@ -64,7 +64,6 @@ import { createLocalDemoFullDidFromKeypair, KeyTool, KeyToolSignCallback, - makeDidSignature, } from '@kiltprotocol/testing' import { u8aToHex } from '@polkadot/util' import { Crypto, SDKErrors } from '@kiltprotocol/utils' @@ -890,10 +889,12 @@ describe('Error checking / Verification', () => { }, metaData: {}, signatures: { - inviter: await makeDidSignature( - 'signature', - identityAlice.uri, - keyAlice.getSignCallback(identityAlice) + inviter: Did.signatureToJson( + await keyAlice.getSignCallback(identityAlice)({ + data: Crypto.coToUInt8('signature'), + did: identityAlice.uri, + keyRelationship: 'authentication', + }) ), }, } @@ -907,15 +908,19 @@ describe('Error checking / Verification', () => { isPCR: false, }, signatures: { - inviter: await makeDidSignature( - 'signature', - identityAlice.uri, - keyAlice.getSignCallback(identityAlice) + inviter: Did.signatureToJson( + await keyAlice.getSignCallback(identityAlice)({ + data: Crypto.coToUInt8('signature'), + did: identityAlice.uri, + keyRelationship: 'authentication', + }) ), - invitee: await makeDidSignature( - 'signature', - identityBob.uri, - keyBob.getSignCallback(identityBob) + invitee: Did.signatureToJson( + await keyBob.getSignCallback(identityBob)({ + data: Crypto.coToUInt8('signature'), + did: identityBob.uri, + keyRelationship: 'authentication', + }) ), }, } diff --git a/packages/testing/src/TestUtils.ts b/packages/testing/src/TestUtils.ts index 2fa692391..0e5e0648b 100644 --- a/packages/testing/src/TestUtils.ts +++ b/packages/testing/src/TestUtils.ts @@ -12,8 +12,6 @@ import type { DidDocument, DidKey, DidServiceEndpoint, - DidSignature, - DidUri, DidVerificationKey, EncryptCallback, KeyRelationship, @@ -333,19 +331,3 @@ export async function createFullDidFromSeed( const sign = makeStoreDidCallback(keypair) return createFullDidFromLightDid(payer, lightDid, sign) } - -export async function makeDidSignature( - data: string, - didUri: DidUri, - signCallback: SignCallback -): Promise { - const { signature, keyUri } = await signCallback({ - data: Crypto.coToUInt8(data), - did: didUri, - keyRelationship: 'authentication', - }) - return { - signature: Crypto.u8aToHex(signature), - keyUri, - } -} diff --git a/packages/utils/src/Crypto.ts b/packages/utils/src/Crypto.ts index a841fb1a6..197c6575e 100644 --- a/packages/utils/src/Crypto.ts +++ b/packages/utils/src/Crypto.ts @@ -117,14 +117,14 @@ export function signStr( * * @param message Original signed message to be verified. * @param signature Signature as hex string or byte array. - * @param address Substrate address or public key of the signer. + * @param addressOrPublicKey Substrate address or public key of the signer. */ export function verify( message: CryptoInput, signature: CryptoInput, - address: Address + addressOrPublicKey: Address | HexString | Uint8Array ): void { - if (signatureVerify(message, signature, address).isValid !== true) + if (signatureVerify(message, signature, addressOrPublicKey).isValid !== true) throw new SDKErrors.SignatureUnverifiableError() }