-
Notifications
You must be signed in to change notification settings - Fork 1
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
Presentation with key binding #1
Merged
+470
−112
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import moment from 'moment' | ||
import { NextResponse } from 'next/server' | ||
|
||
import transmute from '@transmute/verifiable-credentials' | ||
|
||
export type PostChallengeTokenParams = { | ||
token: string | ||
} | ||
|
||
type VerifiedSpiceChallengeToken = { | ||
protectedHeader: { | ||
kid: string | ||
alg: string | ||
} | ||
claimset: { | ||
iss: string | ||
iat: number | ||
exp: number | ||
aud: string | ||
} | ||
} | ||
|
||
export async function POST(request: Request, {params}: { params: PostChallengeTokenParams }) { | ||
const challenge = params.token; | ||
const secretKeyJwk = JSON.parse(process.env.PRIVATE_KEY_JWK as string) | ||
const {d, ...publicKeyJwk} = secretKeyJwk | ||
|
||
const isChallengeUnixTimestamp = !Number.isNaN(parseInt(challenge, 10)) | ||
let audienceForChallenge = 'https://dune.did.ai' | ||
let nonceForChallenge = challenge | ||
if (isChallengeUnixTimestamp){ | ||
// check time here | ||
const now = moment() | ||
const nonceTime = moment.unix(parseInt(challenge, 10)) | ||
// const nonceAge = nonceTime.fromNow() | ||
// console.log('Key binding token nonce age: ', nonceAge) | ||
if (now.isAfter(nonceTime.add(5, 'minutes'))){ | ||
throw new Error('nonce for key binding token is too stale to accept') | ||
} | ||
} else { | ||
try { | ||
const verifiedChallengeToken = await transmute.vc.sd.verifier<VerifiedSpiceChallengeToken>({ | ||
resolver: { | ||
resolve: async (kid: string) => { | ||
if (kid === `did:web:dune.did.ai#${publicKeyJwk.kid}`){ | ||
return publicKeyJwk | ||
} | ||
throw new Error('Unsupported kid: ' + kid) | ||
} | ||
} | ||
}).verify({ | ||
token: challenge | ||
}) | ||
const {iss, iat, exp, aud} = verifiedChallengeToken.claimset | ||
if (iss !== 'did:web:dune.did.ai') { | ||
throw new Error('Unknown challenge token issuer.') | ||
} | ||
const now = moment(); | ||
if (now.isBefore(moment.unix(iat))) { | ||
throw new Error('Challenge token cannot be issued in the future.') | ||
} | ||
if (now.isAfter(moment.unix(exp))) { | ||
throw new Error('Challenge token cannot be expired in the past.') | ||
} | ||
if (aud !== 'https://dune.did.ai') { | ||
throw new Error('Challenge token must be issued for https://dune.did.ai') | ||
} | ||
audienceForChallenge = aud; | ||
} catch(e){ | ||
console.error(e) | ||
return NextResponse.json({type: 'Verification Failed', detail: 'Challenge token was not signed for this audience' }, { | ||
status: 500, | ||
}) | ||
} | ||
} | ||
|
||
|
||
try { | ||
const token = await request.json(); | ||
await transmute.vc.sd.verifier({ | ||
resolver: { | ||
resolve: async (kid: string) => { | ||
if (kid === `did:web:dune.did.ai#${publicKeyJwk.kid}`){ | ||
return publicKeyJwk | ||
} | ||
throw new Error('Unsupported kid: ' + kid) | ||
} | ||
} | ||
}).verify({ | ||
audience: audienceForChallenge, | ||
nonce: nonceForChallenge, | ||
token | ||
}) | ||
return NextResponse.json({message: "Challenge accepted"}) | ||
} catch(e){ | ||
console.error(e) | ||
return NextResponse.json({type: 'Verification Failed', detail: 'Verification Failed' }, { | ||
status: 500, | ||
}) | ||
} | ||
} | ||
|
||
// forces the route handler to be dynamic | ||
export const dynamic = "force-dynamic"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import moment from 'moment' | ||
import { NextResponse } from 'next/server' | ||
|
||
import transmute from '@transmute/verifiable-credentials' | ||
|
||
export async function GET(request: Request) { | ||
const secretKeyJwk = JSON.parse(process.env.PRIVATE_KEY_JWK as string) | ||
const iat = moment().subtract(1, 'second').unix() | ||
const exp = moment().add(5, 'minutes').unix() | ||
const claimset = ` | ||
iss: "did:web:dune.did.ai" | ||
aud: "https://dune.did.ai" | ||
iat: ${iat} | ||
exp: ${exp} | ||
`.trim() | ||
const token = await transmute.vc.sd.issuer({ | ||
kid: `did:web:dune.did.ai#${secretKeyJwk.kid}`, | ||
secretKeyJwk, | ||
}).issue({ | ||
claimset | ||
}) | ||
|
||
const newHeaders = new Headers(request.headers) | ||
newHeaders.set('Content-Type', 'application/sd-jwt') | ||
return new NextResponse(Buffer.from(token), { | ||
headers: newHeaders, | ||
}) | ||
} | ||
|
||
// forces the route handler to be dynamic | ||
export const dynamic = "force-dynamic"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a challenge is supposed to change during time, while a .well-known probably would be provided as a static content (in my relative assumption).
I'm wondering if a wellknown endpoint is something good for a dynamic value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree... I tried to keep the interfaces similar to https://www.rfc-editor.org/rfc/rfc8555.html
But its not doing the same thing (its inverted, the prover posts to the challenge endpoint, instead of provisioning the challenge endpoint).
The idea was that serving a challenge token is very similar to serving a "status list credential".
Both contain signed claims that can have
nbf
andexp
.As a verifier, my policy might be to update my challenge once a day, or once a minute.
This is similar to the scenario in OIDC, where i might rotate parts of my open id configuration:
I know these are not necessarily dynamic, or frequently changing, but it would be a security issue if they were immutable, since the issuer would not be able to advertise that they removed support for a now broken, but previously supported configuration.
I'm sure changes to configuration resources would break things, but I imagine it is allowed.
I do think it might be a better pattern to discover the "challenge" and "presentation" endpoints from the issuer metadata, assuming they are part of interacting with the issuer.
In a scenario where presentations are encrypted to the issuer, the issuer's keys also need to be discovered... the the challenge token could contain everything needs to reply, or the presenter could dereference several URLs and assemble what is needed to submit a presentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was some discussion in the last WICG call about a "minimal version of OIDC4VP" that just delivered nonces,
my goal was to define an http interface that had the minimally required properties, not one that was necessarily compatible with OIDC... although it both can be accomplished at the same time, that would be the best case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering about a nonce/challenge endpoint I wrote the following draft, trying to put all the things discussed with other authors and looking for something reusable in context even different from a specific protocol
https://peppelinux.github.io/draft-demarco-nonce-endpoint/draft-demarco-nonce-endpoint.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you wrote is essentially what I want, in your example the token is encrypted to the verifier / nonce issuer, in mine it is just signed as an SD-JWT (for no reason, since no SD features are used).
Our use case for the nonce is just to support the proof of possession / key binding token.
We'd prefer to not need all of OIDC4VP to submit presentations with key binding, and we are interested in delivering the nonce over channels other than HTTPS, perhaps even RF / QR channels.
I think some informative round trip examples of the nonce use might be helpful, especially over http.
You would not need to add text, just a section with introductory context and then a reference to:
(and gather a few other specs, that require opaque nonces).