Skip to content

transmute-industries/did-transmute

Repository files navigation

did:transmute

CI Branches Functions Lines Statements Jest coverage

This repository contains experimental implementations of various DID Methods.

A primary use case for this is "DID Method Projection", in which an existing identifier space such as all JWK or JWT, is projected into a Decentralizied Identifier space, such as did:jwk: or did:jwt.

This is accomplished by defining resolution and dereferencing for the DID URLs under the "projection method".

Composition

%%{
  init: {
    'flowchart': { 'curve': 'monotoneX' },
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#2a2d4c',
      'primaryTextColor': '#565a7c',
      'nodeBorder': '#565a7c',
      'edgeLabelBackground': '#2a2d4c',
      'clusterBkg': '#2a2d4c',
      'clusterBorder': '#2a2d4c',
      'lineColor': '#565a7c',
      'fontFamily': 'monospace',
      'darkmode': true
    }
  }
}%%
%% Support https://transmute.industries
graph LR
	subgraph &nbsp
		direction LR
        root("did:web:  ")
        0("did:jwk: base64url ( json-web-key ) ")
        1("did:jwt: compact-json-web-token ")
        2("compact-json-web-signature ")
        3("compact-json-web-encryption ")
        root -- derive -->  0
        0 -- sign --> 1
        0 -- encrypt --> 1
        1 -- as --> 2
        1 -- as --> 3
        root -- derive -->  1
	end

style root color: #fff, fill: #594aa8
style 0 color: #fcb373, stroke: #fcb373
style 1 color: #fcb373, stroke: #fcb373
style 2 color: #8286a3, stroke: #8286a3
linkStyle 0,5 color:#2cb3d9, stroke-width: 2.0px
linkStyle 1,2 color:#ff605d, stroke:#8286a3, stroke-width: 2.0px
linkStyle 3,4 color:#48caca, stroke-width: 2.0px
%% export const transmute = {
%%   primary: {
%%     purple: { dark: "#27225b", light: "#594aa8" },
%%     red: "#ff605d",
%%     orange: "#fcb373",
%%     grey: "#f5f7fd",
%%     white: "#fff",
%%   },
%%   secondary: {
%%     teal: "#48caca",
%%     aqua: "#2cb3d9",
%%     dark: "#2a2d4c",
%%     medium: "#565a7c",
%%     light: "#8286a3",
%%   },
%% };
Loading

Usage

npm install '@transmute/did-transmute'
import transmute from '@transmute/did-transmute';
const transmute = require('@transmute/did-transmute');

See also transmute-industries/verifiable-credentials.

This api is exposed on the default export, for example:

const actor = await transmute.did.jwk.exportable({
  alg: "ES384",
});
const issuer = await transmute.w3c.vc.issuer({ 
  signer: await transmute.w3c.controller.key.attached.signer({ 
    privateKey: actor.key.privateKey 
  }) 
});
// issue a vc+ld+jwt
const vc = await issuer.issue({
  protectedHeader: {
    kid: actor.did + '#0',
    alg: actor.key.publicKey.alg,
  },
  claimset: {
    "@context": [
      "https://www.w3.org/ns/credentials/v2",
      "https://www.w3.org/ns/credentials/examples/v2"
    ],
    "id": "https://contoso.example/credentials/35327255",
    "type": ["VerifiableCredential", "KYCExample"],
    "issuer": "did:web:contoso.example",
    "validFrom": "2019-05-25T03:10:16.992Z",
    "validUntil": "2027-05-25T03:10:16.992Z",
    "credentialStatus": {
      "id": "https://contoso.example/credentials/status/4#3",
      "type": "StatusList2021Entry",
      "statusPurpose": "suspension",
      "statusListIndex": "3",
      "statusListCredential": "https://contoso.example/credentials/status/4"
    },
    "credentialSchema": {
      "id": "https://contoso.example/bafybeigdyr...lqabf3oclgtqy55fbzdi",
      "type": "JsonSchema"
    },
    "credentialSubject": {
      "id": "did:example:1231588",
      "type": "Person"
    }
  },
});

did:jwk

Generate

const actor1 = await transmute.did.jwk.exportable({
  alg: 'ES256',
});
// Use software isolation: 
// See https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/exportKey
const actor2 = await transmute.did.jwk.isolated({
  alg: 'ES256',
});

Resolve & Dereference

const { 
  did 
} = await transmute.did.jwk.exportable({
  alg: 'ES256',
});
const didDocument = await transmute.did.jwk.resolve({ 
  id: did,
  documentLoader: transmute.did.jwk.documentLoader
});
// See https://www.w3.org/TR/did-core/#verification-relationships
const { publicKeyJwk } = await transmute.did.jwk.dereference({
  id: `${did}#0`,
  documentLoader: transmute.did.jwk.documentLoader
});

Sign & Verify

const { 
  key: { privateKey, publicKey } 
} = await transmute.did.jwk.exportable({
  alg: transmute.jose.alg.ES256,
});
const jws = await transmute.sign({
  privateKey: privateKey,
  protectedHeader: {
    alg: privateKey.alg,
  },
  payload: new TextEncoder().encode("It’s a dangerous business, Frodo, going out your door. 🧠💎"),
});
const v = await transmute.verify({
  jws,
  publicKey: publicKey,
});

Encrypt & Decrypt

const { 
  key: { privateKey, publicKey } 
} = await transmute.did.jwk.exportable({
  alg: transmute.jose.alg.ECDH_ES_A256KW,
});
const jwe = await transmute.encrypt({
  publicKey: publicKey,
  plaintext: new TextEncoder().encode("It’s a dangerous business, Frodo, going out your door. 🧠💎"),
  protectedHeader: {
    alg: publicKey.alg,
    enc: transmute.jose.enc.A256GCM,
  },
});
const v = await transmute.decrypt({
  jwe,
  privateKey: privateKey,
});

did:jwt

This method is very 🚧 experimental 🏗️.

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#2a2d4c',
      'primaryTextColor': '#565a7c',
      'nodeBorder': '#565a7c',
      'edgeLabelBackground': '#2a2d4c',
      'clusterBkg': '#2a2d4c',
      'clusterBorder': '#2a2d4c',
      'lineColor': '#565a7c',
      'fontFamily': 'monospace',
      'darkmode': true
    }
  }
}%%
%% Support https://transmute.industries
graph LR
	subgraph &nbsp
		direction LR
    Did("did:jwt: compact-json-web-token ")
    KeyDereference{"Key Resolver"}
    DidResolution{"Did Resolution"}
    DidDocument("Did Document")
    DidDocumentMetadata("Did Document Metadata")
    ProtectedHeader("ProtectedHeader")
    ClaimSet("Claim Set")
    Trusted{{"Is Issuer Trusted?"}}
    Untrusted("No")

    Did -- Protected Header  --> KeyDereference
    KeyDereference --> Trusted
    
    Trusted -- Payload --> DidResolution
    DidResolution --> DidDocument
    DidDocument --> ClaimSet
    DidResolution -.-> DidDocumentMetadata
    DidDocumentMetadata -.-> ProtectedHeader

    Trusted -.-> Untrusted

	end

%% orange
style Did color: #fcb373, stroke: #fcb373
style DidDocument color: #fcb373, stroke: #fcb373

%% teal
style Trusted color: #27225b, fill: #2cb3d9

%% purple
style ProtectedHeader color: #fff, fill: #594aa8
style ClaimSet color: #fff, fill: #594aa8

%% light grey
style DidDocumentMetadata color: #8286a3, stroke: #8286a3

%% red
style KeyDereference color: #ff605d, stroke: #ff605d
style DidResolution color: #ff605d, stroke: #ff605d


%% red lines
linkStyle 0,2 color:#ff605d, stroke-width: 2.0px

%% linkStyle 1,2,4 color:#ff605d, stroke:#8286a3, stroke-width: 2.0px
%% export const transmute = {
%%   primary: {
%%     purple: { dark: "#27225b", light: "#594aa8" },
%%     red: "#ff605d",
%%     orange: "#fcb373",
%%     grey: "#f5f7fd",
%%     white: "#fff",
%%   },
%%   secondary: {
%%     teal: "#48caca",
%%     aqua: "#2cb3d9",
%%     dark: "#2a2d4c",
%%     medium: "#565a7c",
%%     light: "#8286a3",
%%   },
%% };
Loading

There are several different ways to "trust" a JSON Web Token issuer, based exclusively or the header and verify or decrypt operations.

Embedding keys

Using jwk and x5c.

Embedding the key within the token is a straightforward way to enable key distribution. To ensure the security of this mechanism, the consumer of the JWT needs to restrict which keys it accepts. Failure to do so allows an attacker to generate tokens signed with a malicious private key. An overly permitting consumer would merely use the embedded public key to verify the signature, which will be valid. To avoid such issues, the consumer needs to match the key used against a set of explicitly whitelisted keys. In case the key comes in the form of an X509 certificate, the consumer can use the certificate information to verify the authenticity.

When jwk is present in the Protected Header of a JWT, a custom did:jwk resoler will be used as the the allow-list.

A null resolution is treated as a deny operation.

See also panva/jose.

Distributing keys

Using jku and x5u.

TODO

See RFC7515

Proof of Possession

TODO

See RFC7800

Using OpenID Connect Discovery

🚧 Experimental 🏗️.

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#2a2d4c',
      'primaryTextColor': '#565a7c',
      'nodeBorder': '#565a7c',
      'edgeLabelBackground': '#2a2d4c',
      'clusterBkg': '#2a2d4c',
      'clusterBorder': '#2a2d4c',
      'lineColor': '#565a7c',
      'fontFamily': 'monospace',
      'darkmode': true
    }
  }
}%%
%% Support https://transmute.industries

flowchart LR
subgraph 0 [Open ID Connect 'DID Method']
direction LR
ProtectedHeader("Protected Header")
ProtectedClaimSet("Protected Claim Set")
DecodedVerificationMethodComponents("{ iss, kid }")
WellKnownConfig(".well-known/openid-configuration")
WellKnownJwks(".well-known/jwks.json")
DidDocument("DID Document")

ProtectedHeader -.-> DecodedVerificationMethodComponents
ProtectedClaimSet -.-> DecodedVerificationMethodComponents
DecodedVerificationMethodComponents -.-> WellKnownConfig
WellKnownConfig -.-> WellKnownJwks
WellKnownJwks -.-> DidDocument

VerificationMethod("{{iss}}#{{kid}}")
DecodedVerificationMethodComponents -.-> VerificationMethod

DidDocument -.-> publicKey
publicKey("{ publicKeyJwk }")
VerificationMethod -.-> publicKey

class ProtectedHeader,ProtectedClaimSet,publicKey PurpleNode
class DecodedVerificationMethodComponents,WellKnownConfig,WellKnownJwks RedNode
class DidDocument,VerificationMethod TealNode

classDef PurpleNode color:#fff, fill:#594aa8, stroke:#27225b, stroke-width:1px;
classDef RedNode color:#ff605d, stroke:#ff605d, stroke-width:1px;
classDef OrangeNode color:#fcb373, stroke:#fcb373, stroke-width:1px;
classDef GreyNode fill:#f5f7fd, stroke:#f5f7fd, stroke-width:1px;
classDef WhiteNode color:#fff, stroke:#fff, stroke-width:1px;
classDef DarkPurpleNode color:#f5f7fd, fill:#27225b, stroke:#f5f7fd, stroke-width:1px;
classDef TealNode color:#48caca, stroke:#48caca, stroke-width:1px;
classDef AquaNode color:#2cb3d9, stroke:#2cb3d9, stroke-width:1px;
end
Loading

Example DID Document

{
  "id": "{{iss}}",
  "verificationMethod":[{
    "id": "#{{kid}}",
    "type": "JsonWebKey",
    "controller": "{{iss}}",
    "publicKey":{
      "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:AXRYM9BnKWZj6c84ykLX6D-fE9FRV2_f3pRDwcJGSU0",
      "kty": "OKP",
      "crv": "Ed25519",
      "alg": "EdDSA",
      "x": "dh2c41edqveCxEzw3OVjtAmdcJPwe4lAg2fJ10rsZk0",
    }
  }],
  "assertionMethod": ["#{{kid}}"]
}

See openid-connect-discovery

Verifiable Credential's JSON Web Token Profile

This approach relies on the resolver to act as an allow list for absolute did urls, constructed from kid or a combination of kid and iss.

For example, a protectedHeader might look like:

{
  "alg": "ES256",
  "iss": "did:example:123",
  "kid": "#key-4"
}

or

{
  "alg": "ES256",
  "kid": "did:example:123#key-4"
}

This header will be used to dereference a verificationMethod which is expected to contain a publicKey.

For example:

{
  "id": "#key-4",
  "type": "JsonWebKey",
  "controller": "did:example:123",
  "publicKeyJwk": {
    "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:AXRYM9BnKWZj6c84ykLX6D-fE9FRV2_f3pRDwcJGSU0",
    "kty": "OKP",
    "crv": "Ed25519",
    "alg": "EdDSA",
    "x": "dh2c41edqveCxEzw3OVjtAmdcJPwe4lAg2fJ10rsZk0",
  }
}

or

{
  "id": "did:example:123#urn:ietf:params:oauth:jwk-thumbprint:sha-256:AXRYM9BnKWZj6c84ykLX6D-fE9FRV2_f3pRDwcJGSU0",
  "type": "JsonWebKey",
  "controller": "did:example:123",
  "publicKeyJwk": {
    "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:AXRYM9BnKWZj6c84ykLX6D-fE9FRV2_f3pRDwcJGSU0",
    "kty": "OKP",
    "crv": "Ed25519",
    "alg": "EdDSA",
    "x": "dh2c41edqveCxEzw3OVjtAmdcJPwe4lAg2fJ10rsZk0",
  }
}

See jwt-vc-presentation-profile

Sign

const issuer = await transmute.did.jwk.exportable({
    alg: alg.ES256,
  });
const subject = await transmute.did.jwt.sign({
  issuer: "did:example:123",
  audience: "did:example:456",
  protectedHeader: {
    alg: issuer.key.publicKey.alg,
  },
  claimSet: {
    "urn:example:claim": true,
  },
  privateKey: issuer.key.privateKey,
});

Encrypt

const issuer = await transmute.did.jwk.exportable({
  alg: alg.ECDH_ES_A256KW,
});
const subject = await transmute.did.jwt.encrypt({
  issuer: "did:example:123",
  protectedHeader: {
    alg: issuer.key.publicKey.alg,
    iss: "did:example:123",
    kid: "#0",
    enc: transmute.jose.enc.A256GCM,
  },
  claimSet: {
    service: [
      {
        id: "#dwn",
        type: "DecentralizedWebNode",
        serviceEndpoint: {
          nodes: ["https://dwn.example.com", "https://example.org/dwn"],
        },
      },
    ],
  },
  publicKey: issuer.key.publicKey,
});

Resolve

const didDocument = await transmute.did.jwt.resolve({
  id: subject.did,
  privateKeyLoader: async (id: string) => {
    if (id.startsWith("did:example:123")) {
      return issuer.key.privateKey;
    }
    throw new Error("privateKeyLoader does not support identifier: " + id);
  },
  profiles: ["encrypted-jwt"],
});

Dereference

type DwnService = {
  id: "#dwn";
  type: "DecentralizedWebNode";
  serviceEndpoint: {
    nodes: ["https://dwn.example.com", "https://example.org/dwn"];
  };
};
const service = await transmute.did.jwt.dereference<DwnService>({
  id: `${subject.did}#dwn`,
  privateKeyLoader: async (id: string) => {
    if (id.startsWith("did:example:123")) {
      return issuer.key.privateKey;
    }
    throw new Error("privateKeyLoader does not support identifier: " + id);
  },
  profiles: ["encrypted-jwt"],
});

did:web

This method is very 🚧 experimental 🏗️.

Generate

 const { did, didDocument, key } = await transmute.did.web.exportable({
  url: "https://id.gs1.transmute.example/01/9506000134352",
  alg: transmute.jose.alg.ES256,
  documentLoader: transmute.did.jwk.documentLoader,
});

From Private Key

const { 
  key: {privateKey} 
} = await transmute.did.jwk.exportable({
  alg: 'ES256',
});
const issuer = await transmute.did.web.fromPrivateKey({
  url: "https://id.gs1.transmute.example/01/9506000134352",
  privateKey: privateKey,
});

From Dids

const { 
  did
} = await transmute.did.jwk.exportable({
  alg: 'ES256',
});
const issuer = await transmute.did.web.fromDids({
  url: "https://id.gs1.transmute.example/01/9506000134352",
  dids: [did],
  documentLoader: transmute.did.jwk.documentLoader,
});

Resolve

const { 
  key: { privateKey } 
} = await transmute.did.jwk.exportable({
  alg: 'ES256',
});
const issuer = await transmute.did.web.fromPrivateKey({
  url: "https://id.gs1.transmute.example/01/9506000134352",
  privateKey: privateKey,
});
const didDocument = await transmute.did.web.resolve({
  id: issuer.did,
  documentLoader: async (iri: string) => {
    // for test purposes.
    if (iri === "https://id.gs1.transmute.example/01/9506000134352/did.json") {
      return { document: issuer.didDocument };
    }
    throw new Error("Unsupported IRI " + iri);
  },
});
// didDocument.id = "did:web:id.gs1.transmute.example:01:9506000134352"

Dereference

const issuer = await transmute.did.web.fromPrivateKey({
  url: "https://id.gs1.transmute.example/01/9506000134352",
  privateKey: {
    kid: "urn:ietf:params:oauth:jwk-thumbprint:sha-256:a9EEmV5OPmFQlAVU2EDuKB3cp5JpirRwnD12UdHc91Q",
    kty: "EC",
    crv: "P-256",
    alg: "ES256",
    x: "D1ygYPasDI88CrYAF_Ga_4aXEhp5fWetEXzyitdt1K8",
    y: "dkxXWzis0tQQIctZRzSvf6tdeITCLXim8HgTUhMOTrg",
    d: "RWgQ966yzek12KSlDJ-hmlqckRUhZzKDqJeM_QdbT-E",
  },
});
const verificationMethod = await transmute.did.web.dereference({
  id: `${issuer.did}#a9EEmV5OPmFQlAVU2EDuKB3cp5JpirRwnD12UdHc91Q`,
  documentLoader: async (iri: string) => {
    // for test purposes.
    if (
      iri === "https://id.gs1.transmute.example/01/9506000134352/did.json"
    ) {
      return { document: issuer.didDocument };
    }
    throw new Error("Unsupported IRI " + iri);
  },
});
const v = await transmute.verify({
  jws,
  publicKey: verificationMethod.publicKeyJwk,
});

Develop

npm i
npm t
npm run lint
npm run build