Skip to content

Commit

Permalink
feat: ADL implementation (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gozala authored Apr 21, 2022
1 parent 7705512 commit fdc2113
Show file tree
Hide file tree
Showing 15 changed files with 889 additions and 376 deletions.
84 changes: 48 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# @ipld/dag-ucan

An implementation of [UCAN][]s representation in [IPLD][], designed for use with [multiformats][].
An implementation of [UCAN][]s in [IPLD][] via [Advanced Data Layout (ADL)](ADL), designed for use with [multiformats][].

## Overview

This library implements multicodec for represenating [UCAN]s natively in [IPLD][]. It uses [DAG-CBOR][] as a primary encoding, which is more compact and has a better hash consitency than a secondary RAW JWT encoding. Every UCAN in primary encoding can be formatted into a JWT string and consumed by spec compliant [UCAN][] implementations. However not every [UCAN][] can be encoded in a primary CBOR representation, as loss of whitespaces and key order would lead to mismatched signature. Library issues UCANs only in primary CBOR representation. When parsing UCANs that can not have valid CBOR representation, secondary RAW representation is used, which allows interop with all existing tokens in the wild.
This library implements [ADL][] for representing [UCAN]s natively in [IPLD][]. It uses [DAG-CBOR][] as a primary encoding, which is hash consistent and more compact than a secondary RAW JWT encoding. Every UCAN in either encoding can be formatted into a valid JWT string and consumed by other spec compliant [UCAN][] implementations. However [UCAN][]s issued by other libraries may end up in represented in secondory RAW JWT encoding, that is because whitespaces and key order in JWT affects signatures and there for can't be represented accurately in CBOR. When parsing UCANs library will use CBOR representation and fallback to RAW JWT, which allows interop with all existing tokens in the wild.

### Primary Representation

Expand All @@ -13,47 +13,50 @@ UCANs in primary representation are encoded in [DAG-CBOR][] and have following

```ipldsch
type UCAN struct {
header Heder
body Body
signature Signature
}
type Header struct {
version String
algorithm Algorithm
}
type Body struct {
issuer String
audience String,
issuer SigningKey
audience SigningKey
signature Signature
capabilities [Capability]
expiration Int
proofs [&UCAN]
-- If empty omitted
facts optional [Fact]
expiration Int
facts [Fact]
nonce optional String
notBefore optional Int
} representation map {
field facts default []
field proofs default []
}
type Capability struct {
with String
-- Must be all lowercase
can String
-- can have other fields
with Resource
can Ability
-- can have arbitrary other fields
}
type Fact { String: Any }
enum Algorithm {
EdDSA (237) -- 0xed Ed25519 multicodec
RS256 (4613) -- 0x1205 RSA multicodec
} representation int
-- The resource pointer in URI format
type Resource = String
-- Must be all lower-case `/` delimeted with at least one path segment
type Ability = String
-- Signature is computed by seralizing header & body
-- into corresponding JSON with DAG-JSON (to achieve
-- for hash consitency) then encoded into base64 and
-- then signed by issuers private key
type Signature = Bytes
-- multicodec tagged public key
-- 0xed Ed25519
-- 0x1205 RSA
type SigningKey = Bytes
```

## API
Expand All @@ -62,13 +65,13 @@ type Signature = Bytes
import * as UCAN from "@ipld/dag-ucan"
```

#### `UCAN.parse(jwt: string): UCAN.UCAN`
#### `UCAN.parse(jwt: string): UCAN.View`

Parses UCAN formatted as JWT string into a representatino that can be encoded, formatted and queried.

```ts
const ucan = UCAN.parse(jwt)
ucan.issuer // did:key:zAlice
ucan.issuer.did() // did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi
```

#### `UCAN.format(ucan: UCAN.UCAN): string`
Expand All @@ -83,29 +86,37 @@ UCAN.format(UCAN.parse(jwt)) === jwt // true

Encodes UCAN into a binary representation.

```ts
UCAN.encode(UCAN.parse(jwt)) // Uint8Array(679)
```

#### `UCAN.decode(bytes: Uint8Array): UCAN.UCAN`

Decodes byte encoded UCAN.
Decodes UCAN from binary representation into object representation.

```ts
UCAN.decode(UCAN.encode(ucan))
```

#### `UCAN.issue(options: UCAN.UCANOptions): Promise<UCAN.UCAN>`

Issues or derives a UCAN.
Issues a signed UCAN.

> Please note that no capability or time bound validation takes place
> Please note that no capability or time bound validation takes place.
```ts
const ucan = await UCAN.issue({
issuer: boris,
audience: 'did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob'
issuer: alice,
audience: bob,
capabilities: [
{
with: "wnfs://boris.fission.name/public/photos/",
can: "wnfs/append",
can: "fs/read",
with: `storage://${alice.did()}/public/photos/`,
},
{
with: "mailto:boris@fission.codes",
can: "msg/send"
}
can: "pin/add",
with: alice.did(),
},
],
})
```
Expand All @@ -115,3 +126,4 @@ const ucan = await UCAN.issue({
[ipld schema]: https://ipld.io/docs/schemas/using/authoring-guide/
[dag-cbor]: https://ipld.io/docs/codecs/known/dag-cbor/
[multiformats]: https://github.com/multiformats/js-multiformats
[adl]: https://ipld.io/docs/advanced-data-layouts/
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
"./src/lib.js": {
"types": "./dist/src/lib.d.ts",
"import": "./src/lib.js"
},
"./src/codec/cbor.js": {
"types": "./dist/src/codec/cbor.d.ts",
"import": "./src/codec/cbor.js"
},
"./src/codec/raw.js": {
"types": "./dist/src/codec/raw.d.ts",
"import": "./src/codec/raw.js"
}
},
"c8": {
Expand Down
Empty file removed src/codec.js
Empty file.
85 changes: 85 additions & 0 deletions src/codec/cbor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as UCAN from "../ucan.js"
import * as CBOR from "@ipld/dag-cbor"
import * as Parser from "../parser.js"
import * as View from "../view.js"
import * as DID from "../did.js"
import { CID } from "multiformats/cid"

export const name = "dag-ucan"
export const code = CBOR.code

/**
* Encodes given UCAN (in either IPLD or JWT representation) and encodes it into
* corresponding bytes representation. UCAN in IPLD representation is encoded as
* DAG-CBOR which JWT representation is encoded as raw bytes of JWT string.
*
* @template {UCAN.Capability} C
* @param {UCAN.Model<C>} ucan
* @returns {UCAN.ByteView<UCAN.Model<C>>}
*/
export const encode = ucan => {
const { facts, nonce, notBefore, ...rest } = match(ucan)
return CBOR.encode({
...rest,
// leave out optionals unless they are set
...(facts.length > 0 && { facts }),
...(ucan.nonce && { nonce }),
...(ucan.notBefore && { notBefore: ucan.notBefore }),
signature: Parser.parseBytes(ucan.signature, "signature"),
})
}

/**
* Decodes UCAN in primary CBOR representation. It does not validate UCAN, it's
* signature or proof chain. This is to say decoded UCAN may be invalid.
*
* @template {UCAN.Capability} C
* @param {UCAN.ByteView<UCAN.Model<C>>} bytes
* @returns {UCAN.View<C>}
*/
export const decode = bytes => View.cbor(match(CBOR.decode(bytes)))

/**
* @template {UCAN.Capability} C
* @param {{[key in PropertyKey]: unknown}|UCAN.Model<C>} data
* @returns {UCAN.Model<C>}
*/
export const match = data => ({
version: Parser.parseVersion(data.version, "version"),
issuer: parseDID(data.issuer, "issuer"),
audience: parseDID(data.audience, "audience"),
capabilities: /** @type {C[]} */ (
Parser.parseCapabilities(data.capabilities, "capabilities")
),
expiration: Parser.parseInt(data.expiration, "expiration"),
proofs: Parser.parseOptionalArray(data.proofs, parseProof, "proofs") || [],
signature: Parser.parseBytes(data.signature, "signature"),
nonce: Parser.parseOptionalString(data.nonce, "nonce"),
facts: Parser.parseOptionalArray(data.facts, Parser.parseFact, "facts") || [],
notBefore: Parser.parseOptionalInt(data.notBefore, "notBefore"),
})

/**
* @template {UCAN.Capability} C
* @param {unknown} cid
* @param {string} context
*/
const parseProof = (cid, context) =>
/** @type {UCAN.Proof<C>} */ (CID.asCID(cid)) ||
Parser.ParseError.throw(
`Expected ${context} to be CID, instead got ${JSON.stringify(cid)}`
)

/**
*
* @param {unknown} input
* @param {string} context
*/
const parseDID = (input, context) =>
input instanceof Uint8Array
? DID.decode(input)
: Parser.ParseError.throw(
`Expected ${context} to be Uint8Array, instead got ${JSON.stringify(
input
)}`
)
26 changes: 26 additions & 0 deletions src/codec/raw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as UCAN from "../ucan.js"
import * as RAW from "multiformats/codecs/raw"
import * as View from "../view.js"
import * as UTF8 from "../utf8.js"
import * as Parser from "../parser.js"

export const name = "dag-ucan"
export const code = RAW.code

/**
* Encodes given UCAN (in either JWT representation) and encodes it into
* corresponding bytes representation.
*
* @template {UCAN.Capability} C
* @param {UCAN.RAW<C>} ucan
* @returns {UCAN.ByteView<UCAN.JWT<C>>}
*/
export const encode = ucan =>
new Uint8Array(ucan.buffer, ucan.byteOffset, ucan.byteLength)

/**
* @template {UCAN.Capability} C
* @param {UCAN.ByteView<UCAN.JWT<C>>} bytes
* @returns {UCAN.View<C>}
*/
export const decode = bytes => View.jwt(Parser.parse(UTF8.decode(bytes)), bytes)
2 changes: 1 addition & 1 deletion src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface SyncVerifier<A extends number> {
): T
}

export interface Verifier<A extends number> {
export interface Verifier<A extends number = number> {
readonly algorithm: A
verify<S extends Signer<A>, T>(
payload: ByteView<T>,
Expand Down
70 changes: 70 additions & 0 deletions src/did.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as UCAN from "./ucan.js"
import { base58btc } from "multiformats/bases/base58"
import { varint } from "multiformats"

const DID_KEY_PREFIX = `did:key:`
export const ED25519 = 0xed
export const RSA = 0x1205

/**
* @param {Uint8Array} key
* @returns {Code}
*/
export const algorithm = key => {
const [code] = varint.decode(key)
switch (code) {
case ED25519:
case RSA:
return code
default:
throw new RangeError(
`Unsupported key algorithm with multicode 0x${code.toString(16)}.`
)
}
}

/**
* @typedef {typeof ED25519|typeof RSA} Code
*/

/**
* @param {UCAN.DID} did
* @returns {UCAN.DIDView}
*/
export const parse = did => {
if (!did.startsWith(DID_KEY_PREFIX)) {
throw new RangeError(`Invalid DID "${did}", must start with 'did:key:'`)
}
return decode(base58btc.decode(did.slice(DID_KEY_PREFIX.length)))
}

/**
* @param {UCAN.ByteView<UCAN.DID>} key
* @returns {UCAN.DID}
*/
export const format = key =>
/** @type {UCAN.DID} */ (`${DID_KEY_PREFIX}${base58btc.encode(encode(key))}`)

/**
* @param {Uint8Array} bytes
* @returns {UCAN.DIDView}
*/
export const decode = bytes => {
const _ = algorithm(bytes)
return new DID(bytes.buffer, bytes.byteOffset, bytes.byteLength)
}

/**
* @param {Uint8Array} bytes
* @returns {UCAN.ByteView<UCAN.DID>}
*/
export const encode = bytes => {
const _ = algorithm(bytes)
return bytes
}

class DID extends Uint8Array {
did() {
return format(this)
}
}
Loading

0 comments on commit fdc2113

Please sign in to comment.