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

Modular credential format support for oid4vci #772

Merged
merged 8 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading