Delegated Signing¶
Harbour's delegated signing feature enables users to authorize blockchain transactions through any VC wallet, with a signing service executing on their behalf. This decouples wallet choice from blockchain capability.
Problem¶
Traditional blockchain transactions require a wallet that can both:
- Hold Verifiable Credentials (for identity)
- Sign blockchain transactions (for execution)
Currently, only specialized wallets (like Altme) offer both capabilities. This creates vendor lock-in and limits user choice.
Solution¶
Harbour separates these concerns:
- User's wallet: Holds credentials, creates consent proofs (VPs)
- Harbour signing service: Executes blockchain transactions on behalf of users
The key innovation is cryptographic proof of consent — the user's VP serves as auditable evidence that they authorized the transaction.
How It Works¶
User Signing Service Blockchain
| | |
| 1. Request transaction | |
| ─────────────────────► | |
| | |
| 2. Consent request | |
| ◄───────────────────── | |
| (OID4VP transaction_data,| |
| nonce, audience) | |
| | |
| 3. Create SD-JWT VP | |
| (consent proof with | |
| KB-JWT binding to | |
| transaction_data_hash) | |
| ─────────────────────► | |
| | |
| | 4. Verify VP |
| | ✓ Signature valid |
| | ✓ Credential valid |
| | ✓ Transaction matches |
| | |
| | 5. Execute transaction |
| | ─────────────────────► |
| | |
| | 6. Issue receipt VC |
| | (DelegatedSignature- |
| | Evidence + CRSet) |
| | |
User Setup¶
1. Harbour Credential¶
The user needs a Harbour credential (e.g., NaturalPersonCredential) issued as an SD-JWT-VC with disclosable claims:
{
"type": ["VerifiableCredential", "harbour:NaturalPersonCredential"],
"issuer": "did:ethr:0x14a34:0x31f1ca3dc5da9f83f360d805662d11a418950202",
"credentialSubject": {
"id": "did:ethr:0x14a34:0x26e4...16c9",
"type": "harbour:NaturalPerson",
"name": "Alice Smith", // ← Disclosable (PII)
"email": "alice.smith@example.com", // ← Disclosable (PII)
"memberOf": "did:ethr:0x14a34:0xf7ef...dab"
}
}
2. DID Document¶
The user's did:ethr DID document must expose the same P-256 public key as a
local #controller verification method:
{
"@context": [
"https://www.w3.org/ns/did/v1",
{
"JsonWebKey": "https://w3id.org/security#JsonWebKey",
"publicKeyJwk": {
"@id": "https://w3id.org/security#publicKeyJwk",
"@type": "@json"
}
}
],
"id": "did:ethr:0x14a34:0x26e4...16c9",
"verificationMethod": [
{
"id": "did:ethr:0x14a34:0x26e4...16c9#controller",
"type": "JsonWebKey",
"controller": "did:ethr:0x14a34:0x26e4...16c9",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
}
],
"authentication": [
"did:ethr:0x14a34:0x26e4...16c9#controller"
],
"assertionMethod": [
"did:ethr:0x14a34:0x26e4...16c9#controller"
]
}
See examples/did-ethr/ for complete DID documents.
Repository Boundary (did:ethr)¶
This repository verifies signatures and hash bindings, but it does not host or publish DID documents.
- Integrators must run the appropriate
did:ethrresolver for their Base deployment. - Integrators must pass the resolved holder key into
verify_sd_jwt_vp(...). - Repository examples now use
did:ethridentifiers for person subjects. Seeexamples/did-ethr/for static example DID documents used byexamples/*.json. - Naming policy in examples:
- All identifiers use UUID-based path segments (no real names or organization names in DID paths).
Current integration hooks and TODOs:
issue_sd_jwt_vp(..., holder_did=...)allows the wallet DID to be embedded in the consent VP.verify_sd_jwt_vp(..., holder_public_key=...)accepts the DID-resolved public key from your resolver stack.- TODO: Add optional resolver callback adapters for
did:ethrso verification can resolve custom P-256 controller keys in-process.
OID4VP Transaction Data¶
The signing service creates an OID4VP-aligned transaction data object (see Delegation Challenge Encoding):
{
"type": "harbour.delegate:data.purchase",
"credential_ids": ["harbour_natural_person"],
"transaction_data_hashes_alg": ["sha-256"],
"nonce": "da9b1009",
"iat": 1771934400,
"txn": {
"asset_id": "urn:uuid:550e8400-e29b-41d4-a716-446655440000",
"price": "100",
"currency": "ENVITED",
"marketplace": "did:ethr:0x14a34:0x89fe5e7f506d992f76bcba309773c0ee3ee6039c"
}
}
Naming note:
transaction_dataandcredential_idsare OID4VP-defined snake_case fields.txnis profile-defined payload; Harbour v1 standardizes snake_case keys such asasset_id.
Creating the Consent VP¶
When the signing service requests consent, the user creates an SD-JWT VP with:
- Selective disclosure: Only non-PII claims disclosed
- Evidence: Transaction data proving what was consented to
- KB-JWT: Bound to the transaction data hash
- Signature: Signed with the user's P-256 key
Python Example¶
from harbour.sd_jwt_vp import issue_sd_jwt_vp
# User's SD-JWT-VC (with all disclosures)
sd_jwt_vc = "eyJ...~disclosure1~disclosure2~..."
# Transaction evidence (OID4VP-aligned)
evidence = [{
"type": "DelegatedSignatureEvidence",
"transaction_data": {
"type": "harbour.delegate:data.purchase",
"credential_ids": ["harbour_natural_person"],
"nonce": "da9b1009",
"iat": 1771934400,
"txn": {
"asset_id": "urn:uuid:550e8400-e29b-41d4-a716-446655440000",
"price": "100",
"currency": "ENVITED"
}
},
"delegatedTo": "did:ethr:0x14a34:0x31f1ca3dc5da9f83f360d805662d11a418950202"
}]
# Create VP with selective disclosure (redact PII)
sd_jwt_vp = issue_sd_jwt_vp(
sd_jwt_vc,
holder_private_key,
disclosures=["memberOf"], # Only disclose non-PII claims
evidence=evidence,
nonce="da9b1009",
audience="did:ethr:0x14a34:0x31f1ca3dc5da9f83f360d805662d11a418950202"
)
TypeScript Example¶
import { issueSdJwtVp } from '@reachhaven/harbour-credentials';
const sdJwtVp = await issueSdJwtVp(sdJwtVc, holderPrivateKey, {
disclosures: ['memberOf'],
evidence: [{
type: 'DelegatedSignatureEvidence',
transaction_data: {
type: 'harbour.delegate:data.purchase',
credential_ids: ['harbour_natural_person'],
nonce: 'da9b1009',
iat: 1771934400,
txn: {
asset_id: 'urn:uuid:550e8400-e29b-41d4-a716-446655440000',
price: '100',
currency: 'ENVITED'
}
},
delegatedTo: 'did:ethr:0x14a34:0x31f1ca3dc5da9f83f360d805662d11a418950202'
}],
nonce: 'da9b1009',
audience: 'did:ethr:0x14a34:0x31f1ca3dc5da9f83f360d805662d11a418950202'
});
issue_sd_jwt_vp / issueSdJwtVp derives the delegation challenge (<nonce> HARBOUR_DELEGATE <sha256(canonical(transaction_data))>) and writes it to evidence[].challenge. It also computes the OID4VP transaction_data_hashes value (base64url(SHA-256(transaction_data request string))) and binds/verifies that in KB-JWT on verify_sd_jwt_vp / verifySdJwtVp.
Verification¶
The signing service verifies the VP before executing the transaction:
from harbour.sd_jwt_vp import verify_sd_jwt_vp
result = verify_sd_jwt_vp(
sd_jwt_vp,
issuer_public_key, # From credential issuer's DID
holder_public_key, # From user's DID document
expected_nonce="da9b1009",
expected_audience="did:ethr:0x14a34:0x31f1ca3dc5da9f83f360d805662d11a418950202"
)
# Check transaction data matches original request
tx = result["evidence"][0]["transaction_data"]
assert tx["type"] == "harbour.delegate:data.purchase"
assert tx["txn"]["asset_id"] == "urn:uuid:550e8400-e29b-41d4-a716-446655440000"
# Check credential is still valid (CRSet)
# ... revocation check ...
# All checks pass -> execute transaction
Receipt Credential¶
After executing the transaction, the signing service issues a receipt credential (SD-JWT-VC) with DelegatedSignatureEvidence:
{
"type": ["VerifiableCredential", "harbour:DelegatedSigningReceipt"],
"issuer": "did:ethr:0x14a34:0x31f1ca3dc5da9f83f360d805662d11a418950202",
"evidence": [{
"type": "harbour:DelegatedSignatureEvidence",
"verifiablePresentation": "<consent VP with PII redacted>",
"delegatedTo": "did:ethr:0x14a34:0x31f1ca3dc5da9f83f360d805662d11a418950202",
"transaction_data": { "..." }
}],
"credentialStatus": [{
"type": "harbour:CRSetEntry",
"statusPurpose": "revocation"
}]
}
The receipt credential enables three-layer privacy via selective disclosure (see Evidence).
Privacy Model¶
The SD-JWT VP enables three-layer privacy-preserving audit:
| Data | Layer 1 (Public) | Layer 2 (Authorized) | Layer 3 (Full Audit) |
|---|---|---|---|
| CRSet entry (credential exists) | Yes | Yes | Yes |
| Transaction data hash on-chain | Yes | Yes | Yes |
| KB-JWT signature valid | Yes | Yes | Yes |
| Transaction details (asset, price) | No | Yes | Yes |
| Consent VP hash verification | No | Yes | Yes |
| User name | No | No | Yes |
| User email | No | No | Yes |
Security Considerations¶
Replay Protection¶
The nonce in transaction data prevents replay attacks:
- Signing service generates unique nonce per request
- VP must contain matching nonce in KB-JWT
- Nonce is single-use
Audience Binding¶
The audience field ensures the VP was created for a specific verifier:
verify_sd_jwt_vp(
vp,
...,
expected_audience="did:ethr:0x14a34:0x31f1ca3dc5da9f83f360d805662d11a418950202"
)
Revocation Checking¶
Before executing, verify the credential hasn't been revoked:
# Check CRSet entry
crset_entry = result["credential"]["credentialStatus"][0]
is_revoked = check_crset(crset_entry["id"])
if is_revoked:
raise Error("Credential has been revoked")
DID Document Verification¶
Verify the VP signature matches the public key in the user's DID document:
# Resolve DID document (integrator-provided resolver)
did_doc = resolve_did("did:ethr:0x14a34:0x26e4...16c9")
# Extract public key
public_key = did_doc["verificationMethod"][0]["publicKeyJwk"]
# Verify VP was signed with this key
verify_sd_jwt_vp(vp, issuer_key, public_key_from_did_doc, ...)
Use Cases¶
Data Marketplace¶
User purchases dataset through blockchain:
- User browses marketplace, selects dataset
- App creates OID4VP transaction data: "Purchase 'Weather Data 2024' for 100 ENVITED"
- User creates consent VP with wallet
- Harbour executes blockchain transaction
- Receipt credential issued with
DelegatedSignatureEvidence
Contract Signing¶
User signs legal contract:
- Contract platform prepares document
- Creates transaction data:
harbour.delegate:contract.sign - User creates consent VP
- Harbour records signature on blockchain
- Receipt VP serves as proof of signing intent
Access Delegation¶
User grants access to resource:
- Service creates transaction data:
harbour.delegate:data.access - User creates consent VP
- Harbour updates access control on blockchain
- Receipt VP serves as access grant evidence
Related Documentation¶
- Evidence Types — All Harbour evidence types
- Delegation Challenge Encoding — OID4VP transaction data spec
- SD-JWT-VC — SD-JWT credential issuance
- ADR-001: VC Securing Mechanism — Why SD-JWT
- ADR-004: Key Management — P-256 keys