Skip to content

Commit

Permalink
feat: SD-JWT credential proof type, with support for selective disclo…
Browse files Browse the repository at this point in the history
…sure (#314)
  • Loading branch information
severinstampler authored Jun 7, 2023
2 parents 32da174 + b3eab47 commit bfc1c44
Show file tree
Hide file tree
Showing 35 changed files with 344 additions and 117 deletions.
4 changes: 3 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ repositories {
maven("https://jitpack.io")
maven("https://maven.walt.id/repository/waltid/")
maven("https://maven.walt.id/repository/waltid-ssi-kit/")
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
mavenLocal()
}

Expand All @@ -37,6 +38,7 @@ dependencies {
implementation("com.microsoft.azure:azure-client-authentication:1.7.14")
implementation("com.nimbusds:nimbus-jose-jwt:9.30.2")
implementation("com.nimbusds:oauth2-oidc-sdk:10.7")
implementation("id.walt:waltid-sd-jwt-jvm:1.2306071206.0")

implementation("org.bouncycastle:bcprov-jdk15to18:1.72")
implementation("org.bouncycastle:bcpkix-jdk15to18:1.72")
Expand All @@ -61,7 +63,7 @@ dependencies {
implementation("org.xerial:sqlite-jdbc:3.40.1.0")
implementation("com.zaxxer:HikariCP:5.0.1")

// CLI
// CLI-SNAPSHOT
implementation("com.github.ajalt.clikt:clikt-jvm:3.5.2")
implementation("com.github.ajalt.clikt:clikt:3.5.0")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class SignaturePolicy : SimpleVerificationPolicy() {
private val jwtCredentialService = JwtCredentialService.getService()

override fun doVerify(vc: VerifiableCredential) = runCatching {
log.debug { "is jwt: ${vc.jwt != null}" }
log.debug { "is jwt: ${vc.sdJwt != null}" }
vc.verifyByFormatType(
{ jwtCredentialService.verify(it) },
{ jsonLdCredentialService.verify(it) }
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/id/walt/cli/OidcCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.nimbusds.oauth2.sdk.util.URLUtils
import com.nimbusds.openid.connect.sdk.Nonce
import id.walt.common.KlaxonWithConverters
import id.walt.common.prettyPrint
import id.walt.credentials.w3c.PresentableCredential
import id.walt.credentials.w3c.toVerifiablePresentation
import id.walt.custodian.Custodian
import id.walt.model.dif.InputDescriptor
Expand Down Expand Up @@ -446,7 +447,7 @@ class OidcVerificationRespondCommand :
val req = OIDC4VPService.parseOIDC4VPRequestUri(URI.create(authUrl))
val nonce = req.getCustomParameter("nonce")?.firstOrNull()
val vp = Custodian.getService().createPresentation(
vcs = credentialIds.map { Custodian.getService().getCredential(it)!!.encode() },
vcs = credentialIds.map { Custodian.getService().getCredential(it)?.let { PresentableCredential(it) } ?: throw Exception("Credential with given ID $it not found") },
holderDid = did,
challenge = nonce,
).toVerifiablePresentation()
Expand Down
90 changes: 83 additions & 7 deletions src/main/kotlin/id/walt/cli/VcCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,23 @@ import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.file
import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.path
import id.walt.auditor.Auditor
import id.walt.auditor.PolicyRegistry
import id.walt.auditor.dynamic.DynamicPolicyArg
import id.walt.auditor.dynamic.PolicyEngineType
import id.walt.common.prettyPrint
import id.walt.common.resolveContent
import id.walt.credentials.w3c.PresentableCredential
import id.walt.credentials.w3c.VerifiableCredential
import id.walt.credentials.w3c.VerifiablePresentation
import id.walt.credentials.w3c.toVerifiableCredential
import id.walt.crypto.LdSignatureType
import id.walt.custodian.Custodian
import id.walt.model.credential.status.CredentialStatus
import id.walt.sdjwt.DecoyMode
import id.walt.sdjwt.SDMap
import id.walt.signatory.Ecosystem
import id.walt.signatory.ProofConfig
import id.walt.signatory.ProofType
Expand Down Expand Up @@ -81,12 +87,19 @@ class VcIssueCommand : CliktCommand(
"--status-type",
help = "Specify the credentialStatus type"
).enum<CredentialStatus.Types>()
val decoyMode: DecoyMode by option("--decoy-mode", help = "SD-JWT Decoy mode: random|fixed|none, default: none").enum<DecoyMode>().default(DecoyMode.NONE)
val numDecoys: Int by option("--num-decoys", help = "Number of SD-JWT decoy digests to add (fixed mode), or max num of decoy digests (random mode)").int().default(0)
val selectiveDisclosurePaths: List<String>? by option("--sd", "--selective-disclosure", help = "Path to selectively disclosable fields (if supported by chosen proof type), in a simplified JsonPath format, can be specified multiple times, e.g.: \"credentialSubject.familyName\".").multiple()

private val signatory = Signatory.getService()

override fun run() {
val selectiveDisclosure = selectiveDisclosurePaths?.let { SDMap.generateSDMap(it, decoyMode, numDecoys) }
echo("Issuing a verifiable credential (using template ${template})...")

selectiveDisclosure?.also {
echo("with selective disclosure:")
echo(it.prettyPrint(2))
}
// Loading VC template
log.debug { "Loading credential template: $template" }

Expand All @@ -102,7 +115,8 @@ class VcIssueCommand : CliktCommand(
ecosystem = ecosystem,
statusType = statusType,
//creator = if (ecosystem == Ecosystem.GAIAX) null else issuerDid
creator = issuerDid
creator = issuerDid,
selectiveDisclosure = selectiveDisclosure
), when (interactive) {
true -> CLIDataProvider
else -> null
Expand Down Expand Up @@ -151,28 +165,54 @@ class VcImportCommand : CliktCommand(
class PresentVcCommand : CliktCommand(
name = "present", help = """Present VC
"""
""",
epilog = """Note about selective disclosure:
Selective disclosure flags have NO EFFECT, if the proof type of the presented credential doesn't support selective disclosure!
Which fields can be selectively disclosed, depends on the credential proof type and credential issuer.
To select SD-enabled fields for disclosure, refer to the help text of the --sd, --sd-all-for and --sd-all flags.
""".trimMargin()
) {
val src: List<Path> by argument().path(mustExist = true).multiple()
val holderDid: String by option("-i", "--holder-did", help = "DID of the holder (owner of the VC)").required()
val verifierDid: String? by option("-v", "--verifier-did", help = "DID of the verifier (recipient of the VP)")
val domain: String? by option("-d", "--domain", help = "Domain name to be used in the LD proof")
val challenge: String? by option("-c", "--challenge", help = "Challenge to be used in the LD proof")
val selectiveDisclosure: Map<Int, SDMap>? by option("--sd", "--selective-disclosure", help = "Path to selectively disclosed fields, in a simplified JsonPath format. Can be specified multiple times. By default NONE of the sd fields are disclosed, for multiple credentials, the path can be prefixed with the index of the presented credential, e.g. \"credentialSubject.familyName\", \"0.credentialSubject.familyName\", \"1.credentialSubject.dateOfBirth\".")
.transformAll { paths ->
paths.map { path ->
val hasIdxInPath = path.substringBefore(".").toIntOrNull() != null
val idx = path.substringBefore('.').toIntOrNull() ?: 0
Pair(idx, if(hasIdxInPath) {
path.substringAfter(".")
} else {
path
})
}.groupBy { pair -> pair.first }
.mapValues { entry -> SDMap.generateSDMap(entry.value.map { item -> item.second }) }
}
val discloseAllFor: Set<Int>? by option("--sd-all-for", help = "Selects all selective disclosures for the credential at the specified index to be disclosed. Overrides --sd flags!").int()
.transformAll { it.toSet() }
val discloseAllOfAll: Boolean by option("--sd-all", help = "Selects all selective disclosures for all presented credentials to be disclosed. Overrides --sd and --sd-all-for flags!").flag()

override fun run() {
echo("Creating a verifiable presentation for DID \"$holderDid\"...")
echo("Using ${src.size} ${if (src.size > 1) "VCs" else "VC"}:")

val vcSources: Map<Path, String> = src.associateWith { it.readText() }
val vcSources: Map<Path, VerifiableCredential> = src.associateWith { it.readText().toVerifiableCredential() }

src.forEachIndexed { index, vcPath ->
echo("- ${index + 1}. $vcPath (${vcSources[vcPath]!!.toVerifiableCredential().type.last()})")
echo("- ${index + 1}. $vcPath (${vcSources[vcPath]!!.type.last()})")
selectiveDisclosure?.get(index)?.let {
echo(" with selective disclosure:")
echo(it.prettyPrint(4))
}
}

val vcStrList = vcSources.values.toList()
val presentableList = vcSources.values.mapIndexed { idx, cred -> PresentableCredential(cred, selectiveDisclosure?.get(idx), discloseAllOfAll || (discloseAllFor?.contains(idx) == true)) }
.toList()

// Creating the Verifiable Presentation
val vp = Custodian.getService().createPresentation(vcStrList, holderDid, verifierDid, domain, challenge, null)
val vp = Custodian.getService().createPresentation(presentableList, holderDid, verifierDid, domain, challenge, null)

log.debug { "Presentation created:\n$vp" }

Expand Down Expand Up @@ -250,6 +290,42 @@ class ListVcCommand : CliktCommand(
Custodian.getService().listCredentials().forEachIndexed { index, vc -> echo("- ${index + 1}: $vc") }
}
}

class ParseVcCommand : CliktCommand(
name = "parse", help = """Parse VC from JWT or SD-JWT representation and display JSON body
"""
) {
val vc by option("-c", help = "Credential content or file path").required()
val recursive by option("-r", help = "Recursively parse credentials in presentation").flag()
override fun run() {
echo("\nParsing verifiable credential...")

echo("\nResults:\n")

val parsedVc = resolveContent(vc).toVerifiableCredential()
echo(if(parsedVc is VerifiablePresentation) "- Presentation:" else "- Credential:")
echo()
println(parsedVc.toJson().prettyPrint())
echo()
parsedVc.selectiveDisclosure?.let {
echo(" with selective disclosure:")
echo(it.prettyPrint(4))
}
if(parsedVc is VerifiablePresentation && recursive) {
parsedVc.verifiableCredential?.forEachIndexed { idx, cred ->
echo("---")
echo("- Credential ${idx + 1}")
echo()
println(cred.toJson().prettyPrint())
cred.selectiveDisclosure?.let {
echo(" with selective disclosure:")
echo(it.prettyPrint(4))
}
}
}
}
}
//endregion

//region -Policy Commands-
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/id/walt/cli/WaltCLI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ object WaltCLI {
VcRevocationCheckCommand(),
VcRevocationRevokeCommand(),
),
ParseVcCommand()
),
EssifCommand().subcommands(
EssifOnboardingCommand(),
Expand Down
42 changes: 24 additions & 18 deletions src/main/kotlin/id/walt/credentials/w3c/VerifiableCredential.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package id.walt.credentials.w3c

import com.nimbusds.jwt.SignedJWT
import id.walt.credentials.w3c.builder.CredentialFactory
import id.walt.sdjwt.SDJwt
import id.walt.sdjwt.SDMap
import kotlinx.serialization.json.*

open class VerifiableCredential internal constructor(
Expand All @@ -14,10 +15,11 @@ open class VerifiableCredential internal constructor(
var validFrom: String? = null,
var expirationDate: String? = null,
var proof: W3CProof? = null,
var jwt: String? = null,
var credentialSchema: W3CCredentialSchema? = null,
var credentialSubject: W3CCredentialSubject? = null,
override val properties: Map<String, Any?> = mapOf()
override val properties: Map<String, Any?> = mapOf(),
var sdJwt: SDJwt? = null,
var selectiveDisclosure: SDMap? = null
) : ICredentialElement {

internal constructor(jsonObject: JsonObject) : this(
Expand Down Expand Up @@ -50,9 +52,9 @@ open class VerifiableCredential internal constructor(
get() = credentialSubject?.id

open val challenge
get() = when (this.jwt) {
get() = when (this.sdJwt) {
null -> this.proof?.nonce
else -> SignedJWT.parse(this.jwt).jwtClaimsSet.getStringClaim("nonce")
else -> sdJwt!!.sdPayload.undisclosedPayload["nonce"]?.jsonPrimitive?.contentOrNull
}

fun toJsonObject() = buildJsonObject {
Expand All @@ -78,9 +80,10 @@ open class VerifiableCredential internal constructor(
return toJsonObject().toString()
}

fun toJsonElement() = jwt?.let { JsonPrimitive(it) } ?: toJsonObject()
fun toJsonElement() = (sdJwt)?.let { JsonPrimitive(it.toString()) } ?: toJsonObject()

override fun toString(): String {
return jwt ?: toJson()
return sdJwt?.toString() ?: toJson()
}

fun encode() = toString()
Expand Down Expand Up @@ -108,26 +111,26 @@ open class VerifiableCredential internal constructor(
private const val JWT_VC_CLAIM = "vc"
private const val JWT_VP_CLAIM = "vp"

val possibleClaimKeys = listOf(JWT_VP_CLAIM, JWT_VC_CLAIM)

fun isJWT(data: String): Boolean {
return Regex(JWT_PATTERN).matches(data)
}

private val possibleClaimKeys = listOf(JWT_VP_CLAIM, JWT_VC_CLAIM)

private fun fromJwt(jwt: String): VerifiableCredential {
val claims = SignedJWT.parse(jwt).jwtClaimsSet.claims
fun isSDJwt(data: String) = SDJwt.isSDJwt(data)

val claimKey = possibleClaimKeys.first { it in claims }

val claim = claims[claimKey]
return fromJsonObject(JsonConverter.toJsonElement(claim).jsonObject).apply {
this.jwt = jwt
private fun fromSdJwt(sdJwt: SDJwt): VerifiableCredential {
val resolvedObject = sdJwt.sdPayload.fullPayload
val claimKey = possibleClaimKeys.first { it in resolvedObject.keys }
return fromJsonObject(resolvedObject[claimKey]!!.jsonObject).apply {
this.sdJwt = sdJwt
this.selectiveDisclosure = sdJwt.sdPayload.sdMap[claimKey]?.children
}
}

fun fromString(data: String): VerifiableCredential {
return when {
isJWT(data) -> fromJwt(data)
SDJwt.isSDJwt(data) -> fromSdJwt(SDJwt.parse(data))
else -> fromJson(data)
}
}
Expand All @@ -143,7 +146,10 @@ fun String.toVerifiableCredential(): VerifiableCredential {
}
}

fun <T> VerifiableCredential.verifyByFormatType(jwt: (String) -> T, ld: (String) -> T): T = when (this.jwt) {
fun String.toPresentableCredential(sdMap: SDMap? = null, discloseAll: Boolean = false)
= PresentableCredential(this.toVerifiableCredential(), sdMap, discloseAll)

fun <T> VerifiableCredential.verifyByFormatType(jwt: (String) -> T, ld: (String) -> T): T = when (this.sdJwt) {
null -> ld(this.encode())
else -> jwt(this.encode())
}
29 changes: 27 additions & 2 deletions src/main/kotlin/id/walt/credentials/w3c/VerifiablePresentation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package id.walt.credentials.w3c

import id.walt.credentials.w3c.builder.AbstractW3CCredentialBuilder
import id.walt.credentials.w3c.builder.CredentialFactory
import id.walt.sdjwt.SDField
import id.walt.sdjwt.SDMap
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

Expand All @@ -28,7 +30,7 @@ class VerifiablePresentation internal constructor(jsonObject: JsonObject) : Veri
override fun fromJsonObject(jsonObject: JsonObject) = VerifiablePresentation(jsonObject)
fun fromVerifiableCredential(verifiableCredential: VerifiableCredential) =
VerifiablePresentation(verifiableCredential.toJsonObject()).apply {
this.jwt = verifiableCredential.jwt
this.sdJwt = verifiableCredential.sdJwt
}

fun fromString(data: String) = fromVerifiableCredential(data.toVerifiableCredential())
Expand All @@ -40,8 +42,31 @@ class VerifiablePresentationBuilder : AbstractW3CCredentialBuilder<VerifiablePre
VerifiablePresentation
) {
fun setHolder(holder: String) = setProperty("holder", holder)
fun setVerifiableCredentials(verifiableCredentials: List<VerifiableCredential>) =

fun setVerifiableCredentials(verifiableCredentials: List<PresentableCredential>) =
setProperty("verifiableCredential", verifiableCredentials.map { it.toJsonElement() }.toList())
}

fun String.toVerifiablePresentation() = VerifiablePresentation.fromString(this)

data class PresentableCredential(
val verifiableCredential: VerifiableCredential,
val selectiveDisclosure: SDMap? = null,
val discloseAll: Boolean = false
) {
fun toJsonElement() =
if(verifiableCredential.sdJwt != null) {
val claimKey = VerifiableCredential.possibleClaimKeys.first { it in verifiableCredential.sdJwt!!.sdPayload.undisclosedPayload.keys }
val presentedJwt = if(discloseAll) {
verifiableCredential.sdJwt!!.present(discloseAll)
} else {
verifiableCredential.sdJwt!!.present(selectiveDisclosure?.let { mapOf(
claimKey to SDField(true, it)
)})
}
JsonPrimitive(presentedJwt.toString(formatForPresentation = true))
} else verifiableCredential.toJsonElement()

val isJwt
get() = verifiableCredential.sdJwt != null
}
4 changes: 4 additions & 0 deletions src/main/kotlin/id/walt/crypto/UVarInt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class UVarInt(val value: UInt) {
return varInt.toByteArray()
}

override fun toString(): String {
return "0x${value.toString(16)}"
}

companion object {
val MSB = 0x80u
val LSB = 0x7Fu
Expand Down
Loading

0 comments on commit bfc1c44

Please sign in to comment.