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

fix: key id for jwt and sdjwt #1420

Merged
merged 11 commits into from
Oct 31, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -38,45 +38,59 @@ object MockDIDService extends Mock[DIDService] {
}
}

def createDID(
verificationRelationship: VerificationRelationship
private def createDIDInternal(
verificationRelationship: VerificationRelationship,
addEd25519Key: Boolean = false
): (PrismDIDOperation.Create, Secp256k1KeyPair, DIDMetadata, DIDData) = {
val masterKeyPair = Apollo.default.secp256k1.generateKeyPair
val keyPair = Apollo.default.secp256k1.generateKeyPair

val createOperation = PrismDIDOperation.Create(
publicKeys = Seq(
InternalPublicKey(
id = KeyId("master-0"),
purpose = InternalKeyPurpose.Master,
publicKeyData = PublicKeyData.ECCompressedKeyData(
crv = EllipticCurve.SECP256K1,
data = Base64UrlString.fromByteArray(masterKeyPair.publicKey.getEncodedCompressed)
)
),
PublicKey(
id = KeyId("key-0"),
purpose = verificationRelationship,
publicKeyData = PublicKeyData.ECCompressedKeyData(
crv = EllipticCurve.SECP256K1,
data = Base64UrlString.fromByteArray(keyPair.publicKey.getEncodedCompressed)
)
),
val basePublicKeys = Seq(
InternalPublicKey(
id = KeyId("master-0"),
purpose = InternalKeyPurpose.Master,
publicKeyData = PublicKeyData.ECCompressedKeyData(
crv = EllipticCurve.SECP256K1,
data = Base64UrlString.fromByteArray(masterKeyPair.publicKey.getEncodedCompressed)
)
),
PublicKey(
id = KeyId("key-0"),
purpose = verificationRelationship,
publicKeyData = PublicKeyData.ECCompressedKeyData(
crv = EllipticCurve.SECP256K1,
data = Base64UrlString.fromByteArray(keyPair.publicKey.getEncodedCompressed)
)
)
)

val publicKeys = if (addEd25519Key) {
val keyPair2 = Apollo.default.ed25519.generateKeyPair
basePublicKeys :+ PublicKey(
id = KeyId("key-1"),
purpose = verificationRelationship,
publicKeyData = PublicKeyData.ECKeyData(
crv = EllipticCurve.ED25519,
x = Base64UrlString.fromByteArray(keyPair2.publicKey.getEncoded),
y = Base64UrlString.fromByteArray(Array.emptyByteArray),
)
)
} else basePublicKeys

val createOperation = PrismDIDOperation.Create(
publicKeys = publicKeys,
services = Nil,
context = Nil,
)
val longFormDid = PrismDID.buildLongFormFromOperation(createOperation)
// val canonicalDid = longFormDid.asCanonical

val didMetadata =
DIDMetadata(
lastOperationHash = ArraySeq.from(longFormDid.stateHash.toByteArray),
canonicalId = None, // unpublished DID must not contain canonicalId
deactivated = false, // unpublished DID cannot be deactivated
created = None, // unpublished DID cannot have timestamp
updated = None // unpublished DID cannot have timestamp
)
val didMetadata = DIDMetadata(
lastOperationHash = ArraySeq.from(longFormDid.stateHash.toByteArray),
canonicalId = None,
deactivated = false,
created = None,
updated = None
)
val didData = DIDData(
id = longFormDid.asCanonical,
publicKeys = createOperation.publicKeys.collect { case pk: PublicKey => pk },
Expand All @@ -87,6 +101,18 @@ object MockDIDService extends Mock[DIDService] {
(createOperation, keyPair, didMetadata, didData)
}

def createDIDOIDC(
verificationRelationship: VerificationRelationship
): (PrismDIDOperation.Create, Secp256k1KeyPair, DIDMetadata, DIDData) = {
createDIDInternal(verificationRelationship, addEd25519Key = false)
}

def createDID(
verificationRelationship: VerificationRelationship
): (PrismDIDOperation.Create, Secp256k1KeyPair, DIDMetadata, DIDData) = {
createDIDInternal(verificationRelationship, addEd25519Key = true)
}

def resolveDIDExpectation(didMetadata: DIDMetadata, didData: DIDData): Expectation[DIDService] =
MockDIDService.ResolveDID(
assertion = Assertion.anything,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ object OIDCCredentialIssuerServiceSpec
)

private val (_, issuerKp, issuerDidMetadata, issuerDidData) =
MockDIDService.createDID(VerificationRelationship.AssertionMethod)
MockDIDService.createDIDOIDC(VerificationRelationship.AssertionMethod)

private val (holderOp, holderKp, holderDidMetadata, holderDidData) =
MockDIDService.createDID(VerificationRelationship.AssertionMethod)
MockDIDService.createDIDOIDC(VerificationRelationship.AssertionMethod)

private val holderDidServiceExpectations =
MockDIDService.resolveDIDExpectation(holderDidMetadata, holderDidData)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,13 @@ object CredentialServiceError {
StatusCode.NotFound,
s"A key with the given purpose was not found in the DID: did=${did.toString}, purpose=${verificationRelationship.name}"
)

final case class MultipleKeysWithSamePurposeFoundInDID(
Copy link
Member

Choose a reason for hiding this comment

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

@mineme0110, is it possible to keep two keys (secp256 and de25519) with the same purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it is possible to have multiple keys with same purpose as per the spec

did: PrismDID,
verificationRelationship: VerificationRelationship
) extends CredentialServiceError(
StatusCode.BadRequest,
s"A key with the given purpose was found multiple times in the DID: did=${did.toString}, purpose=${verificationRelationship.name}"
)
final case class InvalidCredentialRequest(cause: String)
extends CredentialServiceError(
StatusCode.BadRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ class CredentialServiceImpl(
claims = attributes,
thid = thid,
UUID.randomUUID().toString,
"domain",
"domain", // TODO remove the hardcoded domain
yshyn-iohk marked this conversation as resolved.
Show resolved Hide resolved
IssueCredentialOfferFormat.JWT
)
record <- createIssueCredentialRecord(
Expand Down Expand Up @@ -559,8 +559,8 @@ class CredentialServiceImpl(
private[this] def getKeyId(
did: PrismDID,
verificationRelationship: VerificationRelationship,
ellipticCurve: EllipticCurve
): UIO[KeyId] = {
keyId: Option[KeyId]
): UIO[PublicKey] = {
for {
maybeDidData <- didService
.resolveDID(did)
Expand All @@ -569,15 +569,25 @@ class CredentialServiceImpl(
.fromOption(maybeDidData)
.mapError(_ => DIDNotResolved(did))
.orDieAsUnmanagedFailure
keyId <- ZIO
.fromOption(
didData._2.publicKeys
.find(pk => pk.purpose == verificationRelationship && pk.publicKeyData.crv == ellipticCurve)
.map(_.id)
)
.mapError(_ => KeyNotFoundInDID(did, verificationRelationship))
.orDieAsUnmanagedFailure
} yield keyId
matchingKeys = didData._2.publicKeys.filter(pk => pk.purpose == verificationRelationship)
result <- (matchingKeys, keyId) match {
case (Seq(), _) =>
ZIO.fail(KeyNotFoundInDID(did, verificationRelationship)).orDieAsUnmanagedFailure
case (Seq(singleKey), None) =>
ZIO.succeed(singleKey)
case (multipleKeys, Some(kid)) =>
ZIO
.fromOption(multipleKeys.find(_.id.value.endsWith(kid.value)))
.mapError(_ => KeyNotFoundInDID(did, verificationRelationship))
.orDieAsUnmanagedFailure
case (multipleKeys, None) =>
ZIO
.fail(
MultipleKeysWithSamePurposeFoundInDID(did, verificationRelationship)
)
.orDieAsUnmanagedFailure
}
} yield result
}

override def getJwtIssuer(
Expand All @@ -586,34 +596,46 @@ class CredentialServiceImpl(
keyId: Option[KeyId] = None
): URIO[WalletAccessContext, JwtIssuer] = {
for {
issuingKeyId <- getKeyId(jwtIssuerDID, verificationRelationship, EllipticCurve.SECP256K1)
ecKeyPair <- managedDIDService
.findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId)
issuingPublicKey <- getKeyId(jwtIssuerDID, verificationRelationship, keyId)
jwtIssuer <- managedDIDService
.findDIDKeyPair(jwtIssuerDID.asCanonical, issuingPublicKey.id)
.flatMap {
case Some(keyPair: Secp256k1KeyPair) => ZIO.some(keyPair)
case _ => ZIO.none
case Some(keyPair: Secp256k1KeyPair) => {
val jwtIssuer = JwtIssuer(
jwtIssuerDID.did,
ES256KSigner(keyPair.privateKey.toJavaPrivateKey, keyId),
keyPair.publicKey.toJavaPublicKey
)
ZIO.some(jwtIssuer)
}
case Some(keyPair: Ed25519KeyPair) => {
val jwtIssuer = JwtIssuer(
jwtIssuerDID.did,
EdSigner(keyPair, keyId),
keyPair.publicKey.toJava
)
ZIO.some(jwtIssuer)
}
case _ => ZIO.none
}
.someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingKeyId, "Secp256k1"))
.someOrFail(
KeyPairNotFoundInWallet(jwtIssuerDID, issuingPublicKey.id, issuingPublicKey.publicKeyData.crv.name)
)
.orDieAsUnmanagedFailure
Secp256k1KeyPair(publicKey, privateKey) = ecKeyPair
jwtIssuer = JwtIssuer(
jwtIssuerDID.did,
ES256KSigner(privateKey.toJavaPrivateKey, keyId),
publicKey.toJavaPublicKey
)
} yield jwtIssuer
}

private def getEd25519SigningKeyPair(
jwtIssuerDID: PrismDID,
verificationRelationship: VerificationRelationship
verificationRelationship: VerificationRelationship,
keyId: Option[KeyId] = None
): URIO[WalletAccessContext, Ed25519KeyPair] = {
for {
issuingKeyId <- getKeyId(jwtIssuerDID, verificationRelationship, EllipticCurve.ED25519)
issuingPublicKey <- getKeyId(jwtIssuerDID, verificationRelationship, keyId)
ed25519keyPair <- managedDIDService
.findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId)
.findDIDKeyPair(jwtIssuerDID.asCanonical, issuingPublicKey.id)
.map(_.collect { case keyPair: Ed25519KeyPair => keyPair })
.someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingKeyId, "Ed25519"))
.someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingPublicKey.id, issuingPublicKey.publicKeyData.crv.name))
.orDieAsUnmanagedFailure
} yield ed25519keyPair
}
Expand All @@ -635,7 +657,7 @@ class CredentialServiceImpl(
keyId: Option[KeyId]
): URIO[WalletAccessContext, JwtIssuer] = {
for {
ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship)
ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship, keyId)
} yield {
JwtIssuer(
jwtIssuerDID.did,
Expand Down Expand Up @@ -1163,7 +1185,7 @@ class CredentialServiceImpl(
.orElse(ZIO.dieMessage(s"Offer credential data not found in record: ${recordId.value}"))
preview = offerCredentialData.body.credential_preview
claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes).orDieAsUnmanagedFailure
jwtIssuer <- getJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod)
jwtIssuer <- getJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod, record.keyId)
jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt)
.tapError(error =>
credentialRepository
Expand Down Expand Up @@ -1243,7 +1265,11 @@ class CredentialServiceImpl(
case ZValidation.Success(log, header) => ZIO.succeed(header)
case ZValidation.Failure(log, failure) =>
ZIO.fail(VCJwtHeaderParsingError(s"Extraction of JwtHeader failed ${failure.toChunk.toString}"))
ed25519KeyPair <- getEd25519SigningKeyPair(longFormPrismDID, VerificationRelationship.AssertionMethod)
ed25519KeyPair <- getEd25519SigningKeyPair(
longFormPrismDID,
VerificationRelationship.AssertionMethod,
record.keyId
)
sdJwtPrivateKey = sdjwt.IssuerPrivateKey(ed25519KeyPair.privateKey)
jsonWebKey <- didResolver.resolve(jwtPresentation.iss) flatMap {
case failed: DIDResolutionFailed =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ class CredentialRepositoryInMemory(
updatedAt = Some(Instant.now),
protocolState = protocolState,
subjectId = Some(subjectId),
keyId = keyId,
metaRetries = maxRetries,
metaLastFailure = None,
)
Expand Down
Loading
Loading