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!: fetch attestation as part of verifyCredential #708

Merged
merged 15 commits into from
Mar 1, 2023

Conversation

rflechtner
Copy link
Contributor

@rflechtner rflechtner commented Feb 1, 2023

fixes https://github.com/KILTprotocol/ticket/issues/2397

also fixes https://github.com/KILTprotocol/ticket/issues/2305

This changes the function signature of verifyCredential and verifyPresentation so that all verification steps are covered, including querying the attestation. They now return the credential enriched with attester and revocation info. Assertions about both are NOT made, but these depend a lot on your application logic and are trivial to do with the data returned from these functions.

I like the idea of having a data model for a VerifiedCredential bc it allows introducing another function, called refreshRevocationStatus here, which queries the attestation again and throws not only failing if the attestation has been deleted or has been recreated with incoherent data, but also if it has been recreated by a different attester. This avoids potential exploits where a verifier who only checks the revoked flag once a credential has been verified is tricked by a claimer who simply self-attests their credential after the original attester had deleted the attestation.

If preferred, I could return just revocation status and attester though, which would then mean refreshRevocationStatus would take both the credential and the last status as input parameters.

Usage Example

const didsWithAccess = new Set()
try {
  const verifiedCredential = await verifyPresentation(presentation, {
    ctype,
    challenge,
  })
  if(verifiedCredential.revoke === false) {
    // access granted
    didsWithAccess.add(presentation.claim.owner)
  }
  // periodically recheck if credential is still valid
  setInterval(() => {
    recheckRevocationStatus(verifiedCredential)
      .then(c => if(c.revoked !== false) throw new Error())
      .catch(
        () => {
          // access revoked
          didsWithAccess.delete(presentation.claim.owner)
        }
      )
  }, 60_000)
} catch {
  // access denied
}

How to test:

Test coverage in unit tests and integration tests has been added.

Checklist:

  • I have verified that the code works
  • I have verified that the code is easy to understand
    • If not, I have left a well-balanced amount of inline comments
  • I have left the code in a better state
  • I have documented the changes (where applicable)

@rflechtner rflechtner mentioned this pull request Feb 1, 2023
5 tasks
Copy link
Member

@ntn-x2 ntn-x2 left a comment

Choose a reason for hiding this comment

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

So now verifying a delegation and the list of trusted attesters would be outside of any function calls, is that correct?

* @returns Information on the attester and revocation status of the on-chain attestation, as well as info on which trust policy led to acceptance of the credential.
*/
export async function verifyAttested(
credential: ICredential,
allowedAuthorities?: TrustPolicies,
allowRevoked = false
throwOnRevoked = true
Copy link
Member

Choose a reason for hiding this comment

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

Why doesn't this function also returns a VerifiedCredential? Plus, I agree that removing the throwOnRevoked flag is more future-proof and less prone to mistakes, as people might want to check other stuff as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it does not actually verify a credential. Just checks the attestation. Name suggestions for that function are welcome.

@rflechtner
Copy link
Contributor Author

So now verifying a delegation and the list of trusted attesters would be outside of any function calls, is that correct?

Correct! And given you agree that the revocation check can go to, so would be checking if revoked === false.

@ntn-x2
Copy link
Member

ntn-x2 commented Feb 2, 2023

+1 for this solution then. I like it. Not sure if the additional type would be more confusing for the users. Maybe @weichweich can provide his opinion on this a well. Maybe @arty-name also?

@rflechtner
Copy link
Contributor Author

Yes, would love to hear their opinions. Compare this one as well against having a separate function for checking the attestation (i.e. not changing verifyCredential at all - see for example #709, but could also be a regular function) and let me know which of the three you think is best.

@arty-name
Copy link
Collaborator

I like this much more than the proposal I have seen previously! The alternative of having a separate function does not appeal to me either.

@rflechtner
Copy link
Contributor Author

So to summarise, this seems to be the preferred solution rn, and we'd probably drop the throwOnRevoked parameter too. In that case I'd probably rename recheckRevocationStatus to refreshRevocationStatus (or updateRevocationStatus).
You'd get an object back which gives you revocation status and attester, and you'd compare their values against your expectations (e.g. you reject revoked credentials or not, or you only allow attesters from a self-curated list).

If the VerifiedCredential type is confusing, we can also introduce a CredentialStatus type instead, which would change the signature of refreshRevocationStatus too:

type CredentialStatus = {
  attester: DidUri
  revoked: boolean
}

async function refreshRevocationStatus(
   credential: ICredential,
   lastStatus: CredentialStatus
): Promise<CredentialStatus>

Copy link
Member

@Dudleyneedham Dudleyneedham left a comment

Choose a reason for hiding this comment

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

Mostly comments. The code looks good.

/**
* Verifies data structure & data integrity of a credential object.
* This combines all offline sanity checks that can be performed on an ICredential object.
* A credential is valid only iff it is well formed and there is an onchain attestation record referencing its root hash.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* A credential is valid only iff it is well formed and there is an onchain attestation record referencing its root hash.
* A credential is valid only if it is well formed and there is an on-chain attestation record referencing its root hash.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I’m not the only one thrown off by the usage of "iff" in the sense "if and only if" :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this even true? If I have a well structured credential with an on-chain attestation record it can still be invalid? E.g. if the delegation record is wrong or doesn't exist.

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're right, this isn't complete; the attestation also needs to match the credential, i.e. delegation and ctype ids need to be identical to what was committed to in the credential. BTW that's something we should have designed differently; the runtime should combine at least delegation & ctype info into the credential hash so there is no chance these data do not match if the hash is identical. Ideally also the attester and block number, just to make sure the same attestation is never re-created.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Anyway, we would do these checks as part of verifyCredential now.

Copy link
Member

Choose a reason for hiding this comment

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

This should be part of the discussion for credentials v2. We can build on the current limitations and do better.

): Promise<VerifiedCredential> {
const { revoked, attester } = await verifyAttested(
verifiedCredential,
throwOnRevoked
Copy link
Member

Choose a reason for hiding this comment

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

Do we not want this always to be the case? We decided that if there was an error, we'd always throw it.

Is there a reason why we might not?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The consensus was actually to get rid of this flag entirely, so that it never throws when the attestation is revoked. This was to control whether or not you consider a revoked attestation invalid for the sake of you application.

packages/core/src/credential/Credential.ts Show resolved Hide resolved
*/
isDelegator?: boolean
type VerifyOptions = {
ctype?: ICType
Copy link
Member

Choose a reason for hiding this comment

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

I find it strange that CType isn't required. Is there a specific reason why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see previous comment

@rflechtner rflechtner changed the base branch from rf-verification-helpers to develop February 20, 2023 14:50
@rflechtner rflechtner changed the title refactor: return from verifyCredential instead refactor!: return from verifyCredential instead Feb 20, 2023
@rflechtner
Copy link
Contributor Author

If the VerifiedCredential type is confusing, we can also introduce a CredentialStatus type instead, which would change the signature of refreshRevocationStatus too:

type CredentialStatus = {
  attester: DidUri
  revoked: boolean
}

async function refreshRevocationStatus(
   credential: ICredential,
   lastStatus: CredentialStatus
): Promise<CredentialStatus>

@arty-name @ntn-x2 @weichweich @Dudleyneedham
Can I ask again about people's preferences regarding the return type of verifyCredential & verifyPresentation ? I liked the VerifiedCredential type for having all relevant info in one place, plus it's a good way to communicate at which point you can trust the information in a credential (-> as soon as it's a VerifiedCredential). The CredentialStatus felt a bit like a duplication of the IAttestation object, so personally I'm not too fond of it, but I wouldn't want to return an IAttestation either, simply because the attester is called owner there, which I think is very confusing in this context. If you like the CredentialStatus better though, I'd go this route, would be interested to hear where you're coming from though.

@rflechtner rflechtner changed the title refactor!: return from verifyCredential instead refactor!: fetch attestation as part of verifyCredential Feb 21, 2023
@ntn-x2
Copy link
Member

ntn-x2 commented Feb 21, 2023

Just a point, that relying too much on Typescript could leave Javascript developers in the dark. But I like the name VerifiedCredential. It also does not feel right that a verify function returns a CredentialStatus. Different is, if the function is renamed, then it might make sense.

@rflechtner rflechtner force-pushed the rf-return-from-verify branch from cdd2782 to 8a1f011 Compare February 21, 2023 20:39
@rflechtner rflechtner marked this pull request as ready for review February 21, 2023 21:13
@rflechtner rflechtner self-assigned this Feb 21, 2023
@arty-name
Copy link
Collaborator

I think both options are fine, and I have a slight preference for the current VerifiedCredential

@rflechtner
Copy link
Contributor Author

Just a point, that relying too much on Typescript could leave Javascript developers in the dark.

I hope that can be resolved with good docstrings.

@weichweich
Copy link
Contributor

Just a point, that relying too much on Typescript could leave Javascript developers in the dark. But I like the name VerifiedCredential. It also does not feel right that a verify function returns a CredentialStatus. Different is, if the function is renamed, then it might make sense.

I agree with Antonio. In JS code i would just use

if (verifyCredential(...)) {
  ...
}

And that's always true.

@rflechtner
Copy link
Contributor Author

In JS code i would just use

if (verifyCredential(...)) {
  ...
}

And that's always true.

And thaaaaaat's why we banished you to the Rust dungeon! Please don't ever do that ever. You're calling an async function, so it returns a Promise which is always truthy, even if the function throws an exception, and even if the value it wraps is itself falsy.

So I don't think the average JS developer works on the basis of 'just call some function and assume it returns a boolean'. You read the docstring at the very least, how else would you know what it does, when it throws, and what it returns? The same approach would fail miserably with vc.js's verify function signature (https://github.com/digitalbazaar/vc#verifying-a-verifiable-presentation), where you get the final result from the verified property of the (awaited) return value. I think Antonio's point was rather to not assume people have access to interfaces and types, which I don't think is a big issue here though. I think we can describe the required structure by saying 'this structure, but with two fields added' or simply by saying 'the return value of this function', and we should have JS developers covered?

@rflechtner rflechtner requested a review from ntn-x2 February 27, 2023 10:54
Copy link
Member

@ntn-x2 ntn-x2 left a comment

Choose a reason for hiding this comment

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

Looks very good to me! I could not find any mention in the JSDocs about the consumer actually having to check the trusted attester list and the delegation information.

packages/core/src/credential/Credential.ts Show resolved Hide resolved
@rflechtner
Copy link
Contributor Author

Looks very good to me! I could not find any mention in the JSDocs about the consumer actually having to check the trusted attester list and the delegation information.

Again, you don't have to match the attester to some list of trusted attesters in your application; your application could also limit itself to simply displaying the attester DID or web3name to the user, like most of our apps do. That's why this solution is better than the previous candidate, which I think was too opinionated. I could add something like "this data can be used in logic deciding whether to accept or reject a credential submission", but a naive user would probably need more context to understand what that means, which they can only get from the docs - the docstring wouldn't be the right place to explain conceptual fundamentals, would it?

Copy link
Member

@ntn-x2 ntn-x2 left a comment

Choose a reason for hiding this comment

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

I reviewed a VERY old version of this PR, somehow. The new changes look way better, sorry about that. Great work!

@rflechtner rflechtner merged commit f8b54cd into develop Mar 1, 2023
@rflechtner rflechtner deleted the rf-return-from-verify branch March 1, 2023 11:52
@rflechtner rflechtner mentioned this pull request Jun 8, 2023
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.

5 participants