PRISM Identity with UCAN - Documentation
This document provides a comprehensive guide on using PRISM (Decentralized Identifier) identities with UCAN (User Controlled Authorization Network) tokens in the Device Management Service.
Table of Contents
- Overview
- Hyperledger Identus Compatibility
- PRISM DID Basics
- Importing PRISM Keys
- Using PRISM Identity with UCAN
- Running Tests
- Examples
- Troubleshooting
- Running NeoPRISM Locally
- Additional Resources
- API Reference
Overview
PRISM is a DID method that runs on the Cardano blockchain, providing decentralized identity management. This system integrates PRISM DIDs with UCAN tokens, allowing you to:
- Create and manage PRISM identities
- Sign UCAN tokens with PRISM DIDs
- Verify UCAN tokens signed by PRISM identities
- Interoperate between
did:prismanddid:keyidentities
We specifically offer interoperability between the neoprism implementation (Hyperledger Identus implementation for PRISM) and UCAN. This integration has been tested against hyperledgeridentus/identus-neoprism:0.9.1 image.
Key Components
- PRISM DID: A decentralized identifier following the format
did:prism:<hash> - Provider: An interface for signing with a PRISM DID's private key
- Anchor: A public key extracted from a PRISM DID document for verification
- TrustContext: Manages providers and anchors for UCAN operations
Hyperledger Identus Compatibility
This implementation is fully compatible with Hyperledger Identus NeoPRISM, the Hyperledger Identus implementation for PRISM DIDs. The compatibility has been tested and verified against the hyperledgeridentus/identus-neoprism:0.9.1 Docker image.
Implementation Details
The following implementation details ensure compatibility with Hyperledger Identus NeoPRISM:
1. W3C-Compliant DID Resolution
The implementation uses NeoPRISM's W3C-compliant DID resolution endpoint:
- Endpoint:
/api/dids/{did} - Format: Returns W3C-compliant JSON DID resolution result
- Accept Header:
application/did+json,application/json - URL Encoding: DIDs are properly URL-encoded in the path (e.g.,
:becomes%3A)
This endpoint is used instead of alternative formats (like OpenPrismNode's /api/v1/identifiers/{did}) to ensure compatibility with NeoPRISM's API structure.
2. Protobuf Operation Encoding
PRISM operations are encoded using Protocol Buffers (protobuf) as specified by the PRISM protocol:
- Operation Format: Uses
PrismOperationandSignedPrismOperationprotobuf messages - Encoding: Operations are serialized to bytes, signed, and hex-encoded for submission
- Key Types: Supports both Ed25519 and secp256k1 keys in protobuf format
- Key Usage: Properly maps key usage types (MASTER_KEY, AUTHENTICATION_KEY, etc.) to protobuf enums
3. Operation Submission API
Operations are submitted to NeoPRISM using the standard submission endpoint:
- Endpoint:
/api/signed-operation-submissions - Method: POST
- Content-Type:
application/json - Request Format:
{"signed_operations": ["<hex-encoded-signed-operation>"]} - Response Format:
{"tx_id": "<transaction-id>", "operation_ids": ["<operation-id>"]}
4. Key Type Requirements
NeoPRISM has specific requirements for key types:
- Master Keys: NeoPRISM requires Secp256k1 keys for master keys (not Ed25519)
- Authentication Keys: Both Ed25519 and secp256k1 are supported
- Key Format: Master keys must be provided in the correct protobuf format with proper curve specification
The implementation automatically handles key type detection and conversion, ensuring master keys use Secp256k1 when creating DIDs via NeoPRISM.
5. DID Format Compliance
The implementation ensures DIDs follow NeoPRISM's expected format:
- Format:
did:prism:{64-character-hexadecimal-string} - Validation: DIDs are validated to ensure they match the canonical PRISM format
- Resolution: DIDs are properly URL-encoded when used in API paths
6. Verification Method Support
DID documents are parsed with support for NeoPRISM's verification method structure:
- Type:
JsonWebKey2020verification methods - Key Curves: Ed25519 and secp256k1
- Relationships: Supports
authentication,assertionMethod, andcapabilityInvocationrelationships - JWK Parsing: Properly extracts public keys from
publicKeyJwkfields in base64url encoding
7. DID Document Structure
The implementation correctly handles W3C DID document structure as returned by NeoPRISM:
- Context: Supports multiple
@contextvalues - Verification Methods: Handles both embedded and referenced verification methods
- Service Endpoints: Supports service definitions in DID documents
- ID Matching: Validates that resolved DID document ID matches the requested DID
Testing and Validation
Compatibility is validated through integration tests that:
- Create DIDs via NeoPRISM's submission API
- Resolve DID documents from NeoPRISM's resolution endpoint
- Extract public keys from resolved documents
- Sign and verify UCAN tokens using PRISM identities
- Test interoperability with
did:keyidentities
See the Running Tests section for instructions on running tests against NeoPRISM.
Version Compatibility
- Tested Version:
hyperledgeridentus/identus-neoprism:0.9.1 - API Compatibility: Compatible with NeoPRISM API v1
- Protocol Compatibility: Follows PRISM protocol specification for operation encoding
PRISM DID Basics
DID Format
PRISM DIDs follow the format:
did:prism:<method-specific-identifier>
Example:
did:prism:4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b
DID Resolution
PRISM DIDs are resolved to DID documents via HTTP requests to PRISM resolver endpoints. The system supports:
- Default resolver:
https://prism-agent.example.com - OpenPrismNode format:
/api/v1/identifiers/{did} - Custom resolvers: Configurable via
PRISMResolverConfig
Supported Key Types
- Ed25519: Most common, recommended for new identities
- secp256k1: Alternative for compatibility
Importing PRISM Keys
There are several ways to import PRISM private keys depending on your use case.
Method 1: Import from JWK Format
If you have a PRISM private key in JSON Web Key (JWK) format (e.g., from a PRISM agent API response):
Note: The JWK import function may require manual parsing of the d field. See Method 4 for a complete example of extracting keys from PRISM agent responses.
import (
"encoding/json"
"gitlab.com/nunet/device-management-service/lib/did"
)
// JWK data from PRISM agent (typically from didState.secret.verificationMethod[0].privateKeyJwk)
jwkData := []byte(`{
"kty": "OKP",
"crv": "Ed25519",
"d": "base64url-encoded-private-key",
"x": "base64url-encoded-public-key"
}`)
prismDID, err := did.FromString("did:prism:...")
if err != nil {
return err
}
// Import the private key
// Note: This may require the JWK struct to include the 'd' field
privKey, err := did.ImportPRISMPrivateKeyFromJWK(jwkData, prismDID)
if err != nil {
return err
}
// Create a provider
provider, err := did.ProviderFromPRISMPrivateKey(prismDID, privKey)
if err != nil {
return err
}
Method 2: Import from Hex-Encoded Key (CLI)
Using the CLI command to import a PRISM identity:
nunet key import-prism <name> <prism-did> <private-key-hex>
Supported formats:
- libp2p protobuf format (hex encoded)
- Raw Ed25519 seed (32 bytes, hex encoded)
- Raw Ed25519 private key (64 bytes, hex encoded)
- Raw secp256k1 private key (32 bytes, hex encoded)
Example:
nunet key import-prism myprism \
did:prism:9b5118411248d9663b6ab15128fba8106511230ff654e7514cdcc4ce919bde9b \
08011240...
Method 3: Generate Keys Locally
For testing or when you control key generation:
import (
"gitlab.com/nunet/device-management-service/lib/crypto"
"gitlab.com/nunet/device-management-service/lib/did"
)
// Generate a key pair
privKey, pubKey, err := crypto.GenerateKeyPair(crypto.Ed25519)
if err != nil {
return err
}
// Create a PRISM DID (in real scenario, this would be created on Cardano)
prismDID, err := did.FromString("did:prism:...")
if err != nil {
return err
}
// Create provider
provider, err := did.ProviderFromPRISMPrivateKey(prismDID, privKey)
if err != nil {
return err
}
Method 4: Extract from PRISM Agent API Response
When creating a DID via PRISM agent API (e.g., OpenPrismNode), extract the private key from the response:
// After creating DID via PRISM agent API
var createDIDResponse struct {
DIDState struct {
DID string `json:"did"`
Secret map[string]interface{} `json:"secret,omitempty"`
} `json:"didState"`
}
// Extract private key from response
secret := createDIDResponse.DIDState.Secret
vmList := secret["verificationMethod"].([]interface{})
vmData := vmList[0].(map[string]interface{})
privateKeyJWK := vmData["privateKeyJwk"].(map[string]interface{})
// Convert to JSON
jwkBytes, err := json.Marshal(privateKeyJWK)
if err != nil {
return err
}
// Import
prismDID, _ := did.FromString(createDIDResponse.DIDState.DID)
privKey, err := did.ImportPRISMPrivateKeyFromJWK(jwkBytes, prismDID)
if err != nil {
return err
}
provider, err := did.ProviderFromPRISMPrivateKey(prismDID, privKey)
Using PRISM Identity with UCAN
Step 1: Configure PRISM Resolver
Set up the PRISM resolver configuration:
import "gitlab.com/nunet/device-management-service/lib/did"
// Configure resolver
originalConfig := did.GetPRISMResolverConfig()
defer did.SetPRISMResolverConfig(originalConfig)
did.SetPRISMResolverConfig(did.PRISMResolverConfig{
ResolverURL: "https://opn.preprod.blocktrust.dev",
PreferredVerificationMethod: "authentication",
})
Step 2: Resolve PRISM DID to Get Anchor
An anchor is needed for verification:
prismDID, err := did.FromString("did:prism:...")
if err != nil {
return err
}
// Resolve DID to get anchor (public key)
anchor, err := did.GetAnchorForDID(prismDID)
if err != nil {
return err
}
Step 3: Create Provider
Create a provider from your PRISM private key:
provider, err := did.ProviderFromPRISMPrivateKey(prismDID, privKey)
if err != nil {
return err
}
Step 4: Set Up Trust Context
Create a trust context and add your provider and anchor:
import "gitlab.com/nunet/device-management-service/lib/ucan"
trustCtx := did.NewTrustContext()
trustCtx.AddProvider(provider)
trustCtx.AddAnchor(anchor)
Step 5: Create Capability Context
Create a capability context for UCAN operations:
capCtx, err := ucan.NewCapabilityContext(
trustCtx,
provider.DID(), // issuer DID
[]did.DID{provider.DID()}, // roots
ucan.TokenList{}, // require
ucan.TokenList{}, // provide
ucan.TokenList{}, // revoke
)
if err != nil {
return err
}
Step 6: Grant Capabilities
Grant capabilities using your PRISM identity:
subjectDID := provider.DID()
audienceDID := provider.DID()
expire := uint64(time.Now().Add(1 * time.Hour).UnixNano())
capability := ucan.Capability("/test/prism/capability")
tokens, err := capCtx.Grant(
ucan.Delegate,
subjectDID,
audienceDID,
nil, // topics
expire,
0, // depth
[]ucan.Capability{capability},
)
if err != nil {
return err
}
token := tokens.Tokens[0]
Step 7: Verify Tokens
Verify UCAN tokens signed by PRISM identities:
now := uint64(time.Now().UnixNano())
revokeSet := &ucan.RevocationSet{Revoked: make(map[string]*ucan.Token)}
err = token.Verify(trustCtx, now, revokeSet)
if err != nil {
return err
}
Interoperability with did:key
PRISM identities can seamlessly interact with did:key identities:
// PRISM identity can delegate to did:key
prismProvider, _ := did.ProviderFromPRISMPrivateKey(prismDID, prismPrivKey)
keyProvider, _ := did.ProviderFromPrivateKey(keyPrivKey)
// Both can be used in the same TrustContext
trustCtx.AddProvider(prismProvider)
trustCtx.AddProvider(keyProvider)
trustCtx.AddAnchor(prismAnchor)
trustCtx.AddAnchor(keyAnchor)
// PRISM can delegate capabilities to did:key and vice versa
Running Tests
Prerequisites
- Go 1.21+ installed
- Integration test tag: Tests are marked with
//go:build integration