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!: cryptosuite-style signers #801

Merged
merged 16 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 14 additions & 12 deletions packages/core/src/credentialsV1/KiltAttestationProofV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ import type {
RuntimeCommonAuthorizationAuthorizationId,
} from '@kiltprotocol/augment-api'
import type {
DidDocument,
Did,
ICType,
IDelegationNode,
KiltAddress,
SignExtrinsicCallback,
SignerInterface,
} from '@kiltprotocol/types'
import * as CType from '../ctype/index.js'

Expand Down Expand Up @@ -659,19 +660,14 @@ export type AttestationHandler = (
timestamp?: number
}>

export interface DidSigner {
did: Did
signer: SignExtrinsicCallback
}

export type TxHandler = {
account: KiltAddress
signAndSubmit?: AttestationHandler
signer?: Signer
}

export type IssueOpts = {
didSigner: DidSigner
signers: readonly SignerInterface[]
transactionHandler: TxHandler
} & Parameters<typeof authorizeTx>[4]

Expand All @@ -695,8 +691,9 @@ function makeDefaultTxSubmit(
* Creates a complete [[KiltAttestationProofV1]] for issuing a new credential.
*
* @param credential A [[KiltCredentialV1]] for which a proof shall be created.
* @param issuer The DID or DID Document of the DID acting as the issuer.
* @param opts Additional parameters.
* @param opts.didSigner Object containing the attester's `did` and a `signer` callback which authorizes the on-chain anchoring of the credential with the attester's signature.
* @param opts.signers An array of signer interfaces related to the issuer's keys. The function selects the appropriate handlers for all signatures required for issuance (e.g., authorizing the on-chain anchoring of the credential).
* @param opts.transactionHandler Object containing the submitter `address` that's going to cover the transaction fees as well as either a `signer` or `signAndSubmit` callback handling extrinsic signing and submission.
* The signAndSubmit callback receives an unsigned extrinsic and is expected to return the `blockHash` and (optionally) `timestamp` when the extrinsic was included in a block.
* This callback must thus take care of signing and submitting the extrinsic to the KILT blockchain as well as noting the inclusion block.
Expand All @@ -705,18 +702,23 @@ function makeDefaultTxSubmit(
*/
export async function issue(
credential: Omit<UnissuedCredential, 'issuer'>,
{ didSigner, transactionHandler, ...otherParams }: IssueOpts
issuer: Did | DidDocument,
{ signers, transactionHandler, ...otherParams }: IssueOpts
): Promise<KiltCredentialV1> {
const updatedCredential = { ...credential, issuer: didSigner.did }
const updatedCredential = {
...credential,
issuer: typeof issuer === 'string' ? issuer : issuer.id,
}
const [proof, callArgs] = initializeProof(updatedCredential)
const api = ConfigService.get('api')
const call = api.tx.attestation.add(...callArgs)
const txSubmissionHandler =
transactionHandler.signAndSubmit ?? makeDefaultTxSubmit(transactionHandler)

const didSigned = await authorizeTx(
didSigner.did,
issuer,
call,
didSigner.signer,
signers,
transactionHandler.account,
otherParams
)
Expand Down
55 changes: 27 additions & 28 deletions packages/core/src/delegation/DelegationNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import type {
IAttestation,
IDelegationHierarchyDetails,
IDelegationNode,
SignCallback,
SignerInterface,
SubmittableExtrinsic,
DidUrl,
} from '@kiltprotocol/types'
import { Crypto, SDKErrors, UUID } from '@kiltprotocol/utils'
import { Crypto, SDKErrors, Signers, UUID } from '@kiltprotocol/utils'
import { ConfigService } from '@kiltprotocol/config'
import * as Did from '@kiltprotocol/did'

Expand Down Expand Up @@ -259,38 +258,38 @@ export class DelegationNode implements IDelegationNode {
* This is required to anchor the delegation node on chain in order to enforce the delegate's consent.
*
* @param delegateDid The DID of the delegate.
* @param sign The callback to sign the delegation creation details for the delegate.
* @param signers An array of signer interfaces, one of which will be selected to sign the delegation creation details for the delegate.
* @returns The DID signature over the delegation **as a hex string**.
*/
public async delegateSign(
delegateDid: DidDocument,
sign: SignCallback
signers: readonly SignerInterface[]
): Promise<Did.EncodedSignature> {
const delegateSignature = await sign({
data: this.generateHash(),
did: delegateDid.id,
verificationRelationship: 'authentication',
})
const signerUrl =
`${delegateDid.id}${delegateSignature.verificationMethod.id}` as DidUrl
const { fragment } = Did.parse(signerUrl)
if (!fragment) {
throw new SDKErrors.DidError(
`DID verification method URL "${signerUrl}" couldn't be parsed`
)
}
const verificationMethod = delegateDid.verificationMethod?.find(
({ id }) => id === fragment
const { byDid, verifiableOnChain } = Signers.select
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does byDid mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you select a Signer byDid (-> by it's association to a Did, via the DidDocument). I thought that was self-explanatory, how can this be improved?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think on its own it might not make sense, but when it's exported with a named object, it made more sense to me, also the way it's used in the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any suggestions here?

const signer = await Signers.selectSigner(
signers,
verifiableOnChain(),
byDid(delegateDid, {
verificationRelationship: 'authentication',
})
)
if (!verificationMethod) {
throw new SDKErrors.DidError(
`Verification method "${signerUrl}" was not found on DID: "${delegateDid.id}"`
)
if (!signer) {
throw new SDKErrors.NoSuitableSignerError(undefined, {
signerRequirements: {
did: delegateDid.id,
verificationRelationship: 'authentication',
algorithm: Signers.DID_PALLET_SUPPORTED_ALGORITHMS,
},
})
}
return Did.didSignatureToChain(
verificationMethod,
delegateSignature.signature
)
const signature = await signer.sign({
data: this.generateHash(),
})

return Did.didSignatureToChain({
algorithm: signer.algorithm,
signature,
})
}

/**
Expand Down
144 changes: 78 additions & 66 deletions packages/did/src/Did.chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,16 @@ import type {
BN,
Deposit,
Did,
DidUrl,
KiltAddress,
Service,
SignatureVerificationRelationship,
SignExtrinsicCallback,
SignRequestData,
SignResponseData,
SignerInterface,
SubmittableExtrinsic,
UriFragment,
VerificationMethod,
} from '@kiltprotocol/types'

import { ConfigService } from '@kiltprotocol/config'
import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils'
import { Crypto, SDKErrors, Signers, ss58Format } from '@kiltprotocol/utils'

import type {
DidEncryptionMethodType,
Expand All @@ -43,7 +40,6 @@ import type {

import { isValidVerificationMethodType } from './DidDetails/DidDetails.js'
import {
multibaseKeyToDidKey,
keypairToMultibaseKey,
getAddressFromVerificationMethod,
getFullDid,
Expand Down Expand Up @@ -296,14 +292,6 @@ export function serviceFromChain(
}
}

export type AuthorizeCallInput = {
did: Did
txCounter: AnyNumber
call: Extrinsic
submitter: KiltAddress
blockNumber?: AnyNumber
}

export function publicKeyToChain(
key: NewDidVerificationKey
): EncodedVerificationKey
Expand Down Expand Up @@ -331,14 +319,6 @@ interface GetStoreTxInput {
service?: NewService[]
}

type GetStoreTxSignCallbackResponse = Pick<SignResponseData, 'signature'> & {
// We don't need the key ID to dispatch the tx.
verificationMethod: Pick<VerificationMethod, 'publicKeyMultibase'>
}
export type GetStoreTxSignCallback = (
signData: Omit<SignRequestData, 'did'>
) => Promise<GetStoreTxSignCallbackResponse>

/**
* Create a DID creation operation which includes the information provided.
*
Expand All @@ -352,14 +332,15 @@ export type GetStoreTxSignCallback = (
*
* @param input The DID keys and services to store.
* @param submitter The KILT address authorized to submit the creation operation.
* @param sign The sign callback. The authentication key has to be used.
* @param signers An array of signer interfaces. A suitable signer will be selected if available.
* The signer has to use the authentication public key encoded as a Kilt Address or as a hex string as its id.
*
* @returns The SubmittableExtrinsic for the DID creation operation.
*/
export async function getStoreTx(
input: GetStoreTxInput,
submitter: KiltAddress,
sign: GetStoreTxSignCallback
signers: readonly SignerInterface[]
): Promise<SubmittableExtrinsic> {
const api = ConfigService.get('api')

Expand Down Expand Up @@ -437,19 +418,78 @@ export async function getStoreTx(
.createType(api.tx.did.create.meta.args[0].type.toString(), apiInput)
.toU8a()

const { signature } = await sign({
const signer = await Signers.selectSigner(
signers,
Signers.select.verifiableOnChain(),
Signers.select.bySignerId([
did,
Crypto.u8aToHex(authenticationKey.publicKey),
])
)

if (!signer) {
throw new SDKErrors.NoSuitableSignerError(
'Did creation requires an account signer where the address is equal to the Did identifier (did:kilt:{identifier}).',
{
availableSigners: signers,
signerRequirements: {
id: [did, Crypto.u8aToHex(authenticationKey.publicKey)], // TODO: we could compute the key id and accept it too, or accept light Dids as signers
algorithm: [Signers.DID_PALLET_SUPPORTED_ALGORITHMS],
},
}
)
}

const signature = await signer.sign({
data: encoded,
verificationRelationship: 'authentication',
})
const encodedSignature = {
[authenticationKey.type]: signature,
} as EncodedSignature
return api.tx.did.create(encoded, encodedSignature)
}

export interface SigningOptions {
sign: SignExtrinsicCallback
verificationRelationship: SignatureVerificationRelationship
/**
* Compiles an enum-type key-value pair representation of a signature created with a signer associated with a full DID verification method. Required for creating full DID signed extrinsics.
*
* @param input Signature and algorithm.
* @param input.algorithm Descriptor of the signature algorithm used by the signer.
* @param input.signature The signature generated by the signer.
* @returns Data restructured to allow SCALE encoding by polkadot api.
*/
export function didSignatureToChain({
algorithm,
signature,
}: {
algorithm: string
signature: Uint8Array
}): EncodedSignature {
const lower = algorithm.toLowerCase()
const keyType =
lower === Signers.ALGORITHMS.ECRECOVER_SECP256K1_BLAKE2B.toLowerCase()
? 'ecdsa'
: lower
if (!isValidVerificationMethodType(keyType)) {
throw new SDKErrors.DidError(
`encodedDidSignature requires a signature algorithm in ${Signers.DID_PALLET_SUPPORTED_ALGORITHMS}. A key of type "${algorithm}" was used instead`
)
}

return { [keyType]: signature } as EncodedSignature
}

export type DidPalletSigner = SignerInterface<
Signers.DidPalletSupportedAlgorithms,
DidUrl
>

export type AuthorizeCallInput = {
did: Did
txCounter: AnyNumber
call: Extrinsic
submitter: KiltAddress
blockNumber?: AnyNumber
signer: DidPalletSigner
}

/**
Expand All @@ -458,8 +498,7 @@ export interface SigningOptions {
*
* @param params Object wrapping all input to the function.
* @param params.did Full DID.
* @param params.verificationRelationship DID verification relationship to be used for authorization.
* @param params.sign The callback to interface with the key store managing the private key to be used.
* @param params.signer The signer interface with the key store managing the private key to be used.
* @param params.call The call or extrinsic to be authorized.
* @param params.txCounter The nonce or txCounter value for this extrinsic, which must be on larger than the current txCounter value of the authorizing full DID.
* @param params.submitter Payment account allowed to submit this extrinsic and cover its fees, which will end up owning any deposit associated with newly created records.
Expand All @@ -468,13 +507,12 @@ export interface SigningOptions {
*/
export async function generateDidAuthenticatedTx({
did,
verificationRelationship,
sign,
signer,
call,
txCounter,
submitter,
blockNumber,
}: AuthorizeCallInput & SigningOptions): Promise<SubmittableExtrinsic> {
}: AuthorizeCallInput): Promise<SubmittableExtrinsic> {
const api = ConfigService.get('api')
const signableCall =
api.registry.createType<DidDidDetailsDidAuthorizedCallOperation>(
Expand All @@ -487,38 +525,12 @@ export async function generateDidAuthenticatedTx({
blockNumber: blockNumber ?? (await api.query.system.number()),
}
)
const { signature, verificationMethod } = await sign({
const signature = await signer.sign({
data: signableCall.toU8a(),
verificationRelationship,
did,
})
const { keyType } = multibaseKeyToDidKey(
verificationMethod.publicKeyMultibase
)
const encodedSignature = {
[keyType]: signature,
} as EncodedSignature
const encodedSignature = didSignatureToChain({
signature,
algorithm: signer.algorithm,
})
return api.tx.did.submitDidCall(signableCall, encodedSignature)
}

/**
* Compiles an enum-type key-value pair representation of a signature created with a full DID verification method. Required for creating full DID signed extrinsics.
*
* @param key Object describing data associated with a public key.
* @param key.publicKeyMultibase The multibase, multicodec representation of the signing public key.
* @param signature The signature generated with the full DID associated public key.
* @returns Data restructured to allow SCALE encoding by polkadot api.
*/
export function didSignatureToChain(
{ publicKeyMultibase }: VerificationMethod,
signature: Uint8Array
): EncodedSignature {
const { keyType } = multibaseKeyToDidKey(publicKeyMultibase)
if (!isValidVerificationMethodType(keyType)) {
throw new SDKErrors.DidError(
`encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead`
)
}

return { [keyType]: signature } as EncodedSignature
}
Loading
Loading