Code Monkey home page Code Monkey logo

ts-ucan's People

Contributors

appcypher avatar bgins avatar blaine avatar dholms avatar frando avatar icidasset avatar jeffgca avatar kshinn avatar matheus23 avatar nichoth avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ts-ucan's Issues

UCAN store & indexer abstraction

Clients will usually have multiple UCANs lying around for different purposes, i.e. for each capability delegated to them.

When they make a request to some server or delegate these capabilities to another client, they'll need to collect all UCANs that grant the necessary capabilities.

For that, a lookup-by-capability UCAN store is a really useful abstraction. This library could totally support this in an extensible way.

Rough outline:

/** Storage abstraction for abstracting over e.g. localforage/files, thus web/nodejs */
interface Storage {
  saveUCAN(key: string, ucan: UCAN): Promise<void>
  loadUCAN(key: string): Promise<UCAN | null>
  keys(): Iterable<string>
}

interface UCANStore {
  saveUCAN(ucan: UCAN): Promise<void>
  lookupUCANByCapability(capabilityType: string): AsyncIterable<UCAN>
  // maybe also this?
  lookupUCANByIssuer(issuerDID: string): AsyncIterable<UCAN>
}

function createUCANStore(baseStore: Storage): UCANStore

The exact signatures for the lookup* functions are the most unclear to me as of yet. They do somewhat depend on what the types for custom capabilities will look like. E.g. instead of querying by capability type (resource type?), we could query something >= capabilities for overwriting /public/abc.

`npm install` fails

In the root of this repo, npm i fails

ts-ucan main % npm i
npm ERR! code 2
npm ERR! path /Users/nick/code/ts-ucan/packages/ucans
npm ERR! command failed
npm ERR! command sh -c yarn build
npm ERR! yarn run v1.22.19
npm ERR! $ rimraf dist
npm ERR! $ yarn run dist
npm ERR! $ yarn run dist:prep && yarn run dist:src && yarn run dist:cjs && yarn run dist:esm && yarn run dist:types && yarn run dist:pkg
npm ERR! $ copyfiles --error tsconfig.json ./dist/
npm ERR! $ copyfiles --error --up 1 "./src/**/*" ./dist/src/
npm ERR! $ tsc --project ./dist/ --module commonjs --outDir ./dist/cjs/ --sourceMap
npm ERR! ../../node_modules/@types/node/globals.d.ts(72,13): error TS2403: Subsequent variable declarations must have the same type.  Variable 'AbortSignal' must be of type '{ new (): AbortSignal; prototype: AbortSignal; abort(reason?: any): AbortSignal; timeout(milliseconds: number): AbortSignal; }', but here has type '{ new (): AbortSignal; prototype: AbortSignal; }'.
npm ERR! dist/src/index.ts(1,26): error TS2307: Cannot find module '@ucans/default-plugins' or its corresponding type declarations.
npm ERR! dist/src/index.ts(2,23): error TS2307: Cannot find module '@ucans/core' or its corresponding type declarations.
npm ERR! dist/src/index.ts(4,15): error TS2307: Cannot find module '@ucans/core' or its corresponding type declarations.
npm ERR! dist/src/index.ts(5,15): error TS2307: Cannot find module '@ucans/default-plugins' or its corresponding type declarations.
npm ERR! info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
npm ERR! info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
npm ERR! info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
npm ERR! error Command failed with exit code 2.
npm ERR! error Command failed with exit code 2.
npm ERR! error Command failed with exit code 2.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/nick/.npm/_logs/2023-02-13T00_02_51_094Z-debug-0.log
ts-ucan main % npm i | pbc
npm ERR! code 2
npm ERR! path /Users/nick/code/ts-ucan/packages/ucans
npm ERR! command failed
npm ERR! command sh -c yarn build
npm ERR! yarn run v1.22.19
npm ERR! $ rimraf dist
npm ERR! $ yarn run dist
npm ERR! $ yarn run dist:prep && yarn run dist:src && yarn run dist:cjs && yarn run dist:esm && yarn run dist:types && yarn run dist:pkg
npm ERR! $ copyfiles --error tsconfig.json ./dist/
npm ERR! $ copyfiles --error --up 1 "./src/**/*" ./dist/src/
npm ERR! $ tsc --project ./dist/ --module commonjs --outDir ./dist/cjs/ --sourceMap
npm ERR! ../../node_modules/@types/node/globals.d.ts(72,13): error TS2403: Subsequent variable declarations must have the same type.  Variable 'AbortSignal' must be of type '{ new (): AbortSignal; prototype: AbortSignal; abort(reason?: any): AbortSignal; timeout(milliseconds: number): AbortSignal; }', but here has type '{ new (): AbortSignal; prototype: AbortSignal; }'.
npm ERR! dist/src/index.ts(1,26): error TS2307: Cannot find module '@ucans/default-plugins' or its corresponding type declarations.
npm ERR! dist/src/index.ts(2,23): error TS2307: Cannot find module '@ucans/core' or its corresponding type declarations.
npm ERR! dist/src/index.ts(4,15): error TS2307: Cannot find module '@ucans/core' or its corresponding type declarations.
npm ERR! dist/src/index.ts(5,15): error TS2307: Cannot find module '@ucans/default-plugins' or its corresponding type declarations.
npm ERR! info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
npm ERR! info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
npm ERR! info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
npm ERR! error Command failed with exit code 2.
npm ERR! error Command failed with exit code 2.
npm ERR! error Command failed with exit code 2.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/nick/.npm/_logs/2023-02-13T00_03_37_963Z-debug-0.log
ts-ucan main % 

What prevents restricted token holder from using it’s proof token instead ?

I assumed that proofs would be distinguishable from a tokens via extra signature or something so that derived token holder could not simply pull out more capable proof in to escalate privileges. However I can’t see evidence of that in the implementation and spec also seems to suggest that:

Inline proofs MUST include the entire encoded token, since they will be validated by the receiver.
From: https://whitepaper.fission.codes/access-control/ucan/jwt-authentication

Tests in the repo also seem to suggest the same

https://github.com/ucan-wg/ucan/blob/bfbdbdac3cd7cd58d0382c683d2843a536e1eb9d/tests/token.test.ts#L57-L69

I may be misunderstanding something and if so it is probably a good idea to update language to communicate this more clearly.

Add unsafe decoding?

Summary

Problem

ts-ucan does not provide a raw decoding function.

Impact

Tools that wish to inspect a UCAN in its raw form are unable to do so without some level of parsing/validation.

Solution

Provide a raw decoding function.

Detail

Is your feature request related to a problem? Please describe.

Tools that wish to inspect a defective UCAN are unable to do so.

Describe the solution you'd like

The raw decoding function should:

  • Not validate UCANs
  • Should only parse at the top level to check for a header, payload, signature, and . separators
  • Be marked as explicitly unsafe

Naming and/or namespacing will be an important consideration for the last point. A few possibilities:

  • ucan.unsafeDecode
  • ucan.unutterableFoulDecode
  • Namespace unsafe with subtle, for example ucan.subtle.unsafeDecode
  • internal as a namespace where ucan.internal.decode + ucan.internal.validate => ucan.validate
  • parse instead of validate, because the that's decode + validate

{insert-bikeshedding-here}

Describe alternatives you've considered

Developers can write their own raw decoding function.

Canonicalize UCANs by default

IMO (feel free to disagree), following Postel's Law, UCAN libraries should be rigid in what they produce, and liberal in what they accept. To this end, it would be amazing if this library produced canonicalized UCANs compatible with ucan-ipld.

This gets us a few things:

  • Slick tooling (over time)
  • Storage savings form content addressing / deduplication / binary data
  • Convert UCANs into arbitrary formats (e.g. machine friendly, PB, CBOR, Arrow, etc)

We will still need to read in arbitrary JWTs, but the more canonicalized UCANs that exist, the better a world we live in 😉

`rootIssuer` method

Another bit of knowledge I wanted to document -- the rootIssuer method

This was removed because multiple roots are possible, I think:
#24

I had used rootIssuer here in the past in the demo app:
https://github.com/nichoth/ucan-demo/blob/f63365fb4039b5d922eee46c02d1b69609bab31b/src/index.js#L297

This is for a use case where all UCANs are descended from a single keypair that is unique to the server. I'm not sure if that is a use case that should be supported. You check that the chain of UCANs is valid, and also check that the rootIssuer is the server's ID.

Is there a plan to provide for something equivalent in the future? Or is this best left to user-land to implement?

I actually attempted to implement here -- https://github.com/nichoth/ucan-demo/blob/better-validation/src/index.js#L314 -- but as far as I made it was getting an error in the decode function:

SyntaxError: Non-base64url character

encode + parse

In packages/core/src/token.ts, we have this parse function, that throws an error Expected JWT format: 3 dot-separated base64url-encoded values:

export function parse(encodedUcan: string): UcanParts {
  const [ encodedHeader, encodedPayload, signature ] = encodedUcan.split(".")

  if (encodedHeader == null || encodedPayload == null || signature == null) {
    throw new Error(`Can't parse UCAN: ${encodedUcan}: Expected JWT format: 3 dot-separated base64url-encoded values.`)
  }

But in encode.ts, we have this encode function, that returns two dot separated segments:

/**
 * Encode a UCAN.
 *
 * @param ucan The UCAN to encode
 */
export function encode(ucan: Ucan<unknown>): string { 
  return `${ucan.signedData}.${ucan.signature}`
}

Does the parse function need updating? (yarn run test failed on my machine)

Leveraging WebAssembly?

What have been the ucan-wg's thoughts on leveraging WebAssembly? I'd imagine it could help:

  • Reduce code duplication in the various implementations that exist today.
  • Allow for dynamic user-defined logic to be built into the protocol, making it more future proof.

Disclaimer - I work on the Polywrap project, a WebAssembly toolchain + runtime that could offer a solution here https://polywrap.io/

Allow `nbf` and `nnc` to be `null`

Summary

Problem

ts-ucan does not allow nbf and nnc to be null.

Impact

ts-ucan reports an invalid payload when either of these fields are null.

Solution

Allow the fields to be null when validating payloads.

The fields are checked here:

ts-ucan/src/types.ts

Lines 122 to 123 in 978dfd2

&& (!util.hasProp(obj, "nbf") || typeof obj.nbf === "number")
&& (!util.hasProp(obj, "nnc") || typeof obj.nnc === "string")

Introduce common capabilities & builder DSL

Some use cases are very common across resource types. For example, focusing on paths below some pointer is widely used. These should be included directly in the library, with a nice, high level DSL:

// Something like
const ucanBuilder = new Ucan.Builder(keypair);
ucanBuilder.allow_subpath_write({at: "mymachine.local/photos/vacation/", proof: I_AM_ROOT})
ucanBuilder.allow_http_post({at: "example.com/api/v2/", proof: exampleDotComUcan})
ucanBuilder.build()

Ideally these DSLs are built on other library functions (in layers) rather than being totally bespoke (easier to extend down the road)

Support CIDs as proofs

The spec allows to use CIDs as proofs:

You can substitute [encoded proofs] for CIDs of the proofs as long as the proof is reachable over IPFS.

I'd like to support this without having to pull in a js-ipfs dependency. Esp. because ipfs node setup is highly dependent on the use case. Plus, we don't want to force everyone using ts-ucan to run js-ipfs in their frontends.

So, instead I'd pull in multiformats to be able to parse CIDs (we already have multibase in our dependencies anyway). On top of that, we can just require a function load: (cid: CID) => Promise<string> and we should be fine.

As a second step we can think of providing ways to construct CID-backed UCAN chains.

Lock uint8arrays at 3.0.0

Uint8Arrays version 3.1.0 included a breaking change.

When Buffer is available (for instance in Node), it will return Buffers instead of Uint8Arrays.

The WebCrypto API expects Uint8Arrays or ArrayBuffers and does not work as expected with Buffers.

We need to lock uint8arrays at 3.0.0 or wrap the library & convert all Buffers to Uint8Arrays.

Should not use `dist` in NPM package

Having to import ucans/dist/capability.js is not user friendly.
We should make that ucans/capability.js instead.

I'd also like a ESM build 👀

Should encode tokens in base64 without padding & url-safe

Specifically,

ts-ucan/src/base64.ts

Lines 4 to 10 in bfbdbda

export function decode(base64: string, encoding: Encodings = 'base64pad'): string {
return uint8arrays.toString(uint8arrays.fromString(base64, encoding))
}
export function encode(str: string, encoding: Encodings = 'base64pad'): string {
return uint8arrays.toString(uint8arrays.fromString(str), encoding)
}

Should use base64url, not base64pad.

If you paste in a UCAN generated by ts-ucan into https://jwt.io, you'll see some warnings about padding generated.

Also, the RFC 7515 demands that encoding should be base64url: https://datatracker.ietf.org/doc/html/rfc7515#section-2

More bounds checking

When building a UCAN, it's really easy (esp. from js vs ts) to incorrectly set fields; e.g.,

ucan.build({ ..., facts: { xyz: "abc" } })

will silently omit the facts, as will

ucan.build({ ..., facts: [ { "xyz": "abc" } ] })

This is probably true for a variety of other fields, too. I would suggest adding explicit checks and refusing to build a UCAN if the fields aren't properly formatted, including errors for why the UCAN creation failed. Happy to take on this work if others are in agreement, but it might be worth waiting until we update to 0.10. Just leaving this issue here in the meantime, nothing urgent.

Consider using @stablelib/ed25519

Noticed that you are using the tweetnacl lib for ed25519 support. The @stablelib/ed25519 library should provide the same functionality while reducing package size. Worth noting is that the two packages mentioned are from the same author.

Support WebCrypto's ECDSA

ECDSA is widely suspected of being "cooked", but it's so widely used that it should be available for those that have use cases and know what they're doing. I'd like to put a warning on it, but as an example, short lived ECDSA UCANs running inside an intranet are probably fine.

@dholms thoughts?

reconcile the UCAN keypair API with `keystore-idb`

The ucans library has a method ucan.keypair.create, which returns a keypair object with a .did method. In real life you would want to use the keystore-idb library though to manage persistent keys over time. However, the keystore-idb library returns a different API for a keypair. I started by forking the keystore-idb repo here; this gives a method getKeypair that should return the same API that is returned by ucans.

It uses a different test library and bundler though, so not ideal for merging at this point

Representation of capabilities

Following example illustrated the issue https://observablehq.com/d/7fb5d13d63f667b9, for convenience including snippet here as well

const service = await UCAN.EdKeypair.create()
const user = await UCAN.EdKeypair.create()

const root = await UCAN.build({
  issuer: service
  audience: user.did(),
  capabilities: [
    {
          "wnfs": "gozala/",
          "cap": "OVERWRITE"
        }
  ]
})

const user2 = await UCAN.EdKeypair.create()
const child = await UCAN.build({
  issuer: user
  audience: user2.did(),
  capabilities: [
    {
          "wnfs": "gozala/photos",
          "cap": "OVERWRITE"
        }
  ]
  proof: UCAN.encode(root)
})

assert(await UCAN.isValid(child) === true)

Parse old UCAN versions

We should be able to parse old UCAN versions and transform them into a canonical inner representation.

For example, this is an old-version UCAN we should be able to parse:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInVhdiI6IjEuMC4wIn0.eyJhdWQiOiJkaWQ6a2V5OnoxM1YzU29nMllhVUtoZEdDbWd4OVVadVcxbzFTaEZKWWM2RHZHWWU3TlR0Njg5Tm9MM05NTTQyblBuWFJWUnZnNW9TU1BldjNSNmJpQURTYnZ6WVNnNFBibWZWQ2I0YU56YjdHb2FUS2V4Vjl5SGNGcFN0NlEzSjNtVFNhdTNLajNqUFVkdmZWMXF4a2tYQU00cHM1RFJFRXptY2ZjQkZ0aHp5WmlnNmpBdlpKVERvVkwyTllwMnh4VHF0aExuVXpaN01mdGRHV2p4b1JtVkszdFI0b2FQcENUUVZEZXFBNWt6YVpGTEdHRTdWaDluUnh6eTFTZzJTYWNiNE5TTHN4NzFQWTgyUUtBM2RIVFBXYktMakZ5THdob0NuWUpYSGd2eVBnenF5QkJoelVvM2U1NXF4b29LYVF5ZFBuUUdBbUFXblNndnNKbk5QZGlIV2h1VE1FS1k4TDRXUVlUQTJMQzFvWFpKdXVIRGhhRGJCaHdlY2l6a1BTU0xTZUxKTHhKUG1KRHlaODY5cHpKOFF4ZG5ONWpOIiwiZXhwIjozMjc0MjI2NTcxNiwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6MTNWM1NvZzJZYVVLaGRHQ21neDlVWnVXMW8xU2hGSlljNkR2R1llN05UdDY4OU5vTDNXa0IxcHkxUXd1ZGJINVdqbkZFa1hnN25kRkMyMVJjRnNheTNrN0V0Z3J0NlgzVjUxajhFV2JOQVFDR1h2WHpqMnZCZkYzdkdvMWRoWU5yWmJiblJLZVFqMzVlaWtZOXBTWWhNb3BONmZxTUNrd2RDcnJmVmd6UFNxUndZdXczV0ZaOFphQWIzRDdGNjQ0WVhFaURpb3g0bjhaSEtxeXc2WGtta1htcXlDWmE4eGdpYUVETDdVeW90OWlZMVN4Zkp3ZlZ3UVhDdzU3d1EyZkRReHVrVndOVGVFaXdlRDNMbXpveWVQd3pMTmVKUEsyUWd1ZlI0MW1BVXdDbjR3RldKOVJjamdjMlc1cFdOWWFpY3RWakNRWko0S1FpYVF5Z1B6bjRQenMxZlRQcnZmTGY1Z3NaRW5BRDhhcm5aOXRkaVhrNG9Sb1F0bXRuOVk0eFVORlZNNE1CMkFwcHJId1B1ZHdRTCIsIm5iZiI6MTYzODI2NTY1NiwicHRjIjoiU1VQRVJfVVNFUiIsInJzYyI6IioifQ.ijri5HjjJme0izpz0OILS-6KbDamiXbSLllsTPfhEnyWj_xRw1aKU-O8Al0oc8BPE5N8hY-RivqD893eNer39YBqOiyv2W1NoypBL94AQZb4OtShq6b0N8qJ6fA8nKqzLWSed2II2jCsTCy12mqYQtyShE7E9OliF_p-IswnauxQQYhKSNGNJgINVLAMCrhQzeSuLjTocPPbRpsoo8ra1UwYXh34XM2nupBDwtQp8DdUMShIsFBKFk4Gcy3vRQP0E2BWaNTZ5oMYzo0u54VcLwMMlV5Sh7-VDk_vJFOlHmGEev022s06lejWPv3YuP5itoIi6CdejZdZGjLH8jL1dA

With the ucan library in webnative, it decodes into this:

{
  "header": {
    "alg": "RS256",
    "typ": "JWT",
    "uav": "1.0.0"
  },
  "payload": {
    "aud": "did:key:z13V3Sog2YaUKhdGCmgx9UZuW1o1ShFJYc6DvGYe7NTt689NoL3NMM42nPnXRVRvg5oSSPev3R6biADSbvzYSg4PbmfVCb4aNzb7GoaTKexV9yHcFpSt6Q3J3mTSau3Kj3jPUdvfV1qxkkXAM4ps5DREEzmcfcBFthzyZig6jAvZJTDoVL2NYp2xxTqthLnUzZ7MftdGWjxoRmVK3tR4oaPpCTQVDeqA5kzaZFLGGE7Vh9nRxzy1Sg2Sacb4NSLsx71PY82QKA3dHTPWbKLjFyLwhoCnYJXHgvyPgzqyBBhzUo3e55qxooKaQydPnQGAmAWnSgvsJnNPdiHWhuTMEKY8L4WQYTA2LC1oXZJuuHDhaDbBhwecizkPSSLSeLJLxJPmJDyZ869pzJ8QxdnN5jN",
    "exp": 32742265716,
    "fct": [],
    "iss": "did:key:z13V3Sog2YaUKhdGCmgx9UZuW1o1ShFJYc6DvGYe7NTt689NoL3WkB1py1QwudbH5WjnFEkXg7ndFC21RcFsay3k7Etgrt6X3V51j8EWbNAQCGXvXzj2vBfF3vGo1dhYNrZbbnRKeQj35eikY9pSYhMopN6fqMCkwdCrrfVgzPSqRwYuw3WFZ8ZaAb3D7F644YXEiDiox4n8ZHKqyw6XkmkXmqyCZa8xgiaEDL7Uyot9iY1SxfJwfVwQXCw57wQ2fDQxukVwNTeEiweD3LmzoyePwzLNeJPK2QgufR41mAUwCn4wFWJ9Rcjgc2W5pWNYaictVjCQZJ4KQiaQygPzn4Pzs1fTPrvfLf5gsZEnAD8arnZ9tdiXk4oRoQtmtn9Y4xUNFVM4MB2ApprHwPudwQL",
    "nbf": 1638265656,
    "ptc": "SUPER_USER",
    "rsc": "*"
  },
  "signature": "ijri5HjjJme0izpz0OILS-6KbDamiXbSLllsTPfhEnyWj_xRw1aKU-O8Al0oc8BPE5N8hY-RivqD893eNer39YBqOiyv2W1NoypBL94AQZb4OtShq6b0N8qJ6fA8nKqzLWSed2II2jCsTCy12mqYQtyShE7E9OliF_p-IswnauxQQYhKSNGNJgINVLAMCrhQzeSuLjTocPPbRpsoo8ra1UwYXh34XM2nupBDwtQp8DdUMShIsFBKFk4Gcy3vRQP0E2BWaNTZ5oMYzo0u54VcLwMMlV5Sh7-VDk_vJFOlHmGEev022s06lejWPv3YuP5itoIi6CdejZdZGjLH8jL1dA"
}

We can detect these old versions by looking for the uav field in the header (instead of the ucv field, as you'd expect from the spec).

It's open for discussion what ucv version we map uav: 1.0.0 to. For that it'd be useful to know what version strings we've used for ucv so far. I've seen 0.5.0 in some comments and 0.7.0 in the spec so far. Is there a history of version strings @expede ?

I'd propose just mapping uav: 1.0.0 to ucv: 0.0.1 for now.

tsconfig monorepo tricks like config inheritance & a composite config with references to other packages

By @dholms in #82:

I briefly tried to do some tsconfig monorepo tricks like config inheritance & a composite config with references to other packages. But the current build setup was preventing me from doing that. Namely, copying the tsconfig into the dist folder which would add an extra director to the file hierarchy & screw up relative paths. I think this should be doable, but we may need to chat through the current build process. & maybe belongs in a future PR?

Fix readme

the readme says

Installation
NPM:
npm install --save ucans

But if you go to ucans in npm, it says it is deprecated, and you should use @ucans/ucans instead.

0.9 is broken in ESM

Here is what I'm getting

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/gozala/Projects/js-dag-ucan/node_modules/ucans/dist/esm/attenuation' imported from /Users/gozala/Projects/js-dag-ucan/node_modules/ucans/dist/esm/index.js
    at new NodeError (node:internal/errors:371:5)
    at finalizeResolution (node:internal/modules/esm/resolve:416:11)
    at moduleResolve (node:internal/modules/esm/resolve:932:10)
    at defaultResolve (node:internal/modules/esm/resolve:1044:11)
    at ESMLoader.resolve (node:internal/modules/esm/loader:422:30)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:222:40)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:76:40)
    at link (node:internal/modules/esm/module_job:75:36)

Problem is lack of .js extensions in imports see

https://unpkg.com/browse/[email protected]/dist/esm/index.js

Remove dependencies

Throwing this out as an idea. If we're using the UCAN library in tightly-secured environments, it may make sense to harden it against supply chain attacks. Cryptography libraries often have 0 dependencies. If we're managing keypairs in a service worker, for instance, I would love to only depend on ts-ucan & tweetnacl: two libraries I trust and be essentially protected from supply chain attacks.

Granted right now the lib has only 4 dependencies & 1 of them is in house, so this isn't a terrible risk. But on the flip side, it also means it's not a terrible task to get to ✨ 0 dependency land ✨

  • uint8arrays: utility functions for uint8arrays. These are mainly used for going from bytes <-> hex/b64 or vice versa. I think we could easily replicate these within the library
  • semver: used for version check on token. would also be easy to replicate within the lib
  • one-webcrypto: this is already in house so less of a security concern. but could still be nice to pull it over. unfortunately the trick for switching based on environment with the package.json wouldn't work then...
  • @stablelib/ed25519: we'll be adding more crypto libraries, and we may want to leave it up to the user to decide which one(s) they wish to support (& which implementation they prefer). For instance, we're considering using tweetnacl instead. Could necessary libraries be dynamically injected at runtime? Maybe WebCrypto functions could be injected as well?

Thoughts? I'd be happy to help.

Release please?

Is #92 going to make it into a release soon?

The current version of ucans on npm (0.10.0 at the time of writing) can't be used with node in ESM mode without this fix.

Run tests in CI

We should write a github CI (?) file that runs the test suite we have in tests/ for every PR.

What is taking up so much space?

Putting this here to remind myself to look into the file size when using this. I thought it was all mostly using the crypto API that is built in to the browser, which makes it surprising to see the file size.

In a demo app, if I build then do a quick ll public, these are the files:

total 6176
-rw-r--r--  1 nick  staff   2.7M Oct 25 20:43 bundle.js
-rw-r--r--  1 nick  staff   314B Oct 25 20:43 index.html
-rw-r--r--  1 nick  staff   671B Oct 25 20:43 style.css
-rw-r--r--  1 nick  staff   290B Oct 25 20:43 style.css.map

Then if I comment out the ucans import:

total 400
-rw-r--r--  1 nick  staff   186K Oct 25 20:43 bundle.js
-rw-r--r--  1 nick  staff   314B Oct 25 20:43 index.html
-rw-r--r--  1 nick  staff   671B Oct 25 20:43 style.css
-rw-r--r--  1 nick  staff   290B Oct 25 20:43 style.css.map

ESM build is incompatible with Node.js ESM

Hi,
the published ESM build of @ucans/ucans is wrong, it misses the .js file endings that are required for ESM imports.

To reproduce:
package.json

{
  "type": "module",
  "dependencies": {
    "@ucans/ucans": "^0.11.0-alpha"
  }
}

test.js

import * as ucans from '@ucans/ucans'

Run

$ node test.js
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/tmp/ucantest/node_modules/@ucans/default-plugins/dist/esm/prefixes' imported from /tmp/ucantest/node_modules/@ucans/default-plugins/dist/esm/rsa/crypto.js
    at new NodeError (node:internal/errors:372:5)
    at finalizeResolution (node:internal/modules/esm/resolve:437:11)
    at moduleResolve (node:internal/modules/esm/resolve:1009:10)
    at defaultResolve (node:internal/modules/esm/resolve:1218:11)
    at ESMLoader.resolve (node:internal/modules/esm/loader:580:30)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:294:18)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:80:40)
    at link (node:internal/modules/esm/module_job:78:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}
}

`prf` field should be an array

In previous versions of UCANs, UCANs were always a chain. In version 0.7.0 of the UCAN spec, it's supposed to be possible to use multiple other UCANs as a proof for one UCAN.

See this section in the UCAN spec: https://whitepaper.fission.codes/access-control/ucan/jwt-authentication#proofs

However, at the moment the library only allows 0 or 1 entry for the UCAN prf key:

ts-ucan/src/token.ts

Lines 63 to 125 in bfbdbda

export function buildParts(params: {
// to/from
audience: string
issuer: string
keyType: KeyType
// capabilities
capabilities?: Array<Capability>
// time bounds
lifetimeInSeconds?: number // expiration overrides lifetimeInSeconds
expiration?: number
notBefore?: number
// proof / other info
facts?: Array<Fact>
proof?: string
addNonce?: boolean
// in the weeds
ucanVersion?: string
}): { header: UcanHeader, payload: UcanPayload } {
const {
audience,
issuer,
capabilities = [],
keyType,
lifetimeInSeconds = 30,
expiration,
notBefore,
facts,
proof = null,
addNonce = false,
ucanVersion = "0.7.0"
} = params
// Timestamps
const currentTimeInSeconds = Math.floor(Date.now() / 1000)
let exp = expiration || (currentTimeInSeconds + lifetimeInSeconds)
let nbf = notBefore || currentTimeInSeconds - 60
const header = {
alg: jwtAlgorithm(keyType),
typ: "JWT",
ucv: ucanVersion,
} as UcanHeader
const payload = {
aud: audience,
att: capabilities,
exp,
fct: facts,
iss: issuer,
nbf,
prf: proof,
} as UcanPayload
if (addNonce) {
payload.nnc = util.generateNonce()
}
return { header, payload }
}

(See lines 79 and 117)

Check original encoded strings against the signature

To verify the UCAN signature, at the moment we're decoding the base64 parts of the UCAN into JSON, then stringify that JSON back into base64 and then bytes, and then check those bytes against the signature.

This process is likely to be lossy in unexpected ways. JSON.stringify(JSON.parse(x)) === x doesn't hold for all values of x.

This results in some UCANs being rejected even though their signatures are correct.

Relevant code:

ts-ucan/src/token.ts

Lines 204 to 210 in 6a75d56

const encodedHeader = encodeHeader(ucan.header)
const encodedPayload = encodePayload(ucan.payload)
const data = uint8arrays.fromString(`${encodedHeader}.${encodedPayload}`)
const sig = uint8arrays.fromString(ucan.signature, 'base64urlpad')
const valid = await verifySignature(data, sig, ucan.payload.iss)

I think we should change the API somewhat to fix this issue:

async function checkSignature(string: encodedUcan): Promise<boolean>
function decode(string: encodedUcan): UCAN
async function validate(string: encodedUcan): Promise<UCAN> { // throws if invalid? Or just returns Result<>
  // ...
  ... = await checkSignature(encodedUcan)
  // ...
  const ucan = decode(encodedUcan)
  // ...
}

Check capability info in `hasCapability`

There's a test case that should be failing in #58: The hasCapabiltiy function should make sure that given UCAN actually has a capability that actually originates in what's passed in & that the given interval is in the capability's time bounds.

I think it should not use delegateCapabiltiyInfo to modify the time bounds using the given time bounds, but check them instead.

Validation should compare that root "iss" matches outermost "aud"

In my first prototype with UCANs I've made a mistake of not creating nor validating that outermost UCAN was issued for the service receiving a request. I think that is fairly easy mistake given how conventional token systems work on the web.

I think it would make a lot more sense if validation step checked that proof chain makes the full loop & maybe optionally allowed providing alternative "key" to compare outmost "aud" in case service is providing service on others behalf.

Impossible to construct a UCAN without `nbf`

The current low-level API will always generate an nbf field, defaulting to currentTime - 60, but in seconds.

Which on the one hand is a bug: nbf is a timestamp meant to be set as milliseconds, not seconds, but also, it's thus impossible to create a UCAN without an nbf.

I'm not actually sure. Maybe that's fine?

Design & Implement Validation Helpers

Manual validation is a bad experience. We should provide a top level

// PSEUDOCODE

enum ValidationStatus = {
  Valid,
  Escalation,
  Unrecognized
}

// Passed to higher order functions
function compareAll(targetCap: MyCapability, proofCaps: Capability[]): ValidationStatus
function compareAny(targetCap: MyCapability, proofCap: Capability): ValidationStatus
function compare(targetCap: MyCapability, proofCap: MyCapability): ValidationStatus

interface Authorization<caps> {
  from: Did, // top-level issuer,
  to: Did, // top-level audience
  allowed: Capability<caps>[],
  unrecognized: object[]
}

interface FailureReason = "EXPIRED" | "TOO_EARLY" | {escelated: object} // ...

function validate(ucan: Ucan): Result<FailureReason, Authorization>

Here, compare can be expressed in terms of compareAll.

Compare All

Compares the focused capability against the entire list of proof capabilities. This is especially useful for cases of rights amplification, where more than one proof is needed.

All of the compare functions should get translated into compareAll under the hood

Compare Any

Especially useful when there's a version update or some backwards compatible change. Compares the focused capability one-at-a-time with the proof's capabilities, but checks against all of them.

Compare (Exact)

The usual case. Compare only if the capabilities are in the same namespace/semantics.

Support redelegation capability

The spec mentions a "*" resource: https://whitepaper.fission.codes/access-control/ucan/jwt-authentication#resource-identifier

Delegate all resources of any type that are in scope

Fission will need that to be implemented for device linking.

We need to add some logic to parsing & capability checking to support this. In capability checking it essentially allows you to "forward" who the "originator" is for all capabilities without having to mention concrete capabilities.

consider using dag-json instead of json.stringify

dag-ucan had been updated to use dag-cbor as it's primary representation (which is more compact and hash consistent). However while all UCANs in primary representation can be formatted into a valid JWT UCAN string and be parsed / validated by this library (and I have tests in place to ensure) not all UCANs can be represented in that representation, because key ordering and white-spacing is not preserved. For that reason dag-ucan has secondary representation which is basically RAW bytes of JWT string. This allows dag-ucan to use optimal representation on all UCANs it produces but still interop with UCANs that can't be represented that way.

Unfortunately UCANs produced by this library can't be represented by primary representation, I am guessing due to key ordering (DAG-CBOR orders those alphabetically for hash consistency). For that reason I would like propose adopting dag-json in place of JSON.stringify which adds same key ordering as dag-cbor. If adopted tokens issued by this library could be parsed into primary and more compact representation in dag-ucan. In addition it would provide few additional benefits:

  1. IPLD linking support (see https://ipld.io/specs/codecs/dag-json/spec/#link-kind)
  2. Native binary support (see https://ipld.io/specs/codecs/dag-json/spec/#bytes-kind)

Caveats

  1. dag-ucan also lower cases all cans since according to spec they are case insensitive and normalizing would provide better hash consistency. In other words this library would have to do the same for it's tokens to be parsed into compact representation.
    https://github.com/ipld/js-dag-ucan/blob/7bacaf91a90f511ec70d1a4f51ed923947185399/src/parser.js#L104-L120

  2. dag-ucan omits fct field if it an empty array https://github.com/ipld/js-dag-ucan/blob/main/src/formatter.js#L58

Consume test fixtures from spec

The UCAN spec has a set of test fixtures to check for compliance and interoperability across implementations. Let's use check against these tests.

We should wait on two PRs:

Once those are merged, we should be able to test UCAN at version 0.8.1.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.