Verification
These functions check evidence you have been given, rather than producing any.
These functions check evidence you have been given, rather than producing any. They are the Python counterpart of the checks the Integrity explorer runs in the browser, and they answer two separate questions:
- Is this statement still what it says it is? Every lineage statement is identified by a hash of
its own canonicalized content, so recomputing that hash and comparing it with the statement’s
@iddetects any modification after the fact. - Is this credential’s signature good, and is it about the statement I think it is? A valid signature over some other subject proves nothing about the statement in hand, so the two checks belong together.
Both run fully offline. JSON-LD contexts resolve against documents compiled into the package or
supplied by you via contexts=, and credentials are restricted to DID methods that can be resolved
from the identifier itself, so verification never reaches the network. This is what makes them usable
in an air-gapped or reproducible pipeline.
There is no manifest-level entry point. Verifying a whole manifest means looping these two functions
over its statements, which is what the explorer does:
import json
from eqty_sdk import verify_statement, verify_vc
manifest = json.loads(open("manifest.json").read())
contexts = manifest.get("contexts")
for statement in manifest["statements"].values():
if not verify_statement(json.dumps(statement), contexts):
print("modified since it was created:", statement["@id"])verify_statement
Pass the manifest’s contexts map whenever you have one. Statements reference their context by CID
("@context": "urn:cid:..."), and while the common contexts ship inside the package, a statement
written against a custom context cannot be canonicalized without it.
A True result means every field the statement’s @context defines is unmodified. It does not mean
the bytes are unmodified: the identifier commits to the statement’s canonicalized RDF, and JSON-LD
expansion drops keys the context does not define.
eqty_sdk.verify_statement
verify_statement(statement_json: str, contexts: Optional[Dict[str, Any]] = None) -> boolVerifies that a lineage statement’s content still hashes to its @id. Canonicalizes the statement as JSON-LD, recomputes its BLAKE3 RDFC CID and compares it with the @id the statement carries. Returns False when they differ, meaning the statement was modified after it was created. contexts maps a JSON-LD context URI to its context document, for contexts this build does not embed. Values may be dicts or JSON strings, so a manifest’s contexts field can be passed straight through. Note that a supplied context takes precedence over an embedded one of the same URI. Runs fully offline. A context that is neither embedded nor supplied raises rather than being fetched. A True result means every field the statement’s @context defines is unmodified. It does not mean the bytes are unmodified: the identifier commits to the statement’s canonicalized RDF, and JSON-LD expansion drops keys the context does not define. Raises ValueError if the statement is not a JSON object or has no string @id, and RuntimeError if a context cannot be resolved.
verify_vc
Supply statement_id whenever you know which statement the credential is supposed to attest. Without
it, only the signature is checked.
Supply contexts whenever the credential’s @context references a document this build does not
embed. Verification re-expands the credential, so an unresolvable context means the proof cannot be
checked at all — and because nothing is fetched, that surfaces as False rather than an error,
alongside a bad signature. If a credential you expect to be valid returns False, call
verify_vc_detailed to see why; an unsupplied context reads
unresolved_context.
A credential’s validFrom and validUntil are judged against now, before its proof. One that
has since expired therefore returns False without its signature ever being checked, which is the
usual state of a short-lived credential by the time anyone verifies the lineage it vouches for. Pass
at= a time inside its validity period, as an RFC 3339 string or a timezone-aware datetime, to
check the signature as of then. at has no
effect on credentials with issuanceDate: they are checked by an older verifier that always judges
dates at the current time, so the result is the same as without at. verify_vc_detailed reports
this as at_applied: False.
This verifies the cryptographic proof alone. Whether a credential has since been revoked or suspended lives in its status list, which has to be fetched over the network and is deliberately not consulted here.
eqty_sdk.verify_vc
verify_vc(vc_json: str, statement_id: Optional[str] = None, contexts: Optional[Dict[str, Any]] = None, at: Optional[Union[str, datetime]] = None) -> boolVerifies a W3C Verifiable Credential’s proof offline. When statement_id is given, also checks that the credential’s credentialSubject.id is that statement. A valid signature over some other subject says nothing about the statement in hand. Returns False when the credential does not verify or is bound to a different subject. Raises ValueError when the input is not a credential, meaning it is not JSON, has no credentialSubject, or has more than one subject. Raises RuntimeError when the credential’s DID method would need network access: only did:key, did:jwk and did:pkh resolve offline. Checks the cryptographic proof only. Revocation and suspension live in the credential’s status list, which is fetched over the network and is not consulted here. contexts maps a JSON-LD context URI to its context document, for contexts this build does not embed. Values may be dicts or JSON strings, so a manifest’s contexts field can be passed straight through. Note that a supplied context takes precedence over an embedded one of the same URI. Runs fully offline. Verifying a credential re-expands it, so a context that is neither embedded nor supplied is never fetched — it reports False, alongside the other reasons a proof may not check out. at, an RFC 3339 timestamp or a timezone-aware datetime, judges the credential’s validFrom and validUntil at that moment instead of now. Those dates are checked before the proof, so without it a credential that has since expired returns False with its signature never checked. Pass a time inside its validity period, such as the proof’s created when that falls inside it, to learn whether it was genuinely signed and in force then. at has no effect on credentials with issuanceDate: they are checked by an older verifier that always judges dates at the current time, so the result is the same as without at. verify_vc_detailed reports this as at_applied: False. Raises ValueError if at is a string that is not an RFC 3339 timestamp or a naive datetime, and TypeError if it is neither a string nor a datetime.
verify_vc_detailed
The same check as verify_vc, with the same arguments and the same exceptions, returning a dict
instead of a bool: valid, reason (None when valid, otherwise a code), detail and
at_applied (whether the dates were judged at at). Use it whenever False is not enough, for
example to tell an expired credential from a forged one:
import json
from eqty_sdk import verify_vc_detailed
result = verify_vc_detailed(json.dumps(credential), statement_id, contexts)
if result["reason"] == "expired":
# The signature was not checked. Judge it as of a time inside its validity period.
result = verify_vc_detailed(json.dumps(credential), statement_id, contexts, at=when_it_was_used)expired and not_yet_valid mean the dates excluded the time it was judged at and the signature
was not checked. invalid_signature is a verdict on the signature. unsupported_suite,
unresolved_context, unresolved_key and unverifiable mean the proof could not be completed,
because its suite is not supported here, a context did not load, the issuer’s key did not resolve,
or for another reason. malformed means the input does not parse as a credential, or its JSON-LD
does not expand. Every reason is a rejection, never “not checked”: a credential’s suite,
contexts, key and shape are part of it, so an altered credential can produce any of them. The
reason is read from the verifier’s typed error, never from its message.
at_applied is False when at was given but the credential has issuanceDate (it was judged
now, so valid: True means valid now, not at at), and when the credential was never checked
(malformed, subject_mismatch, or unverifiable when verification failed for an internal reason
before any check). In the type stubs the result is the VcVerification TypedDict.
eqty_sdk.verify_vc_detailed
verify_vc_detailed(vc_json: str, statement_id: Optional[str] = None, contexts: Optional[Dict[str, Any]] = None, at: Optional[Union[str, datetime]] = None) -> VcVerificationVerifies a W3C Verifiable Credential’s proof offline, and says why when it does not verify. Takes the same arguments as verify_vc and raises in the same cases. Returns a dict: valid (bool), reason (None when valid, otherwise a code), detail (the verifier’s message, or None) and at_applied (bool). at_applied says whether the credential’s dates were judged at at (or now, without at). It is False when the credential was never checked (malformed, subject_mismatch, or unverifiable when verification failed for an internal reason before any check), and when at was given but the credential has issuanceDate: its verifier judges dates only now, so valid: True then means valid now, not at at. Every reason is a rejection, never “not checked”: a credential’s suite, contexts, key and shape are part of it, so an altered credential can produce any of these codes. The reason codes are: - expired, not_yet_valid: the credential’s validUntil / validFrom excludes the time it was judged at (now, or at). Its signature was not checked, because dates are validated first; verify again with at inside its validity period to check it. - invalid_signature: the signature does not verify under the issuer’s key. - missing_proof: the credential has no proof. - invalid_proof: the proof does not fit the key or algorithm it names. - malformed: the credential does not parse as a credential (for example a signature that is not a JWS), or its JSON-LD does not expand. - invalid_claims: another claim failed validation. - subject_mismatch: the credential is about a subject other than statement_id. - unsupported_suite: the proof’s cryptographic suite is not one this build verifies, so the proof could not be completed. - unresolved_context: a JSON-LD context the credential names is neither embedded nor supplied in contexts, so it did not load. - unresolved_key: the issuer’s DID did not resolve, or its document has no such key. - unverifiable: the proof could not be completed, for a reason none of the above names. - legacy_rejected: a credential with issuanceDate failed the legacy verifier.