Skip to content

Commit

Permalink
Modular credential format support for oid4vci (#772)
Browse files Browse the repository at this point in the history
* modular credential format support for oid4vci

Signed-off-by: Ivan Wei <ivan.wei@ontario.ca>

* github checks

Signed-off-by: Ivan Wei <ivan.wei@ontario.ca>

* ruff linting fixes

Signed-off-by: Ivan Wei <ivan.wei@ontario.ca>

* "lite" version of credential format plugins

Signed-off-by: Ivan Wei <ivan.wei@ontario.ca>

* review fixes

Signed-off-by: Ivan Wei <ivan.wei@ontario.ca>

* fix out-sync portry.lock file

Signed-off-by: Ivan Wei <ivan.wei@ontario.ca>

---------

Signed-off-by: Ivan Wei <ivan.wei@ontario.ca>
  • Loading branch information
weiiv authored Jul 24, 2024
1 parent acf88cb commit f0ddcdc
Show file tree
Hide file tree
Showing 52 changed files with 1,660 additions and 81 deletions.
197 changes: 194 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,200 @@
###
### Python
###

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
test-reports/

# Translations
*.mo
*.pot

# Django stuff:
*.log
*.lock
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
Pipfile
Pipfile.lock

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

###
### Visual Studio Code
###

.vscode/

###
### MacOS
###

# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

###
### IntelliJ IDEs
###

.idea/*
**/.idea/*

###
### Windows
###

# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db

# Dump file
*.stackdump

# Folder config file
[Dd]esktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp

# Windows shortcuts
*.lnk

# Docs build
_build/
**/*.iml

# Open API build
open-api/.build

# devcontainer
.pytest.ini

# project specific
.ruff_cache/
.test-reports/
**/test-reports/
.coverage
coverage.xml
settings.json
.env
7 changes: 7 additions & 0 deletions jwt_vc_json/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# JWT_VC_JSON credential format plugin

This plugin provides `jwt_vc_json` credential support for the OID4VCI plugin. It acts as a module, dynamically loaded by the OID4VCI plugin, takes input parameters, and constructs and signs `jwt_vc_json` credentials.

## Configuration:

No configuration is required for this plugin.
1 change: 1 addition & 0 deletions jwt_vc_json/jwt_vc_json/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""jwt_vc_json credential handler plugin."""
6 changes: 6 additions & 0 deletions jwt_vc_json/jwt_vc_json/v1_0/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Initialize processor."""

from .cred_processor import CredProcessor


cred_processor = CredProcessor()
64 changes: 64 additions & 0 deletions jwt_vc_json/jwt_vc_json/v1_0/cred_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Issue a jwt_vc_json credential."""

import datetime
import logging
import uuid

from aries_cloudagent.admin.request_context import AdminRequestContext
from aries_cloudagent.wallet.jwt import jwt_sign

from oid4vci.models.exchange import OID4VCIExchangeRecord
from oid4vci.models.supported_cred import SupportedCredential
from oid4vci.public_routes import types_are_subset
from oid4vci.pop_result import PopResult
from oid4vci.cred_processor import ICredProcessor, CredIssueError

LOGGER = logging.getLogger(__name__)


class CredProcessor(ICredProcessor):
"""Credential processor class for jwt_vc_json format."""

async def issue_cred(
self,
body: any,
supported: SupportedCredential,
ex_record: OID4VCIExchangeRecord,
pop: PopResult,
context: AdminRequestContext,
):
"""Return signed credential in JWT format."""
if not types_are_subset(body.get("types"), supported.format_data.get("types")):
raise CredIssueError("Requested types does not match offer.")

current_time = datetime.datetime.now(datetime.timezone.utc)
current_time_unix_timestamp = int(current_time.timestamp())
formatted_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ")
cred_id = str(uuid.uuid4())

# note: Some wallets require that the "jti" and "id" are a uri
payload = {
"vc": {
**(supported.vc_additional_data or {}),
"id": f"urn:uuid:{cred_id}",
"issuer": ex_record.issuer_id,
"issuanceDate": formatted_time,
"credentialSubject": {
**(ex_record.credential_subject or {}),
"id": pop.holder_kid,
},
},
"iss": ex_record.issuer_id,
"nbf": current_time_unix_timestamp,
"jti": f"urn:uuid:{cred_id}",
"sub": pop.holder_kid,
}

jws = await jwt_sign(
context.profile,
{},
payload,
verification_method=ex_record.verification_method,
)

return jws
1 change: 1 addition & 0 deletions jwt_vc_json/jwt_vc_json/v1_0/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""CredentialProcessor test."""
62 changes: 62 additions & 0 deletions jwt_vc_json/jwt_vc_json/v1_0/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pytest
from unittest.mock import MagicMock

from aries_cloudagent.admin.request_context import AdminRequestContext

from oid4vci.models.exchange import OID4VCIExchangeRecord
from oid4vci.models.supported_cred import SupportedCredential
from oid4vci.public_routes import PopResult


@pytest.fixture
def body():
items = {"format": "jwt_vc_json", "types": ["OntarioTestPhotoCard"], "proof": {}}
mock = MagicMock()
mock.__getitem__ = lambda _, k: items[k]
yield mock


@pytest.fixture
def supported():
yield SupportedCredential(
format_data={"types": ["VerifiableCredential", "PhotoCard"]},
vc_additional_data={
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://issuer-controller1.stg.ngrok.io/url/schema/photo-card.jsonld",
],
"type": ["VerifiableCredential", "PhotoCard"],
},
)


@pytest.fixture
def ex_record():
yield OID4VCIExchangeRecord(
state=OID4VCIExchangeRecord.STATE_OFFER_CREATED,
verification_method="did:example:123#key-1",
issuer_id="did:example:123",
supported_cred_id="456",
credential_subject={"name": "alice"},
nonce="789",
pin="000",
code="111",
token="222",
)


@pytest.fixture
def pop():
yield PopResult(
headers=None,
payload=None,
verified=True,
holder_kid="did:key:example-kid#0",
holder_jwk=None,
)


@pytest.fixture
def context():
"""Test AdminRequestContext."""
yield AdminRequestContext.test_context()
29 changes: 29 additions & 0 deletions jwt_vc_json/jwt_vc_json/v1_0/tests/test_cred_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest
from aries_cloudagent.admin.request_context import AdminRequestContext

from oid4vci.models.exchange import OID4VCIExchangeRecord
from oid4vci.models.supported_cred import SupportedCredential
from oid4vci.public_routes import PopResult

from ..cred_processor import CredProcessor


class TestCredentialProcessor:
"""Tests for CredentialProcessor."""

@pytest.mark.asyncio
async def test_issue_credential(
self,
body: any,
supported: SupportedCredential,
ex_record: OID4VCIExchangeRecord,
pop: PopResult,
context: AdminRequestContext,
):
"""Test issue_credential method."""

cred_processor = CredProcessor()

jws = cred_processor.issue_cred(body, supported, ex_record, pop, context)

assert jws
12 changes: 12 additions & 0 deletions jwt_vc_json/jwt_vc_json/v1_0/tests/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pytest

from ..cred_processor import CredProcessor


@pytest.mark.asyncio
async def test__init__():
"""Test __init."""

cred_processor = CredProcessor()

assert cred_processor
Loading

0 comments on commit f0ddcdc

Please sign in to comment.