Skip to content

Commit

Permalink
feat: SDJWT holder key binding (#1185)
Browse files Browse the repository at this point in the history
Signed-off-by: mineme0110 <[email protected]>
  • Loading branch information
mineme0110 authored Jun 14, 2024
1 parent 8b91eed commit 628f2f0
Show file tree
Hide file tree
Showing 32 changed files with 433 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -122,36 +122,79 @@ trait BackgroundJobsHelper {
} yield walletAccessContext
}

def getEd25519SigningKeyPair(
jwtIssuerDID: PrismDID,
verificationRelationship: VerificationRelationship
def findHolderEd25519SigningKey(
proverDid: PrismDID,
verificationRelationship: VerificationRelationship,
keyId: String
): ZIO[DIDService & ManagedDIDService & WalletAccessContext, RuntimeException, Ed25519KeyPair] = {
for {
managedDIDService <- ZIO.service[ManagedDIDService]
didService <- ZIO.service[DIDService]
issuingKeyId <- didService
.resolveDID(jwtIssuerDID)
.resolveDID(proverDid)
.mapError(e => RuntimeException(s"Error occured while resolving Issuing DID during VC creation: ${e.toString}"))
.someOrFail(RuntimeException(s"Issuing DID resolution result is not found"))
.map { case (_, didData) =>
didData.publicKeys
.find(pk => pk.purpose == verificationRelationship && pk.publicKeyData.crv == EllipticCurve.ED25519)
.find(pk =>
pk.id == keyId
&& pk.purpose == verificationRelationship && pk.publicKeyData.crv == EllipticCurve.ED25519
)
.map(_.id)
}
.someOrFail(
RuntimeException(s"Issuing DID doesn't have a key in ${verificationRelationship.name} to use: $jwtIssuerDID")
RuntimeException(s"Issuing DID doesn't have a key in ${verificationRelationship.name} to use: $proverDid")
)
ed25519keyPair <- managedDIDService
.findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId)
.findDIDKeyPair(proverDid.asCanonical, issuingKeyId)
.map(_.collect { case keyPair: Ed25519KeyPair => keyPair })
.someOrFail(
RuntimeException(s"Issuer key-pair does not exist in the wallet: ${jwtIssuerDID.toString}#$issuingKeyId")
RuntimeException(s"Issuer key-pair does not exist in the wallet: ${proverDid.toString}#$issuingKeyId")
)
} yield ed25519keyPair
}
def getEd25519SigningKeyPair(
proverDid: PrismDID,
verificationRelationship: VerificationRelationship,
keyId: Option[String] = None
): ZIO[DIDService & ManagedDIDService & WalletAccessContext, RuntimeException, Ed25519KeyPair] = {
for {
managedDIDService <- ZIO.service[ManagedDIDService]
didService <- ZIO.service[DIDService]
issuingKeyId <- didService
.resolveDID(proverDid)
.mapError(e => RuntimeException(s"Error occured while resolving Issuing DID during VC creation: ${e.toString}"))
.someOrFail(RuntimeException(s"Issuing DID resolution result is not found"))
.map { case (_, didData) =>
keyId match {
case Some(kid) =>
didData.publicKeys
.find(pk =>
pk.id.endsWith(
s"#$kid"
) && pk.purpose == verificationRelationship && pk.publicKeyData.crv == EllipticCurve.ED25519
)
.map(_.id)
case None => // TODO Remove this None mean we cannot use the holder binding In SDJWT you will always have keyID with credentil since you did when you accept the offer with keyId
didData.publicKeys
.find(pk => pk.purpose == verificationRelationship && pk.publicKeyData.crv == EllipticCurve.ED25519)
.map(_.id)
}
}
.someOrFail(
RuntimeException(s"Issuing DID doesn't have a key in ${verificationRelationship.name} to use: $proverDid")
)
ed25519keyPair <- managedDIDService
.findDIDKeyPair(proverDid.asCanonical, issuingKeyId)
.map(_.collect { case keyPair: Ed25519KeyPair => keyPair })
.someOrFail(
RuntimeException(s"Issuer key-pair does not exist in the wallet: ${proverDid.toString}#$issuingKeyId")
)
} yield ed25519keyPair
}

/** @param jwtIssuerDID
* This can holder prism did / issuer prism did
/** @param proverDid
* This is holder prism did
* @param verificationRelationship
* Holder it Authentication and Issuer it is AssertionMethod
* @return
Expand All @@ -160,15 +203,16 @@ trait BackgroundJobsHelper {
* org.hyperledger.identus.pollux.vc.jwt.Issuer
*/
def getSDJwtIssuer(
jwtIssuerDID: PrismDID,
verificationRelationship: VerificationRelationship
proverDid: PrismDID,
verificationRelationship: VerificationRelationship,
keyId: Option[String]
): ZIO[DIDService & ManagedDIDService & WalletAccessContext, RuntimeException, JwtIssuer] = {
for {
ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship)
ed25519keyPair <- getEd25519SigningKeyPair(proverDid, verificationRelationship, keyId)
} yield {
JwtIssuer(
org.hyperledger.identus.pollux.vc.jwt.DID(jwtIssuerDID.toString),
EdSigner(ed25519keyPair),
org.hyperledger.identus.pollux.vc.jwt.DID(proverDid.toString),
EdSigner(ed25519keyPair, keyId),
Ed25519PublicKey.toJavaEd25519PublicKey(ed25519keyPair.publicKey.getEncoded)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
_,
_,
_,
_,
OfferPending,
Some(offer),
_,
Expand Down Expand Up @@ -208,6 +209,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
Some(subjectId),
_,
_,
_,
RequestPending,
Some(offer),
None,
Expand Down Expand Up @@ -247,6 +249,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
CredentialFormat.SDJWT,
Role.Holder,
Some(subjectId),
keyId,
_,
_,
RequestPending,
Expand Down Expand Up @@ -290,6 +293,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
None,
_,
_,
_,
RequestPending,
Some(offer),
None,
Expand Down Expand Up @@ -332,6 +336,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
_,
_,
_,
_,
RequestGenerated,
_,
Some(request),
Expand Down Expand Up @@ -387,6 +392,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
Role.Issuer,
_,
_,
_,
Some(true),
RequestReceived,
_,
Expand Down Expand Up @@ -428,6 +434,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
_,
_,
_,
_,
CredentialPending,
_,
_,
Expand Down Expand Up @@ -474,6 +481,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
_,
_,
_,
_,
CredentialPending,
_,
_,
Expand Down Expand Up @@ -519,6 +527,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
_,
_,
_,
_,
CredentialPending,
_,
_,
Expand Down Expand Up @@ -561,6 +570,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper {
_,
_,
_,
_,
CredentialGenerated,
_,
_,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.hyperledger.identus.pollux.core.model.error.{CredentialServiceError,
import org.hyperledger.identus.pollux.core.model.error.PresentationError.*
import org.hyperledger.identus.pollux.core.service.{CredentialService, PresentationService}
import org.hyperledger.identus.pollux.core.service.serdes.AnoncredCredentialProofsV1
import org.hyperledger.identus.pollux.sdjwt.{IssuerPublicKey, PresentationCompact, SDJWT}
import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, IssuerPublicKey, PresentationCompact, SDJWT}
import org.hyperledger.identus.pollux.vc.jwt.{DidResolver as JwtDidResolver, JWT, JwtPresentation}
import org.hyperledger.identus.resolvers.DIDResolver
import org.hyperledger.identus.shared.http.*
Expand Down Expand Up @@ -100,6 +100,51 @@ object PresentBackgroundJobs extends BackgroundJobsHelper {
} yield jwtIssuer

// Holder / Prover Get the Holder/Prover PrismDID from the IssuedCredential
// SDJWT Only currrently When holder accepts offer he provides the subjectDid and optional keyId which is used for key binding
private def getHolderPrivateKeyFromCredentials(
presentationId: DidCommID,
credentialsToUse: Seq[String]
) =
for {
credentialService <- ZIO.service[CredentialService]
// Choose first credential from the list to detect the subject DID to be used in Presentation.
// Holder binding check implies that any credential record can be chosen to detect the DID to use in VP.
credentialRecordId <- ZIO
.fromOption(credentialsToUse.headOption)
.mapError(_ =>
PresentationError.UnexpectedError(s"No credential found in the Presentation record: $presentationId")
)
credentialRecordUuid <- ZIO
.attempt(DidCommID(credentialRecordId))
.mapError(_ => PresentationError.UnexpectedError(s"$credentialRecordId is not a valid DidCommID"))
credentialRecord <- credentialService
.findById(credentialRecordUuid)
.someOrFail(CredentialServiceError.RecordNotFound(credentialRecordUuid))
vcSubjectId <- ZIO
.fromOption(credentialRecord.subjectId)
.orDieWith(_ => RuntimeException(s"VC SubjectId not found in credential record: $credentialRecordUuid"))

proverDID <- ZIO
.fromEither(PrismDID.fromString(vcSubjectId))
.mapError(e =>
PresentationError
.UnexpectedError(
s"One of the credential(s) subject is not a valid Prism DID: ${vcSubjectId}"
)
)
longFormPrismDID <- getLongForm(proverDID, true)

optionalHolderPrivateKey <- credentialRecord.keyId match
case Some(keyId) =>
findHolderEd25519SigningKey(
longFormPrismDID,
VerificationRelationship.Authentication,
keyId
).map(ed25519keyPair => Option(HolderPrivateKey(ed25519keyPair.privateKey)))
case None => ZIO.succeed(None)

} yield optionalHolderPrivateKey
// Holder / Prover Get the Holder/Prover PrismDID from the IssuedCredential
// When holder accepts offer he provides the subjectdid
private def getPrismDIDForHolderFromCredentials(
presentationId: DidCommID,
Expand All @@ -117,11 +162,13 @@ object PresentBackgroundJobs extends BackgroundJobsHelper {
credentialRecordUuid <- ZIO
.attempt(DidCommID(credentialRecordId))
.mapError(_ => PresentationError.UnexpectedError(s"$credentialRecordId is not a valid DidCommID"))
vcSubjectId <- credentialService
credentialRecord <- credentialService
.findById(credentialRecordUuid)
.someOrFail(CredentialServiceError.RecordNotFound(credentialRecordUuid))
.map(_.subjectId)
.someOrElseZIO(ZIO.dieMessage(s"VC SubjectId not found in credential record: $credentialRecordUuid"))
vcSubjectId <- ZIO
.fromOption(credentialRecord.subjectId)
.orDieWith(_ => RuntimeException(s"VC SubjectId not found in credential record: $credentialRecordUuid"))

proverDID <- ZIO
.fromEither(PrismDID.fromString(vcSubjectId))
.mapError(e =>
Expand All @@ -130,8 +177,12 @@ object PresentBackgroundJobs extends BackgroundJobsHelper {
s"One of the credential(s) subject is not a valid Prism DID: ${vcSubjectId}"
)
)
longFormPrismDID <- getLongForm(proverDID, true)
jwtIssuer <- getSDJwtIssuer(longFormPrismDID, VerificationRelationship.Authentication)
longFormProverPrismDID <- getLongForm(proverDID, true)
jwtIssuer <- getSDJwtIssuer(
longFormProverPrismDID,
VerificationRelationship.Authentication,
credentialRecord.keyId
)
} yield jwtIssuer

private def performPresentProofExchange(record: PresentationRecord): URIO[
Expand Down Expand Up @@ -480,12 +531,12 @@ object PresentBackgroundJobs extends BackgroundJobsHelper {
walletAccessContext <- buildWalletAccessContextLayer(requestPresentation.to)
result <- (for {
presentationService <- ZIO.service[PresentationService]
prover <- getPrismDIDForHolderFromCredentials(id, credentialsToUse.getOrElse(Nil))
optionalHolderPrivateKey <- getHolderPrivateKeyFromCredentials(id, credentialsToUse.getOrElse(Nil))
.provideSomeLayer(ZLayer.succeed(walletAccessContext))
presentation <-
for {
presentation <- presentationService
.createSDJwtPresentation(id, requestPresentation)
.createSDJwtPresentation(id, requestPresentation, optionalHolderPrivateKey)
.provideSomeLayer(ZLayer.succeed(walletAccessContext))
} yield presentation
_ <- presentationService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ class IssueControllerImpl(
case Some(did) => extractPrismDIDFromString(did).flatMap(validatePrismDID(_, true, Role.Holder))
case None => ZIO.succeed(())
id <- extractDidCommIdFromString(recordId)
outcome <- credentialService.acceptCredentialOffer(id, request.subjectId)
outcome <- credentialService.acceptCredentialOffer(id, request.subjectId, request.keyId)
} yield IssueCredentialRecord.fromDomain(outcome)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder}
final case class AcceptCredentialOfferRequest(
@description(annotations.subjectId.description)
@encodedExample(annotations.subjectId.example)
subjectId: Option[String]
subjectId: Option[String],
@description(annotations.subjectId.description)
@encodedExample(annotations.subjectId.example)
keyId: Option[String]
)

object AcceptCredentialOfferRequest {
Expand All @@ -29,7 +32,17 @@ object AcceptCredentialOfferRequest {
|""".stripMargin,
example = Some("did:prism:3bb0505d13fcb04d28a48234edb27b0d4e6d7e18a81e2c1abab58f3bbc21ce6f")
)

object keyId
extends Annotation[Option[String]](
description = """
|The KeyID (KID / key ID) parameter is used to identify a specific key within
|the subject Prism DID, allowing for the selection of a particular key when the
|DID contains multiple keys. This is relevant for issuing the SDJWT verifiable credential.
|This parameter is only applicable if the offer type is 'SDJWT'.
|SDJWT credential issued will be bound to this key.
|""".stripMargin,
example = Some("my-auth-key")
)
}

given encoder: JsonEncoder[AcceptCredentialOfferRequest] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ case class OIDCCredentialIssuerServiceImpl(
.map(WalletAccessContext.apply)

jwtIssuer <- credentialService
.getJwtIssuer(issuingDID, VerificationRelationship.AssertionMethod)
.getJwtIssuer(issuingDID, VerificationRelationship.AssertionMethod, None)
.provideSomeLayer(ZLayer.succeed(wac))

jwtVC <- buildJwtVerifiableCredential(
Expand Down
Loading

0 comments on commit 628f2f0

Please sign in to comment.