Skip to content

Commit f4d28b6

Browse files
pavanputhraclaude
andauthored
Add SCITT storage backend for post-chain transparency registration (#155)
Ports the storage/scitt module from PR #136 to the new common/storage/ path. Registers per-participant SCITT entries as a storage backend without writing receipts back to Redis (avoids races with parallel storage writers). Supports signing_key_pem (base64 env var) and signing_key_path, consistent with the scitt link. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 262d262 commit f4d28b6

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

common/storage/scitt/__init__.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import base64
2+
import hashlib
3+
from ecdsa import SigningKey
4+
from links.scitt import create_hashed_signed_statement, register_signed_statement
5+
from lib.vcon_redis import VconRedis
6+
from lib.logging_utils import init_logger
7+
8+
logger = init_logger(__name__)
9+
10+
default_options = {
11+
"scrapi_url": "http://scittles:8000",
12+
"signing_key_pem": None, # Base64-encoded PEM (preferred for containers/k8s)
13+
"signing_key_path": "/etc/scitt/signing-key.pem", # Fallback for local dev
14+
"issuer": "conserver",
15+
"key_id": "conserver-key-1",
16+
"operations": ["vcon_enhanced"],
17+
}
18+
19+
20+
def save(vcon_id, opts=default_options):
21+
"""Register per-participant SCITT entries for a vCon as a storage backend.
22+
23+
Runs in parallel with other storages (e.g. webhook). Does NOT write receipts
24+
back to the vCon in Redis to avoid races with parallel storage writers.
25+
The transparency service is the authoritative store for receipts.
26+
27+
Each party with a tel field gets a separate SCITT entry per operation, with
28+
subject=tel:+number for portal queryability. Falls back to vcon://{vcon_id}
29+
when no parties have tel.
30+
"""
31+
merged = default_options.copy()
32+
merged.update(opts)
33+
opts = merged
34+
35+
vcon_redis = VconRedis()
36+
vcon = vcon_redis.get_vcon(vcon_id)
37+
if not vcon:
38+
logger.warning("scitt storage: vCon not found: %s", vcon_id)
39+
return
40+
41+
payload = vcon.hash
42+
43+
if opts.get("signing_key_pem"):
44+
pem = base64.b64decode(opts["signing_key_pem"]).decode("utf-8")
45+
signing_key = SigningKey.from_pem(pem, hashlib.sha256)
46+
else:
47+
signing_key = create_hashed_signed_statement.open_signing_key(opts["signing_key_path"])
48+
49+
party_tels = []
50+
for party in (vcon.parties or []):
51+
tel = party.get("tel") if isinstance(party, dict) else getattr(party, "tel", None)
52+
if tel:
53+
party_tels.append(tel)
54+
else:
55+
logger.warning("scitt storage: party without tel in %s, skipping", vcon_id)
56+
57+
if not party_tels:
58+
party_tels = [None]
59+
60+
scrapi_url = opts["scrapi_url"]
61+
62+
for operation in opts.get("operations", ["vcon_enhanced"]):
63+
for tel in party_tels:
64+
if tel:
65+
subject = f"tel:{tel}"
66+
operation_payload = f"{payload}:{operation}:{tel}"
67+
meta_map = {"vcon_operation": operation, "party_tel": tel}
68+
else:
69+
subject = f"vcon://{vcon_id}"
70+
operation_payload = f"{payload}:{operation}"
71+
meta_map = {"vcon_operation": operation}
72+
73+
signed_statement = create_hashed_signed_statement.create_hashed_signed_statement(
74+
issuer=opts["issuer"],
75+
signing_key=signing_key,
76+
subject=subject,
77+
kid=opts["key_id"].encode("utf-8"),
78+
meta_map=meta_map,
79+
payload=operation_payload.encode("utf-8"),
80+
payload_hash_alg="SHA-256",
81+
payload_location="",
82+
pre_image_content_type="application/vcon+json",
83+
)
84+
85+
result = register_signed_statement.register_statement(scrapi_url, signed_statement)
86+
logger.info(
87+
"scitt storage: registered %s entry_id=%s subject=%s for %s",
88+
operation,
89+
result["entry_id"],
90+
subject,
91+
vcon_id,
92+
)

0 commit comments

Comments
 (0)