tba-profile-credential-sd-jwt-vc 0.4.0.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/PKG-INFO +16 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/pyproject.toml +10 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/__init__.py +91 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/artifact.py +29 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/bindings.py +37 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/claims.py +66 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/compatibility.py +37 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/errors.py +20 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/features.py +24 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/metadata.py +35 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/migrations.py +16 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/py.typed +0 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/schemas.py +31 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/status.py +28 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/validation.py +37 -0
- tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/versions.py +26 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tba-profile-credential-sd-jwt-vc
|
|
3
|
+
Version: 0.4.0.dev2
|
|
4
|
+
Summary:
|
|
5
|
+
Requires-Python: >=3.10,<3.15
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Dist: tba-concrete-claim-credential-status (==0.4.0.dev2)
|
|
13
|
+
Requires-Dist: tba-concrete-claim-credential-type (==0.4.0.dev2)
|
|
14
|
+
Requires-Dist: tba-concrete-sd-jwt (==0.4.0.dev2)
|
|
15
|
+
Requires-Dist: tba-concrete-sd-jwt-vc-credential (==0.4.0.dev2)
|
|
16
|
+
Requires-Dist: tba-contracts-identity (==0.4.0.dev2)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name="tba-profile-credential-sd-jwt-vc"
|
|
3
|
+
version="0.4.0.dev2"
|
|
4
|
+
requires-python=">=3.10,<3.15"
|
|
5
|
+
dependencies=["tba-contracts-identity==0.4.0.dev2","tba-concrete-claim-credential-type==0.4.0.dev2","tba-concrete-claim-credential-status==0.4.0.dev2","tba-concrete-sd-jwt==0.4.0.dev2","tba-concrete-sd-jwt-vc-credential==0.4.0.dev2"]
|
|
6
|
+
[build-system]
|
|
7
|
+
requires=["poetry-core>=1.0.0"]
|
|
8
|
+
build-backend="poetry.core.masonry.api"
|
|
9
|
+
[tool.poetry]
|
|
10
|
+
packages=[{include="tba_profile_credential_sd_jwt_vc",from="src"}]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Versioned SD-JWT VC draft-profile ownership."""
|
|
2
|
+
|
|
3
|
+
from tba_contracts_identity.protocol_processing import (
|
|
4
|
+
build_protocol_capability_report,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from .bindings import CAPABILITY_REQUIREMENTS
|
|
8
|
+
from .artifact import parse_sd_jwt_vc
|
|
9
|
+
from .claims import (
|
|
10
|
+
SD_JWT_VC_CLAIM_CLASSES,
|
|
11
|
+
SdJwtVcClaimSetPayload,
|
|
12
|
+
compose_sd_jwt_vc_claim_set,
|
|
13
|
+
parse_sd_jwt_vc_claims,
|
|
14
|
+
)
|
|
15
|
+
from .compatibility import (
|
|
16
|
+
COMPATIBILITY_PATHS,
|
|
17
|
+
SdJwtVcCompatibility,
|
|
18
|
+
compatibility,
|
|
19
|
+
)
|
|
20
|
+
from .errors import (
|
|
21
|
+
SdJwtVcKeyBindingError,
|
|
22
|
+
SdJwtVcProfileError,
|
|
23
|
+
UnsupportedSdJwtVcMediaTypeError,
|
|
24
|
+
)
|
|
25
|
+
from .features import FEATURES_BY_VERSION, supports
|
|
26
|
+
from .migrations import migrate_claims
|
|
27
|
+
from .metadata import SdJwtVcTypeMetadata, parse_type_metadata
|
|
28
|
+
from .schemas import (
|
|
29
|
+
LEGACY_SD_JWT_VC_TYP,
|
|
30
|
+
SD_JWT_VC_CARRIER,
|
|
31
|
+
SdJwtVcCarrier,
|
|
32
|
+
select_carrier,
|
|
33
|
+
)
|
|
34
|
+
from .versions import CURRENT_VERSION, VERSION_HISTORY, SdJwtVcVersion, select_version
|
|
35
|
+
from .status import SdJwtVcStatusReference, parse_status_reference
|
|
36
|
+
from .validation import validate_sd_jwt_vc
|
|
37
|
+
from tba_concrete_sd_jwt_vc_credential import SdJwtVcCredential
|
|
38
|
+
|
|
39
|
+
DRAFT_REVISION = "draft-ietf-oauth-sd-jwt-vc-17"
|
|
40
|
+
MEDIA_TYPE = SD_JWT_VC_CARRIER.media_type
|
|
41
|
+
TYP = SD_JWT_VC_CARRIER.typ
|
|
42
|
+
SdJwtVc = SdJwtVcCredential
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def capability_report() -> dict[str, object]:
|
|
46
|
+
return build_protocol_capability_report(
|
|
47
|
+
protocol="sd-jwt-vc",
|
|
48
|
+
revision=CURRENT_VERSION.identifier,
|
|
49
|
+
features=tuple(FEATURES_BY_VERSION[CURRENT_VERSION.identifier]),
|
|
50
|
+
evidence_links=("tests/unit/test_versioned_eat_sd_jwt_vc_profiles.py",),
|
|
51
|
+
extra_requirements=CAPABILITY_REQUIREMENTS,
|
|
52
|
+
include_default_artifact_requirements=False,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"CAPABILITY_REQUIREMENTS",
|
|
58
|
+
"COMPATIBILITY_PATHS",
|
|
59
|
+
"CURRENT_VERSION",
|
|
60
|
+
"DRAFT_REVISION",
|
|
61
|
+
"FEATURES_BY_VERSION",
|
|
62
|
+
"LEGACY_SD_JWT_VC_TYP",
|
|
63
|
+
"MEDIA_TYPE",
|
|
64
|
+
"SD_JWT_VC_CARRIER",
|
|
65
|
+
"SD_JWT_VC_CLAIM_CLASSES",
|
|
66
|
+
"SdJwtVcClaimSetPayload",
|
|
67
|
+
"SdJwtVcCredential",
|
|
68
|
+
"SdJwtVcStatusReference",
|
|
69
|
+
"SdJwtVcTypeMetadata",
|
|
70
|
+
"VERSION_HISTORY",
|
|
71
|
+
"SdJwtVcCarrier",
|
|
72
|
+
"SdJwtVc",
|
|
73
|
+
"SdJwtVcCompatibility",
|
|
74
|
+
"SdJwtVcKeyBindingError",
|
|
75
|
+
"SdJwtVcProfileError",
|
|
76
|
+
"SdJwtVcVersion",
|
|
77
|
+
"TYP",
|
|
78
|
+
"UnsupportedSdJwtVcMediaTypeError",
|
|
79
|
+
"capability_report",
|
|
80
|
+
"compatibility",
|
|
81
|
+
"compose_sd_jwt_vc_claim_set",
|
|
82
|
+
"migrate_claims",
|
|
83
|
+
"parse_sd_jwt_vc",
|
|
84
|
+
"parse_sd_jwt_vc_claims",
|
|
85
|
+
"parse_status_reference",
|
|
86
|
+
"parse_type_metadata",
|
|
87
|
+
"select_carrier",
|
|
88
|
+
"select_version",
|
|
89
|
+
"supports",
|
|
90
|
+
"validate_sd_jwt_vc",
|
|
91
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from typing import Mapping
|
|
4
|
+
|
|
5
|
+
from tba_concrete_sd_jwt import parse_sd_jwt_serialization
|
|
6
|
+
from tba_concrete_sd_jwt_vc_credential import SdJwtVcCredential
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _decode_object(segment: str) -> Mapping[str, object]:
|
|
10
|
+
try:
|
|
11
|
+
value = json.loads(
|
|
12
|
+
base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4))
|
|
13
|
+
)
|
|
14
|
+
except (ValueError, UnicodeDecodeError) as exc:
|
|
15
|
+
raise ValueError("invalid SD-JWT VC JSON segment") from exc
|
|
16
|
+
if not isinstance(value, dict):
|
|
17
|
+
raise ValueError("SD-JWT VC segment must be an object")
|
|
18
|
+
return value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_sd_jwt_vc(value: str) -> SdJwtVcCredential:
|
|
22
|
+
serialization = parse_sd_jwt_serialization(value)
|
|
23
|
+
header_segment, claims_segment, _ = serialization.issuer_jwt.split(".")
|
|
24
|
+
return SdJwtVcCredential(
|
|
25
|
+
serialization, _decode_object(header_segment), _decode_object(claims_segment)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ["parse_sd_jwt_vc"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""SD-JWT VC wire operations mapped to reportable capabilities."""
|
|
2
|
+
|
|
3
|
+
from tba_contracts_identity.capabilities import ProtocolCapabilityRequirement
|
|
4
|
+
|
|
5
|
+
from .versions import CURRENT_VERSION
|
|
6
|
+
|
|
7
|
+
CAPABILITY_REQUIREMENTS = (
|
|
8
|
+
ProtocolCapabilityRequirement(
|
|
9
|
+
"sd-jwt-vc",
|
|
10
|
+
CURRENT_VERSION.identifier,
|
|
11
|
+
"sd-jwt-vc-decode",
|
|
12
|
+
"application/dc+sd-jwt",
|
|
13
|
+
"artifact.processing",
|
|
14
|
+
"decode",
|
|
15
|
+
"sd-jwt-vc-disclosures",
|
|
16
|
+
),
|
|
17
|
+
ProtocolCapabilityRequirement(
|
|
18
|
+
"sd-jwt-vc",
|
|
19
|
+
CURRENT_VERSION.identifier,
|
|
20
|
+
"sd-jwt-vc-validation",
|
|
21
|
+
"issuer-signed JWT and disclosures",
|
|
22
|
+
"artifact.processing",
|
|
23
|
+
"validate",
|
|
24
|
+
"verified-sd-jwt-vc",
|
|
25
|
+
),
|
|
26
|
+
ProtocolCapabilityRequirement(
|
|
27
|
+
"sd-jwt-vc",
|
|
28
|
+
CURRENT_VERSION.identifier,
|
|
29
|
+
"sd-jwt-vc-encode",
|
|
30
|
+
"application/dc+sd-jwt",
|
|
31
|
+
"artifact.processing",
|
|
32
|
+
"encode",
|
|
33
|
+
"sd-jwt-vc",
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = ["CAPABILITY_REQUIREMENTS"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""SD-JWT VC claim-set composition."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
from tba_concrete_claim_credential_status import CredentialStatusClaim
|
|
7
|
+
from tba_concrete_claim_credential_type import CredentialTypeClaim
|
|
8
|
+
from tba_contracts_identity.claims import ClaimSet
|
|
9
|
+
|
|
10
|
+
from .versions import CURRENT_VERSION
|
|
11
|
+
|
|
12
|
+
SD_JWT_VC_CLAIM_CLASSES = (CredentialTypeClaim, CredentialStatusClaim)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class SdJwtVcClaimSetPayload:
|
|
17
|
+
credential_type: str
|
|
18
|
+
issuer: str | None = None
|
|
19
|
+
issued_at: int | None = None
|
|
20
|
+
not_before: int | None = None
|
|
21
|
+
expiration: int | None = None
|
|
22
|
+
confirmation: Mapping[str, object] | None = None
|
|
23
|
+
status: Mapping[str, object] | None = None
|
|
24
|
+
type_integrity: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_sd_jwt_vc_claims(claims: Mapping[str, object]) -> SdJwtVcClaimSetPayload:
|
|
28
|
+
credential_type = claims.get("vct")
|
|
29
|
+
if not isinstance(credential_type, str) or not credential_type:
|
|
30
|
+
raise ValueError("SD-JWT VC requires a non-empty vct claim")
|
|
31
|
+
for name in ("iat", "nbf", "exp"):
|
|
32
|
+
value = claims.get(name)
|
|
33
|
+
if value is not None and (
|
|
34
|
+
not isinstance(value, int) or isinstance(value, bool)
|
|
35
|
+
):
|
|
36
|
+
raise ValueError(f"{name} must be a NumericDate")
|
|
37
|
+
for name in ("cnf", "status"):
|
|
38
|
+
if name in claims and not isinstance(claims[name], Mapping):
|
|
39
|
+
raise ValueError(f"{name} must be an object")
|
|
40
|
+
integrity = claims.get("vct#integrity")
|
|
41
|
+
if integrity is not None and (not isinstance(integrity, str) or not integrity):
|
|
42
|
+
raise ValueError("vct#integrity must be an integrity metadata string")
|
|
43
|
+
return SdJwtVcClaimSetPayload(
|
|
44
|
+
credential_type,
|
|
45
|
+
claims.get("iss") if isinstance(claims.get("iss"), str) else None,
|
|
46
|
+
claims.get("iat"),
|
|
47
|
+
claims.get("nbf"),
|
|
48
|
+
claims.get("exp"),
|
|
49
|
+
claims.get("cnf"),
|
|
50
|
+
claims.get("status"),
|
|
51
|
+
integrity,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def compose_sd_jwt_vc_claim_set(*claims: object) -> ClaimSet:
|
|
56
|
+
if not any(isinstance(claim, CredentialTypeClaim) for claim in claims):
|
|
57
|
+
raise ValueError("SD-JWT VC requires vct")
|
|
58
|
+
return ClaimSet(tuple(claims), "sd-jwt-vc", CURRENT_VERSION.identifier)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"SD_JWT_VC_CLAIM_CLASSES",
|
|
63
|
+
"SdJwtVcClaimSetPayload",
|
|
64
|
+
"compose_sd_jwt_vc_claim_set",
|
|
65
|
+
"parse_sd_jwt_vc_claims",
|
|
66
|
+
]
|
tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/compatibility.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""SD-JWT VC draft compatibility paths."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .versions import CURRENT_VERSION, VERSION_HISTORY
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class SdJwtVcCompatibility:
|
|
10
|
+
source: str
|
|
11
|
+
target: str
|
|
12
|
+
compatible: bool
|
|
13
|
+
migration_required: bool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
COMPATIBILITY_PATHS = tuple(
|
|
17
|
+
SdJwtVcCompatibility(
|
|
18
|
+
version.identifier,
|
|
19
|
+
CURRENT_VERSION.identifier,
|
|
20
|
+
True,
|
|
21
|
+
version.identifier != CURRENT_VERSION.identifier,
|
|
22
|
+
)
|
|
23
|
+
for version in VERSION_HISTORY
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def compatibility(
|
|
28
|
+
source: str,
|
|
29
|
+
target: str = CURRENT_VERSION.identifier,
|
|
30
|
+
) -> SdJwtVcCompatibility:
|
|
31
|
+
for path in COMPATIBILITY_PATHS:
|
|
32
|
+
if path.source == source and path.target == target:
|
|
33
|
+
return path
|
|
34
|
+
return SdJwtVcCompatibility(source, target, False, False)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ["COMPATIBILITY_PATHS", "SdJwtVcCompatibility", "compatibility"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""SD-JWT VC profile mapping errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SdJwtVcProfileError(ValueError):
|
|
5
|
+
"""Base error for a selected SD-JWT VC draft mapping."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UnsupportedSdJwtVcMediaTypeError(SdJwtVcProfileError):
|
|
9
|
+
"""Raised when the current `dc+sd-jwt` carrier is not selected."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SdJwtVcKeyBindingError(SdJwtVcProfileError):
|
|
13
|
+
"""Raised when required KB-JWT holder binding is absent or invalid."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"SdJwtVcKeyBindingError",
|
|
18
|
+
"SdJwtVcProfileError",
|
|
19
|
+
"UnsupportedSdJwtVcMediaTypeError",
|
|
20
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
FEATURES_BY_VERSION = {
|
|
2
|
+
"draft-10": frozenset({"vct", "status", "key-binding"}),
|
|
3
|
+
"draft-13": frozenset({"vct", "status", "key-binding", "type-metadata"}),
|
|
4
|
+
"draft-17": frozenset(
|
|
5
|
+
{
|
|
6
|
+
"vct",
|
|
7
|
+
"status",
|
|
8
|
+
"key-binding",
|
|
9
|
+
"type-metadata",
|
|
10
|
+
"vct-integrity",
|
|
11
|
+
"dc-media-type",
|
|
12
|
+
}
|
|
13
|
+
),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def supports(feature: str, version: str = "draft-17") -> bool:
|
|
18
|
+
try:
|
|
19
|
+
return feature in FEATURES_BY_VERSION[version]
|
|
20
|
+
except KeyError as exc:
|
|
21
|
+
raise ValueError(f"unsupported SD-JWT VC version: {version}") from exc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["FEATURES_BY_VERSION", "supports"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Mapping, Sequence
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True, slots=True)
|
|
6
|
+
class SdJwtVcTypeMetadata:
|
|
7
|
+
credential_type: str
|
|
8
|
+
name: str | None = None
|
|
9
|
+
description: str | None = None
|
|
10
|
+
extends: str | None = None
|
|
11
|
+
claim_metadata: Sequence[Mapping[str, object]] = ()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_type_metadata(value: Mapping[str, object]) -> SdJwtVcTypeMetadata:
|
|
15
|
+
credential_type = value.get("vct")
|
|
16
|
+
if not isinstance(credential_type, str) or not credential_type:
|
|
17
|
+
raise ValueError("type metadata requires vct")
|
|
18
|
+
claims = value.get("claims", ())
|
|
19
|
+
if not isinstance(claims, list) or not all(
|
|
20
|
+
isinstance(item, Mapping) for item in claims
|
|
21
|
+
):
|
|
22
|
+
raise ValueError("type metadata claims must be an array of objects")
|
|
23
|
+
extends = value.get("extends")
|
|
24
|
+
if extends is not None and not isinstance(extends, str):
|
|
25
|
+
raise ValueError("extends must be a string")
|
|
26
|
+
return SdJwtVcTypeMetadata(
|
|
27
|
+
credential_type,
|
|
28
|
+
value.get("name") if isinstance(value.get("name"), str) else None,
|
|
29
|
+
value.get("description") if isinstance(value.get("description"), str) else None,
|
|
30
|
+
extends,
|
|
31
|
+
tuple(dict(item) for item in claims),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = ["SdJwtVcTypeMetadata", "parse_type_metadata"]
|
tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/migrations.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Any, Mapping
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def migrate_claims(
|
|
5
|
+
value: Mapping[str, Any], *, source: str, target: str = "draft-17"
|
|
6
|
+
) -> dict[str, Any]:
|
|
7
|
+
if source == target:
|
|
8
|
+
return dict(value)
|
|
9
|
+
if source not in {"draft-10", "draft-13"} or target != "draft-17":
|
|
10
|
+
raise ValueError(f"unsupported SD-JWT VC migration: {source} -> {target}")
|
|
11
|
+
if not isinstance(value.get("vct"), str):
|
|
12
|
+
raise ValueError("SD-JWT VC migration requires vct")
|
|
13
|
+
return dict(value)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["migrate_claims"]
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Selected SD-JWT VC carrier descriptors."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .errors import UnsupportedSdJwtVcMediaTypeError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class SdJwtVcCarrier:
|
|
10
|
+
media_type: str
|
|
11
|
+
typ: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SD_JWT_VC_CARRIER = SdJwtVcCarrier("application/dc+sd-jwt", "dc+sd-jwt")
|
|
15
|
+
LEGACY_SD_JWT_VC_TYP = "vc+sd-jwt"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def select_carrier(media_type: str) -> SdJwtVcCarrier:
|
|
19
|
+
if media_type != SD_JWT_VC_CARRIER.media_type:
|
|
20
|
+
raise UnsupportedSdJwtVcMediaTypeError(
|
|
21
|
+
f"unsupported SD-JWT VC media type: {media_type}"
|
|
22
|
+
)
|
|
23
|
+
return SD_JWT_VC_CARRIER
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"LEGACY_SD_JWT_VC_TYP",
|
|
28
|
+
"SD_JWT_VC_CARRIER",
|
|
29
|
+
"SdJwtVcCarrier",
|
|
30
|
+
"select_carrier",
|
|
31
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Mapping
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True, slots=True)
|
|
6
|
+
class SdJwtVcStatusReference:
|
|
7
|
+
mechanism: str
|
|
8
|
+
index: int | None = None
|
|
9
|
+
uri: str | None = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_status_reference(value: Mapping[str, object]) -> SdJwtVcStatusReference:
|
|
13
|
+
if len(value) != 1:
|
|
14
|
+
raise ValueError("status must select exactly one status mechanism")
|
|
15
|
+
mechanism, details = next(iter(value.items()))
|
|
16
|
+
if not isinstance(details, Mapping):
|
|
17
|
+
raise ValueError("status mechanism details must be an object")
|
|
18
|
+
index, uri = details.get("idx"), details.get("uri")
|
|
19
|
+
if index is not None and (
|
|
20
|
+
not isinstance(index, int) or isinstance(index, bool) or index < 0
|
|
21
|
+
):
|
|
22
|
+
raise ValueError("status index must be a non-negative integer")
|
|
23
|
+
if uri is not None and (not isinstance(uri, str) or not uri):
|
|
24
|
+
raise ValueError("status URI must be a non-empty string")
|
|
25
|
+
return SdJwtVcStatusReference(mechanism, index, uri)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ["SdJwtVcStatusReference", "parse_status_reference"]
|
tba_profile_credential_sd_jwt_vc-0.4.0.dev2/src/tba_profile_credential_sd_jwt_vc/validation.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from tba_concrete_sd_jwt import decode_disclosure, disclosure_digest
|
|
2
|
+
from tba_concrete_sd_jwt_vc_credential import SdJwtVcCredential
|
|
3
|
+
|
|
4
|
+
from .claims import parse_sd_jwt_vc_claims
|
|
5
|
+
from .schemas import LEGACY_SD_JWT_VC_TYP, SD_JWT_VC_CARRIER
|
|
6
|
+
from .status import parse_status_reference
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_sd_jwt_vc(
|
|
10
|
+
credential: SdJwtVcCredential, *, accept_legacy_typ: bool = False
|
|
11
|
+
) -> None:
|
|
12
|
+
accepted_types = (
|
|
13
|
+
{SD_JWT_VC_CARRIER.typ, LEGACY_SD_JWT_VC_TYP}
|
|
14
|
+
if accept_legacy_typ
|
|
15
|
+
else {SD_JWT_VC_CARRIER.typ}
|
|
16
|
+
)
|
|
17
|
+
if credential.header.get("typ") not in accepted_types:
|
|
18
|
+
raise ValueError("invalid SD-JWT VC typ")
|
|
19
|
+
parsed = parse_sd_jwt_vc_claims(credential.claims)
|
|
20
|
+
if parsed.status is not None:
|
|
21
|
+
parse_status_reference(parsed.status)
|
|
22
|
+
algorithm = str(credential.claims.get("_sd_alg", "sha-256")).replace("-", "")
|
|
23
|
+
referenced = credential.claims.get("_sd", [])
|
|
24
|
+
if not isinstance(referenced, list) or not all(
|
|
25
|
+
isinstance(item, str) for item in referenced
|
|
26
|
+
):
|
|
27
|
+
raise ValueError("_sd must be an array")
|
|
28
|
+
seen = set()
|
|
29
|
+
for encoded in credential.disclosures:
|
|
30
|
+
decode_disclosure(encoded)
|
|
31
|
+
digest = disclosure_digest(encoded, algorithm)
|
|
32
|
+
if digest not in referenced or digest in seen:
|
|
33
|
+
raise ValueError("unreferenced or duplicate SD-JWT VC disclosure")
|
|
34
|
+
seen.add(digest)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ["validate_sd_jwt_vc"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True, slots=True)
|
|
5
|
+
class SdJwtVcVersion:
|
|
6
|
+
identifier: str
|
|
7
|
+
status: str
|
|
8
|
+
published: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VERSION_HISTORY = (
|
|
12
|
+
SdJwtVcVersion("draft-10", "superseded-draft", "2024-09"),
|
|
13
|
+
SdJwtVcVersion("draft-13", "superseded-draft", "2025-02"),
|
|
14
|
+
SdJwtVcVersion("draft-17", "active-draft", "2025-07"),
|
|
15
|
+
)
|
|
16
|
+
CURRENT_VERSION = VERSION_HISTORY[-1]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def select_version(identifier: str = "draft-17") -> SdJwtVcVersion:
|
|
20
|
+
for version in VERSION_HISTORY:
|
|
21
|
+
if version.identifier == identifier:
|
|
22
|
+
return version
|
|
23
|
+
raise ValueError(f"unsupported SD-JWT VC version: {identifier}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["CURRENT_VERSION", "VERSION_HISTORY", "SdJwtVcVersion", "select_version"]
|