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

feat: Refactor AppModule and Add Credential Management with Revocation Support #64

Open
wants to merge 124 commits into
base: main
Choose a base branch
from

Conversation

lotharking
Copy link
Contributor

@lotharking lotharking commented Dec 16, 2024

Summary

Flow:

  1. Create a Credential

    • The user can request to create a credential.
    • The library validates whether the credential can be created and ensures there isn't already one associated with the same hash.
      • If a credential with the same hash exists, the credential will be revoked and associated with the new connection (from where the request is sent). A new credential request will then be initiated, ensuring the proper threadId is sent as part of the process
      • If valid, the credential creation request is sent.
  2. Rejection

    • Associates the thread and marks the credential as revoked (revoked only in terms of database nomenclature).
  3. Acceptance

    • Associates the thread with the credential.
  4. Revocation

    • When a credential is revoked, it is marked as revoked in the database.

Additional Constraints:

  • In all cases, a new record is created, ensuring indices do not overlap and atomicity is guaranteed during queries.
  • Records are expected to be assigned from 0 to the maximum value minus 1 (-1).
    • Validation has confirmed that assignments from 1 to the maximum value fail.
  • Two revocation records will always remain available.

Changes

  • A refactoring was implemented in the AppModule of the nestjs-client module. This was necessary because the original main module was not optimized for the recurrent use of variables, requiring multiple instantiations of the same variables. Modifying the AppModule and the configuration approach avoided redundancy when the module is instantiated. Compatibility with individual configurations was maintained.
  • Support for credential management and revocation was implemented when configuring the nestjs-client module. The methods are controlled through the library, allowing for handling rejections and revocations as well.
  • A demo was created to showcase how to use the demo-dts. As observed, the configuration required for complete credential management is minimal.

Related Issues

Testing

Checklist

  • I have commented on my code, especially in areas that are hard to understand.
  • I have added only changes relevant to the issue in question.
  • I have added tests to confirm that my fix is effective or that my feature works as intended.
  • I have modified existing tests as needed to accommodate my changes.
  • I have flagged specific areas for further review, if necessary.
  • I have updated the documentation where necessary.

@lotharking lotharking requested a review from genaris January 16, 2025 06:34
Comment on lines 80 to 81
await this.saveCredential(credential.id, supportRevocation)
await this.saveCredential(credential.id, supportRevocation)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as before: these methods are saving credential types

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix: name credentialType

* @param {object} [options] - Additional options for credential issuance.
*
* ### Options
* - `refId` (optional, `string`): A unique identifier for the credential. If provided:
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a great explanation! But it makes the reader think that the only requirement for credential revocation at issuance is to have the same refId , while currently we also need to set autoRevocationEnabled to do so.

If autoRevocationEnabled is false (default), multiple credentials with the same refId will be created. This is not necessarily wrong, but what happens if afterwards I want to issue a credential with the same refId and turn autoRevocationEnabled on? Will it revoke ALL previous credentials with the same refId?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

docs: update

* - Encrypted in the database for security.
* - `credentialDefinitionId` (optional, `string`): Specifies the ID of the credential definition to use.
* - If not provided, the first available credential definition is used.
* - `autoRevocationEnabled` (optional, `boolean`): Whether automatic revocation is enabled (default false)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be more self-explanatory. Maybe something like: revokeIfAlreadyIssued

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix: add revokeIfAlreadyIssued

threadId?: string

@Column({ type: 'varchar', nullable: true })
hashIdentifier?: string
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this field should be renamed. refIdHash?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix: name refIdHash

@lotharking lotharking requested a review from genaris January 16, 2025 17:45
}
const { id: credentialDefinitionId, revocationSupported } = credentialType

const cred = await this.credentialRepository.findOne({
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that there is a chance that the user issued multiple credentials with the same refId (i.e. they didn't set revokeIfAlreadyIssued) and then they attempt to issue another credential with the same refId but using revokeIfAlreadyIssued = true, only the first credential found here will be revoked.

I think it would be safer to find all non-revoked issued credentials that match the refId and revoke/save them accordingly.

}

// private methods
private async saveCredentialType(
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand the meaning of this method: why does it save a credential type in the credential repository?

Why don't you simply name it createRevocationRegistry and call it only when appropriate (i.e. supportRevocation is true when creating credential type or the current revocation registry is full)?

return revocationRegistry
}

private refIdHash(refId: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

The method itself seems to be a simple hash operation. What about making it general and simply call it hash(value: string)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix: change name refIdHash by hash

* @param threadId - The thread ID to link with the credential.
* @throws Error if no credential is found with the specified connection ID.
*/
async accept(connectionId: string, threadId: string): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Now I understand why you called this service an events service in the beginning: accept and reject are user actions, so they are called by event handlers coming from the agent. Since they are not actions performed by us (such as credential issuance and revocation), they can be called `processAcceptance/processRejection' or 'handleAcceptance/handleRejection', showing that they are supposed to act as a result from an event coming from the DIDComm layer.

* @throws Error if no credential is found with the specified connection ID.
*/
async accept(connectionId: string, threadId: string): Promise<void> {
const cred = await this.credentialRepository.findOne({
Copy link
Contributor

Choose a reason for hiding this comment

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

A problem I see here is that you are using connectionId to identify the credential the event is related to. This is not correct, since you can issue multiple credentials to a single DIDComm connection.

The proper way you should identify a credential is by its threadId. Therefore, these accept and reject methods should only receive the threadId.

You can find the threadId in the response of the Message endpoint (the id correspond to the threadId).

if (!cred) throw new Error(`Credencial with connectionId ${connectionId} not found.`)

cred.threadId = threadId
cred.revoked = true
Copy link
Contributor

Choose a reason for hiding this comment

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

A rejection by the user does not mean that a credential has been revoked. Maybe you can create a status field for the credential entity that covers the entire life cycle of a credential: probably offered, accepted, rejected and revoked may be enough.

maximumCredentialNumber?: number
} = {},
) {
const { name = 'Chatbot', version = '1.0', supportRevocation, maximumCredentialNumber } = options
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it is correct to leave name and version as optional fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix: remove name and version from optionals fields

revocationRegistryIndex: undefined,
}
}
const invalidRegistries = await transaction.find(CredentialEntity, {
Copy link
Contributor

Choose a reason for hiding this comment

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

This logic is quite obscure to me. I don't think it will work.

I guess that somewhere (maybe another table/entity) you should keep track of the current revocation registry and index for each credential definition, and use that as the source of truth to assign the revocation registry/index of each issued credential, and tell if it is needed to create a new revocation registry or not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support revocable credentials
2 participants