-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ATL-1342] feat(pollux): Created JWT Verifiable Credential (#55)
* [ATL-1342] feat(pollux): Created JWT Verfiable Credential * [ATL-1342] feat(pollux): Added conversion from JWT to W3C and W3C to JWT. Added Json Encodingl
- Loading branch information
1 parent
496337b
commit 7108401
Showing
7 changed files
with
472 additions
and
0 deletions.
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
pollux/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/DidJWT.scala
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,58 @@ | ||
package io.iohk.atala.pollux.vc.jwt | ||
|
||
import java.security.PrivateKey | ||
import java.security.spec.ECGenParameterSpec | ||
import java.sql.Timestamp | ||
import java.time.{Instant, ZonedDateTime} | ||
import io.circe.* | ||
import pdi.jwt.JwtClaim | ||
import pdi.jwt.{JwtAlgorithm, JwtCirce} | ||
|
||
import java.security.* | ||
import java.security.spec.* | ||
import java.time.Instant | ||
import net.reactivecore.cjs.Loader | ||
import net.reactivecore.cjs.{DocumentValidator, Loader, Result} | ||
import net.reactivecore.cjs.resolver.Downloader | ||
import cats.implicits.* | ||
import io.circe.Json | ||
import pdi.jwt.algorithms.JwtECDSAAlgorithm | ||
|
||
case class JWT(jwt: String) | ||
case class JWTHeader(typ: String = "JWT", alg: Option[String]) | ||
case class JWTPayload( | ||
iss: Option[String], | ||
sub: Option[String], | ||
aud: Vector[String], | ||
iat: Option[Instant], | ||
nbf: Option[Instant], | ||
exp: Option[Instant], | ||
rexp: Option[Instant] | ||
) | ||
trait JWTVerified( | ||
verified: Boolean, | ||
payload: JWTPayload, | ||
didResolutionResult: DIDResolutionResult, | ||
issuer: String, | ||
signer: VerificationMethod, | ||
jwt: String, | ||
policies: Option[JWTVerifyPolicies] | ||
) | ||
|
||
case class JWTVerifyPolicies( | ||
now: Option[Boolean], | ||
nbf: Option[Boolean], | ||
exp: Option[Boolean], | ||
aud: Vector[Boolean] | ||
) | ||
|
||
trait Signer { | ||
def encode(claim: Json): String | ||
} | ||
|
||
class ES256Signer(privateKey: PrivateKey) extends Signer { | ||
val algorithm: JwtECDSAAlgorithm = JwtAlgorithm.ES256 | ||
override def encode(claim: Json): String = { | ||
return JwtCirce.encode(claim, privateKey, algorithm) | ||
} | ||
} |
69 changes: 69 additions & 0 deletions
69
pollux/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/DidResolver.scala
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,69 @@ | ||
package io.iohk.atala.pollux.vc.jwt | ||
|
||
import java.time.Instant | ||
|
||
trait DIDResolutionResult( | ||
`@context`: Vector[String] = Vector("https://w3id.org/did-resolution/v1") | ||
) | ||
trait DIDResolutionFailed( | ||
error: DIDResolutionError | ||
) extends DIDResolutionResult | ||
trait DIDResolutionSucceeded( | ||
didDocument: DIDDocument, | ||
contentType: String, | ||
didDocumentMetadata: DIDDocumentMetadata | ||
) extends DIDResolutionResult | ||
|
||
trait DIDResolutionError(error: String, message: String) { | ||
class InvalidDid(message: String) extends DIDResolutionError("invalidDid", message) | ||
class NotFound(message: String) extends DIDResolutionError("notFound", message) | ||
class RepresentationNotSupported(message: String) extends DIDResolutionError("RepresentationNotSupported", message) | ||
class UnsupportedDidMethod(message: String) extends DIDResolutionError("unsupportedDidMethod", message) | ||
class Error(error: String, message: String) extends DIDResolutionError(error, message) | ||
} | ||
trait DIDDocumentMetadata( | ||
created: Option[Instant], | ||
updated: Option[Instant], | ||
deactivated: Option[Boolean], | ||
versionId: Option[Instant], | ||
nextUpdate: Option[Instant], | ||
nextVersionId: Option[Instant], | ||
equivalentId: Option[Instant], | ||
canonicalId: Option[Instant] | ||
) | ||
|
||
trait DIDDocument( | ||
`@context`: Vector[String] = Vector("https://www.w3.org/ns/did/v1"), | ||
id: String, | ||
alsoKnowAs: Vector[String], | ||
controller: Vector[String], | ||
verificationMethod: Vector[VerificationMethod], | ||
service: Vector[Service] | ||
) | ||
trait VerificationMethod( | ||
id: String, | ||
`type`: String, | ||
controller: String, | ||
publicKeyBase58: Option[String], | ||
publicKeyBase64: Option[String], | ||
publicKeyJwk: Option[JsonWebKey], | ||
publicKeyHex: Option[String], | ||
publicKeyMultibase: Option[String], | ||
blockchainAccountId: Option[String], | ||
ethereumAddress: Option[String] | ||
) | ||
trait JsonWebKey( | ||
alg: Option[String], | ||
crv: Option[String], | ||
e: Option[String], | ||
ext: Option[Boolean], | ||
key_ops: Vector[String], | ||
kid: Option[String], | ||
kty: String, | ||
n: Option[String], | ||
use: Option[String], | ||
x: Option[String], | ||
y: Option[String] | ||
) | ||
trait Service(id: String, `type`: String, serviceEndpoint: Vector[ServiceEndpoint]) | ||
trait ServiceEndpoint(id: String, `type`: String) |
264 changes: 264 additions & 0 deletions
264
pollux/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/VerifiableCredential.scala
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,264 @@ | ||
package io.iohk.atala.pollux.vc.jwt | ||
|
||
import cats.data.{Validated, ValidatedNel} | ||
import cats.implicits._ | ||
import cats.Applicative | ||
import pdi.jwt.Jwt | ||
import io.circe.generic.auto._, io.circe.syntax._ | ||
import io.circe.{Decoder, Encoder, HCursor, Json} | ||
|
||
import java.security.{KeyPairGenerator, PublicKey} | ||
import java.time.{Instant, ZonedDateTime} | ||
|
||
case class Proof(`type`: String) | ||
trait Verifiable(proof: Proof) | ||
case class IssuerDID(id: String) | ||
case class Issuer(did: IssuerDID, signer: Signer, publicKey: PublicKey) | ||
trait W3CCredential( | ||
`@context`: Vector[String], | ||
`type`: Vector[String], | ||
issuer: IssuerDID, | ||
issuanceDate: ZonedDateTime, | ||
expirationDate: ZonedDateTime | ||
) | ||
|
||
trait VerifiableCredential | ||
trait W3CVerifiableCredential extends W3CCredential, Verifiable | ||
case class JwtVerifiableCredential(jwt: JWT) extends VerifiableCredential | ||
trait VerifiedCredential extends JWTVerified, W3CVerifiableCredential | ||
|
||
case class CredentialStatus( | ||
id: String, | ||
`type`: String | ||
) | ||
case class RefreshService( | ||
id: String, | ||
`type`: String | ||
) | ||
case class CredentialSchema( | ||
id: String, | ||
`type`: String | ||
) | ||
sealed trait CredentialPayload( | ||
maybeSub: Option[String], | ||
`@context`: Vector[String], | ||
`type`: Vector[String], | ||
maybeJti: Option[String], | ||
maybeNbf: Option[Instant], | ||
aud: Vector[String], | ||
maybeExp: Option[Instant], | ||
maybeIss: Option[String], | ||
maybeConnectionStatus: Option[CredentialStatus], | ||
maybeRefreshService: Option[RefreshService], | ||
maybeEvidence: Option[Json], | ||
maybeTermsOfUse: Option[Json], | ||
maybeCredentialSchema: Option[CredentialSchema], | ||
credentialSubject: Json | ||
) { | ||
def toJwtCredentialPayload: JwtCredentialPayload = | ||
JwtCredentialPayload( | ||
maybeIss = maybeIss, | ||
maybeSub = maybeSub, | ||
vc = JwtVc( | ||
`@context` = `@context`, | ||
`type` = `type`, | ||
maybeCredentialSchema = maybeCredentialSchema, | ||
credentialSubject = credentialSubject, | ||
maybeCredentialStatus = maybeConnectionStatus, | ||
maybeRefreshService = maybeRefreshService, | ||
maybeEvidence = maybeEvidence, | ||
maybeTermsOfUse = maybeTermsOfUse | ||
), | ||
maybeNbf = maybeNbf, | ||
aud = aud, | ||
maybeExp = maybeExp, | ||
maybeJti = maybeJti | ||
) | ||
|
||
def toW3CCredentialPayload: ValidatedNel[String, W3CCredentialPayload] = | ||
( | ||
Validated | ||
.cond(maybeIss.isDefined, maybeIss.get, "Iss must be defined") | ||
.toValidatedNel, | ||
Validated | ||
.cond(maybeNbf.isDefined, maybeNbf.get, "Nbf must be defined") | ||
.toValidatedNel | ||
).mapN((iss, nbf) => | ||
W3CCredentialPayload( | ||
`@context` = `@context`.distinct, | ||
id = maybeJti, | ||
`type` = `type`.distinct, | ||
issuer = IssuerDID(id = iss), | ||
issuanceDate = nbf, | ||
maybeExpirationDate = maybeExp, | ||
maybeCredentialSchema = maybeCredentialSchema, | ||
credentialSubject = credentialSubject, | ||
maybeCredentialStatus = maybeConnectionStatus, | ||
maybeRefreshService = maybeRefreshService, | ||
maybeEvidence = maybeEvidence, | ||
maybeTermsOfUse = maybeTermsOfUse | ||
) | ||
) | ||
} | ||
|
||
case class JwtVc( | ||
`@context`: Vector[String], | ||
`type`: Vector[String], | ||
maybeCredentialSchema: Option[CredentialSchema], | ||
credentialSubject: Json, | ||
maybeCredentialStatus: Option[CredentialStatus], | ||
maybeRefreshService: Option[RefreshService], | ||
maybeEvidence: Option[Json], | ||
maybeTermsOfUse: Option[Json] | ||
) | ||
case class JwtCredentialPayload( | ||
maybeIss: Option[String], | ||
maybeSub: Option[String], | ||
vc: JwtVc, | ||
maybeNbf: Option[Instant], | ||
aud: Vector[String], | ||
maybeExp: Option[Instant], | ||
maybeJti: Option[String] | ||
) extends CredentialPayload( | ||
maybeSub = maybeSub.orElse(vc.credentialSubject.hcursor.downField("id").as[String].toOption), | ||
`@context` = vc.`@context`.distinct, | ||
`type` = vc.`type`.distinct, | ||
maybeJti = maybeJti, | ||
maybeNbf = maybeNbf, | ||
aud = aud, | ||
maybeExp = maybeExp, | ||
maybeIss = maybeIss, | ||
maybeConnectionStatus = vc.maybeCredentialStatus, | ||
maybeRefreshService = vc.maybeRefreshService, | ||
maybeEvidence = vc.maybeEvidence, | ||
maybeTermsOfUse = vc.maybeTermsOfUse, | ||
maybeCredentialSchema = vc.maybeCredentialSchema, | ||
credentialSubject = vc.credentialSubject | ||
) | ||
|
||
object JwtCredentialPayload {} | ||
case class W3CCredentialPayload( | ||
`@context`: Vector[String], | ||
id: Option[String], | ||
`type`: Vector[String], | ||
issuer: IssuerDID, | ||
issuanceDate: Instant, | ||
maybeExpirationDate: Option[Instant], | ||
maybeCredentialSchema: Option[CredentialSchema], | ||
credentialSubject: Json, | ||
maybeCredentialStatus: Option[CredentialStatus], | ||
maybeRefreshService: Option[RefreshService], | ||
maybeEvidence: Option[Json], | ||
maybeTermsOfUse: Option[Json] | ||
) extends CredentialPayload( | ||
maybeSub = credentialSubject.hcursor.downField("id").as[String].toOption, | ||
`@context` = `@context`.distinct, | ||
`type` = `type`.distinct, | ||
maybeJti = id, | ||
maybeNbf = Some(issuanceDate), | ||
aud = Vector.empty, | ||
maybeExp = maybeExpirationDate, | ||
maybeIss = Some(issuer.id), | ||
maybeConnectionStatus = maybeCredentialStatus, | ||
maybeRefreshService = maybeRefreshService, | ||
maybeEvidence = maybeEvidence, | ||
maybeTermsOfUse = maybeTermsOfUse, | ||
maybeCredentialSchema = maybeCredentialSchema, | ||
credentialSubject = credentialSubject | ||
) | ||
|
||
object VerifiedCredentialJson { | ||
object Encoders { | ||
object Implicits { | ||
implicit val refreshServiceEncoder: Encoder[RefreshService] = | ||
(refreshService: RefreshService) => | ||
Json | ||
.obj( | ||
("id", refreshService.id.asJson), | ||
("type", refreshService.`type`.asJson) | ||
) | ||
|
||
implicit val credentialSchemaEncoder: Encoder[CredentialSchema] = | ||
(credentialSchema: CredentialSchema) => | ||
Json | ||
.obj( | ||
("id", credentialSchema.id.asJson), | ||
("type", credentialSchema.`type`.asJson) | ||
) | ||
|
||
implicit val credentialStatusEncoder: Encoder[CredentialStatus] = | ||
(credentialStatus: CredentialStatus) => | ||
Json | ||
.obj( | ||
("id", credentialStatus.id.asJson), | ||
("type", credentialStatus.`type`.asJson) | ||
) | ||
|
||
implicit val w3cCredentialPayloadEncoder: Encoder[W3CCredentialPayload] = | ||
(w3cCredentialPayload: W3CCredentialPayload) => | ||
Json | ||
.obj( | ||
("@context", w3cCredentialPayload.`@context`.asJson), | ||
("id", w3cCredentialPayload.id.asJson), | ||
("type", w3cCredentialPayload.`type`.asJson), | ||
("issuer", Json.fromString(w3cCredentialPayload.issuer.id)), | ||
("issuanceDate", w3cCredentialPayload.issuanceDate.asJson), | ||
("expirationDate", w3cCredentialPayload.maybeExpirationDate.asJson), | ||
("credentialSchema", w3cCredentialPayload.maybeCredentialSchema.asJson), | ||
("credentialSubject", w3cCredentialPayload.credentialSubject), | ||
("credentialStatus", w3cCredentialPayload.maybeCredentialStatus.asJson), | ||
("refreshService", w3cCredentialPayload.maybeRefreshService.asJson), | ||
("evidence", w3cCredentialPayload.maybeEvidence.asJson), | ||
("termsOfUse", w3cCredentialPayload.maybeTermsOfUse.asJson) | ||
) | ||
.deepDropNullValues | ||
.dropEmptyValues | ||
|
||
implicit val jwtVcEncoder: Encoder[JwtVc] = | ||
(jwtVc: JwtVc) => | ||
Json | ||
.obj( | ||
("@context", jwtVc.`@context`.asJson), | ||
("type", jwtVc.`type`.asJson), | ||
("credentialSchema", jwtVc.maybeCredentialSchema.asJson), | ||
("credentialSubject", jwtVc.credentialSubject), | ||
("credentialStatus", jwtVc.maybeCredentialStatus.asJson), | ||
("refreshService", jwtVc.maybeRefreshService.asJson), | ||
("evidence", jwtVc.maybeEvidence.asJson), | ||
("termsOfUse", jwtVc.maybeTermsOfUse.asJson) | ||
) | ||
.deepDropNullValues | ||
.dropEmptyValues | ||
|
||
implicit val jwtCredentialPayloadEncoder: Encoder[JwtCredentialPayload] = | ||
(jwtCredentialPayload: JwtCredentialPayload) => | ||
Json | ||
.obj( | ||
("iss", jwtCredentialPayload.maybeIss.asJson), | ||
("sub", jwtCredentialPayload.maybeSub.asJson), | ||
("vc", jwtCredentialPayload.vc.asJson), | ||
("nbf", jwtCredentialPayload.maybeNbf.asJson), | ||
("aud", jwtCredentialPayload.aud.asJson), | ||
("exp", jwtCredentialPayload.maybeExp.asJson), | ||
("jti", jwtCredentialPayload.maybeJti.asJson) | ||
) | ||
.deepDropNullValues | ||
.dropEmptyValues | ||
} | ||
} | ||
|
||
} | ||
|
||
object VerifiableCredential { | ||
import VerifiedCredentialJson.Encoders.Implicits._ | ||
def createJwt(payload: JwtCredentialPayload, issuer: Issuer): JWT = { | ||
val jwtCredentialPayload = payload | ||
/* val validationResult = jwtCredentialPayload | ||
.map(payload => issuer.signer.encode(payload.toJson)) | ||
*/ | ||
JWT("") | ||
} | ||
|
||
def createJsonW3CCredential(payload: W3CCredentialPayload): Json = | ||
payload.asJson | ||
} |
Oops, something went wrong.