-
Notifications
You must be signed in to change notification settings - Fork 202
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(anoncreds): issue revocable credentials #1427
feat(anoncreds): issue revocable credentials #1427
Conversation
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
return new Promise<string>((resolve, reject) => { | ||
const shasum = createHash(algorithm) | ||
try { | ||
const s = fs.createReadStream(filePath) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression
app.get('/:tailsFileId', async (req, res) => { | ||
logger.debug(`requested file`) | ||
|
||
const tailsFileId = req.params.tailsFileId | ||
if (!tailsFileId) { | ||
res.status(409).end() | ||
return | ||
} | ||
|
||
const fileName = tailsIndex[tailsFileId] | ||
|
||
if (!fileName) { | ||
logger.debug(`no entry found for tailsFileId: ${tailsFileId}`) | ||
res.status(404).end() | ||
return | ||
} | ||
|
||
const path = `${baseFilePath}/${fileName}` | ||
try { | ||
logger.debug(`reading file: ${path}`) | ||
|
||
if (!fs.existsSync(path)) { | ||
logger.debug(`file not found: ${path}`) | ||
res.status(404).end() | ||
return | ||
} | ||
|
||
const file = fs.createReadStream(path) | ||
res.setHeader('Content-Disposition', `attachment: filename="${fileName}"`) | ||
file.pipe(res) | ||
} catch (error) { | ||
logger.debug(`error reading file: ${path}`) | ||
res.status(500).end() | ||
} | ||
}) |
Check failure
Code scanning / CodeQL
Missing rate limiting
app.put('/:tailsFileId', multer({ storage: fileStorage }).single('file'), async (req, res) => { | ||
logger.info(`tails file upload: ${req.params.tailsFileId}`) | ||
|
||
const file = req.file | ||
|
||
if (!file) { | ||
logger.info(`No file found: ${JSON.stringify(req.headers)}`) | ||
return res.status(400).send('No files were uploaded.') | ||
} | ||
|
||
const tailsFileId = req.params.tailsFileId | ||
if (!tailsFileId) { | ||
// Clean up temporary file | ||
fs.rmSync(file.path) | ||
return res.status(409).send('Missing tailsFileId') | ||
} | ||
|
||
const item = tailsIndex[tailsFileId] | ||
|
||
if (item) { | ||
logger.debug(`there is already an entry for: ${tailsFileId}`) | ||
res.status(409).end() | ||
return | ||
} | ||
|
||
const hash = await fileHash(file.path) | ||
const destinationPath = `${baseFilePath}/${hash}` | ||
|
||
if (fs.existsSync(destinationPath)) { | ||
logger.warn('tails file already exists') | ||
} else { | ||
fs.copyFileSync(file.path, destinationPath) | ||
fs.rmSync(file.path) | ||
} | ||
|
||
// Store filename in index | ||
tailsIndex[tailsFileId] = hash | ||
fs.writeFileSync(indexFilePath, JSON.stringify(tailsIndex)) | ||
|
||
res.status(200).end() | ||
}) |
Check failure
Code scanning / CodeQL
Missing rate limiting
Codecov Report
@@ Coverage Diff @@
## main #1427 +/- ##
===========================================
+ Coverage 62.44% 84.83% +22.39%
===========================================
Files 775 972 +197
Lines 17906 23619 +5713
Branches 3081 4168 +1087
===========================================
+ Hits 11181 20038 +8857
+ Misses 6185 3375 -2810
+ Partials 540 206 -334
|
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
This is great @genaris, I need some more time to look at this, but really looking forward to merge this 🚀 |
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Discussed in AFJ WG call. Changes have been reviewed multiple times. Some things left to improve / change, but good to get this merged. Will be available as an experimental feature. The breaking changes are to the AnonCreds registry and services, but those are marked as experimental for implementing your own (besides the ones provided by the framework), so this can be released as 0.4.1. @genaris to update with latest main |
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
@TimoGlastra I did a few updates based on the last feedback from last month. Would be good to have a brief discussion of hyperledger/anoncreds-rs#227 with other AnonCreds maintainers just to be sure that our reasoning is correct. |
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
@genaris — wondering about the approach used here. We’ve found in with ACA-Py that it is very difficult for the controller (application code) to manage RevRegs, and have put almost all the work on the Framework. Are you doing that here? It will also make it easier to use other methods (e.g., StatusList2021) since the interface stays much the same. Notably:
This limits the tracking to be done by controller to a minimum, and relieves it of ever dealing with RevRegs. From experience, we’ve added to two features to deal with unexpected conditions:
|
Thank you for the comments @swcurran , this is certainly very valuable! At the beginning we started a similar approach, but as it would require more work (especially in regards to the synchronization and possible race conditions with multiple agent instances) for the moment we kept it more simple at AFJ layer and left the logic for an upper layer like AFJ REST package. Personally, I'd like to create an example and tutorial showing how to use this feature. For sure I'll integrate or at least mention the concepts you've mentioned here. Once we find out how to better handle multi-instance and horizontal scaling use cases I think it will be good to offer right into AFJ's AnonCreds module an automated way like ACA-Py, especially to make the life simpler to framework users. |
adb811e
to
8f5d6bf
Compare
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
@genaris is this ready to be merged? Any things you're waiting on? |
Yes, I think so! |
Thanks @genaris ❤️ |
This PR includes some initial work done for supporting the issuance of revocable AnonCreds credentials in AFJ.
Brief summary of the changes:
AnonCredsRegistry
andAnonCredsIssuerService
interfaces to add registration of revocation registry definitions and revocation status listsAnonCredsApi
, now allows to register these objects and update revocation status listsAnonCredsCredentialFormatService
has been updated to handle accept revocable credential requests, retrieving a Revocation Registry Definition and assigning an index to the credential that is going to be issued. Both the revocation registry definition Id and index are specified by agent controller when creating the credential offer.indy-anoncreds
and a potentially newanoncreds
revocation format. This is very basic right now and needs some more work to support future formats and get information directly from the credential record to be more aligned with other modules API.TailsFileService
, which is an interface for downloading and uploading tails files. The default implementation works as right now (only allows to download files), while a custom one can be used to upload files to any chosen tails server. Insamples
directory there is an example of tails server and service interface (not sure if they belong there but for the moment didn't find a better place). For the tests there is a dummy implementation that allows us to run them without the need of having an actual HTTP serverFlow:
For the moment, as seen in the few tests added in anoncreds-rs package, all VDR objects are managed exclusively from AnonCreds API and created/registered separately in order to have a simpler state management. For instance, if we want to create a revocable credential definition, we'd need to call:
The only step that does two actions atomically is
registerRevocationRegistryDefinition
, as it will attempt to successfully uploading the tails file before registering the object in the VDR (this is mandatory because we may not know upfront what's the publicly available tails location).To revoke a credential (or a number of credentials),
AnonCredsApi.updateRevocationStatusList()
must be called using the revocation registry definition id and credential indexes as an input. If we want to notify the holder/s about this revocation, we must use Credentials API afterwards.Some notes/missing pieces
InMemoryAnonCredsRegistry
. There is not yet any implementation for Indy VDR or Indy SDKrevokeCredentials
inCredentialsApi
that does both the revocation state update and send notification. However, this integration would need us to deal with the different credential formats and, after all, I'm not sure if it will be really useful in real world scenarios, because I think that usually an issuer would prefer to revoke multiple credentials at once (to avoid creating lots of ledger updates)