Skip to content

Commit f0ddcdc

Browse files
authored
Modular credential format support for oid4vci (openwallet-foundation#772)
* 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>
1 parent acf88cb commit f0ddcdc

52 files changed

Lines changed: 1660 additions & 81 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 194 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,200 @@
1+
###
2+
### Python
3+
###
4+
5+
# Byte-compiled / optimized / DLL files
16
__pycache__/
7+
*.py[cod]
8+
*$py.class
9+
10+
# C extensions
11+
*.so
12+
13+
# Distribution / packaging
14+
.Python
15+
build/
16+
develop-eggs/
17+
dist/
18+
downloads/
19+
eggs/
20+
.eggs/
21+
lib/
22+
lib64/
23+
parts/
24+
sdist/
25+
var/
26+
wheels/
27+
*.egg-info/
28+
.installed.cfg
29+
*.egg
30+
MANIFEST
31+
32+
# PyInstaller
33+
# Usually these files are written by a python script from a template
34+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
35+
*.manifest
36+
*.spec
37+
38+
# Installer logs
39+
pip-log.txt
40+
pip-delete-this-directory.txt
41+
42+
# Unit test / coverage reports
43+
htmlcov/
44+
.tox/
45+
.coverage
46+
.coverage.*
47+
.cache
48+
nosetests.xml
49+
coverage.xml
50+
*.cover
51+
.hypothesis/
252
.pytest_cache/
53+
test-reports/
54+
55+
# Translations
56+
*.mo
57+
*.pot
58+
59+
# Django stuff:
60+
*.log
61+
*.lock
62+
local_settings.py
63+
db.sqlite3
64+
65+
# Flask stuff:
66+
instance/
67+
.webassets-cache
68+
69+
# Scrapy stuff:
70+
.scrapy
71+
72+
# Sphinx documentation
73+
docs/_build/
74+
75+
# PyBuilder
76+
target/
77+
78+
# Jupyter Notebook
79+
.ipynb_checkpoints
80+
81+
# pyenv
82+
.python-version
83+
84+
# celery beat schedule file
85+
celerybeat-schedule
86+
87+
# SageMath parsed files
88+
*.sage.py
89+
90+
# Environments
91+
.env
92+
.venv
93+
env/
94+
venv/
95+
ENV/
96+
env.bak/
97+
venv.bak/
98+
Pipfile
99+
Pipfile.lock
100+
101+
# Spyder project settings
102+
.spyderproject
103+
.spyproject
104+
105+
# Rope project settings
106+
.ropeproject
107+
108+
# mkdocs documentation
109+
/site
110+
111+
# mypy
112+
.mypy_cache/
113+
114+
###
115+
### Visual Studio Code
116+
###
117+
118+
.vscode/
119+
120+
###
121+
### MacOS
122+
###
123+
124+
# General
125+
.DS_Store
126+
.AppleDouble
127+
.LSOverride
128+
129+
# Icon must end with two \r
130+
Icon
131+
132+
# Thumbnails
133+
._*
134+
135+
# Files that might appear in the root of a volume
136+
.DocumentRevisions-V100
137+
.fseventsd
138+
.Spotlight-V100
139+
.TemporaryItems
140+
.Trashes
141+
.VolumeIcon.icns
142+
.com.apple.timemachine.donotpresent
143+
144+
# Directories potentially created on remote AFP share
145+
.AppleDB
146+
.AppleDesktop
147+
Network Trash Folder
148+
Temporary Items
149+
.apdisk
150+
151+
###
152+
### IntelliJ IDEs
153+
###
154+
155+
.idea/*
156+
**/.idea/*
157+
158+
###
159+
### Windows
160+
###
161+
162+
# Windows thumbnail cache files
163+
Thumbs.db
164+
ehthumbs.db
165+
ehthumbs_vista.db
166+
167+
# Dump file
168+
*.stackdump
169+
170+
# Folder config file
171+
[Dd]esktop.ini
172+
173+
# Recycle Bin used on file shares
174+
$RECYCLE.BIN/
175+
176+
# Windows Installer files
177+
*.cab
178+
*.msi
179+
*.msix
180+
*.msm
181+
*.msp
182+
183+
# Windows shortcuts
184+
*.lnk
185+
186+
# Docs build
187+
_build/
188+
**/*.iml
189+
190+
# Open API build
191+
open-api/.build
192+
193+
# devcontainer
194+
.pytest.ini
195+
196+
# project specific
3197
.ruff_cache/
4198
.test-reports/
5199
**/test-reports/
6-
.coverage
7-
coverage.xml
8200
settings.json
9-
.env

jwt_vc_json/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# JWT_VC_JSON credential format plugin
2+
3+
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.
4+
5+
## Configuration:
6+
7+
No configuration is required for this plugin.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""jwt_vc_json credential handler plugin."""
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Initialize processor."""
2+
3+
from .cred_processor import CredProcessor
4+
5+
6+
cred_processor = CredProcessor()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Issue a jwt_vc_json credential."""
2+
3+
import datetime
4+
import logging
5+
import uuid
6+
7+
from aries_cloudagent.admin.request_context import AdminRequestContext
8+
from aries_cloudagent.wallet.jwt import jwt_sign
9+
10+
from oid4vci.models.exchange import OID4VCIExchangeRecord
11+
from oid4vci.models.supported_cred import SupportedCredential
12+
from oid4vci.public_routes import types_are_subset
13+
from oid4vci.pop_result import PopResult
14+
from oid4vci.cred_processor import ICredProcessor, CredIssueError
15+
16+
LOGGER = logging.getLogger(__name__)
17+
18+
19+
class CredProcessor(ICredProcessor):
20+
"""Credential processor class for jwt_vc_json format."""
21+
22+
async def issue_cred(
23+
self,
24+
body: any,
25+
supported: SupportedCredential,
26+
ex_record: OID4VCIExchangeRecord,
27+
pop: PopResult,
28+
context: AdminRequestContext,
29+
):
30+
"""Return signed credential in JWT format."""
31+
if not types_are_subset(body.get("types"), supported.format_data.get("types")):
32+
raise CredIssueError("Requested types does not match offer.")
33+
34+
current_time = datetime.datetime.now(datetime.timezone.utc)
35+
current_time_unix_timestamp = int(current_time.timestamp())
36+
formatted_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ")
37+
cred_id = str(uuid.uuid4())
38+
39+
# note: Some wallets require that the "jti" and "id" are a uri
40+
payload = {
41+
"vc": {
42+
**(supported.vc_additional_data or {}),
43+
"id": f"urn:uuid:{cred_id}",
44+
"issuer": ex_record.issuer_id,
45+
"issuanceDate": formatted_time,
46+
"credentialSubject": {
47+
**(ex_record.credential_subject or {}),
48+
"id": pop.holder_kid,
49+
},
50+
},
51+
"iss": ex_record.issuer_id,
52+
"nbf": current_time_unix_timestamp,
53+
"jti": f"urn:uuid:{cred_id}",
54+
"sub": pop.holder_kid,
55+
}
56+
57+
jws = await jwt_sign(
58+
context.profile,
59+
{},
60+
payload,
61+
verification_method=ex_record.verification_method,
62+
)
63+
64+
return jws
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""CredentialProcessor test."""
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import pytest
2+
from unittest.mock import MagicMock
3+
4+
from aries_cloudagent.admin.request_context import AdminRequestContext
5+
6+
from oid4vci.models.exchange import OID4VCIExchangeRecord
7+
from oid4vci.models.supported_cred import SupportedCredential
8+
from oid4vci.public_routes import PopResult
9+
10+
11+
@pytest.fixture
12+
def body():
13+
items = {"format": "jwt_vc_json", "types": ["OntarioTestPhotoCard"], "proof": {}}
14+
mock = MagicMock()
15+
mock.__getitem__ = lambda _, k: items[k]
16+
yield mock
17+
18+
19+
@pytest.fixture
20+
def supported():
21+
yield SupportedCredential(
22+
format_data={"types": ["VerifiableCredential", "PhotoCard"]},
23+
vc_additional_data={
24+
"@context": [
25+
"https://www.w3.org/2018/credentials/v1",
26+
"https://issuer-controller1.stg.ngrok.io/url/schema/photo-card.jsonld",
27+
],
28+
"type": ["VerifiableCredential", "PhotoCard"],
29+
},
30+
)
31+
32+
33+
@pytest.fixture
34+
def ex_record():
35+
yield OID4VCIExchangeRecord(
36+
state=OID4VCIExchangeRecord.STATE_OFFER_CREATED,
37+
verification_method="did:example:123#key-1",
38+
issuer_id="did:example:123",
39+
supported_cred_id="456",
40+
credential_subject={"name": "alice"},
41+
nonce="789",
42+
pin="000",
43+
code="111",
44+
token="222",
45+
)
46+
47+
48+
@pytest.fixture
49+
def pop():
50+
yield PopResult(
51+
headers=None,
52+
payload=None,
53+
verified=True,
54+
holder_kid="did:key:example-kid#0",
55+
holder_jwk=None,
56+
)
57+
58+
59+
@pytest.fixture
60+
def context():
61+
"""Test AdminRequestContext."""
62+
yield AdminRequestContext.test_context()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
from aries_cloudagent.admin.request_context import AdminRequestContext
3+
4+
from oid4vci.models.exchange import OID4VCIExchangeRecord
5+
from oid4vci.models.supported_cred import SupportedCredential
6+
from oid4vci.public_routes import PopResult
7+
8+
from ..cred_processor import CredProcessor
9+
10+
11+
class TestCredentialProcessor:
12+
"""Tests for CredentialProcessor."""
13+
14+
@pytest.mark.asyncio
15+
async def test_issue_credential(
16+
self,
17+
body: any,
18+
supported: SupportedCredential,
19+
ex_record: OID4VCIExchangeRecord,
20+
pop: PopResult,
21+
context: AdminRequestContext,
22+
):
23+
"""Test issue_credential method."""
24+
25+
cred_processor = CredProcessor()
26+
27+
jws = cred_processor.issue_cred(body, supported, ex_record, pop, context)
28+
29+
assert jws
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import pytest
2+
3+
from ..cred_processor import CredProcessor
4+
5+
6+
@pytest.mark.asyncio
7+
async def test__init__():
8+
"""Test __init."""
9+
10+
cred_processor = CredProcessor()
11+
12+
assert cred_processor

0 commit comments

Comments
 (0)