DID EBSI Plugin for Veramo

Blockchain Lab:UM
5 min readApr 26, 2023

--

We are excited to announce we are open sourcing DID EBSI plugin for Veramo. The plugin enables you to create Decentralized Identifiers (DID) for European Blockchain Services Infrastructure (EBSI) and anchor them to the EBSI blockchain.

Here is the link to the NPM package.

European Blockchain Services Infrastructure (EBSI)

What is EBSI?

EBSI is a permissioned blockchain network with nodes distributed across Europe. Its mission is to spread the adoption of cross-border public services by applying blockchain technology’s composability, decentralization, and self-sovereignty. Building on the foundations of Ethereum (using Hyperledger Besu) and decentralized identity, many tools and parts of the Web3 stack can be used in the EBSI ecosystem.

There are two types of entities defined by the EBSI framework: Legal Entities and Natural Persons. Legal Entities are trustworthy entities that issue VCs and their identities can be publicly resolved by anyone. Natural Persons are EU (European Union) citizens that receive and hold VCs. Identity management for Natural Persons must strictly follow all aspects of regulation in the EU, such as GDPR (General Data Protection Regulation).

DID EBSI method is a DID method for Legal Entities that want to interact with the EBSI blockchain and issue Verifiable Credentials (VCs) defined by the EBSI trust framework. Their identifiers are public and resolvable by anyone. The Veramo plugin enables you to create DID EBSI identifiers and register/anchor them on the EBSI blockchain.

DID EBSI method for Natural Persons also exists, which is very similar to `did:key`. We are already working on it at the time of writing, and it will also be released as a Veramo plugin.

How to use the plugin?

You can find the source code of the plugin on our GitHub repo here.

import { EbsiDIDProvider, ebsiDidResolver } from "@blockchain-lab-um/did-provider-ebsi"

In order to use the plugin with the standard Veramo agent, DID EBSI implements Veramo’s abstract class AbstractIdentifierProvider and function type DIDResolver, which can be added to Veramo’s agent during the setup.

const agent = createAgent<IDIDManager & IResolver>({
plugins: [
// other plugins above ...
new DIDResolverPlugin({
resolver: new Resolver({
// ...
...keyDidResolver(),
...ebsiDidResolver(),
// ...
}),
}),
new DIDManager({
store: new SnapDIDStore(snap, ethereum),
defaultProvider: 'metamask',
providers: [
"did:key": new KeyDIDProvider({ kms: "local" }),
"did:ebsi": new EbsiDIDProvider({ kms: "local" })
],
}),
],
});

After the Veramo agent is set up, we can use it to create, register, and resolve EBSI DIDs. For example, we can claim a bearer token needed for onboarding and pass it to didManagerCreate() method.

The bearer token is needed only when onboarding a DID. Resolving a DID EBSI doesn’t need authorization of any kind.

const identifier = await agent.didManagerCreate({
provider: 'did:ebsi',
options: {
bearer: "eyJhbG...",
},
})

The created identifier and its DID document looks like this:

{
"did": "did:ebsi:zZ6fw6wW24KQrYD6TR4o3j7",
"controllerKeyId": "did:ebsi:zZ6fw6wW24KQrYD6TR4o3j7#BmmTd2PcQoZIcdkmmU-quStsPdmDypyQqQtGSaCHoBs",
"keys": [
{
"type": "Secp256k1",
"kid": "did:ebsi:zZ6fw6wW24KQrYD6TR4o3j7#BmmTd2PcQoZIcdkmmU-quStsPdmDypyQqQtGSaCHoBs",
"publicKeyHex": "0472e78698b9d6a001cd7e97267b945bff6493751bbe325a76180360ffd7390a54aa17263ac9f9767471e748f4195948e6cd6fb0420cac45e314d48cf8c9cce0a6",
"meta": {
"algorithms": [
"ES256K",
"ES256K-R",
"eth_signTransaction",
"eth_signTypedData",
"eth_signMessage",
"eth_rawSign"
]
},
"kms": "local"
}
],
"services": [],
"provider": "did:ebsi",
"alias": "default"
}

EBSI DIDs can be resolved simply by calling agent’s resolveDid() method.

const result = await agent.resolveDid({ didUrl: "did:ebsi:zZ6fw6wW24KQrYD6TR4o3j7" });
const didDoc = result.didDocument;

This will resolve the passed didUrl into DID document:

{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"id": "did:ebsi:zZ6fw6wW24KQrYD6TR4o3j7",
"verificationMethod": [
{
"id": "did:ebsi:zZ6fw6wW24KQrYD6TR4o3j7#BmmTd2PcQoZIcdkmmU-quStsPdmDypyQqQtGSaCHoBs",
"type": "JsonWebKey2020",
"controller": "did:ebsi:zZ6fw6wW24KQrYD6TR4o3j7",
"publicKeyJwk": {
"kty": "EC",
"x": "cueGmLnWoAHNfpcme5Rb_2STdRu-Mlp2GANg_9c5ClQ",
"y": "qhcmOsn5dnRx50j0GVlI5s1vsEIMrEXjFNSM-MnM4KY",
"crv": "secp256k1"
}
}
],
"authentication": [
"did:ebsi:zZ6fw6wW24KQrYD6TR4o3j7#BmmTd2PcQoZIcdkmmU-quStsPdmDypyQqQtGSaCHoBs"
],
"assertionMethod": [
"did:ebsi:zZ6fw6wW24KQrYD6TR4o3j7#BmmTd2PcQoZIcdkmmU-quStsPdmDypyQqQtGSaCHoBs"
]
}

DID can also be verified by using EBSI’s online resolver or DIF’s Universal Resolver.

Behind the scenes

This part of the post goes into technical details, so feel free to skip to the end if you’re not interested.

Veramo’s AbstractIdentifierProvider implements the function createIdentifier(), which executes when agent.didManagerCreate({ provider: “did:ebsi” }) is called. If passing did:ebsi as the provider argument, the agent creates the EBSI identifier.

If the created identifier is not registered in the EBSI’s DID Registry, a bearer parameter (JWT) is required when calling agent.didManagerCreate({provider: “did:ebsi”, bearer: “ey…”}). This invokes the onboarding procedure using the provided bearer.

// NOTE:
// - Currently supported key types are "Secp256k1" and "P-256"
// - Currently supported hash type is "sha256"
// Code below generates a new key pair and gets
// the needed values for further process
const keys = await jose.generateKeyPair(algorithm);
const privateKeyJwk = await jose.exportJWK(keys.privateKey);
const publicKeyJwk = await jose.exportJWK(keys.publicKey);
const keyJwks = { publicKeyJwk, privateKeyJwk }
const jwkThumbprint = await jose.calculateJwkThumbprint(privateKeyJwk, 'sha256');
const subjectIdentifier = Buffer.from(
base58btc.encode(Buffer.concat([new Uint8Array([1]), randomBytes(16)]))
).toString();
const privateKeyHex = privateKeyJwkToHex(privateKeyJwk);

We pass on the private key because we need it when using EBSI’s libraries for onboarding the DID. Next, we create a kid and did and build Veramo’s identifier object.

const kid = `did:ebsi:${subjectIdentifier}#${jwkThumbprint}`;
const did = `did:ebsi:${subjectIdentifier}`;
const key = await context.agent.keyManagerImport({
kid,
privateKeyHex,
type: 'Secp256k1',
kms: this.defaultKms || 'local',
});
const identifier = {
did,
controllerKeyId: kid,
keys: [key],
services: [],
};

The identifier is now created. If the identifier is already registered, we simply return it.

const resolution = await context.agent.resolveDid({ didUrl: did });
if (resolution.didDocument) {
return identifier;
}

If DID doesn’t exist in EBSI DID Registry yet, we need to onboard it with the previously mentioned bearer token. We build and sign the idToken needed when requesting the Verifiable Authorization.

const alg = 'ES256K';
const privateKey = await jose.importJWK(privateKeyJwk, alg);

// Build and sign idToken
const idToken = {
sub: subject,
sub_jwk: publicKeyJwk,
nonce: uuidv4(),
responseMode: 'form_post',
};
const idTokenJwt = await new jose.SignJWT(idToken)
.setProtectedHeader({ alg, typ: 'JWT', kid })
.setIssuedAt()
.setAudience(`${EbsiConfig.BASE_URL}${EbsiEndpoints.AUTH_RESPONSE}`)
.setIssuer('https://self-issued.me/v2')
.setExpirationTime('1h')
.sign(privateKey);

const bearer = "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJK..."
// Request VA in VC format which will later be exchanged for access token
const verifiableAuthorization = await requestVerifiableAuthorization({
idTokenJwt,
bearer,
});

The requested Verifiable Authorization must be exchanged for an access token (required header argument when making calls to EBSI JSON RPC API). Plugin then exchanges the VA for an access token.

// Exchange previously retrieved VA
const accessToken = await exchangeVerifiableAuthorization({
verifiableAuthorization,
keyJwks: keyJwks,
identifier: identifier,
});

The access token is passed on as a bearer when inserting the DID document via EBSI JSON RPC API.

// Build and insert the DID Doc to the registry
const rpcResult = await insertDidDocument({
identifier: identifier,
bearer: accessToken,
keyJwks,
});

What’s next?

The next step is integrating DID EBSI plugin into Masca (prev. SSI Snap), which will enable the creation of the EBSI identifiers directly on the dApp (or by calling the Masca’s RPC method). This will make creating the new identifiers (using the cryptographic keys derived from the seed phrase in the MetaMask) very straightforward!

Stay tuned for more updates!

By Blockchain Lab:UM
Website | LinkedIn | Twitter | Discord | YouTube | GitHub | Email

--

--

Blockchain Lab:UM

A multidisciplinary team of researchers, developers and consultants who develop and evaluate blockchain-based services.