Skip to content

Commit

Permalink
refactor!: cryptosuite-style signers (#801)
Browse files Browse the repository at this point in the history
  • Loading branch information
rflechtner authored Oct 31, 2023
2 parents 801211d + 29d1534 commit 88fb8c0
Show file tree
Hide file tree
Showing 33 changed files with 1,179 additions and 622 deletions.
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
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

0 comments on commit 88fb8c0

Please sign in to comment.