Skip to content

Commit

Permalink
Adds support for direct_post for verification.
Browse files Browse the repository at this point in the history
Previously we only supported direct_post.jwt. This adds support for
direct_post. The main difference is in OpenID4VPPresentationActivity.kt,
the new getDirectPostAuthorizationResponseBody function (in contrast
to the getDirectPostJwtAuthorizationResponseBody function). Most of the
other changes are just plumbing, or allowing the user to decide between
direct_post and direct_post.jwt.

Tested by:
- Manual testing of both direct_post and direct_post.jwt, using openid4vp://
  and both mdoc and VC.
- ./gradlew check
- ./gradlew connectedCheck

Signed-off-by: Kevin Deus <[email protected]>
  • Loading branch information
kdeus committed Nov 24, 2024
1 parent 6109ab6 commit c513920
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ private data class OpenID4VPBeginRequest(
val requestId: String,
val protocol: String,
val origin: String,
val scheme: String
val scheme: String,
val responseMode: String
)

@Serializable
Expand Down Expand Up @@ -140,6 +141,7 @@ data class Session(
val nonce: String,
val origin: String,
val encryptionKey: EcPrivateKey,
val responseMode: String? = null,
var responseUri: String? = null,
var deviceResponse: ByteArray? = null,
var sessionTranscript: ByteArray? = null
Expand Down Expand Up @@ -780,6 +782,7 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI=
}

// Create a new session
Logger.i(TAG, "Response mode is: ${request.responseMode}")
val session = Session(
id = Random.Default.nextBytes(16).toHex(),
nonce = Random.Default.nextBytes(16).toHex(),
Expand All @@ -788,7 +791,8 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI=
requestFormat = request.format,
requestDocType = request.docType,
requestId = request.requestId,
protocol = protocol
protocol = protocol,
responseMode = request.responseMode
)
runBlocking {
storage.insert(
Expand Down Expand Up @@ -884,7 +888,7 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI=
.claim("client_id", clientId)
.claim("response_uri", responseUri)
.claim("response_type", "vp_token")
.claim("response_mode", "direct_post.jwt")
.claim("response_mode", session.responseMode ?: "direct_post.jwt")
.claim("nonce", session.nonce)
.claim("state", session.id)
.claim("presentation_definition", presentationDefinition)
Expand Down Expand Up @@ -967,43 +971,12 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI=
kvPairs[parts[0]] = parts[1]
}

val response = kvPairs["response"]
val encryptedJWT = EncryptedJWT.parse(response)

val encPub = session.encryptionKey.publicKey.javaPublicKey as ECPublicKey
val encPriv = session.encryptionKey.javaPrivateKey as ECPrivateKey
val encKey = ECKey(
Curve.P_256,
encPub,
encPriv,
null,
null,
null,
null,
null,
null,
null,
null,
null
)

val decrypter = ECDHDecrypter(encKey)
encryptedJWT.decrypt(decrypter)

val vpToken = encryptedJWT.jwtClaimsSet.getClaim("vp_token") as String
if (session.requestFormat == "mdoc") {
session.deviceResponse = vpToken.fromBase64Url()
} else {
session.deviceResponse = vpToken.toByteArray()
if (session.responseMode == "direct_post.jwt") {
updateSessionFromDirectPostJwtResponse(kvPairs, session)
} else if (session.responseMode == "direct_post") {
updateSessionFromDirectPostResponse(kvPairs, session)
}

session.sessionTranscript = createSessionTranscriptOpenID4VP(
clientId = clientId,
responseUri = session.responseUri!!,
authorizationRequestNonce = encryptedJWT.header.agreementPartyVInfo.toString(),
mdocGeneratedNonce = encryptedJWT.header.agreementPartyUInfo.toString()
)

// Save `deviceResponse` and `sessionTranscript`, for later
runBlocking {
storage.update(
Expand All @@ -1013,7 +986,6 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI=
ByteString(session.toCbor())
)
}

} catch (e: Throwable) {
Logger.w(TAG, "$remoteHost: handleResponse: Error getting response", e)
resp.status = HttpServletResponse.SC_BAD_REQUEST
Expand All @@ -1030,6 +1002,67 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI=
resp.status = HttpServletResponse.SC_OK
}

private fun updateSessionFromDirectPostJwtResponse(
kvPairs: MutableMap<String, String>,
session: Session
) {
val response = kvPairs["response"]
val encryptedJWT = EncryptedJWT.parse(response)

val encPub = session.encryptionKey.publicKey.javaPublicKey as ECPublicKey
val encPriv = session.encryptionKey.javaPrivateKey as ECPrivateKey
val encKey = ECKey(
Curve.P_256,
encPub,
encPriv,
null,
null,
null,
null,
null,
null,
null,
null,
null
)

val decrypter = ECDHDecrypter(encKey)
encryptedJWT.decrypt(decrypter)

val vpToken = encryptedJWT.jwtClaimsSet.getClaim("vp_token") as String
if (session.requestFormat == "mdoc") {
session.deviceResponse = vpToken.fromBase64Url()
} else {
session.deviceResponse = vpToken.toByteArray()
}

session.sessionTranscript = createSessionTranscriptOpenID4VP(
clientId = clientId,
responseUri = session.responseUri!!,
authorizationRequestNonce = encryptedJWT.header.agreementPartyVInfo.toString(),
mdocGeneratedNonce = encryptedJWT.header.agreementPartyUInfo.toString()
)
}

private fun updateSessionFromDirectPostResponse(
kvPairs: MutableMap<String, String>,
session: Session
) {
val vpToken = kvPairs["vp_token"]!!
if (session.requestFormat == "mdoc") {
session.deviceResponse = vpToken.fromBase64Url()
} else {
session.deviceResponse = vpToken.toByteArray()
}

session.sessionTranscript = createSessionTranscriptOpenID4VP(
clientId = clientId,
responseUri = session.responseUri!!,
authorizationRequestNonce = "",
mdocGeneratedNonce = ""
)
}

private fun handleOpenID4VPGetData(
remoteHost: String,
req: HttpServletRequest,
Expand Down Expand Up @@ -1436,7 +1469,7 @@ private fun calcClientMetadata(session: Session, format: String): JSONObject {
val client_metadata = JSONObject()
client_metadata.put("authorization_encrypted_response_alg", "ECDH-ES")
client_metadata.put("authorization_encrypted_response_enc", "A128CBC-HS256")
client_metadata.put("response_mode", "direct_post.jwt")
client_metadata.put("response_mode", session.responseMode ?: "direct_post.jwt")

val vpFormats = when (format) {
"vc" -> {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/webapp/WEB-INF/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@
works out of the box for development without any configuration.
-->
<param-name>verifierBaseUrl</param-name>
<param-value></param-value>
<param-value>http://127.0.0.1:8080/server</param-value>
</init-param>

<init-param>
Expand Down
14 changes: 14 additions & 0 deletions server/src/main/webapp/verifier.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ <h1 class="text-body-emphasis">Request Digital Documents</h1>
</div>
</div>

Response mode
<div class="d-flex gap-4 flex-wrap">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle btn-lg overflow-visible" type="button"
data-bs-toggle="dropdown" aria-expanded="false" id="responseModeDropdown">
direct_post.jwt
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">direct_post.jwt</a></li>
<li><a class="dropdown-item" href="#">direct_post</a></li>
</ul>
</div>
</div>

</main>
<footer class="pt-5 my-5 text-body-secondary border-top">
This verifier is part of the <a href="https://github.com/openwallet-foundation-labs/identity-credential">OWF Identity Credential</a> project
Expand Down
54 changes: 27 additions & 27 deletions server/src/main/webapp/verifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ var preferredProtocol = selectedProtocol

var openid4vpUri = ''

var selectedResponseMode = 'direct_post.jwt'

async function onLoad() {
const protocolDropdown = document.getElementById('protocolDropdown')
protocolDropdown.addEventListener('hide.bs.dropdown', event => {
Expand All @@ -29,6 +31,12 @@ async function onLoad() {
scheme.hidden = selected !== 'openid4vp_custom';
}
})
const responseModeDropdown = document.getElementById('responseModeDropdown')
responseModeDropdown.addEventListener('hide.bs.dropdown', event => {
var target = event.clickEvent.target
responseModeDropdown.innerHTML = target.innerHTML
selectedResponseMode = target.innerHTML
})

// Ask server what document types / requests are available and use this to
// dynamically generate the UI..
Expand Down Expand Up @@ -145,38 +153,30 @@ function redirectClose() {

async function requestDocument(format, docType, requestId) {
console.log('requestDocument, format=' + format + ' docType=' + docType + ' requestId=' + requestId + ' protocol=' + selectedProtocol)
let scheme = ''
if (selectedProtocol === 'openid4vp_custom') {
if (document.getElementById("scheme-input").value === "") {
alert("You must specify a non-empty scheme when performing a custom OpenID4VP request.")
return
}
const response = await callServer(
'openid4vpBegin',
{
format: format,
docType: docType,
requestId: requestId,
protocol: selectedProtocol,
origin: location.origin,
scheme: document.getElementById("scheme-input").value
}
)
console.log("URI " + response.uri)
window.open(response.uri, '_blank').focus()
} else if (selectedProtocol.startsWith('openid4vp_')) {
const response = await callServer(
'openid4vpBegin',
{
format: format,
docType: docType,
requestId: requestId,
protocol: selectedProtocol,
origin: location.origin,
scheme: ""
}
)
console.log("URI " + response.uri)
window.open(response.uri, '_blank').focus()
scheme = document.getElementById("scheme-input").value
}

if (selectedProtocol.startsWith('openid4vp_')) {
const response = await callServer(
'openid4vpBegin',
{
format: format,
docType: docType,
requestId: requestId,
protocol: selectedProtocol,
origin: location.origin,
scheme: scheme,
responseMode: selectedResponseMode
}
)
console.log("URI " + response.uri)
window.open(response.uri, '_blank').focus()
} else if (selectedProtocol === "w3c_dc_preview") {
try {
const response = await callServer(
Expand Down
Loading

0 comments on commit c513920

Please sign in to comment.